diff --git a/internal/lang/globalref/analyzer_contributing_resources_test.go b/internal/lang/globalref/analyzer_contributing_resources_test.go index 038b3ed54..79c441c43 100644 --- a/internal/lang/globalref/analyzer_contributing_resources_test.go +++ b/internal/lang/globalref/analyzer_contributing_resources_test.go @@ -1,6 +1,7 @@ package globalref import ( + "sort" "testing" "github.com/google/go-cmp/cmp" @@ -94,3 +95,96 @@ func TestAnalyzerContributingResources(t *testing.T) { }) } } + +func TestAnalyzerContributingResourceAttrs(t *testing.T) { + azr := testAnalyzer(t, "contributing-resources") + + tests := map[string]struct { + StartRefs func() []Reference + WantAttrs []string + }{ + "root output 'network'": { + func() []Reference { + return azr.ReferencesFromOutputValue( + addrs.OutputValue{Name: "network"}.Absolute(addrs.RootModuleInstance), + ) + }, + []string{ + `data.test_thing.environment.any.base_cidr_block`, + `data.test_thing.environment.any.subnet_count`, + `module.network.test_thing.subnet`, + `module.network.test_thing.vpc.string`, + }, + }, + "root output 'c10s_url'": { + func() []Reference { + return azr.ReferencesFromOutputValue( + addrs.OutputValue{Name: "c10s_url"}.Absolute(addrs.RootModuleInstance), + ) + }, + []string{ + `data.test_thing.environment.any.base_cidr_block`, + `data.test_thing.environment.any.subnet_count`, + `module.compute.test_thing.load_balancer.string`, + `module.network.test_thing.subnet`, + `module.network.test_thing.vpc.string`, + }, + }, + "module.compute.test_thing.load_balancer": { + func() []Reference { + return azr.ReferencesFromResourceInstance( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "load_balancer", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("compute", addrs.NoKey)), + ) + }, + []string{ + `data.test_thing.environment.any.base_cidr_block`, + `data.test_thing.environment.any.subnet_count`, + `module.compute.test_thing.controller`, + `module.network.test_thing.subnet`, + `module.network.test_thing.vpc.string`, + }, + }, + "data.test_thing.environment": { + func() []Reference { + return azr.ReferencesFromResourceInstance( + addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "test_thing", + Name: "environment", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ) + }, + []string{ + // Nothing! This one only refers to an input variable. + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + startRefs := test.StartRefs() + refs := azr.ContributingResourceReferences(startRefs...) + + want := test.WantAttrs + got := make([]string, len(refs)) + for i, ref := range refs { + resAttr, ok := ref.ResourceAttr() + if !ok { + t.Errorf("%s is not a resource attr reference", resAttr.DebugString()) + continue + } + got[i] = resAttr.DebugString() + } + + sort.Strings(got) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong addresses\n%s", diff) + } + }) + } +} diff --git a/internal/lang/globalref/reference.go b/internal/lang/globalref/reference.go index 47f48c3c8..4fc2f14a9 100644 --- a/internal/lang/globalref/reference.go +++ b/internal/lang/globalref/reference.go @@ -3,7 +3,10 @@ package globalref import ( "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" ) // Reference combines an addrs.Reference with the address of the module @@ -125,6 +128,45 @@ func (r Reference) DebugString() string { return r.ContainerAddr.String() + "::" + r.LocalRef.DisplayString() } +// ResourceAttr converts the Reference value to a more specific ResourceAttr +// value. +// +// Because not all references belong to resources, the extra boolean return +// value indicates whether the returned address is valid. +func (r Reference) ResourceAttr() (ResourceAttr, bool) { + res, ok := r.ResourceAddr() + if !ok { + return ResourceAttr{}, ok + } + + traversal := r.LocalRef.Remaining + + path := make(cty.Path, len(traversal)) + for si, step := range traversal { + switch ts := step.(type) { + case hcl.TraverseRoot: + path[si] = cty.GetAttrStep{ + Name: ts.Name, + } + case hcl.TraverseAttr: + path[si] = cty.GetAttrStep{ + Name: ts.Name, + } + case hcl.TraverseIndex: + path[si] = cty.IndexStep{ + Key: ts.Key, + } + default: + panic(fmt.Sprintf("unsupported traversal step %#v", step)) + } + } + + return ResourceAttr{ + Resource: res, + Attr: path, + }, true +} + // addrKey returns the referenceAddrKey value for the item that // this reference refers to, discarding any source location information. // @@ -146,3 +188,15 @@ func (r Reference) addrKey() referenceAddrKey { // make it easier to see when we're intentionally using strings to uniquely // identify absolute reference addresses. type referenceAddrKey string + +// ResourceAttr represents a global resource and attribute reference. +// This is a more specific form of the Reference type since it can only refer +// to a specific AbsResource and one of its attributes. +type ResourceAttr struct { + Resource addrs.AbsResource + Attr cty.Path +} + +func (r ResourceAttr) DebugString() string { + return r.Resource.String() + tfdiags.FormatCtyPath(r.Attr) +}