diff --git a/config/hcl2shim/paths.go b/config/hcl2shim/paths.go index 5d2fb02d9..99437cbb1 100644 --- a/config/hcl2shim/paths.go +++ b/config/hcl2shim/paths.go @@ -28,6 +28,10 @@ func RequiresReplace(attrs []string, ty cty.Type) ([]cty.Path, error) { paths = append(paths, p) } + // now trim off any trailing paths that aren't GetAttrSteps, since only an + // attribute itself can require replacement + paths = trimPaths(paths) + // There may be redundant paths due to set elements or index attributes // Do some ugly n^2 filtering, but these are always fairly small sets. for i := 0; i < len(paths)-1; i++ { @@ -44,6 +48,30 @@ func RequiresReplace(attrs []string, ty cty.Type) ([]cty.Path, error) { return paths, nil } +// trimPaths removes any trailing steps that aren't of type GetAttrSet, since +// only an attribute itself can require replacement +func trimPaths(paths []cty.Path) []cty.Path { + var trimmed []cty.Path + for _, path := range paths { + path = trimPath(path) + if len(path) > 0 { + trimmed = append(trimmed, path) + } + } + return trimmed +} + +func trimPath(path cty.Path) cty.Path { + for len(path) > 0 { + _, isGetAttr := path[len(path)-1].(cty.GetAttrStep) + if isGetAttr { + break + } + path = path[:len(path)-1] + } + return path +} + // requiresReplacePath takes a key from a flatmap along with the cty.Type // describing the structure, and returns the cty.Path that would be used to // reference the nested value in the data structure. diff --git a/config/hcl2shim/paths_test.go b/config/hcl2shim/paths_test.go index ff52c92d9..cffbe6b5a 100644 --- a/config/hcl2shim/paths_test.go +++ b/config/hcl2shim/paths_test.go @@ -6,9 +6,18 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" ) +var ( + ignoreUnexported = cmpopts.IgnoreUnexported(cty.GetAttrStep{}, cty.IndexStep{}) + valueComparer = cmp.Comparer(cty.Value.RawEquals) +) + func TestPathFromFlatmap(t *testing.T) { tests := []struct { Flatmap string @@ -221,3 +230,140 @@ func TestPathFromFlatmap(t *testing.T) { }) } } + +func TestRequiresReplace(t *testing.T) { + for _, tc := range []struct { + name string + attrs []string + expected []cty.Path + ty cty.Type + }{ + { + name: "basic", + attrs: []string{ + "foo", + }, + ty: cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + expected: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "foo"}}, + }, + }, + { + name: "two", + attrs: []string{ + "foo", + "bar", + }, + ty: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.String, + }), + expected: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "foo"}}, + cty.Path{cty.GetAttrStep{Name: "bar"}}, + }, + }, + { + name: "nested object", + attrs: []string{ + "foo.bar", + }, + ty: cty.Object(map[string]cty.Type{ + "foo": cty.Object(map[string]cty.Type{ + "bar": cty.String, + }), + }), + expected: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "foo"}, cty.GetAttrStep{Name: "bar"}}, + }, + }, + { + name: "nested objects", + attrs: []string{ + "foo.bar.baz", + }, + ty: cty.Object(map[string]cty.Type{ + "foo": cty.Object(map[string]cty.Type{ + "bar": cty.Object(map[string]cty.Type{ + "baz": cty.String, + }), + }), + }), + expected: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "foo"}, cty.GetAttrStep{Name: "bar"}, cty.GetAttrStep{Name: "baz"}}, + }, + }, + { + name: "nested map", + attrs: []string{ + "foo.%", + "foo.bar", + }, + ty: cty.Object(map[string]cty.Type{ + "foo": cty.Map(cty.String), + }), + expected: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "foo"}}, + }, + }, + { + name: "nested list", + attrs: []string{ + "foo.#", + "foo.1", + }, + ty: cty.Object(map[string]cty.Type{ + "foo": cty.Map(cty.String), + }), + expected: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "foo"}}, + }, + }, + { + name: "object in map", + attrs: []string{ + "foo.bar.baz", + }, + ty: cty.Object(map[string]cty.Type{ + "foo": cty.Map(cty.Object( + map[string]cty.Type{ + "baz": cty.String, + }, + )), + }), + expected: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.StringVal("bar")}, cty.GetAttrStep{Name: "baz"}}, + }, + }, + { + name: "object in list", + attrs: []string{ + "foo.1.baz", + }, + ty: cty.Object(map[string]cty.Type{ + "foo": cty.List(cty.Object( + map[string]cty.Type{ + "baz": cty.String, + }, + )), + }), + expected: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, cty.GetAttrStep{Name: "baz"}}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + rp, err := RequiresReplace(tc.attrs, tc.ty) + if err != nil { + t.Fatal(err) + } + if !cmp.Equal(tc.expected, rp, ignoreUnexported, valueComparer) { + t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.expected, rp) + } + }) + + } + +}