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 }