addrs: Implement ModuleInstance.MoveDestination

This method encapsulates the move-processing rules for applying move
statements to ModuleInstance addresses. It honors both module call moves
and module instance moves by trying to find a subsequence of the input
that matches the "from" endpoint and then, if found, replacing it with
the "to" endpoint while preserving the prefix and suffix around the match,
if any.
This commit is contained in:
Martin Atkins 2021-07-26 16:05:05 -07:00
parent 2e82e55268
commit 4d733b4d2d
2 changed files with 389 additions and 1 deletions

View File

@ -157,7 +157,90 @@ func (e *MoveEndpointInModule) NestedWithin(other *MoveEndpointInModule) bool {
// 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 (m ModuleInstance) MoveDestination(fromMatch, toMatch *MoveEndpointInModule) (ModuleInstance, bool) {
// NOTE: This implementation assumes the invariant that fromMatch and
// toMatch both belong to the same configuration statement, and thus they
// will both have the same address type and the same declaration module.
// The root module instance is not itself moveable.
if m.IsRoot() {
return nil, false
}
// The two endpoints must either be module call or module instance
// addresses, or else this statement can never match.
if fromMatch.ObjectKind() != MoveEndpointModule {
return nil, false
}
// The given module instance must have a prefix that matches the
// declaration module of the two endpoints.
if len(fromMatch.module) > len(m) {
return nil, false // too short to possibly match
}
for i := range fromMatch.module {
if fromMatch.module[i] != m[i].Name {
return nil, false // this step doesn't match
}
}
// The rest of our work will be against the part of the reciever that's
// relative to the declaration module. mRel is a weird abuse of
// ModuleInstance that represents a relative module address, similar to
// what we do for MoveEndpointInModule.relSubject.
mPrefix, mRel := m[:len(fromMatch.module)], m[len(fromMatch.module):]
// Our next goal is to split mRel into two parts: the match (if any) and
// the suffix. Our result will then replace the match with the replacement
// in toMatch while preserving the prefix and suffix.
var mSuffix, mNewMatch ModuleInstance
switch relSubject := fromMatch.relSubject.(type) {
case ModuleInstance:
if len(relSubject) > len(mRel) {
return nil, false // too short to possibly match
}
for i := range relSubject {
if relSubject[i] != mRel[i] {
return nil, false // this step doesn't match
}
}
// If we get to here then we've found a match. Since the statement
// addresses are already themselves ModuleInstance fragments we can
// just slice out the relevant parts.
mNewMatch = toMatch.relSubject.(ModuleInstance)
mSuffix = mRel[len(relSubject):]
case AbsModuleCall:
// The module instance part of relSubject must be a prefix of
// mRel, and mRel must be at least one step longer to account for
// the call step itself.
if len(relSubject.Module) > len(mRel)-1 {
return nil, false
}
for i := range relSubject.Module {
if relSubject.Module[i] != mRel[i] {
return nil, false // this step doesn't match
}
}
// The call name must also match the next step of mRel, after
// the relSubject.Module prefix.
callStep := mRel[len(relSubject.Module)]
if callStep.Name != relSubject.Call.Name {
return nil, false
}
// If we get to here then we've found a match. We need to construct
// a new mNewMatch that's an instance of the "new" relSubject with
// the same key as our call.
mNewMatch = toMatch.relSubject.(AbsModuleCall).Instance(callStep.InstanceKey)
mSuffix = mRel[len(relSubject.Module)+1:]
default:
panic("invalid address type for module-kind move endpoint")
}
ret := make(ModuleInstance, 0, len(mPrefix)+len(mNewMatch)+len(mSuffix))
ret = append(ret, mPrefix...)
ret = append(ret, mNewMatch...)
ret = append(ret, mSuffix...)
return ret, true
}
// MoveDestination considers a an address representing a resource

View File

