implement addrs.ConfigResource

Core needs a way to address resources through unexpanded modules, as
they are present in the configuration. There are already some cases of
paring `addrs.Module` with `addrs.Resource` for this purpose, but it is
going to be helpful to have a single type to describe that pair, as
well as have the ability to use TargetContains.
This commit is contained in:
James Bardin 2020-03-12 13:59:53 -04:00
parent 482ae66e18
commit 9054716caf
3 changed files with 264 additions and 0 deletions

View File

@ -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{

View File

@ -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

164
addrs/target_test.go Normal file
View File

@ -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
}