-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: phantom attributes on multi-resources (CLOUD-1843)
We were failing to resolve the `bucket = aws_s3_bucket.nativebucket[0].id` attribute in the following snippet: resource "aws_s3_bucket" "nativebucket" { count = 1 bucket = "test" } resource "aws_s3_bucket_versioning" "nativebucket" { count = 1 bucket = aws_s3_bucket.nativebucket[0].id versioning_configuration { status = "Enabled" } } This is problematic since it is extremely common to use `count = var.create ? 1 : 0` in terraform modules. This is fixed by a number of changes: 1. We currently reference attributes by `LocalName`, which is a list of strings. This cannot represent an accessor that also has numbers in it, like `aws_s3_bucket.nativebucket[0].id`. A new `accessor` is added to take care of this case, including conversion functions to and from `LocalName`. 2. We take care to keep the trailing part of the `accessor`s around in any conversion functions; so that `aws_s3_bucket.nativebucket[0].id` gets split into `aws_s3_bucket.nativebucket` and `[0].id`. 3. We refactor the `phantomAttrs` type so it receives a way to tell whether or not a resource is a multi-resource (rather than passing this in through an argument in `phantomAttrs.add()`). 4. Finally, now that we have all the machinery from 1-3 available, we can adjust `phantomAttrs` to remove one element from the trailing accessors when applicable. I added this snippet as a golden test, and included one for the `for_each` version as well.
- Loading branch information
1 parent
fca577d
commit 1f77093
Showing
10 changed files
with
358 additions
and
46 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
kind: Fixed | ||
body: phantom attributes on multi-resources | ||
time: 2023-11-22T13:48:18.832055057+01:00 |
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,116 @@ | ||
package hcl_interpreter | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/hashicorp/hcl/v2" | ||
"github.com/zclconf/go-cty/cty" | ||
) | ||
|
||
// accessor represents paths in HCL that can contain both string or int parts, | ||
// e.g. "foo.bar[3].qux". | ||
type accessor []interface{} | ||
|
||
func (a accessor) toString() string { | ||
buf := &strings.Builder{} | ||
for i, p := range a { | ||
switch p := p.(type) { | ||
case int: | ||
fmt.Fprintf(buf, "[%d]", p) | ||
case string: | ||
if i == 0 { | ||
fmt.Fprintf(buf, "%s", p) | ||
} else { | ||
fmt.Fprintf(buf, ".%s", p) | ||
} | ||
} | ||
} | ||
return buf.String() | ||
} | ||
|
||
func stringToAccessor(input string) (accessor, error) { | ||
parts := []interface{}{} | ||
for len(input) > 0 { | ||
if input[0] == '[' { | ||
end := strings.IndexByte(input, ']') | ||
if end < 0 { | ||
return nil, fmt.Errorf("unmatched [") | ||
} | ||
num, err := strconv.Atoi(input[1:end]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
parts = append(parts, num) | ||
input = input[end+1:] | ||
if len(input) > 0 && input[0] == '.' { | ||
input = input[1:] // Consume extra '.' after ']' | ||
} | ||
} else { | ||
end := strings.IndexAny(input, ".[") | ||
if end < 0 { | ||
parts = append(parts, input) | ||
input = "" | ||
} else { | ||
parts = append(parts, input[:end]) | ||
if input[end] == '.' { | ||
input = input[end+1:] | ||
} else { | ||
input = input[end:] | ||
} | ||
} | ||
} | ||
} | ||
return parts, nil | ||
} | ||
|
||
func traversalToAccessor(traversal hcl.Traversal) (accessor, error) { | ||
parts := make(accessor, 0) | ||
for _, traverser := range traversal { | ||
switch t := traverser.(type) { | ||
case hcl.TraverseRoot: | ||
parts = append(parts, t.Name) | ||
case hcl.TraverseAttr: | ||
parts = append(parts, t.Name) | ||
case hcl.TraverseIndex: | ||
val := t.Key | ||
if val.IsKnown() { | ||
if val.Type() == cty.Number { | ||
n := val.AsBigFloat() | ||
if n.IsInt() { | ||
i, _ := n.Int64() | ||
parts = append(parts, int(i)) | ||
} else { | ||
return nil, fmt.Errorf("Non-int number type in TraverseIndex") | ||
} | ||
} else if val.Type() == cty.String { | ||
parts = append(parts, val.AsString()) | ||
} else { | ||
return nil, fmt.Errorf("Unsupported type in TraverseIndex: %s", val.Type().GoString()) | ||
} | ||
} else { | ||
return nil, fmt.Errorf("Unknown value in TraverseIndex") | ||
} | ||
} | ||
} | ||
return parts, nil | ||
} | ||
|
||
// toLocalName tries to convert the accessor to a local name starting from the | ||
// front. As soon as a non-string part is encountered, we stop and return the | ||
// trailing accessor as well. | ||
func (a accessor) toLocalName() (LocalName, accessor) { | ||
name := make(LocalName, 0) | ||
trailing := make(accessor, len(a)) | ||
copy(trailing, a) | ||
for len(trailing) > 0 { | ||
if str, ok := trailing[0].(string); ok { | ||
name = append(name, str) | ||
trailing = trailing[1:] | ||
} else { | ||
break | ||
} | ||
} | ||
return name, trailing | ||
} |
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,93 @@ | ||
package hcl_interpreter | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestAccessorToString(t *testing.T) { | ||
tests := []struct { | ||
input accessor | ||
expected string | ||
}{ | ||
{ | ||
input: accessor{"foo", "bar", 3, "qux"}, | ||
expected: "foo.bar[3].qux", | ||
}, | ||
} | ||
for i, test := range tests { | ||
t.Run(fmt.Sprintf("case%02d", i), func(t *testing.T) { | ||
actual := test.input.toString() | ||
assert.Equal(t, test.expected, actual) | ||
}) | ||
} | ||
} | ||
|
||
func TestStringToAccessor(t *testing.T) { | ||
tests := []struct { | ||
input string | ||
expected accessor | ||
err bool | ||
}{ | ||
{ | ||
input: "foo.bar[3].qux", | ||
expected: accessor{"foo", "bar", 3, "qux"}, | ||
}, | ||
{ | ||
input: "[1][2][3]", | ||
expected: accessor{1, 2, 3}, | ||
}, | ||
{ | ||
input: "foo[3.qux", | ||
err: true, | ||
}, | ||
{ | ||
input: "foo.bar[three].qux", | ||
err: true, | ||
}, | ||
} | ||
for i, test := range tests { | ||
t.Run(fmt.Sprintf("case%02d", i), func(t *testing.T) { | ||
actual, err := stringToAccessor(test.input) | ||
if test.err { | ||
assert.Error(t, err) | ||
} else { | ||
assert.NoError(t, err) | ||
assert.Equal(t, test.expected, actual) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestAccessorToLocalName(t *testing.T) { | ||
tests := []struct { | ||
input accessor | ||
expected LocalName | ||
trailing accessor | ||
}{ | ||
{ | ||
input: accessor{"aws_s3_bucket", "my_bucket", 0, "id"}, | ||
expected: LocalName{"aws_s3_bucket", "my_bucket"}, | ||
trailing: accessor{0, "id"}, | ||
}, | ||
{ | ||
input: accessor{}, | ||
expected: LocalName{}, | ||
trailing: accessor{}, | ||
}, | ||
{ | ||
input: accessor{"aws_s3_bucket", "my_bucket", "id"}, | ||
expected: LocalName{"aws_s3_bucket", "my_bucket", "id"}, | ||
trailing: accessor{}, | ||
}, | ||
} | ||
for i, test := range tests { | ||
t.Run(fmt.Sprintf("case%02d", i), func(t *testing.T) { | ||
actual, trailing := test.input.toLocalName() | ||
assert.Equal(t, test.expected, actual) | ||
assert.Equal(t, test.trailing, trailing) | ||
}) | ||
} | ||
} |
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
Oops, something went wrong.