addrs: Module move support for AbsResource and AbsResourceInstance

This is a subset of the MoveDestination behavior for AbsResource and
AbsResourceInstance which deals with source and destination addresses that
refer to module calls or module instances.

They both work by delegating to ModuleInstance.MoveDestination and then
applying the same resource or resource instance address to the
newly-chosen module instance address, thus ensuring that when we move
a module we also move all of the resources inside that module in the same
way.

This doesn't yet include support for moving between specific resource or
resource instance addresses; that'll follow later. This commit should have
enough logic to support moving between module names and module instance
keys, including any module calls or resources nested within.
This commit is contained in:
Martin Atkins 2021-07-26 17:33:19 -07:00
parent 4d733b4d2d
commit 994ee23c06
2 changed files with 619 additions and 2 deletions

View File

@ -255,7 +255,24 @@ func (m ModuleInstance) MoveDestination(fromMatch, toMatch *MoveEndpointInModule
// Both of the given endpoints must be from the same move statement and thus
// must have matching object types. If not, MoveDestination will panic.
func (r AbsResource) MoveDestination(fromMatch, toMatch *MoveEndpointInModule) (AbsResource, bool) {
return AbsResource{}, false
switch fromMatch.ObjectKind() {
case MoveEndpointModule:
// If we've moving a module then any resource inside that module
// moves too.
fromMod := r.Module
toMod, match := fromMod.MoveDestination(fromMatch, toMatch)
if !match {
return AbsResource{}, false
}
return r.Resource.Absolute(toMod), true
case MoveEndpointResource:
// TODO: Implement
return AbsResource{}, false
default:
panic("unexpected object kind")
}
}
// MoveDestination considers a an address representing a resource
@ -270,5 +287,33 @@ func (r AbsResource) MoveDestination(fromMatch, toMatch *MoveEndpointInModule) (
// Both of the given endpoints must be from the same move statement and thus
// must have matching object types. If not, MoveDestination will panic.
func (r AbsResourceInstance) MoveDestination(fromMatch, toMatch *MoveEndpointInModule) (AbsResourceInstance, bool) {
return AbsResourceInstance{}, false
switch fromMatch.ObjectKind() {
case MoveEndpointModule:
// If we've moving a module then any resource inside that module
// moves too.
fromMod := r.Module
toMod, match := fromMod.MoveDestination(fromMatch, toMatch)
if !match {
return AbsResourceInstance{}, false
}
return r.Resource.Absolute(toMod), true
case MoveEndpointResource:
switch fromMatch.relSubject.(type) {
case AbsResource:
oldResource := r.ContainingResource()
newResource, match := oldResource.MoveDestination(fromMatch, toMatch)
if !match {
return AbsResourceInstance{}, false
}
return newResource.Instance(r.Resource.Key), true
case AbsResourceInstance:
// TODO: Implement
return AbsResourceInstance{}, false
default:
panic("invalid address type for resource-kind move endpoint")
}
default:
panic("unexpected object kind")
}
}

View File

@ -303,3 +303,575 @@ func TestModuleInstanceMoveDestination(t *testing.T) {
)
}
}
func TestAbsResourceInstanceMoveDestination(t *testing.T) {
tests := []struct {
DeclModule string
StmtFrom, StmtTo string
Reciever string
WantMatch bool
WantResult string
}{
{
``,
`module.foo`,
`module.bar`,
`module.foo.test_object.beep`,
true,
`module.bar.test_object.beep`,
},
{
``,
`module.foo`,
`module.bar`,
`module.foo[1].test_object.beep`,
true,
`module.bar[1].test_object.beep`,
},
{
``,
`module.foo`,
`module.bar`,
`module.foo["a"].test_object.beep`,
true,
`module.bar["a"].test_object.beep`,
},
{
``,
`module.foo`,
`module.bar.module.foo`,
`module.foo.test_object.beep`,
true,
`module.bar.module.foo.test_object.beep`,
},
{
``,
`module.foo.module.bar`,
`module.bar`,
`module.foo.module.bar.test_object.beep`,
true,
`module.bar.test_object.beep`,
},
{
``,
`module.foo[1]`,
`module.foo[2]`,
`module.foo[1].test_object.beep`,
true,
`module.foo[2].test_object.beep`,
},
{
``,
`module.foo[1]`,
`module.foo`,
`module.foo[1].test_object.beep`,
true,
`module.foo.test_object.beep`,
},
{
``,
`module.foo`,
`module.foo[1]`,
`module.foo.test_object.beep`,
true,
`module.foo[1].test_object.beep`,
},
{
``,
`module.foo`,
`module.foo[1]`,
`module.foo.module.bar.test_object.beep`,
true,
`module.foo[1].module.bar.test_object.beep`,
},
{
``,
`module.foo`,
`module.foo[1]`,
`module.foo.module.bar[0].test_object.beep`,
true,
`module.foo[1].module.bar[0].test_object.beep`,
},
{
``,
`module.foo`,
`module.bar.module.foo`,
`module.foo[0].test_object.beep`,
true,
`module.bar.module.foo[0].test_object.beep`,
},
{
``,
`module.foo.module.bar`,
`module.bar`,
`module.foo.module.bar[0].test_object.beep`,
true,
`module.bar[0].test_object.beep`,
},
{
`foo`,
`module.bar`,
`module.baz`,
`module.foo.module.bar.test_object.beep`,
true,
`module.foo.module.baz.test_object.beep`,
},
{
`foo`,
`module.bar`,
`module.baz`,
`module.foo[1].module.bar.test_object.beep`,
true,
`module.foo[1].module.baz.test_object.beep`,
},
{
`foo`,
`module.bar`,
`module.bar[1]`,
`module.foo[1].module.bar.test_object.beep`,
true,
`module.foo[1].module.bar[1].test_object.beep`,
},
{
``,
`module.foo[1]`,
`module.foo[2]`,
`module.foo.test_object.beep`,
false, // the receiver module has a non-matching instance key (NoKey)
``,
},
{
``,
`module.foo[1]`,
`module.foo[2]`,
`module.foo[2].test_object.beep`,
false, // the receiver is already at the "to" address
``,
},
{
`foo`,
`module.bar`,
`module.bar[1]`,
`module.boz.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
`foo.bar`,
`module.bar`,
`module.bar[1]`,
`module.boz.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
`foo.bar`,
`module.a`,
`module.b`,
`module.boz.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`module.a1.module.a2`,
`module.b1.module.b2`,
`module.c.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`module.a1.module.a2[0]`,
`module.b1.module.b2[1]`,
`module.c.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`module.a1.module.a2`,
`module.b1.module.b2`,
`module.a1.module.b2.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`module.a1.module.a2`,
`module.b1.module.b2`,
`module.b1.module.a2.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`module.a1.module.a2[0]`,
`module.b1.module.b2[1]`,
`module.a1.module.b2[0].test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`foo_instance.bar`,
`foo_instance.baz`,
`module.foo.test_object.beep`,
false, // the resource address is unrelated to the move statements
``,
},
}
for _, test := range tests {
t.Run(
fmt.Sprintf(
"%s: %s to %s with %s",
test.DeclModule,
test.StmtFrom, test.StmtTo,
test.Reciever,
),
func(t *testing.T) {
parseStmtEP := func(t *testing.T, input string) *MoveEndpoint {
t.Helper()
traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(input), "", hcl.InitialPos)
if hclDiags.HasErrors() {
// We're not trying to test the HCL parser here, so any
// failures at this point are likely to be bugs in the
// test case itself.
t.Fatalf("syntax error: %s", hclDiags.Error())
}
moveEp, diags := ParseMoveEndpoint(traversal)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err().Error())
}
return moveEp
}
fromEPLocal := parseStmtEP(t, test.StmtFrom)
toEPLocal := parseStmtEP(t, test.StmtTo)
declModule := RootModule
if test.DeclModule != "" {
declModule = strings.Split(test.DeclModule, ".")
}
fromEP, toEP := UnifyMoveEndpoints(declModule, fromEPLocal, toEPLocal)
if fromEP == nil || toEP == nil {
t.Fatalf("invalid test case: non-unifyable endpoints\nfrom: %s\nto: %s", fromEPLocal, toEPLocal)
}
receiverAddr, diags := ParseAbsResourceInstanceStr(test.Reciever)
if diags.HasErrors() {
t.Fatalf("invalid reciever address: %s", diags.Err().Error())
}
gotAddr, gotMatch := receiverAddr.MoveDestination(fromEP, toEP)
if !test.WantMatch {
if gotMatch {
t.Errorf("unexpected match\nreciever: %s\nfrom: %s\nto: %s\nresult: %s", test.Reciever, fromEP, toEP, gotAddr)
}
return
}
if !gotMatch {
t.Errorf("unexpected non-match\nreciever: %s\nfrom: %s\nto: %s", test.Reciever, fromEP, toEP)
}
if gotStr, wantStr := gotAddr.String(), test.WantResult; gotStr != wantStr {
t.Errorf("wrong result\ngot: %s\nwant: %s", gotStr, wantStr)
}
},
)
}
}
func TestAbsResourceMoveDestination(t *testing.T) {
tests := []struct {
DeclModule string
StmtFrom, StmtTo string
Reciever string
WantMatch bool
WantResult string
}{
{
``,
`module.foo`,
`module.bar`,
`module.foo.test_object.beep`,
true,
`module.bar.test_object.beep`,
},
{
``,
`module.foo`,
`module.bar`,
`module.foo[1].test_object.beep`,
true,
`module.bar[1].test_object.beep`,
},
{
``,
`module.foo`,
`module.bar`,
`module.foo["a"].test_object.beep`,
true,
`module.bar["a"].test_object.beep`,
},
{
``,
`module.foo`,
`module.bar.module.foo`,
`module.foo.test_object.beep`,
true,
`module.bar.module.foo.test_object.beep`,
},
{
``,
`module.foo.module.bar`,
`module.bar`,
`module.foo.module.bar.test_object.beep`,
true,
`module.bar.test_object.beep`,
},
{
``,
`module.foo[1]`,
`module.foo[2]`,
`module.foo[1].test_object.beep`,
true,
`module.foo[2].test_object.beep`,
},
{
``,
`module.foo[1]`,
`module.foo`,
`module.foo[1].test_object.beep`,
true,
`module.foo.test_object.beep`,
},
{
``,
`module.foo`,
`module.foo[1]`,
`module.foo.test_object.beep`,
true,
`module.foo[1].test_object.beep`,
},
{
``,
`module.foo`,
`module.foo[1]`,
`module.foo.module.bar.test_object.beep`,
true,
`module.foo[1].module.bar.test_object.beep`,
},
{
``,
`module.foo`,
`module.foo[1]`,
`module.foo.module.bar[0].test_object.beep`,
true,
`module.foo[1].module.bar[0].test_object.beep`,
},
{
``,
`module.foo`,
`module.bar.module.foo`,
`module.foo[0].test_object.beep`,
true,
`module.bar.module.foo[0].test_object.beep`,
},
{
``,
`module.foo.module.bar`,
`module.bar`,
`module.foo.module.bar[0].test_object.beep`,
true,
`module.bar[0].test_object.beep`,
},
{
`foo`,
`module.bar`,
`module.baz`,
`module.foo.module.bar.test_object.beep`,
true,
`module.foo.module.baz.test_object.beep`,
},
{
`foo`,
`module.bar`,
`module.baz`,
`module.foo[1].module.bar.test_object.beep`,
true,
`module.foo[1].module.baz.test_object.beep`,
},
{
`foo`,
`module.bar`,
`module.bar[1]`,
`module.foo[1].module.bar.test_object.beep`,
true,
`module.foo[1].module.bar[1].test_object.beep`,
},
{
``,
`module.foo[1]`,
`module.foo[2]`,
`module.foo.test_object.beep`,
false, // the receiver module has a non-matching instance key (NoKey)
``,
},
{
``,
`module.foo[1]`,
`module.foo[2]`,
`module.foo[2].test_object.beep`,
false, // the receiver is already at the "to" address
``,
},
{
`foo`,
`module.bar`,
`module.bar[1]`,
`module.boz.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
`foo.bar`,
`module.bar`,
`module.bar[1]`,
`module.boz.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
`foo.bar`,
`module.a`,
`module.b`,
`module.boz.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`module.a1.module.a2`,
`module.b1.module.b2`,
`module.c.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`module.a1.module.a2[0]`,
`module.b1.module.b2[1]`,
`module.c.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`module.a1.module.a2`,
`module.b1.module.b2`,
`module.a1.module.b2.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`module.a1.module.a2`,
`module.b1.module.b2`,
`module.b1.module.a2.test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`module.a1.module.a2[0]`,
`module.b1.module.b2[1]`,
`module.a1.module.b2[0].test_object.beep`,
false, // the receiver module is outside the declaration module
``,
},
{
``,
`foo_instance.bar`,
`foo_instance.baz`,
`module.foo.test_object.beep`,
false, // the resource address is unrelated to the move statements
``,
},
}
for _, test := range tests {
t.Run(
fmt.Sprintf(
"%s: %s to %s with %s",
test.DeclModule,
test.StmtFrom, test.StmtTo,
test.Reciever,
),
func(t *testing.T) {
parseStmtEP := func(t *testing.T, input string) *MoveEndpoint {
t.Helper()
traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(input), "", hcl.InitialPos)
if hclDiags.HasErrors() {
// We're not trying to test the HCL parser here, so any
// failures at this point are likely to be bugs in the
// test case itself.
t.Fatalf("syntax error: %s", hclDiags.Error())
}
moveEp, diags := ParseMoveEndpoint(traversal)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err().Error())
}
return moveEp
}
fromEPLocal := parseStmtEP(t, test.StmtFrom)
toEPLocal := parseStmtEP(t, test.StmtTo)
declModule := RootModule
if test.DeclModule != "" {
declModule = strings.Split(test.DeclModule, ".")
}
fromEP, toEP := UnifyMoveEndpoints(declModule, fromEPLocal, toEPLocal)
if fromEP == nil || toEP == nil {
t.Fatalf("invalid test case: non-unifyable endpoints\nfrom: %s\nto: %s", fromEPLocal, toEPLocal)
}
// We only have an AbsResourceInstance parser, not an
// AbsResourceParser, and so we'll just cheat and parse this
// as a resource instance but fail if it includes an instance
// key.
receiverInstanceAddr, diags := ParseAbsResourceInstanceStr(test.Reciever)
if diags.HasErrors() {
t.Fatalf("invalid reciever address: %s", diags.Err().Error())
}
if receiverInstanceAddr.Resource.Key != NoKey {
t.Fatalf("invalid reciever address: must be a resource, not a resource instance")
}
receiverAddr := receiverInstanceAddr.ContainingResource()
gotAddr, gotMatch := receiverAddr.MoveDestination(fromEP, toEP)
if !test.WantMatch {
if gotMatch {
t.Errorf("unexpected match\nreciever: %s\nfrom: %s\nto: %s\nresult: %s", test.Reciever, fromEP, toEP, gotAddr)
}
return
}
if !gotMatch {
t.Errorf("unexpected non-match\nreciever: %s\nfrom: %s\nto: %s", test.Reciever, fromEP, toEP)
}
if gotStr, wantStr := gotAddr.String(), test.WantResult; gotStr != wantStr {
t.Errorf("wrong result\ngot: %s\nwant: %s", gotStr, wantStr)
}
},
)
}
}