diff --git a/addrs/parse_target_test.go b/addrs/parse_target_test.go index 496fb15ce..84796084f 100644 --- a/addrs/parse_target_test.go +++ b/addrs/parse_target_test.go @@ -249,6 +249,51 @@ func TestParseTarget(t *testing.T) { }, ``, }, + { + `module.foo.module.bar[0].data.aws_instance.baz`, + &Target{ + Subject: AbsResource{ + Resource: Resource{ + Mode: DataResourceMode, + Type: "aws_instance", + Name: "baz", + }, + Module: ModuleInstance{ + {Name: "foo", InstanceKey: NoKey}, + {Name: "bar", InstanceKey: IntKey(0)}, + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 47, Byte: 46}, + }, + }, + ``, + }, + { + `module.foo.module.bar["a"].data.aws_instance.baz["hello"]`, + &Target{ + Subject: AbsResourceInstance{ + Resource: ResourceInstance{ + Resource: Resource{ + Mode: DataResourceMode, + Type: "aws_instance", + Name: "baz", + }, + Key: StringKey("hello"), + }, + Module: ModuleInstance{ + {Name: "foo", InstanceKey: NoKey}, + {Name: "bar", InstanceKey: StringKey("a")}, + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 58, Byte: 57}, + }, + }, + ``, + }, { `module.foo.module.bar.data.aws_instance.baz["hello"]`, &Target{ diff --git a/addrs/resource.go b/addrs/resource.go index 94f3c3012..4bad01d15 100644 --- a/addrs/resource.go +++ b/addrs/resource.go @@ -253,6 +253,61 @@ func (r AbsResourceInstance) Less(o AbsResourceInstance) bool { } } +// ConfigResource is an address for a resource within a configuration. +type ConfigResource struct { + targetable + Module Module + Resource Resource +} + +// Resource returns the address of a particular resource within the module. +func (m Module) Resource(mode ResourceMode, typeName string, name string) ConfigResource { + return ConfigResource{ + Module: m, + Resource: Resource{ + Mode: mode, + Type: typeName, + Name: name, + }, + } +} + +// Absolute produces the address for the receiver within a specific module instance. +func (r ConfigResource) Absolute(module ModuleInstance) AbsResource { + return AbsResource{ + Module: module, + Resource: r.Resource, + } +} + +// TargetContains implements Targetable by returning true if the given other +// address is either equal to the receiver or is an instance of the +// receiver. +func (r ConfigResource) TargetContains(other Targetable) bool { + switch to := other.(type) { + case ConfigResource: + // We'll use our stringification as a cheat-ish way to test for equality. + return to.String() == r.String() + case AbsResource: + return r.TargetContains(ConfigResource{Module: to.Module.Module(), Resource: to.Resource}) + case AbsResourceInstance: + return r.TargetContains(to.ContainingResource()) + default: + return false + } +} + +func (r ConfigResource) String() string { + if len(r.Module) == 0 { + return r.Resource.String() + } + return fmt.Sprintf("%s.%s", r.Module.String(), r.Resource.String()) +} + +func (r ConfigResource) Equal(o ConfigResource) bool { + return r.String() == o.String() +} + // ResourceMode defines which lifecycle applies to a given resource. Each // resource lifecycle has a slightly different address format. type ResourceMode rune diff --git a/addrs/target_test.go b/addrs/target_test.go new file mode 100644 index 000000000..a16111560 --- /dev/null +++ b/addrs/target_test.go @@ -0,0 +1,164 @@ +package addrs + +import ( + "fmt" + "testing" +) + +func TestTargetContains(t *testing.T) { + for _, test := range []struct { + addr, other Targetable + expect bool + }{ + { + mustParseTarget("module.foo"), + mustParseTarget("module.bar"), + false, + }, + { + mustParseTarget("module.foo"), + mustParseTarget("module.foo"), + true, + }, + { + // module.foo is an unkeyed module instance here, so it cannot + // contain another instance + mustParseTarget("module.foo"), + mustParseTarget("module.foo[0]"), + false, + }, + { + RootModuleInstance, + mustParseTarget("module.foo"), + true, + }, + { + mustParseTarget("module.foo"), + RootModuleInstance, + false, + }, + { + mustParseTarget("module.foo"), + mustParseTarget("module.foo.module.bar[0]"), + true, + }, + { + mustParseTarget("module.foo"), + mustParseTarget("module.foo.module.bar[0]"), + true, + }, + { + mustParseTarget("module.foo[2]"), + mustParseTarget("module.foo[2].module.bar[0]"), + true, + }, + { + mustParseTarget("module.foo"), + mustParseTarget("module.foo.test_resource.bar"), + true, + }, + { + mustParseTarget("module.foo"), + mustParseTarget("module.foo.test_resource.bar[0]"), + true, + }, + + // Resources + { + mustParseTarget("test_resource.foo"), + mustParseTarget("test_resource.foo[\"bar\"]"), + true, + }, + { + mustParseTarget(`test_resource.foo["bar"]`), + mustParseTarget(`test_resource.foo["bar"]`), + true, + }, + { + mustParseTarget("test_resource.foo"), + mustParseTarget("test_resource.foo[2]"), + true, + }, + { + mustParseTarget("test_resource.foo"), + mustParseTarget("module.bar.test_resource.foo[2]"), + false, + }, + { + mustParseTarget("module.bar.test_resource.foo"), + mustParseTarget("module.bar.test_resource.foo[2]"), + true, + }, + { + mustParseTarget("module.bar.test_resource.foo"), + mustParseTarget("module.bar[0].test_resource.foo[2]"), + false, + }, + + // Config paths, while never returned from parsing a target, must still be targetable + { + ConfigResource{ + Module: []string{"bar"}, + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "test_resource", + Name: "foo", + }, + }, + mustParseTarget("module.bar.test_resource.foo[2]"), + true, + }, + { + ConfigResource{ + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "test_resource", + Name: "foo", + }, + }, + mustParseTarget("module.bar.test_resource.foo[2]"), + false, + }, + { + ConfigResource{ + Module: []string{"bar"}, + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "test_resource", + Name: "foo", + }, + }, + mustParseTarget("module.bar[0].test_resource.foo"), + true, + }, + } { + t.Run(fmt.Sprintf("%s-in-%s", test.other, test.addr), func(t *testing.T) { + got := test.addr.TargetContains(test.other) + if got != test.expect { + t.Fatalf("expected %q.TargetContains(%q) == %t", test.addr, test.other, test.expect) + } + }) + } +} + +func TestResourceContains(t *testing.T) { + for _, test := range []struct { + in, other Targetable + expect bool + }{} { + t.Run(fmt.Sprintf("%s-in-%s", test.other, test.in), func(t *testing.T) { + got := test.in.TargetContains(test.other) + if got != test.expect { + t.Fatalf("expected %q.TargetContains(%q) == %t", test.in, test.other, test.expect) + } + }) + } +} + +func mustParseTarget(str string) Targetable { + t, diags := ParseTargetStr(str) + if diags != nil { + panic(fmt.Sprintf("%s: %s", str, diags.ErrWithWarnings())) + } + return t.Subject +}