@ -0,0 +1,305 @@
package addrs
import (
"fmt"
"strings"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestModuleInstanceMoveDestination(t *testing.T) {
tests := []struct {
DeclModule string
StmtFrom, StmtTo string
Reciever string
WantMatch bool
WantResult string
}{
{
``,
`module.foo`,
`module.bar`,
`module.foo`,
true,
`module.bar`,
},
{
``,
`module.foo`,
`module.bar`,
`module.foo[1]`,
true,
`module.bar[1]`,
},
{
``,
`module.foo`,
`module.bar`,
`module.foo["a"]`,
true,
`module.bar["a"]`,
},
{
``,
`module.foo`,
`module.bar.module.foo`,
`module.foo`,
true,
`module.bar.module.foo`,
},
{
``,
`module.foo.module.bar`,
`module.bar`,
`module.foo.module.bar`,
true,
`module.bar`,
},
{
``,
`module.foo[1]`,
`module.foo[2]`,
`module.foo[1]`,
true,
`module.foo[2]`,
},
{
``,
`module.foo[1]`,
`module.foo`,
`module.foo[1]`,
true,
`module.foo`,
},
{
``,
`module.foo`,
`module.foo[1]`,
`module.foo`,
true,
`module.foo[1]`,
},
{
``,
`module.foo`,
`module.foo[1]`,
`module.foo.module.bar`,
true,
`module.foo[1].module.bar`,
},
{
``,
`module.foo`,
`module.foo[1]`,
`module.foo.module.bar[0]`,
true,
`module.foo[1].module.bar[0]`,
},
{
``,
`module.foo`,
`module.bar.module.foo`,
`module.foo[0]`,
true,
`module.bar.module.foo[0]`,
},
{
``,
`module.foo.module.bar`,
`module.bar`,
`module.foo.module.bar[0]`,
true,
`module.bar[0]`,
},
{
`foo`,
`module.bar`,
`module.baz`,
`module.foo.module.bar`,
true,
`module.foo.module.baz`,
},
{
`foo`,
`module.bar`,
`module.baz`,
`module.foo[1].module.bar`,
true,
`module.foo[1].module.baz`,
},
{
`foo`,
`module.bar`,
`module.bar[1]`,
`module.foo[1].module.bar`,
true,
`module.foo[1].module.bar[1]`,
},
{
``,
`module.foo[1]`,
`module.foo[2]`,
`module.foo`,
false, // the receiver has a non-matching instance key (NoKey)
``,
},
{
``,
`module.foo[1]`,
`module.foo[2]`,
`module.foo[2]`,
false, // the receiver is already the "to" address
``,
},
{
``,
`module.foo`,
`module.bar`,
``,
false, // the root module can never be moved
``,
},
{
`foo`,
`module.bar`,
`module.bar[1]`,
`module.boz`,
false, // the receiver is outside the declaration module
``,
},
{
`foo.bar`,
`module.bar`,
`module.bar[1]`,
`module.boz`,
false, // the receiver is outside the declaration module
``,
},
{
`foo.bar`,
`module.a`,
`module.b`,
`module.boz`,
false, // the receiver is outside the declaration module
``,
},
{
``,
`module.a1.module.a2`,
`module.b1.module.b2`,
`module.c`,
false, // the receiver is outside the declaration module
``,
},
{
``,
`module.a1.module.a2[0]`,
`module.b1.module.b2[1]`,
`module.c`,
false, // the receiver is outside the declaration module
``,
},
{
``,
`module.a1.module.a2`,
`module.b1.module.b2`,
`module.a1.module.b2`,
false, // the receiver is outside the declaration module
``,
},
{
``,
`module.a1.module.a2`,
`module.b1.module.b2`,
`module.b1.module.a2`,
false, // the receiver is outside the declaration module
``,
},
{
``,
`module.a1.module.a2[0]`,
`module.b1.module.b2[1]`,
`module.a1.module.b2[0]`,
false, // the receiver is outside the declaration module
``,
},
{
``,
`foo_instance.bar`,
`foo_instance.baz`,
`module.foo`,
false, // a resource address can never match a module instance
``,
},
}
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 := RootModuleInstance
if test.Reciever != "" {
var diags tfdiags.Diagnostics
receiverAddr, diags = ParseModuleInstanceStr(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)
}
},
)
}
}