core: ResourceAddress.Contains method

This is similar in purpose to Equals but it takes a hierarchical approach
where modules contain their child modules, resources are contained by
their modules, and indexed resource instances are contained by their
resource names.

Unlike "Equals", Contains is intended to be transitive, so if A contains B
and B contains C, then C necessarily contains A. It is also directional:
if A contains B then B does not also contain A unless A and B are
identical. This results in more intuitive behavior for use-cases where
the goal is to select a portion of the address space for an operation.
This commit is contained in:
Martin Atkins 2017-06-15 16:59:20 -07:00
parent 9777174be1
commit d3eb2b2d28
2 changed files with 348 additions and 1 deletions

View File

@ -248,6 +248,53 @@ func ParseResourceAddress(s string) (*ResourceAddress, error) {
}, nil }, nil
} }
// Contains returns true if and only if the given node is contained within
// the receiver.
//
// Containment is defined in terms of the module and resource heirarchy:
// a resource is contained within its module and any ancestor modules,
// an indexed resource instance is contained with the unindexed resource, etc.
func (addr *ResourceAddress) Contains(other *ResourceAddress) bool {
ourPath := addr.Path
givenPath := other.Path
if len(givenPath) < len(ourPath) {
return false
}
for i := range ourPath {
if ourPath[i] != givenPath[i] {
return false
}
}
// If the receiver is a whole-module address then the path prefix
// matching is all we need.
if !addr.HasResourceSpec() {
return true
}
if addr.Type != other.Type || addr.Name != other.Name || addr.Mode != other.Mode {
return false
}
if addr.Index != -1 && addr.Index != other.Index {
return false
}
if addr.InstanceTypeSet && (addr.InstanceTypeSet != other.InstanceTypeSet || addr.InstanceType != other.InstanceType) {
return false
}
return true
}
// Equals returns true if the receiver matches the given address.
//
// The name of this method is a misnomer, since it doesn't test for exact
// equality. Instead, it tests that the _specified_ parts of each
// address match, treating any unspecified parts as wildcards.
//
// See also Contains, which takes a more heirarchical approach to comparing
// addresses.
func (addr *ResourceAddress) Equals(raw interface{}) bool { func (addr *ResourceAddress) Equals(raw interface{}) bool {
other, ok := raw.(*ResourceAddress) other, ok := raw.(*ResourceAddress)
if !ok { if !ok {
@ -324,7 +371,7 @@ func tokenizeResourceAddress(s string) (map[string]string, error) {
// string "aws_instance.web.tainted[1]" // string "aws_instance.web.tainted[1]"
re := regexp.MustCompile(`\A` + re := regexp.MustCompile(`\A` +
// "module.foo.module.bar" (optional) // "module.foo.module.bar" (optional)
`(?P<path>(?:module\.[^.]+\.?)*)` + `(?P<path>(?:module\.(?P<module_name>[^.]+)\.?)*)` +
// possibly "data.", if targeting is a data resource // possibly "data.", if targeting is a data resource
`(?P<data_prefix>(?:data\.)?)` + `(?P<data_prefix>(?:data\.)?)` +
// "aws_instance.web" (optional when module path specified) // "aws_instance.web" (optional when module path specified)

View File

@ -304,6 +304,306 @@ func TestParseResourceAddress(t *testing.T) {
} }
} }
func TestResourceAddressContains(t *testing.T) {
tests := []struct {
Address *ResourceAddress
Other *ResourceAddress
Want bool
}{
{
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: true,
InstanceType: TypePrimary,
Index: 0,
},
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: true,
InstanceType: TypePrimary,
Index: 0,
},
true,
},
{
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: 0,
},
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: true,
InstanceType: TypePrimary,
Index: 0,
},
true,
},
{
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: true,
InstanceType: TypePrimary,
Index: 0,
},
true,
},
{
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: -1,
},
true,
},
{
&ResourceAddress{
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: -1,
},
true,
},
{
&ResourceAddress{
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Path: []string{"bar"},
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: -1,
},
true,
},
{
&ResourceAddress{
Path: []string{"bar"},
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Path: []string{"bar"},
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: -1,
},
true,
},
{
&ResourceAddress{
Path: []string{"bar"},
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Path: []string{"bar", "baz"},
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: -1,
},
true,
},
{
&ResourceAddress{
Path: []string{"bar"},
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Path: []string{"bar", "baz"},
InstanceTypeSet: false,
Index: -1,
},
true,
},
{
&ResourceAddress{
Path: []string{"bar"},
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Path: []string{"bar", "baz", "foo", "pizza"},
InstanceTypeSet: false,
Index: -1,
},
true,
},
{
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
InstanceTypeSet: true,
InstanceType: TypePrimary,
Index: 0,
},
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: true,
InstanceType: TypePrimary,
Index: 0,
},
false,
},
{
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: true,
InstanceType: TypePrimary,
Index: 0,
},
&ResourceAddress{
Mode: config.DataResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: true,
InstanceType: TypePrimary,
Index: 0,
},
false,
},
{
&ResourceAddress{
Path: []string{"bar"},
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Path: []string{"baz"},
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: -1,
},
false,
},
{
&ResourceAddress{
Path: []string{"bar"},
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Path: []string{"baz", "bar"},
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: -1,
},
false,
},
{
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: true,
InstanceType: TypePrimary,
Index: 0,
},
&ResourceAddress{
Mode: config.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
InstanceTypeSet: false,
Index: 0,
},
false,
},
{
&ResourceAddress{
Path: []string{"bar", "baz"},
InstanceTypeSet: false,
Index: -1,
},
&ResourceAddress{
Path: []string{"bar"},
InstanceTypeSet: false,
Index: -1,
},
false,
},
{
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
Index: 1,
InstanceType: TypePrimary,
Mode: config.ManagedResourceMode,
},
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
Index: -1,
InstanceType: TypePrimary,
Mode: config.ManagedResourceMode,
},
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s contains %s", test.Address, test.Other), func(t *testing.T) {
got := test.Address.Contains(test.Other)
if got != test.Want {
t.Errorf(
"wrong result\nrecv: %s\ngiven: %s\ngot: %#v\nwant: %#v",
test.Address, test.Other,
got, test.Want,
)
}
})
}
}
func TestResourceAddressEquals(t *testing.T) { func TestResourceAddressEquals(t *testing.T) {
cases := map[string]struct { cases := map[string]struct {
Address *ResourceAddress Address *ResourceAddress