github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/refactoring/move_execute_test.go (about)

     1  package refactoring
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/davecgh/go-spew/spew"
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/hcl/v2/hclsyntax"
    13  	"github.com/hashicorp/terraform/internal/addrs"
    14  	"github.com/hashicorp/terraform/internal/states"
    15  )
    16  
    17  func TestApplyMoves(t *testing.T) {
    18  	providerAddr := addrs.AbsProviderConfig{
    19  		Module:   addrs.RootModule,
    20  		Provider: addrs.MustParseProviderSourceString("example.com/foo/bar"),
    21  	}
    22  
    23  	moduleBoo, _ := addrs.ParseModuleInstanceStr("module.boo")
    24  	moduleBarKey, _ := addrs.ParseModuleInstanceStr("module.bar[0]")
    25  
    26  	instAddrs := map[string]addrs.AbsResourceInstance{
    27  		"foo.from": addrs.Resource{
    28  			Mode: addrs.ManagedResourceMode,
    29  			Type: "foo",
    30  			Name: "from",
    31  		}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
    32  
    33  		"foo.mid": addrs.Resource{
    34  			Mode: addrs.ManagedResourceMode,
    35  			Type: "foo",
    36  			Name: "mid",
    37  		}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
    38  
    39  		"foo.to": addrs.Resource{
    40  			Mode: addrs.ManagedResourceMode,
    41  			Type: "foo",
    42  			Name: "to",
    43  		}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
    44  
    45  		"foo.from[0]": addrs.Resource{
    46  			Mode: addrs.ManagedResourceMode,
    47  			Type: "foo",
    48  			Name: "from",
    49  		}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
    50  
    51  		"foo.to[0]": addrs.Resource{
    52  			Mode: addrs.ManagedResourceMode,
    53  			Type: "foo",
    54  			Name: "to",
    55  		}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
    56  
    57  		"module.boo.foo.from": addrs.Resource{
    58  			Mode: addrs.ManagedResourceMode,
    59  			Type: "foo",
    60  			Name: "from",
    61  		}.Instance(addrs.NoKey).Absolute(moduleBoo),
    62  
    63  		"module.boo.foo.mid": addrs.Resource{
    64  			Mode: addrs.ManagedResourceMode,
    65  			Type: "foo",
    66  			Name: "mid",
    67  		}.Instance(addrs.NoKey).Absolute(moduleBoo),
    68  
    69  		"module.boo.foo.to": addrs.Resource{
    70  			Mode: addrs.ManagedResourceMode,
    71  			Type: "foo",
    72  			Name: "to",
    73  		}.Instance(addrs.NoKey).Absolute(moduleBoo),
    74  
    75  		"module.boo.foo.from[0]": addrs.Resource{
    76  			Mode: addrs.ManagedResourceMode,
    77  			Type: "foo",
    78  			Name: "from",
    79  		}.Instance(addrs.IntKey(0)).Absolute(moduleBoo),
    80  
    81  		"module.boo.foo.to[0]": addrs.Resource{
    82  			Mode: addrs.ManagedResourceMode,
    83  			Type: "foo",
    84  			Name: "to",
    85  		}.Instance(addrs.IntKey(0)).Absolute(moduleBoo),
    86  
    87  		"module.bar[0].foo.from": addrs.Resource{
    88  			Mode: addrs.ManagedResourceMode,
    89  			Type: "foo",
    90  			Name: "from",
    91  		}.Instance(addrs.NoKey).Absolute(moduleBarKey),
    92  
    93  		"module.bar[0].foo.mid": addrs.Resource{
    94  			Mode: addrs.ManagedResourceMode,
    95  			Type: "foo",
    96  			Name: "mid",
    97  		}.Instance(addrs.NoKey).Absolute(moduleBarKey),
    98  
    99  		"module.bar[0].foo.to": addrs.Resource{
   100  			Mode: addrs.ManagedResourceMode,
   101  			Type: "foo",
   102  			Name: "to",
   103  		}.Instance(addrs.NoKey).Absolute(moduleBarKey),
   104  
   105  		"module.bar[0].foo.from[0]": addrs.Resource{
   106  			Mode: addrs.ManagedResourceMode,
   107  			Type: "foo",
   108  			Name: "from",
   109  		}.Instance(addrs.IntKey(0)).Absolute(moduleBarKey),
   110  
   111  		"module.bar[0].foo.to[0]": addrs.Resource{
   112  			Mode: addrs.ManagedResourceMode,
   113  			Type: "foo",
   114  			Name: "to",
   115  		}.Instance(addrs.IntKey(0)).Absolute(moduleBarKey),
   116  	}
   117  
   118  	emptyResults := map[addrs.UniqueKey]MoveResult{}
   119  
   120  	tests := map[string]struct {
   121  		Stmts []MoveStatement
   122  		State *states.State
   123  
   124  		WantResults       map[addrs.UniqueKey]MoveResult
   125  		WantInstanceAddrs []string
   126  	}{
   127  		"no moves and empty state": {
   128  			[]MoveStatement{},
   129  			states.NewState(),
   130  			emptyResults,
   131  			nil,
   132  		},
   133  		"no moves": {
   134  			[]MoveStatement{},
   135  			states.BuildState(func(s *states.SyncState) {
   136  				s.SetResourceInstanceCurrent(
   137  					instAddrs["foo.from"],
   138  					&states.ResourceInstanceObjectSrc{
   139  						Status:    states.ObjectReady,
   140  						AttrsJSON: []byte(`{}`),
   141  					},
   142  					providerAddr,
   143  				)
   144  			}),
   145  			emptyResults,
   146  			[]string{
   147  				`foo.from`,
   148  			},
   149  		},
   150  		"single move of whole singleton resource": {
   151  			[]MoveStatement{
   152  				testMoveStatement(t, "", "foo.from", "foo.to"),
   153  			},
   154  			states.BuildState(func(s *states.SyncState) {
   155  				s.SetResourceInstanceCurrent(
   156  					instAddrs["foo.from"],
   157  					&states.ResourceInstanceObjectSrc{
   158  						Status:    states.ObjectReady,
   159  						AttrsJSON: []byte(`{}`),
   160  					},
   161  					providerAddr,
   162  				)
   163  			}),
   164  			map[addrs.UniqueKey]MoveResult{
   165  				instAddrs["foo.from"].UniqueKey(): {
   166  					From: instAddrs["foo.from"],
   167  					To:   instAddrs["foo.to"],
   168  				},
   169  				instAddrs["foo.to"].UniqueKey(): {
   170  					From: instAddrs["foo.from"],
   171  					To:   instAddrs["foo.to"],
   172  				},
   173  			},
   174  			[]string{
   175  				`foo.to`,
   176  			},
   177  		},
   178  		"single move of whole 'count' resource": {
   179  			[]MoveStatement{
   180  				testMoveStatement(t, "", "foo.from", "foo.to"),
   181  			},
   182  			states.BuildState(func(s *states.SyncState) {
   183  				s.SetResourceInstanceCurrent(
   184  					instAddrs["foo.from[0]"],
   185  					&states.ResourceInstanceObjectSrc{
   186  						Status:    states.ObjectReady,
   187  						AttrsJSON: []byte(`{}`),
   188  					},
   189  					providerAddr,
   190  				)
   191  			}),
   192  			map[addrs.UniqueKey]MoveResult{
   193  				instAddrs["foo.from[0]"].UniqueKey(): {
   194  					From: instAddrs["foo.from[0]"],
   195  					To:   instAddrs["foo.to[0]"],
   196  				},
   197  				instAddrs["foo.to[0]"].UniqueKey(): {
   198  					From: instAddrs["foo.from[0]"],
   199  					To:   instAddrs["foo.to[0]"],
   200  				},
   201  			},
   202  			[]string{
   203  				`foo.to[0]`,
   204  			},
   205  		},
   206  		"chained move of whole singleton resource": {
   207  			[]MoveStatement{
   208  				testMoveStatement(t, "", "foo.from", "foo.mid"),
   209  				testMoveStatement(t, "", "foo.mid", "foo.to"),
   210  			},
   211  			states.BuildState(func(s *states.SyncState) {
   212  				s.SetResourceInstanceCurrent(
   213  					instAddrs["foo.from"],
   214  					&states.ResourceInstanceObjectSrc{
   215  						Status:    states.ObjectReady,
   216  						AttrsJSON: []byte(`{}`),
   217  					},
   218  					providerAddr,
   219  				)
   220  			}),
   221  			map[addrs.UniqueKey]MoveResult{
   222  				instAddrs["foo.from"].UniqueKey(): {
   223  					From: instAddrs["foo.from"],
   224  					To:   instAddrs["foo.mid"],
   225  				},
   226  				instAddrs["foo.mid"].UniqueKey(): {
   227  					From: instAddrs["foo.mid"],
   228  					To:   instAddrs["foo.to"],
   229  				},
   230  				instAddrs["foo.to"].UniqueKey(): {
   231  					From: instAddrs["foo.mid"],
   232  					To:   instAddrs["foo.to"],
   233  				},
   234  			},
   235  			[]string{
   236  				`foo.to`,
   237  			},
   238  		},
   239  
   240  		"move whole resource into module": {
   241  			[]MoveStatement{
   242  				testMoveStatement(t, "", "foo.from", "module.boo.foo.to"),
   243  			},
   244  			states.BuildState(func(s *states.SyncState) {
   245  				s.SetResourceInstanceCurrent(
   246  					instAddrs["foo.from[0]"],
   247  					&states.ResourceInstanceObjectSrc{
   248  						Status:    states.ObjectReady,
   249  						AttrsJSON: []byte(`{}`),
   250  					},
   251  					providerAddr,
   252  				)
   253  			}),
   254  			map[addrs.UniqueKey]MoveResult{
   255  				instAddrs["foo.from[0]"].UniqueKey(): {
   256  					From: instAddrs["foo.from[0]"],
   257  					To:   instAddrs["module.boo.foo.to[0]"],
   258  				},
   259  				instAddrs["module.boo.foo.to[0]"].UniqueKey(): {
   260  					From: instAddrs["foo.from[0]"],
   261  					To:   instAddrs["module.boo.foo.to[0]"],
   262  				},
   263  			},
   264  			[]string{
   265  				`module.boo.foo.to[0]`,
   266  			},
   267  		},
   268  
   269  		"move resource instance between modules": {
   270  			[]MoveStatement{
   271  				testMoveStatement(t, "", "module.boo.foo.from[0]", "module.bar[0].foo.to[0]"),
   272  			},
   273  			states.BuildState(func(s *states.SyncState) {
   274  				s.SetResourceInstanceCurrent(
   275  					instAddrs["module.boo.foo.from[0]"],
   276  					&states.ResourceInstanceObjectSrc{
   277  						Status:    states.ObjectReady,
   278  						AttrsJSON: []byte(`{}`),
   279  					},
   280  					providerAddr,
   281  				)
   282  			}),
   283  			map[addrs.UniqueKey]MoveResult{
   284  				instAddrs["module.boo.foo.from[0]"].UniqueKey(): {
   285  					From: instAddrs["module.boo.foo.from[0]"],
   286  					To:   instAddrs["module.bar[0].foo.to[0]"],
   287  				},
   288  				instAddrs["module.bar[0].foo.to[0]"].UniqueKey(): {
   289  					From: instAddrs["module.boo.foo.from[0]"],
   290  					To:   instAddrs["module.bar[0].foo.to[0]"],
   291  				},
   292  			},
   293  			[]string{
   294  				`module.bar[0].foo.to[0]`,
   295  			},
   296  		},
   297  
   298  		"move whole single module to indexed module": {
   299  			[]MoveStatement{
   300  				testMoveStatement(t, "", "module.boo", "module.bar[0]"),
   301  			},
   302  			states.BuildState(func(s *states.SyncState) {
   303  				s.SetResourceInstanceCurrent(
   304  					instAddrs["module.boo.foo.from[0]"],
   305  					&states.ResourceInstanceObjectSrc{
   306  						Status:    states.ObjectReady,
   307  						AttrsJSON: []byte(`{}`),
   308  					},
   309  					providerAddr,
   310  				)
   311  			}),
   312  			map[addrs.UniqueKey]MoveResult{
   313  				instAddrs["module.boo.foo.from[0]"].UniqueKey(): {
   314  					From: instAddrs["module.boo.foo.from[0]"],
   315  					To:   instAddrs["module.bar[0].foo.from[0]"],
   316  				},
   317  				instAddrs["module.bar[0].foo.from[0]"].UniqueKey(): {
   318  					From: instAddrs["module.boo.foo.from[0]"],
   319  					To:   instAddrs["module.bar[0].foo.from[0]"],
   320  				},
   321  			},
   322  			[]string{
   323  				`module.bar[0].foo.from[0]`,
   324  			},
   325  		},
   326  
   327  		"move whole module to indexed module and move instance chained": {
   328  			[]MoveStatement{
   329  				testMoveStatement(t, "", "module.boo", "module.bar[0]"),
   330  				testMoveStatement(t, "bar", "foo.from[0]", "foo.to[0]"),
   331  			},
   332  			states.BuildState(func(s *states.SyncState) {
   333  				s.SetResourceInstanceCurrent(
   334  					instAddrs["module.boo.foo.from[0]"],
   335  					&states.ResourceInstanceObjectSrc{
   336  						Status:    states.ObjectReady,
   337  						AttrsJSON: []byte(`{}`),
   338  					},
   339  					providerAddr,
   340  				)
   341  			}),
   342  			map[addrs.UniqueKey]MoveResult{
   343  				instAddrs["module.boo.foo.from[0]"].UniqueKey(): {
   344  					From: instAddrs["module.boo.foo.from[0]"],
   345  					To:   instAddrs["module.bar[0].foo.from[0]"],
   346  				},
   347  				instAddrs["module.bar[0].foo.from[0]"].UniqueKey(): {
   348  					From: instAddrs["module.bar[0].foo.from[0]"],
   349  					To:   instAddrs["module.bar[0].foo.to[0]"],
   350  				},
   351  				instAddrs["module.bar[0].foo.to[0]"].UniqueKey(): {
   352  					From: instAddrs["module.bar[0].foo.from[0]"],
   353  					To:   instAddrs["module.bar[0].foo.to[0]"],
   354  				},
   355  			},
   356  			[]string{
   357  				`module.bar[0].foo.to[0]`,
   358  			},
   359  		},
   360  
   361  		"move instance to indexed module and instance chained": {
   362  			[]MoveStatement{
   363  				testMoveStatement(t, "", "module.boo.foo.from[0]", "module.bar[0].foo.from[0]"),
   364  				testMoveStatement(t, "bar", "foo.from[0]", "foo.to[0]"),
   365  			},
   366  			states.BuildState(func(s *states.SyncState) {
   367  				s.SetResourceInstanceCurrent(
   368  					instAddrs["module.boo.foo.from[0]"],
   369  					&states.ResourceInstanceObjectSrc{
   370  						Status:    states.ObjectReady,
   371  						AttrsJSON: []byte(`{}`),
   372  					},
   373  					providerAddr,
   374  				)
   375  			}),
   376  			map[addrs.UniqueKey]MoveResult{
   377  				instAddrs["module.boo.foo.from[0]"].UniqueKey(): {
   378  					From: instAddrs["module.boo.foo.from[0]"],
   379  					To:   instAddrs["module.bar[0].foo.from[0]"],
   380  				},
   381  				instAddrs["module.bar[0].foo.from[0]"].UniqueKey(): {
   382  					From: instAddrs["module.bar[0].foo.from[0]"],
   383  					To:   instAddrs["module.bar[0].foo.to[0]"],
   384  				},
   385  				instAddrs["module.bar[0].foo.to[0]"].UniqueKey(): {
   386  					From: instAddrs["module.bar[0].foo.from[0]"],
   387  					To:   instAddrs["module.bar[0].foo.to[0]"],
   388  				},
   389  			},
   390  			[]string{
   391  				`module.bar[0].foo.to[0]`,
   392  			},
   393  		},
   394  	}
   395  
   396  	for name, test := range tests {
   397  		t.Run(name, func(t *testing.T) {
   398  			var stmtsBuf strings.Builder
   399  			for _, stmt := range test.Stmts {
   400  				fmt.Fprintf(&stmtsBuf, "- from: %s\n  to:   %s\n", stmt.From, stmt.To)
   401  			}
   402  			t.Logf("move statements:\n%s", stmtsBuf.String())
   403  
   404  			t.Logf("resource instances in prior state:\n%s", spew.Sdump(allResourceInstanceAddrsInState(test.State)))
   405  
   406  			state := test.State.DeepCopy() // don't modify the test case in-place
   407  			gotResults := ApplyMoves(test.Stmts, state)
   408  
   409  			if diff := cmp.Diff(test.WantResults, gotResults); diff != "" {
   410  				t.Errorf("wrong results\n%s", diff)
   411  			}
   412  
   413  			gotInstAddrs := allResourceInstanceAddrsInState(state)
   414  			if diff := cmp.Diff(test.WantInstanceAddrs, gotInstAddrs); diff != "" {
   415  				t.Errorf("wrong resource instances in final state\n%s", diff)
   416  			}
   417  		})
   418  	}
   419  }
   420  
   421  func testMoveStatement(t *testing.T, module string, from string, to string) MoveStatement {
   422  	t.Helper()
   423  
   424  	moduleAddr := addrs.RootModule
   425  	if len(module) != 0 {
   426  		moduleAddr = addrs.Module(strings.Split(module, "."))
   427  	}
   428  
   429  	fromTraversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(from), "from", hcl.InitialPos)
   430  	if hclDiags.HasErrors() {
   431  		t.Fatalf("invalid 'from' argument: %s", hclDiags.Error())
   432  	}
   433  	fromAddr, diags := addrs.ParseMoveEndpoint(fromTraversal)
   434  	if diags.HasErrors() {
   435  		t.Fatalf("invalid 'from' argument: %s", diags.Err().Error())
   436  	}
   437  	toTraversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(to), "to", hcl.InitialPos)
   438  	if diags.HasErrors() {
   439  		t.Fatalf("invalid 'to' argument: %s", hclDiags.Error())
   440  	}
   441  	toAddr, diags := addrs.ParseMoveEndpoint(toTraversal)
   442  	if diags.HasErrors() {
   443  		t.Fatalf("invalid 'from' argument: %s", diags.Err().Error())
   444  	}
   445  
   446  	fromInModule, toInModule := addrs.UnifyMoveEndpoints(moduleAddr, fromAddr, toAddr)
   447  	if fromInModule == nil || toInModule == nil {
   448  		t.Fatalf("incompatible endpoints")
   449  	}
   450  
   451  	return MoveStatement{
   452  		From: fromInModule,
   453  		To:   toInModule,
   454  
   455  		// DeclRange not populated because it's unimportant for our tests
   456  	}
   457  }
   458  
   459  func allResourceInstanceAddrsInState(state *states.State) []string {
   460  	var ret []string
   461  	for _, ms := range state.Modules {
   462  		for _, rs := range ms.Resources {
   463  			for key := range rs.Instances {
   464  				ret = append(ret, rs.Addr.Instance(key).String())
   465  			}
   466  		}
   467  	}
   468  	sort.Strings(ret)
   469  	return ret
   470  }