// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "os" "strings" "testing" "github.com/davecgh/go-spew/spew" "github.com/mitchellh/cli" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" ) func TestApply_destroy(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("apply"), td) defer testChdir(t, td)() originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"bar"}`), Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) }) statePath := testStateFile(t, originalState) p := testProvider() p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, }, }, }, } view, done := testView(t) c := &ApplyCommand{ Destroy: true, Meta: Meta{ testingOverrides: metaOverridesForProvider(p), View: view, }, } // Run the apply command pointing to our existing state args := []string{ "-auto-approve", "-state", statePath, } code := c.Run(args) output := done(t) if code != 0 { t.Log(output.Stdout()) t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) } // Verify a new state exists if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() stateFile, err := statefile.Read(f) if err != nil { t.Fatalf("err: %s", err) } if stateFile.State == nil { t.Fatal("state should not be nil") } actualStr := strings.TrimSpace(stateFile.State.String()) expectedStr := strings.TrimSpace(testApplyDestroyStr) if actualStr != expectedStr { t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) } // Should have a backup file f, err = os.Open(statePath + DefaultBackupExtension) if err != nil { t.Fatalf("err: %s", err) } backupStateFile, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } actualStr = strings.TrimSpace(backupStateFile.State.String()) expectedStr = strings.TrimSpace(originalState.String()) if actualStr != expectedStr { t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) } } func TestApply_destroyApproveNo(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("apply"), td) defer testChdir(t, td)() // Create some existing state originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"bar"}`), Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) }) statePath := testStateFile(t, originalState) p := applyFixtureProvider() defer testInputMap(t, map[string]string{ "approve": "no", })() // Do not use the NewMockUi initializer here, as we want to delay // the call to init until after setting up the input mocks ui := new(cli.MockUi) view, done := testView(t) c := &ApplyCommand{ Destroy: true, Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, View: view, }, } args := []string{ "-state", statePath, } code := c.Run(args) output := done(t) if code != 1 { t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) } if got, want := output.Stdout(), "Destroy cancelled"; !strings.Contains(got, want) { t.Fatalf("expected output to include %q, but was:\n%s", want, got) } state := testStateRead(t, statePath) if state == nil { t.Fatal("state should not be nil") } actualStr := strings.TrimSpace(state.String()) expectedStr := strings.TrimSpace(originalState.String()) if actualStr != expectedStr { t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) } } func TestApply_destroyApproveYes(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("apply"), td) defer testChdir(t, td)() // Create some existing state originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"bar"}`), Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) }) statePath := testStateFile(t, originalState) p := applyFixtureProvider() defer testInputMap(t, map[string]string{ "approve": "yes", })() // Do not use the NewMockUi initializer here, as we want to delay // the call to init until after setting up the input mocks ui := new(cli.MockUi) view, done := testView(t) c := &ApplyCommand{ Destroy: true, Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, View: view, }, } args := []string{ "-state", statePath, } code := c.Run(args) output := done(t) if code != 0 { t.Log(output.Stdout()) t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) } if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } state := testStateRead(t, statePath) if state == nil { t.Fatal("state should not be nil") } actualStr := strings.TrimSpace(state.String()) expectedStr := strings.TrimSpace(testApplyDestroyStr) if actualStr != expectedStr { t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) } } func TestApply_destroyLockedState(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("apply"), td) defer testChdir(t, td)() originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"bar"}`), Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) }) statePath := testStateFile(t, originalState) unlock, err := testLockState(t, testDataDir, statePath) if err != nil { t.Fatal(err) } defer unlock() p := testProvider() view, done := testView(t) c := &ApplyCommand{ Destroy: true, Meta: Meta{ testingOverrides: metaOverridesForProvider(p), View: view, }, } // Run the apply command pointing to our existing state args := []string{ "-auto-approve", "-state", statePath, } code := c.Run(args) output := done(t) if code == 0 { t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) } if !strings.Contains(output.Stderr(), "lock") { t.Fatal("command output does not look like a lock error:", output.Stderr()) } } func TestApply_destroyPlan(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("apply"), td) defer testChdir(t, td)() planPath := testPlanFileNoop(t) p := testProvider() view, done := testView(t) c := &ApplyCommand{ Destroy: true, Meta: Meta{ testingOverrides: metaOverridesForProvider(p), View: view, }, } // Run the apply command pointing to our existing state args := []string{ planPath, } code := c.Run(args) output := done(t) if code != 1 { t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) } if !strings.Contains(output.Stderr(), "plan file") { t.Fatal("expected command output to refer to plan file, but got:", output.Stderr()) } } func TestApply_destroyPath(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("apply"), td) defer testChdir(t, td)() p := applyFixtureProvider() view, done := testView(t) c := &ApplyCommand{ Destroy: true, Meta: Meta{ testingOverrides: metaOverridesForProvider(p), View: view, }, } args := []string{ "-auto-approve", testFixturePath("apply"), } code := c.Run(args) output := done(t) if code != 1 { t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) } if !strings.Contains(output.Stderr(), "-chdir") { t.Fatal("expected command output to refer to -chdir flag, but got:", output.Stderr()) } } // Config with multiple resources with dependencies, targeting destroy of a // root node, expecting all other resources to be destroyed due to // dependencies. func TestApply_destroyTargetedDependencies(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("apply-destroy-targeted"), td) defer testChdir(t, td)() originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "foo", }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"i-ab123"}`), Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_load_balancer", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"i-abc123"}`), Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")}, Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) }) statePath := testStateFile(t, originalState) p := testProvider() p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, }, }, "test_load_balancer": { Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "instances": {Type: cty.List(cty.String), Optional: true}, }, }, }, }, } p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { return providers.PlanResourceChangeResponse{ PlannedState: req.ProposedNewState, } } view, done := testView(t) c := &ApplyCommand{ Destroy: true, Meta: Meta{ testingOverrides: metaOverridesForProvider(p), View: view, }, } // Run the apply command pointing to our existing state args := []string{ "-auto-approve", "-target", "test_instance.foo", "-state", statePath, } code := c.Run(args) output := done(t) if code != 0 { t.Log(output.Stdout()) t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) } // Verify a new state exists if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() stateFile, err := statefile.Read(f) if err != nil { t.Fatalf("err: %s", err) } if stateFile == nil || stateFile.State == nil { t.Fatal("state should not be nil") } spew.Config.DisableMethods = true if !stateFile.State.Empty() { t.Fatalf("unexpected final state\ngot: %s\nwant: empty state", spew.Sdump(stateFile.State)) } // Should have a backup file f, err = os.Open(statePath + DefaultBackupExtension) if err != nil { t.Fatalf("err: %s", err) } backupStateFile, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } actualStr := strings.TrimSpace(backupStateFile.State.String()) expectedStr := strings.TrimSpace(originalState.String()) if actualStr != expectedStr { t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr) } } // Config with multiple resources with dependencies, targeting destroy of a // leaf node, expecting the other resources to remain. func TestApply_destroyTargeted(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("apply-destroy-targeted"), td) defer testChdir(t, td)() originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "foo", }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"i-ab123"}`), Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_load_balancer", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"i-abc123"}`), Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")}, Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) }) wantState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "foo", }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"i-ab123"}`), Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) }) statePath := testStateFile(t, originalState) p := testProvider() p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, }, }, "test_load_balancer": { Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "instances": {Type: cty.List(cty.String), Optional: true}, }, }, }, }, } p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { return providers.PlanResourceChangeResponse{ PlannedState: req.ProposedNewState, } } view, done := testView(t) c := &ApplyCommand{ Destroy: true, Meta: Meta{ testingOverrides: metaOverridesForProvider(p), View: view, }, } // Run the apply command pointing to our existing state args := []string{ "-auto-approve", "-target", "test_load_balancer.foo", "-state", statePath, } code := c.Run(args) output := done(t) if code != 0 { t.Log(output.Stdout()) t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) } // Verify a new state exists if _, err := os.Stat(statePath); err != nil { t.Fatalf("err: %s", err) } f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } defer f.Close() stateFile, err := statefile.Read(f) if err != nil { t.Fatalf("err: %s", err) } if stateFile == nil || stateFile.State == nil { t.Fatal("state should not be nil") } actualStr := strings.TrimSpace(stateFile.State.String()) expectedStr := strings.TrimSpace(wantState.String()) if actualStr != expectedStr { t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr) } // Should have a backup file f, err = os.Open(statePath + DefaultBackupExtension) if err != nil { t.Fatalf("err: %s", err) } backupStateFile, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } backupActualStr := strings.TrimSpace(backupStateFile.State.String()) backupExpectedStr := strings.TrimSpace(originalState.String()) if backupActualStr != backupExpectedStr { t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", backupActualStr, backupExpectedStr) } } const testApplyDestroyStr = ` `