opentofu/internal/command/apply_destroy_test.go
Martin Atkins f40800b3a4 Move states/ to internal/states/
This is part of a general effort to move all of Terraform's non-library
package surface under internal in order to reinforce that these are for
internal use within Terraform only.

If you were previously importing packages under this prefix into an
external codebase, you could pin to an earlier release tag as an interim
solution until you've make a plan to achieve the same functionality some
other way.
2021-05-17 14:09:07 -07:00

681 lines
17 KiB
Go

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 := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(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 := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(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 := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(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 := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(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(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 := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(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 := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(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 := tempDir(t)
testCopyDir(t, testFixturePath("apply-destroy-targeted"), td)
defer os.RemoveAll(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 := tempDir(t)
testCopyDir(t, testFixturePath("apply-destroy-targeted"), td)
defer os.RemoveAll(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 = `
<no state>
`