-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
In addition to the attribute mv command, add a new attribute replace command. This command not only renames the key of an attribute but also sets the value of the attribute. This could be done by combining attribute mv and attribute set, but doing it with a single command would be more convenient. This is because in dependency upgrade scenarios, where attribute A is deprecated and use B instead, it is often the case that not only the name of the attribute changes but also its value. For example, Terraform v1.10 introduced DynamoDB-free S3-native state locking in the s3 backend, and the upcoming Terraform v1.11 will start deprecating the old dynamodb_table attribute and recommend using the new use_lockfile. It's easy to rewrite a few files by hand, but it could be tedious if you have hundreds of backend configurations across many repositories. While the above is the initial motivation for implementing this feature, it is not hard to imagine that the real-world use cases go beyond this. As with the attribute mv command, a forked version of hclwrite is used to implement this feature. So, we need to wait for the upstream patch to be merged before we merge this implementation.
- Loading branch information
1 parent
1a64342
commit 4772d7c
Showing
5 changed files
with
344 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package editor | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/hashicorp/hcl/v2/hclwrite" | ||
) | ||
|
||
// AttributeReplaceFilter is a filter implementation for replacing attribute. | ||
type AttributeReplaceFilter struct { | ||
address string | ||
name string | ||
value string | ||
} | ||
|
||
var _ Filter = (*AttributeReplaceFilter)(nil) | ||
|
||
// NewAttributeReplaceFilter creates a new instance of AttributeReplaceFilter. | ||
func NewAttributeReplaceFilter(address string, name string, value string) Filter { | ||
return &AttributeReplaceFilter{ | ||
address: address, | ||
name: name, | ||
value: value, | ||
} | ||
} | ||
|
||
// Filter reads HCL and replaces both the name and value of matched an | ||
// attribute at a given address. | ||
func (f *AttributeReplaceFilter) Filter(inFile *hclwrite.File) (*hclwrite.File, error) { | ||
attr, body, err := findAttribute(inFile.Body(), f.address) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if attr != nil { | ||
_, fromAttributeName, err := parseAttributeAddress(f.address) | ||
if err != nil { | ||
return nil, err | ||
} | ||
toAttributeName := f.name | ||
|
||
// The Body.RenameAttribute() returns false if fromName does not exist or | ||
// toName already exists. However, here, we want to return an error only | ||
// if toName already exists, so we check it ourselves. | ||
toAttr := body.GetAttribute(toAttributeName) | ||
if toAttr != nil { | ||
return nil, fmt.Errorf("attribute already exists: %s", toAttributeName) | ||
} | ||
|
||
_ = body.RenameAttribute(fromAttributeName, toAttributeName) | ||
|
||
// To delegate expression parsing to the hclwrite parser, | ||
// We build a new expression and set back to the attribute by tokens. | ||
expr, err := buildExpression(toAttributeName, f.value) | ||
if err != nil { | ||
return nil, err | ||
} | ||
body.SetAttributeRaw(toAttributeName, expr.BuildTokens(nil)) | ||
} | ||
|
||
return inFile, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package editor | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func TestAttributeReplaceFilter(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
src string | ||
address string | ||
toName string | ||
toValue string | ||
ok bool | ||
want string | ||
}{ | ||
{ | ||
name: "simple", | ||
src: ` | ||
a0 = v0 | ||
a1 = v1 | ||
`, | ||
address: "a0", | ||
toName: "a2", | ||
toValue: "v2", | ||
ok: true, | ||
want: ` | ||
a2 = v2 | ||
a1 = v1 | ||
`, | ||
}, | ||
{ | ||
name: "with comments", | ||
src: ` | ||
# before attr | ||
a0 = "v0" # inline | ||
a1 = "v1" | ||
`, | ||
address: "a0", | ||
toName: "a2", | ||
toValue: `"v2"`, | ||
ok: true, | ||
want: ` | ||
# before attr | ||
a2 = "v2" # inline | ||
a1 = "v1" | ||
`, | ||
}, | ||
{ | ||
name: "attribute in block", | ||
src: ` | ||
a0 = v0 | ||
b1 "l1" { | ||
a1 = v1 | ||
} | ||
`, | ||
address: "b1.l1.a1", | ||
toName: "a2", | ||
toValue: "v2", | ||
ok: true, | ||
want: ` | ||
a0 = v0 | ||
b1 "l1" { | ||
a2 = v2 | ||
} | ||
`, | ||
}, | ||
{ | ||
name: "not found", | ||
src: ` | ||
a0 = v0 | ||
`, | ||
address: "a1", | ||
toName: "a2", | ||
toValue: "v2", | ||
ok: true, | ||
want: ` | ||
a0 = v0 | ||
`, | ||
}, | ||
{ | ||
name: "attribute not found in block", | ||
src: ` | ||
a0 = v0 | ||
b1 "l1" { | ||
a1 = v1 | ||
} | ||
`, | ||
address: "b1.l1.a2", | ||
toName: "a3", | ||
toValue: "v3", | ||
ok: true, | ||
want: ` | ||
a0 = v0 | ||
b1 "l1" { | ||
a1 = v1 | ||
} | ||
`, | ||
}, | ||
{ | ||
name: "block not found", | ||
src: ` | ||
a0 = v0 | ||
b1 "l1" { | ||
a1 = v1 | ||
} | ||
`, | ||
address: "b2.l1.a1", | ||
toName: "a2", | ||
toValue: "v2", | ||
ok: true, | ||
want: ` | ||
a0 = v0 | ||
b1 "l1" { | ||
a1 = v1 | ||
} | ||
`, | ||
}, | ||
{ | ||
name: "attribute already exists", | ||
src: ` | ||
a0 = v0 | ||
a1 = v1 | ||
`, | ||
address: "a0", | ||
toName: "a1", | ||
toValue: "v2", | ||
ok: false, | ||
want: "", | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
o := NewEditOperator(NewAttributeReplaceFilter(tc.address, tc.toName, tc.toValue)) | ||
output, err := o.Apply([]byte(tc.src), "test") | ||
if tc.ok && err != nil { | ||
t.Fatalf("unexpected err = %s", err) | ||
} | ||
|
||
got := string(output) | ||
if !tc.ok && err == nil { | ||
t.Fatalf("expected to return an error, but no error, outStream: \n%s", got) | ||
} | ||
|
||
if got != tc.want { | ||
t.Fatalf("got:\n%s\nwant:\n%s", got, tc.want) | ||
} | ||
}) | ||
} | ||
} |