github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/refactoring/move_validate_test.go (about) 1 package refactoring 2 3 import ( 4 "strings" 5 "testing" 6 7 "github.com/hashicorp/hcl/v2" 8 "github.com/hashicorp/hcl/v2/hclsyntax" 9 "github.com/hashicorp/terraform/internal/addrs" 10 "github.com/hashicorp/terraform/internal/configs" 11 "github.com/hashicorp/terraform/internal/configs/configload" 12 "github.com/hashicorp/terraform/internal/initwd" 13 "github.com/hashicorp/terraform/internal/instances" 14 "github.com/hashicorp/terraform/internal/registry" 15 "github.com/hashicorp/terraform/internal/tfdiags" 16 "github.com/zclconf/go-cty/cty/gocty" 17 ) 18 19 func TestValidateMoves(t *testing.T) { 20 rootCfg, instances := loadRefactoringFixture(t, "testdata/move-validate-zoo") 21 22 tests := map[string]struct { 23 Statements []MoveStatement 24 WantError string 25 }{ 26 "no move statements": { 27 Statements: nil, 28 WantError: ``, 29 }, 30 "some valid statements": { 31 Statements: []MoveStatement{ 32 // This is just a grab bag of various valid cases that don't 33 // generate any errors at all. 34 makeTestMoveStmt(t, 35 ``, 36 `test.nonexist1`, 37 `test.target1`, 38 ), 39 makeTestMoveStmt(t, 40 `single`, 41 `test.nonexist1`, 42 `test.target1`, 43 ), 44 makeTestMoveStmt(t, 45 ``, 46 `test.nonexist2`, 47 `module.nonexist.test.nonexist2`, 48 ), 49 makeTestMoveStmt(t, 50 ``, 51 `module.single.test.nonexist3`, 52 `module.single.test.single`, 53 ), 54 makeTestMoveStmt(t, 55 ``, 56 `module.single.test.nonexist4`, 57 `test.target2`, 58 ), 59 makeTestMoveStmt(t, 60 ``, 61 `test.single[0]`, // valid because test.single doesn't have "count" set 62 `test.target3`, 63 ), 64 makeTestMoveStmt(t, 65 ``, 66 `test.zero_count[0]`, // valid because test.zero_count has count = 0 67 `test.target4`, 68 ), 69 makeTestMoveStmt(t, 70 ``, 71 `test.zero_count[1]`, // valid because test.zero_count has count = 0 72 `test.zero_count[0]`, 73 ), 74 makeTestMoveStmt(t, 75 ``, 76 `module.nonexist1`, 77 `module.target3`, 78 ), 79 makeTestMoveStmt(t, 80 ``, 81 `module.nonexist1[0]`, 82 `module.target4`, 83 ), 84 makeTestMoveStmt(t, 85 ``, 86 `module.single[0]`, // valid because module.single doesn't have "count" set 87 `module.target5`, 88 ), 89 makeTestMoveStmt(t, 90 ``, 91 `module.for_each["nonexist1"]`, 92 `module.for_each["a"]`, 93 ), 94 makeTestMoveStmt(t, 95 ``, 96 `module.for_each["nonexist2"]`, 97 `module.nonexist.module.nonexist`, 98 ), 99 makeTestMoveStmt(t, 100 ``, 101 `module.for_each["nonexist3"].test.single`, // valid because module.for_each doesn't currently have a "nonexist3" 102 `module.for_each["a"].test.single`, 103 ), 104 }, 105 WantError: ``, 106 }, 107 "two statements with the same endpoints": { 108 Statements: []MoveStatement{ 109 makeTestMoveStmt(t, 110 ``, 111 `module.a`, 112 `module.b`, 113 ), 114 makeTestMoveStmt(t, 115 ``, 116 `module.a`, 117 `module.b`, 118 ), 119 }, 120 WantError: ``, 121 }, 122 "moving nowhere": { 123 Statements: []MoveStatement{ 124 makeTestMoveStmt(t, 125 ``, 126 `module.a`, 127 `module.a`, 128 ), 129 }, 130 WantError: `Redundant move statement: This statement declares a move from module.a to the same address, which is the same as not declaring this move at all.`, 131 }, 132 /* 133 // TODO: This test can't pass until we've implemented 134 // addrs.MoveEndpointInModule.CanChainFrom, which is what 135 // detects the chaining condition this is testing for. 136 "cyclic chain": { 137 Statements: []MoveStatement{ 138 makeTestMoveStmt(t, 139 ``, 140 `module.a`, 141 `module.b`, 142 ), 143 makeTestMoveStmt(t, 144 ``, 145 `module.b`, 146 `module.c`, 147 ), 148 makeTestMoveStmt(t, 149 ``, 150 `module.c`, 151 `module.a`, 152 ), 153 }, 154 WantError: `bad cycle`, 155 }, 156 */ 157 "module.single as a call still exists in configuration": { 158 Statements: []MoveStatement{ 159 makeTestMoveStmt(t, 160 ``, 161 `module.single`, 162 `module.other`, 163 ), 164 }, 165 WantError: `Moved object still exists: This statement declares a move from module.single, but that module call is still declared at testdata/move-validate-zoo/move-validate-root.tf:6,1. 166 167 Change your configuration so that this call will be declared as module.other instead.`, 168 }, 169 "module.single as an instance still exists in configuration": { 170 Statements: []MoveStatement{ 171 makeTestMoveStmt(t, 172 ``, 173 `module.single`, 174 `module.other[0]`, 175 ), 176 }, 177 WantError: `Moved object still exists: This statement declares a move from module.single, but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:6,1. 178 179 Change your configuration so that this instance will be declared as module.other[0] instead.`, 180 }, 181 "module.count[0] still exists in configuration": { 182 Statements: []MoveStatement{ 183 makeTestMoveStmt(t, 184 ``, 185 `module.count[0]`, 186 `module.other`, 187 ), 188 }, 189 WantError: `Moved object still exists: This statement declares a move from module.count[0], but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:12,12. 190 191 Change your configuration so that this instance will be declared as module.other instead.`, 192 }, 193 `module.for_each["a"] still exists in configuration`: { 194 Statements: []MoveStatement{ 195 makeTestMoveStmt(t, 196 ``, 197 `module.for_each["a"]`, 198 `module.other`, 199 ), 200 }, 201 WantError: `Moved object still exists: This statement declares a move from module.for_each["a"], but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:22,14. 202 203 Change your configuration so that this instance will be declared as module.other instead.`, 204 }, 205 "test.single as a resource still exists in configuration": { 206 Statements: []MoveStatement{ 207 makeTestMoveStmt(t, 208 ``, 209 `test.single`, 210 `test.other`, 211 ), 212 }, 213 WantError: `Moved object still exists: This statement declares a move from test.single, but that resource is still declared at testdata/move-validate-zoo/move-validate-root.tf:27,1. 214 215 Change your configuration so that this resource will be declared as test.other instead.`, 216 }, 217 "test.single as an instance still exists in configuration": { 218 Statements: []MoveStatement{ 219 makeTestMoveStmt(t, 220 ``, 221 `test.single`, 222 `test.other[0]`, 223 ), 224 }, 225 WantError: `Moved object still exists: This statement declares a move from test.single, but that resource instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:27,1. 226 227 Change your configuration so that this instance will be declared as test.other[0] instead.`, 228 }, 229 "module.single.test.single as a resource still exists in configuration": { 230 Statements: []MoveStatement{ 231 makeTestMoveStmt(t, 232 ``, 233 `module.single.test.single`, 234 `test.other`, 235 ), 236 }, 237 WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1. 238 239 Change your configuration so that this resource will be declared as test.other instead.`, 240 }, 241 "module.single.test.single as a resource declared in module.single still exists in configuration": { 242 Statements: []MoveStatement{ 243 makeTestMoveStmt(t, 244 `single`, 245 `test.single`, 246 `test.other`, 247 ), 248 }, 249 WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1. 250 251 Change your configuration so that this resource will be declared as module.single.test.other instead.`, 252 }, 253 "module.single.test.single as an instance still exists in configuration": { 254 Statements: []MoveStatement{ 255 makeTestMoveStmt(t, 256 ``, 257 `module.single.test.single`, 258 `test.other[0]`, 259 ), 260 }, 261 WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource instance is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1. 262 263 Change your configuration so that this instance will be declared as test.other[0] instead.`, 264 }, 265 "module.count[0].test.single still exists in configuration": { 266 Statements: []MoveStatement{ 267 makeTestMoveStmt(t, 268 ``, 269 `module.count[0].test.single`, 270 `test.other`, 271 ), 272 }, 273 WantError: `Moved object still exists: This statement declares a move from module.count[0].test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1. 274 275 Change your configuration so that this resource will be declared as test.other instead.`, 276 }, 277 "two different moves from test.nonexist": { 278 Statements: []MoveStatement{ 279 makeTestMoveStmt(t, 280 ``, 281 `test.nonexist`, 282 `test.other1`, 283 ), 284 makeTestMoveStmt(t, 285 ``, 286 `test.nonexist`, 287 `test.other2`, 288 ), 289 }, 290 WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.nonexist moved to test.other1, but this statement instead declares that it moved to test.other2. 291 292 Each resource can move to only one destination resource.`, 293 }, 294 "two different moves to test.single": { 295 Statements: []MoveStatement{ 296 makeTestMoveStmt(t, 297 ``, 298 `test.other1`, 299 `test.single`, 300 ), 301 makeTestMoveStmt(t, 302 ``, 303 `test.other2`, 304 `test.single`, 305 ), 306 }, 307 WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.other1 moved to test.single, but this statement instead declares that test.other2 moved there. 308 309 Each resource can have moved from only one source resource.`, 310 }, 311 "two different moves to module.count[0].test.single across two modules": { 312 Statements: []MoveStatement{ 313 makeTestMoveStmt(t, 314 ``, 315 `test.other1`, 316 `module.count[0].test.single`, 317 ), 318 makeTestMoveStmt(t, 319 `count`, 320 `test.other2`, 321 `test.single`, 322 ), 323 }, 324 WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.other1 moved to module.count[0].test.single, but this statement instead declares that module.count[0].test.other2 moved there. 325 326 Each resource can have moved from only one source resource.`, 327 }, 328 /* 329 // FIXME: This rule requires a deeper analysis to understand that 330 // module.single already contains a test.single and thus moving 331 // it to module.foo implicitly also moves module.single.test.single 332 // module.foo.test.single. 333 "two different moves to nested test.single by different paths": { 334 Statements: []MoveStatement{ 335 makeTestMoveStmt(t, 336 ``, 337 `test.beep`, 338 `module.foo.test.single`, 339 ), 340 makeTestMoveStmt(t, 341 ``, 342 `module.single`, 343 `module.foo`, 344 ), 345 }, 346 WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.beep moved to module.foo.test.single, but this statement instead declares that module.single.test.single moved there. 347 348 Each resource can have moved from only one source resource.`, 349 }, 350 */ 351 "move from resource in another module package": { 352 Statements: []MoveStatement{ 353 makeTestMoveStmt(t, 354 ``, 355 `module.fake_external.test.thing`, 356 `test.thing`, 357 ), 358 }, 359 WantError: `Cross-package move statement: This statement declares a move from an object declared in external module package "fake-external:///". Move statements can be only within a single module package.`, 360 }, 361 "move to resource in another module package": { 362 Statements: []MoveStatement{ 363 makeTestMoveStmt(t, 364 ``, 365 `test.thing`, 366 `module.fake_external.test.thing`, 367 ), 368 }, 369 WantError: `Cross-package move statement: This statement declares a move to an object declared in external module package "fake-external:///". Move statements can be only within a single module package.`, 370 }, 371 "move from module call in another module package": { 372 Statements: []MoveStatement{ 373 makeTestMoveStmt(t, 374 ``, 375 `module.fake_external.module.a`, 376 `module.b`, 377 ), 378 }, 379 WantError: `Cross-package move statement: This statement declares a move from an object declared in external module package "fake-external:///". Move statements can be only within a single module package.`, 380 }, 381 "move to module call in another module package": { 382 Statements: []MoveStatement{ 383 makeTestMoveStmt(t, 384 ``, 385 `module.a`, 386 `module.fake_external.module.b`, 387 ), 388 }, 389 WantError: `Cross-package move statement: This statement declares a move to an object declared in external module package "fake-external:///". Move statements can be only within a single module package.`, 390 }, 391 "move to a call that refers to another module package": { 392 Statements: []MoveStatement{ 393 makeTestMoveStmt(t, 394 ``, 395 `module.nonexist`, 396 `module.fake_external`, 397 ), 398 }, 399 WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to 400 }, 401 "move to instance of a call that refers to another module package": { 402 Statements: []MoveStatement{ 403 makeTestMoveStmt(t, 404 ``, 405 `module.nonexist`, 406 `module.fake_external[0]`, 407 ), 408 }, 409 WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to 410 }, 411 } 412 413 for name, test := range tests { 414 t.Run(name, func(t *testing.T) { 415 gotDiags := ValidateMoves(test.Statements, rootCfg, instances) 416 417 switch { 418 case test.WantError != "": 419 if !gotDiags.HasErrors() { 420 t.Fatalf("unexpected success\nwant error: %s", test.WantError) 421 } 422 if got, want := gotDiags.Err().Error(), test.WantError; got != want { 423 t.Fatalf("wrong error\ngot error: %s\nwant error: %s", got, want) 424 } 425 default: 426 if gotDiags.HasErrors() { 427 t.Fatalf("unexpected error\ngot error: %s", gotDiags.Err().Error()) 428 } 429 } 430 }) 431 } 432 } 433 434 // loadRefactoringFixture reads a configuration from the given directory and 435 // does some naive static processing on any count and for_each expressions 436 // inside, in order to get a realistic-looking instances.Set for what it 437 // declares without having to run a full Terraform plan. 438 func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instances.Set) { 439 t.Helper() 440 441 loader, cleanup := configload.NewLoaderForTests(t) 442 defer cleanup() 443 444 inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) 445 _, instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{}) 446 if instDiags.HasErrors() { 447 t.Fatal(instDiags.Err()) 448 } 449 450 // Since module installer has modified the module manifest on disk, we need 451 // to refresh the cache of it in the loader. 452 if err := loader.RefreshModules(); err != nil { 453 t.Fatalf("failed to refresh modules after installation: %s", err) 454 } 455 456 rootCfg, diags := loader.LoadConfig(dir) 457 if diags.HasErrors() { 458 t.Fatalf("failed to load root module: %s", diags.Error()) 459 } 460 461 expander := instances.NewExpander() 462 staticPopulateExpanderModule(t, rootCfg, addrs.RootModuleInstance, expander) 463 return rootCfg, expander.AllInstances() 464 } 465 466 func staticPopulateExpanderModule(t *testing.T, rootCfg *configs.Config, moduleAddr addrs.ModuleInstance, expander *instances.Expander) { 467 t.Helper() 468 469 modCfg := rootCfg.DescendentForInstance(moduleAddr) 470 if modCfg == nil { 471 t.Fatalf("no configuration for %s", moduleAddr) 472 } 473 474 if len(modCfg.Path) > 0 && modCfg.Path[len(modCfg.Path)-1] == "fake_external" { 475 // As a funny special case we modify the source address of this 476 // module to be something that counts as a separate package, 477 // so we can test rules relating to crossing package boundaries 478 // even though we really just loaded the module from a local path. 479 modCfg.SourceAddr = fakeExternalModuleSource 480 } 481 482 for _, call := range modCfg.Module.ModuleCalls { 483 callAddr := addrs.ModuleCall{Name: call.Name} 484 485 if call.Name == "fake_external" { 486 // As a funny special case we modify the source address of this 487 // module to be something that counts as a separate package, 488 // so we can test rules relating to crossing package boundaries 489 // even though we really just loaded the module from a local path. 490 call.SourceAddr = fakeExternalModuleSource 491 } 492 493 // In order to get a valid, useful set of instances here we're going 494 // to just statically evaluate the count and for_each expressions. 495 // Normally it's valid to use references and functions there, but for 496 // our unit tests we'll just limit it to literal values to avoid 497 // bringing all of the core evaluator complexity. 498 switch { 499 case call.ForEach != nil: 500 val, diags := call.ForEach.Value(nil) 501 if diags.HasErrors() { 502 t.Fatalf("invalid for_each: %s", diags.Error()) 503 } 504 expander.SetModuleForEach(moduleAddr, callAddr, val.AsValueMap()) 505 case call.Count != nil: 506 val, diags := call.Count.Value(nil) 507 if diags.HasErrors() { 508 t.Fatalf("invalid count: %s", diags.Error()) 509 } 510 var count int 511 err := gocty.FromCtyValue(val, &count) 512 if err != nil { 513 t.Fatalf("invalid count at %s: %s", call.Count.Range(), err) 514 } 515 expander.SetModuleCount(moduleAddr, callAddr, count) 516 default: 517 expander.SetModuleSingle(moduleAddr, callAddr) 518 } 519 520 // We need to recursively analyze the child modules too. 521 calledMod := modCfg.Path.Child(call.Name) 522 for _, inst := range expander.ExpandModule(calledMod) { 523 staticPopulateExpanderModule(t, rootCfg, inst, expander) 524 } 525 } 526 527 for _, rc := range modCfg.Module.ManagedResources { 528 staticPopulateExpanderResource(t, moduleAddr, rc, expander) 529 } 530 for _, rc := range modCfg.Module.DataResources { 531 staticPopulateExpanderResource(t, moduleAddr, rc, expander) 532 } 533 534 } 535 536 func staticPopulateExpanderResource(t *testing.T, moduleAddr addrs.ModuleInstance, rCfg *configs.Resource, expander *instances.Expander) { 537 t.Helper() 538 539 addr := rCfg.Addr() 540 switch { 541 case rCfg.ForEach != nil: 542 val, diags := rCfg.ForEach.Value(nil) 543 if diags.HasErrors() { 544 t.Fatalf("invalid for_each: %s", diags.Error()) 545 } 546 expander.SetResourceForEach(moduleAddr, addr, val.AsValueMap()) 547 case rCfg.Count != nil: 548 val, diags := rCfg.Count.Value(nil) 549 if diags.HasErrors() { 550 t.Fatalf("invalid count: %s", diags.Error()) 551 } 552 var count int 553 err := gocty.FromCtyValue(val, &count) 554 if err != nil { 555 t.Fatalf("invalid count at %s: %s", rCfg.Count.Range(), err) 556 } 557 expander.SetResourceCount(moduleAddr, addr, count) 558 default: 559 expander.SetResourceSingle(moduleAddr, addr) 560 } 561 } 562 563 func makeTestMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) MoveStatement { 564 t.Helper() 565 566 module := addrs.RootModule 567 if moduleStr != "" { 568 module = addrs.Module(strings.Split(moduleStr, ".")) 569 } 570 571 traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(fromStr), "", hcl.InitialPos) 572 if hclDiags.HasErrors() { 573 t.Fatalf("invalid from address: %s", hclDiags.Error()) 574 } 575 fromEP, diags := addrs.ParseMoveEndpoint(traversal) 576 if diags.HasErrors() { 577 t.Fatalf("invalid from address: %s", diags.Err().Error()) 578 } 579 580 traversal, hclDiags = hclsyntax.ParseTraversalAbs([]byte(toStr), "", hcl.InitialPos) 581 if hclDiags.HasErrors() { 582 t.Fatalf("invalid to address: %s", hclDiags.Error()) 583 } 584 toEP, diags := addrs.ParseMoveEndpoint(traversal) 585 if diags.HasErrors() { 586 t.Fatalf("invalid to address: %s", diags.Err().Error()) 587 } 588 589 fromInModule, toInModule := addrs.UnifyMoveEndpoints(module, fromEP, toEP) 590 if fromInModule == nil || toInModule == nil { 591 t.Fatalf("incompatible move endpoints") 592 } 593 594 return MoveStatement{ 595 From: fromInModule, 596 To: toInModule, 597 DeclRange: tfdiags.SourceRange{ 598 Filename: "test", 599 Start: tfdiags.SourcePos{Line: 1, Column: 1}, 600 End: tfdiags.SourcePos{Line: 1, Column: 1}, 601 }, 602 } 603 } 604 605 var fakeExternalModuleSource = addrs.ModuleSourceRemote{ 606 PackageAddr: addrs.ModulePackage("fake-external:///"), 607 }