github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/apply_destroy_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "os" 8 "strings" 9 "testing" 10 11 "github.com/davecgh/go-spew/spew" 12 "github.com/mitchellh/cli" 13 "github.com/zclconf/go-cty/cty" 14 15 "github.com/terramate-io/tf/addrs" 16 "github.com/terramate-io/tf/configs/configschema" 17 "github.com/terramate-io/tf/providers" 18 "github.com/terramate-io/tf/states" 19 "github.com/terramate-io/tf/states/statefile" 20 ) 21 22 func TestApply_destroy(t *testing.T) { 23 // Create a temporary working directory that is empty 24 td := t.TempDir() 25 testCopyDir(t, testFixturePath("apply"), td) 26 defer testChdir(t, td)() 27 28 originalState := states.BuildState(func(s *states.SyncState) { 29 s.SetResourceInstanceCurrent( 30 addrs.Resource{ 31 Mode: addrs.ManagedResourceMode, 32 Type: "test_instance", 33 Name: "foo", 34 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 35 &states.ResourceInstanceObjectSrc{ 36 AttrsJSON: []byte(`{"id":"bar"}`), 37 Status: states.ObjectReady, 38 }, 39 addrs.AbsProviderConfig{ 40 Provider: addrs.NewDefaultProvider("test"), 41 Module: addrs.RootModule, 42 }, 43 ) 44 }) 45 statePath := testStateFile(t, originalState) 46 47 p := testProvider() 48 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 49 ResourceTypes: map[string]providers.Schema{ 50 "test_instance": { 51 Block: &configschema.Block{ 52 Attributes: map[string]*configschema.Attribute{ 53 "id": {Type: cty.String, Computed: true}, 54 "ami": {Type: cty.String, Optional: true}, 55 }, 56 }, 57 }, 58 }, 59 } 60 61 view, done := testView(t) 62 c := &ApplyCommand{ 63 Destroy: true, 64 Meta: Meta{ 65 testingOverrides: metaOverridesForProvider(p), 66 View: view, 67 }, 68 } 69 70 // Run the apply command pointing to our existing state 71 args := []string{ 72 "-auto-approve", 73 "-state", statePath, 74 } 75 code := c.Run(args) 76 output := done(t) 77 if code != 0 { 78 t.Log(output.Stdout()) 79 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 80 } 81 82 // Verify a new state exists 83 if _, err := os.Stat(statePath); err != nil { 84 t.Fatalf("err: %s", err) 85 } 86 87 f, err := os.Open(statePath) 88 if err != nil { 89 t.Fatalf("err: %s", err) 90 } 91 defer f.Close() 92 93 stateFile, err := statefile.Read(f) 94 if err != nil { 95 t.Fatalf("err: %s", err) 96 } 97 if stateFile.State == nil { 98 t.Fatal("state should not be nil") 99 } 100 101 actualStr := strings.TrimSpace(stateFile.State.String()) 102 expectedStr := strings.TrimSpace(testApplyDestroyStr) 103 if actualStr != expectedStr { 104 t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) 105 } 106 107 // Should have a backup file 108 f, err = os.Open(statePath + DefaultBackupExtension) 109 if err != nil { 110 t.Fatalf("err: %s", err) 111 } 112 113 backupStateFile, err := statefile.Read(f) 114 f.Close() 115 if err != nil { 116 t.Fatalf("err: %s", err) 117 } 118 119 actualStr = strings.TrimSpace(backupStateFile.State.String()) 120 expectedStr = strings.TrimSpace(originalState.String()) 121 if actualStr != expectedStr { 122 t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) 123 } 124 } 125 126 func TestApply_destroyApproveNo(t *testing.T) { 127 // Create a temporary working directory that is empty 128 td := t.TempDir() 129 testCopyDir(t, testFixturePath("apply"), td) 130 defer testChdir(t, td)() 131 132 // Create some existing state 133 originalState := states.BuildState(func(s *states.SyncState) { 134 s.SetResourceInstanceCurrent( 135 addrs.Resource{ 136 Mode: addrs.ManagedResourceMode, 137 Type: "test_instance", 138 Name: "foo", 139 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 140 &states.ResourceInstanceObjectSrc{ 141 AttrsJSON: []byte(`{"id":"bar"}`), 142 Status: states.ObjectReady, 143 }, 144 addrs.AbsProviderConfig{ 145 Provider: addrs.NewDefaultProvider("test"), 146 Module: addrs.RootModule, 147 }, 148 ) 149 }) 150 statePath := testStateFile(t, originalState) 151 152 p := applyFixtureProvider() 153 154 defer testInputMap(t, map[string]string{ 155 "approve": "no", 156 })() 157 158 // Do not use the NewMockUi initializer here, as we want to delay 159 // the call to init until after setting up the input mocks 160 ui := new(cli.MockUi) 161 view, done := testView(t) 162 c := &ApplyCommand{ 163 Destroy: true, 164 Meta: Meta{ 165 testingOverrides: metaOverridesForProvider(p), 166 Ui: ui, 167 View: view, 168 }, 169 } 170 171 args := []string{ 172 "-state", statePath, 173 } 174 code := c.Run(args) 175 output := done(t) 176 if code != 1 { 177 t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) 178 } 179 if got, want := output.Stdout(), "Destroy cancelled"; !strings.Contains(got, want) { 180 t.Fatalf("expected output to include %q, but was:\n%s", want, got) 181 } 182 183 state := testStateRead(t, statePath) 184 if state == nil { 185 t.Fatal("state should not be nil") 186 } 187 actualStr := strings.TrimSpace(state.String()) 188 expectedStr := strings.TrimSpace(originalState.String()) 189 if actualStr != expectedStr { 190 t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) 191 } 192 } 193 194 func TestApply_destroyApproveYes(t *testing.T) { 195 // Create a temporary working directory that is empty 196 td := t.TempDir() 197 testCopyDir(t, testFixturePath("apply"), td) 198 defer testChdir(t, td)() 199 200 // Create some existing state 201 originalState := states.BuildState(func(s *states.SyncState) { 202 s.SetResourceInstanceCurrent( 203 addrs.Resource{ 204 Mode: addrs.ManagedResourceMode, 205 Type: "test_instance", 206 Name: "foo", 207 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 208 &states.ResourceInstanceObjectSrc{ 209 AttrsJSON: []byte(`{"id":"bar"}`), 210 Status: states.ObjectReady, 211 }, 212 addrs.AbsProviderConfig{ 213 Provider: addrs.NewDefaultProvider("test"), 214 Module: addrs.RootModule, 215 }, 216 ) 217 }) 218 statePath := testStateFile(t, originalState) 219 220 p := applyFixtureProvider() 221 222 defer testInputMap(t, map[string]string{ 223 "approve": "yes", 224 })() 225 226 // Do not use the NewMockUi initializer here, as we want to delay 227 // the call to init until after setting up the input mocks 228 ui := new(cli.MockUi) 229 view, done := testView(t) 230 c := &ApplyCommand{ 231 Destroy: true, 232 Meta: Meta{ 233 testingOverrides: metaOverridesForProvider(p), 234 Ui: ui, 235 View: view, 236 }, 237 } 238 239 args := []string{ 240 "-state", statePath, 241 } 242 code := c.Run(args) 243 output := done(t) 244 if code != 0 { 245 t.Log(output.Stdout()) 246 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 247 } 248 249 if _, err := os.Stat(statePath); err != nil { 250 t.Fatalf("err: %s", err) 251 } 252 253 state := testStateRead(t, statePath) 254 if state == nil { 255 t.Fatal("state should not be nil") 256 } 257 258 actualStr := strings.TrimSpace(state.String()) 259 expectedStr := strings.TrimSpace(testApplyDestroyStr) 260 if actualStr != expectedStr { 261 t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) 262 } 263 } 264 265 func TestApply_destroyLockedState(t *testing.T) { 266 // Create a temporary working directory that is empty 267 td := t.TempDir() 268 testCopyDir(t, testFixturePath("apply"), td) 269 defer testChdir(t, td)() 270 271 originalState := states.BuildState(func(s *states.SyncState) { 272 s.SetResourceInstanceCurrent( 273 addrs.Resource{ 274 Mode: addrs.ManagedResourceMode, 275 Type: "test_instance", 276 Name: "foo", 277 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 278 &states.ResourceInstanceObjectSrc{ 279 AttrsJSON: []byte(`{"id":"bar"}`), 280 Status: states.ObjectReady, 281 }, 282 addrs.AbsProviderConfig{ 283 Provider: addrs.NewDefaultProvider("test"), 284 Module: addrs.RootModule, 285 }, 286 ) 287 }) 288 statePath := testStateFile(t, originalState) 289 290 unlock, err := testLockState(t, testDataDir, statePath) 291 if err != nil { 292 t.Fatal(err) 293 } 294 defer unlock() 295 296 p := testProvider() 297 view, done := testView(t) 298 c := &ApplyCommand{ 299 Destroy: true, 300 Meta: Meta{ 301 testingOverrides: metaOverridesForProvider(p), 302 View: view, 303 }, 304 } 305 306 // Run the apply command pointing to our existing state 307 args := []string{ 308 "-auto-approve", 309 "-state", statePath, 310 } 311 312 code := c.Run(args) 313 output := done(t) 314 if code == 0 { 315 t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) 316 } 317 318 if !strings.Contains(output.Stderr(), "lock") { 319 t.Fatal("command output does not look like a lock error:", output.Stderr()) 320 } 321 } 322 323 func TestApply_destroyPlan(t *testing.T) { 324 // Create a temporary working directory that is empty 325 td := t.TempDir() 326 testCopyDir(t, testFixturePath("apply"), td) 327 defer testChdir(t, td)() 328 329 planPath := testPlanFileNoop(t) 330 331 p := testProvider() 332 view, done := testView(t) 333 c := &ApplyCommand{ 334 Destroy: true, 335 Meta: Meta{ 336 testingOverrides: metaOverridesForProvider(p), 337 View: view, 338 }, 339 } 340 341 // Run the apply command pointing to our existing state 342 args := []string{ 343 planPath, 344 } 345 code := c.Run(args) 346 output := done(t) 347 if code != 1 { 348 t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) 349 } 350 if !strings.Contains(output.Stderr(), "plan file") { 351 t.Fatal("expected command output to refer to plan file, but got:", output.Stderr()) 352 } 353 } 354 355 func TestApply_destroyPath(t *testing.T) { 356 // Create a temporary working directory that is empty 357 td := t.TempDir() 358 testCopyDir(t, testFixturePath("apply"), td) 359 defer testChdir(t, td)() 360 361 p := applyFixtureProvider() 362 363 view, done := testView(t) 364 c := &ApplyCommand{ 365 Destroy: true, 366 Meta: Meta{ 367 testingOverrides: metaOverridesForProvider(p), 368 View: view, 369 }, 370 } 371 372 args := []string{ 373 "-auto-approve", 374 testFixturePath("apply"), 375 } 376 code := c.Run(args) 377 output := done(t) 378 if code != 1 { 379 t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) 380 } 381 if !strings.Contains(output.Stderr(), "-chdir") { 382 t.Fatal("expected command output to refer to -chdir flag, but got:", output.Stderr()) 383 } 384 } 385 386 // Config with multiple resources with dependencies, targeting destroy of a 387 // root node, expecting all other resources to be destroyed due to 388 // dependencies. 389 func TestApply_destroyTargetedDependencies(t *testing.T) { 390 // Create a temporary working directory that is empty 391 td := t.TempDir() 392 testCopyDir(t, testFixturePath("apply-destroy-targeted"), td) 393 defer testChdir(t, td)() 394 395 originalState := states.BuildState(func(s *states.SyncState) { 396 s.SetResourceInstanceCurrent( 397 addrs.Resource{ 398 Mode: addrs.ManagedResourceMode, 399 Type: "test_instance", 400 Name: "foo", 401 }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), 402 &states.ResourceInstanceObjectSrc{ 403 AttrsJSON: []byte(`{"id":"i-ab123"}`), 404 Status: states.ObjectReady, 405 }, 406 addrs.AbsProviderConfig{ 407 Provider: addrs.NewDefaultProvider("test"), 408 Module: addrs.RootModule, 409 }, 410 ) 411 s.SetResourceInstanceCurrent( 412 addrs.Resource{ 413 Mode: addrs.ManagedResourceMode, 414 Type: "test_load_balancer", 415 Name: "foo", 416 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 417 &states.ResourceInstanceObjectSrc{ 418 AttrsJSON: []byte(`{"id":"i-abc123"}`), 419 Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")}, 420 Status: states.ObjectReady, 421 }, 422 addrs.AbsProviderConfig{ 423 Provider: addrs.NewDefaultProvider("test"), 424 Module: addrs.RootModule, 425 }, 426 ) 427 }) 428 statePath := testStateFile(t, originalState) 429 430 p := testProvider() 431 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 432 ResourceTypes: map[string]providers.Schema{ 433 "test_instance": { 434 Block: &configschema.Block{ 435 Attributes: map[string]*configschema.Attribute{ 436 "id": {Type: cty.String, Computed: true}, 437 }, 438 }, 439 }, 440 "test_load_balancer": { 441 Block: &configschema.Block{ 442 Attributes: map[string]*configschema.Attribute{ 443 "id": {Type: cty.String, Computed: true}, 444 "instances": {Type: cty.List(cty.String), Optional: true}, 445 }, 446 }, 447 }, 448 }, 449 } 450 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 451 return providers.PlanResourceChangeResponse{ 452 PlannedState: req.ProposedNewState, 453 } 454 } 455 456 view, done := testView(t) 457 c := &ApplyCommand{ 458 Destroy: true, 459 Meta: Meta{ 460 testingOverrides: metaOverridesForProvider(p), 461 View: view, 462 }, 463 } 464 465 // Run the apply command pointing to our existing state 466 args := []string{ 467 "-auto-approve", 468 "-target", "test_instance.foo", 469 "-state", statePath, 470 } 471 code := c.Run(args) 472 output := done(t) 473 if code != 0 { 474 t.Log(output.Stdout()) 475 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 476 } 477 478 // Verify a new state exists 479 if _, err := os.Stat(statePath); err != nil { 480 t.Fatalf("err: %s", err) 481 } 482 483 f, err := os.Open(statePath) 484 if err != nil { 485 t.Fatalf("err: %s", err) 486 } 487 defer f.Close() 488 489 stateFile, err := statefile.Read(f) 490 if err != nil { 491 t.Fatalf("err: %s", err) 492 } 493 if stateFile == nil || stateFile.State == nil { 494 t.Fatal("state should not be nil") 495 } 496 497 spew.Config.DisableMethods = true 498 if !stateFile.State.Empty() { 499 t.Fatalf("unexpected final state\ngot: %s\nwant: empty state", spew.Sdump(stateFile.State)) 500 } 501 502 // Should have a backup file 503 f, err = os.Open(statePath + DefaultBackupExtension) 504 if err != nil { 505 t.Fatalf("err: %s", err) 506 } 507 508 backupStateFile, err := statefile.Read(f) 509 f.Close() 510 if err != nil { 511 t.Fatalf("err: %s", err) 512 } 513 514 actualStr := strings.TrimSpace(backupStateFile.State.String()) 515 expectedStr := strings.TrimSpace(originalState.String()) 516 if actualStr != expectedStr { 517 t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr) 518 } 519 } 520 521 // Config with multiple resources with dependencies, targeting destroy of a 522 // leaf node, expecting the other resources to remain. 523 func TestApply_destroyTargeted(t *testing.T) { 524 // Create a temporary working directory that is empty 525 td := t.TempDir() 526 testCopyDir(t, testFixturePath("apply-destroy-targeted"), td) 527 defer testChdir(t, td)() 528 529 originalState := states.BuildState(func(s *states.SyncState) { 530 s.SetResourceInstanceCurrent( 531 addrs.Resource{ 532 Mode: addrs.ManagedResourceMode, 533 Type: "test_instance", 534 Name: "foo", 535 }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), 536 &states.ResourceInstanceObjectSrc{ 537 AttrsJSON: []byte(`{"id":"i-ab123"}`), 538 Status: states.ObjectReady, 539 }, 540 addrs.AbsProviderConfig{ 541 Provider: addrs.NewDefaultProvider("test"), 542 Module: addrs.RootModule, 543 }, 544 ) 545 s.SetResourceInstanceCurrent( 546 addrs.Resource{ 547 Mode: addrs.ManagedResourceMode, 548 Type: "test_load_balancer", 549 Name: "foo", 550 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 551 &states.ResourceInstanceObjectSrc{ 552 AttrsJSON: []byte(`{"id":"i-abc123"}`), 553 Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")}, 554 Status: states.ObjectReady, 555 }, 556 addrs.AbsProviderConfig{ 557 Provider: addrs.NewDefaultProvider("test"), 558 Module: addrs.RootModule, 559 }, 560 ) 561 }) 562 wantState := states.BuildState(func(s *states.SyncState) { 563 s.SetResourceInstanceCurrent( 564 addrs.Resource{ 565 Mode: addrs.ManagedResourceMode, 566 Type: "test_instance", 567 Name: "foo", 568 }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), 569 &states.ResourceInstanceObjectSrc{ 570 AttrsJSON: []byte(`{"id":"i-ab123"}`), 571 Status: states.ObjectReady, 572 }, 573 addrs.AbsProviderConfig{ 574 Provider: addrs.NewDefaultProvider("test"), 575 Module: addrs.RootModule, 576 }, 577 ) 578 }) 579 statePath := testStateFile(t, originalState) 580 581 p := testProvider() 582 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 583 ResourceTypes: map[string]providers.Schema{ 584 "test_instance": { 585 Block: &configschema.Block{ 586 Attributes: map[string]*configschema.Attribute{ 587 "id": {Type: cty.String, Computed: true}, 588 }, 589 }, 590 }, 591 "test_load_balancer": { 592 Block: &configschema.Block{ 593 Attributes: map[string]*configschema.Attribute{ 594 "id": {Type: cty.String, Computed: true}, 595 "instances": {Type: cty.List(cty.String), Optional: true}, 596 }, 597 }, 598 }, 599 }, 600 } 601 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 602 return providers.PlanResourceChangeResponse{ 603 PlannedState: req.ProposedNewState, 604 } 605 } 606 607 view, done := testView(t) 608 c := &ApplyCommand{ 609 Destroy: true, 610 Meta: Meta{ 611 testingOverrides: metaOverridesForProvider(p), 612 View: view, 613 }, 614 } 615 616 // Run the apply command pointing to our existing state 617 args := []string{ 618 "-auto-approve", 619 "-target", "test_load_balancer.foo", 620 "-state", statePath, 621 } 622 code := c.Run(args) 623 output := done(t) 624 if code != 0 { 625 t.Log(output.Stdout()) 626 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 627 } 628 629 // Verify a new state exists 630 if _, err := os.Stat(statePath); err != nil { 631 t.Fatalf("err: %s", err) 632 } 633 634 f, err := os.Open(statePath) 635 if err != nil { 636 t.Fatalf("err: %s", err) 637 } 638 defer f.Close() 639 640 stateFile, err := statefile.Read(f) 641 if err != nil { 642 t.Fatalf("err: %s", err) 643 } 644 if stateFile == nil || stateFile.State == nil { 645 t.Fatal("state should not be nil") 646 } 647 648 actualStr := strings.TrimSpace(stateFile.State.String()) 649 expectedStr := strings.TrimSpace(wantState.String()) 650 if actualStr != expectedStr { 651 t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr) 652 } 653 654 // Should have a backup file 655 f, err = os.Open(statePath + DefaultBackupExtension) 656 if err != nil { 657 t.Fatalf("err: %s", err) 658 } 659 660 backupStateFile, err := statefile.Read(f) 661 f.Close() 662 if err != nil { 663 t.Fatalf("err: %s", err) 664 } 665 666 backupActualStr := strings.TrimSpace(backupStateFile.State.String()) 667 backupExpectedStr := strings.TrimSpace(originalState.String()) 668 if backupActualStr != backupExpectedStr { 669 t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", backupActualStr, backupExpectedStr) 670 } 671 } 672 673 const testApplyDestroyStr = ` 674 <no state> 675 `