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