diff --git a/internal/addrs/move_endpoint_module.go b/internal/addrs/move_endpoint_module.go index 2c7ad2276..f1de3b861 100644 --- a/internal/addrs/move_endpoint_module.go +++ b/internal/addrs/move_endpoint_module.go @@ -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") + } } diff --git a/internal/addrs/move_endpoint_module_test.go b/internal/addrs/move_endpoint_module_test.go index 981c50ee5..60f804436 100644 --- a/internal/addrs/move_endpoint_module_test.go +++ b/internal/addrs/move_endpoint_module_test.go @@ -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) + } + }, + ) + } +}