package refactoring import ( "sort" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) func TestImpliedMoveStatements(t *testing.T) { resourceAddr := func(name string) addrs.AbsResource { return addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "foo", Name: name, }.Absolute(addrs.RootModuleInstance) } nestedResourceAddr := func(mod, name string) addrs.AbsResource { return addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "foo", Name: name, }.Absolute(addrs.RootModuleInstance.Child(mod, addrs.NoKey)) } instObjState := func() *states.ResourceInstanceObjectSrc { return &states.ResourceInstanceObjectSrc{} } providerAddr := addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/foo"), } rootCfg, _ := loadRefactoringFixture(t, "testdata/move-statement-implied") prevRunState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( resourceAddr("formerly_count").Instance(addrs.IntKey(0)), instObjState(), providerAddr, ) s.SetResourceInstanceCurrent( resourceAddr("formerly_count").Instance(addrs.IntKey(1)), instObjState(), providerAddr, ) s.SetResourceInstanceCurrent( resourceAddr("now_count").Instance(addrs.NoKey), instObjState(), providerAddr, ) s.SetResourceInstanceCurrent( resourceAddr("formerly_count_explicit").Instance(addrs.IntKey(0)), instObjState(), providerAddr, ) s.SetResourceInstanceCurrent( resourceAddr("formerly_count_explicit").Instance(addrs.IntKey(1)), instObjState(), providerAddr, ) s.SetResourceInstanceCurrent( resourceAddr("now_count_explicit").Instance(addrs.NoKey), instObjState(), providerAddr, ) s.SetResourceInstanceCurrent( resourceAddr("now_for_each_formerly_count").Instance(addrs.IntKey(0)), instObjState(), providerAddr, ) s.SetResourceInstanceCurrent( resourceAddr("now_for_each_formerly_no_count").Instance(addrs.NoKey), instObjState(), providerAddr, ) // This "ambiguous" resource is representing a rare but possible // situation where we end up having a mixture of different index // types in the state at the same time. The main way to get into // this state would be to remove "count = 1" and then have the // provider fail to destroy the zero-key instance even though we // already created the no-key instance. Users can also get here // by using "terraform state mv" in weird ways. s.SetResourceInstanceCurrent( resourceAddr("ambiguous").Instance(addrs.NoKey), instObjState(), providerAddr, ) s.SetResourceInstanceCurrent( resourceAddr("ambiguous").Instance(addrs.IntKey(0)), instObjState(), providerAddr, ) // Add two resource nested in a module to ensure we find these // recursively. s.SetResourceInstanceCurrent( nestedResourceAddr("child", "formerly_count").Instance(addrs.IntKey(0)), instObjState(), providerAddr, ) s.SetResourceInstanceCurrent( nestedResourceAddr("child", "now_count").Instance(addrs.NoKey), instObjState(), providerAddr, ) }) explicitStmts := FindMoveStatements(rootCfg) got := ImpliedMoveStatements(rootCfg, prevRunState, explicitStmts) want := []MoveStatement{ { From: addrs.ImpliedMoveStatementEndpoint(resourceAddr("formerly_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), To: addrs.ImpliedMoveStatementEndpoint(resourceAddr("formerly_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), Implied: true, DeclRange: tfdiags.SourceRange{ Filename: "testdata/move-statement-implied/move-statement-implied.tf", Start: tfdiags.SourcePos{Line: 5, Column: 1, Byte: 180}, End: tfdiags.SourcePos{Line: 5, Column: 32, Byte: 211}, }, }, // Found implied moves in a nested module, ignoring the explicit moves { From: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "formerly_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), To: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "formerly_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), Implied: true, DeclRange: tfdiags.SourceRange{ Filename: "testdata/move-statement-implied/child/move-statement-implied.tf", Start: tfdiags.SourcePos{Line: 5, Column: 1, Byte: 180}, End: tfdiags.SourcePos{Line: 5, Column: 32, Byte: 211}, }, }, { From: addrs.ImpliedMoveStatementEndpoint(resourceAddr("now_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), To: addrs.ImpliedMoveStatementEndpoint(resourceAddr("now_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), Implied: true, DeclRange: tfdiags.SourceRange{ Filename: "testdata/move-statement-implied/move-statement-implied.tf", Start: tfdiags.SourcePos{Line: 10, Column: 11, Byte: 282}, End: tfdiags.SourcePos{Line: 10, Column: 12, Byte: 283}, }, }, // Found implied moves in a nested module, ignoring the explicit moves { From: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "now_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), To: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "now_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), Implied: true, DeclRange: tfdiags.SourceRange{ Filename: "testdata/move-statement-implied/child/move-statement-implied.tf", Start: tfdiags.SourcePos{Line: 10, Column: 11, Byte: 282}, End: tfdiags.SourcePos{Line: 10, Column: 12, Byte: 283}, }, }, // We generate foo.ambiguous[0] to foo.ambiguous here, even though // there's already a foo.ambiguous in the state, because it's the // responsibility of the later ApplyMoves step to deal with the // situation where an object wants to move into an address already // occupied by another object. { From: addrs.ImpliedMoveStatementEndpoint(resourceAddr("ambiguous").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), To: addrs.ImpliedMoveStatementEndpoint(resourceAddr("ambiguous").Instance(addrs.NoKey), tfdiags.SourceRange{}), Implied: true, DeclRange: tfdiags.SourceRange{ Filename: "testdata/move-statement-implied/move-statement-implied.tf", Start: tfdiags.SourcePos{Line: 46, Column: 1, Byte: 806}, End: tfdiags.SourcePos{Line: 46, Column: 27, Byte: 832}, }, }, } sort.Slice(got, func(i, j int) bool { // This is just an arbitrary sort to make the result consistent // regardless of what order the ImpliedMoveStatements function // visits the entries in the state/config. return got[i].DeclRange.Start.Line < got[j].DeclRange.Start.Line }) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("wrong result\n%s", diff) } }