opentofu/internal/backend/local/backend_plan_test.go
2023-08-22 12:49:16 +03:00

916 lines
27 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package local
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/placeholderplaceholderplaceholder/opentf/internal/addrs"
"github.com/placeholderplaceholderplaceholder/opentf/internal/backend"
"github.com/placeholderplaceholderplaceholder/opentf/internal/command/arguments"
"github.com/placeholderplaceholderplaceholder/opentf/internal/command/clistate"
"github.com/placeholderplaceholderplaceholder/opentf/internal/command/views"
"github.com/placeholderplaceholderplaceholder/opentf/internal/configs/configschema"
"github.com/placeholderplaceholderplaceholder/opentf/internal/depsfile"
"github.com/placeholderplaceholderplaceholder/opentf/internal/initwd"
"github.com/placeholderplaceholderplaceholder/opentf/internal/plans"
"github.com/placeholderplaceholderplaceholder/opentf/internal/plans/planfile"
"github.com/placeholderplaceholderplaceholder/opentf/internal/providers"
"github.com/placeholderplaceholderplaceholder/opentf/internal/states"
"github.com/placeholderplaceholderplaceholder/opentf/internal/terminal"
"github.com/placeholderplaceholderplaceholder/opentf/internal/terraform"
)
func TestLocal_planBasic(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test", planFixtureSchema())
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if !p.PlanResourceChangeCalled {
t.Fatal("PlanResourceChange should be called")
}
// the backend should be unlocked after a run
assertBackendStateUnlocked(t, b)
if errOutput := done(t).Stderr(); errOutput != "" {
t.Fatalf("unexpected error output:\n%s", errOutput)
}
}
func TestLocal_planInAutomation(t *testing.T) {
b := TestLocal(t)
TestLocalProvider(t, b, "test", planFixtureSchema())
const msg = `You didn't use the -out option`
// When we're "in automation" we omit certain text from the plan output.
// However, the responsibility for this omission is in the view, so here we
// test for its presence while the "in automation" setting is false, to
// validate that we are calling the correct view method.
//
// Ideally this test would be replaced by a call-logging mock view, but
// that's future work.
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if output := done(t).Stdout(); !strings.Contains(output, msg) {
t.Fatalf("missing next-steps message when not in automation\nwant: %s\noutput:\n%s", msg, output)
}
}
func TestLocal_planNoConfig(t *testing.T) {
b := TestLocal(t)
TestLocalProvider(t, b, "test", providers.ProviderSchema{})
op, configCleanup, done := testOperationPlan(t, "./testdata/empty")
defer configCleanup()
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess {
t.Fatal("plan operation succeeded; want failure")
}
if stderr := output.Stderr(); !strings.Contains(stderr, "No configuration files") {
t.Fatalf("bad: %s", stderr)
}
// the backend should be unlocked after a run
assertBackendStateUnlocked(t, b)
}
// This test validates the state lacking behavior when the inner call to
// Context() fails
func TestLocal_plan_context_error(t *testing.T) {
b := TestLocal(t)
// This is an intentionally-invalid value to make terraform.NewContext fail
// when b.Operation calls it.
// NOTE: This test was originally using a provider initialization failure
// as its forced error condition, but terraform.NewContext is no longer
// responsible for checking that. Invalid parallelism is the last situation
// where terraform.NewContext can return error diagnostics, and arguably
// we should be validating this argument at the UI layer anyway, so perhaps
// in future we'll make terraform.NewContext never return errors and then
// this test will become redundant, because its purpose is specifically
// to test that we properly unlock the state if terraform.NewContext
// returns an error.
if b.ContextOpts == nil {
b.ContextOpts = &terraform.ContextOpts{}
}
b.ContextOpts.Parallelism = -1
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
// we coerce a failure in Context() by omitting the provider schema
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationFailure {
t.Fatalf("plan operation succeeded")
}
// the backend should be unlocked after a run
assertBackendStateUnlocked(t, b)
if got, want := done(t).Stderr(), "Error: Invalid parallelism value"; !strings.Contains(got, want) {
t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want)
}
}
func TestLocal_planOutputsChanged(t *testing.T) {
b := TestLocal(t)
testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) {
ss.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{Name: "changed"},
}, cty.StringVal("before"), false)
ss.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{Name: "sensitive_before"},
}, cty.StringVal("before"), true)
ss.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{Name: "sensitive_after"},
}, cty.StringVal("before"), false)
ss.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{Name: "removed"}, // not present in the config fixture
}, cty.StringVal("before"), false)
ss.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{Name: "unchanged"},
}, cty.StringVal("before"), false)
// NOTE: This isn't currently testing the situation where the new
// value of an output is unknown, because to do that requires there to
// be at least one managed resource Create action in the plan and that
// would defeat the point of this test, which is to ensure that a
// plan containing only output changes is considered "non-empty".
// For now we're not too worried about testing the "new value is
// unknown" situation because that's already common for printing out
// resource changes and we already have many tests for that.
}))
outDir := t.TempDir()
defer os.RemoveAll(outDir)
planPath := filepath.Join(outDir, "plan.tfplan")
op, configCleanup, done := testOperationPlan(t, "./testdata/plan-outputs-changed")
defer configCleanup()
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal(b.StatePath),
})
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
if err != nil {
t.Fatal(err)
}
op.PlanOutBackend = &plans.Backend{
// Just a placeholder so that we can generate a valid plan file.
Type: "local",
Config: cfgRaw,
}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if run.PlanEmpty {
t.Error("plan should not be empty")
}
expectedOutput := strings.TrimSpace(`
Changes to Outputs:
+ added = "after"
~ changed = "before" -> "after"
- removed = "before" -> null
~ sensitive_after = (sensitive value)
~ sensitive_before = (sensitive value)
You can apply this plan to save these new output values to the OpenTF state,
without changing any real infrastructure.
`)
if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
t.Errorf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput)
}
}
// Module outputs should not cause the plan to be rendered
func TestLocal_planModuleOutputsChanged(t *testing.T) {
b := TestLocal(t)
testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) {
ss.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance.Child("mod", addrs.NoKey),
OutputValue: addrs.OutputValue{Name: "changed"},
}, cty.StringVal("before"), false)
}))
outDir := t.TempDir()
defer os.RemoveAll(outDir)
planPath := filepath.Join(outDir, "plan.tfplan")
op, configCleanup, done := testOperationPlan(t, "./testdata/plan-module-outputs-changed")
defer configCleanup()
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal(b.StatePath),
})
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
if err != nil {
t.Fatal(err)
}
op.PlanOutBackend = &plans.Backend{
Type: "local",
Config: cfgRaw,
}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if !run.PlanEmpty {
t.Fatal("plan should be empty")
}
expectedOutput := strings.TrimSpace(`
No changes. Your infrastructure matches the configuration.
`)
if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
t.Fatalf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput)
}
}
func TestLocal_planTainted(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test", planFixtureSchema())
testStateFile(t, b.StatePath, testPlanState_tainted())
outDir := t.TempDir()
planPath := filepath.Join(outDir, "plan.tfplan")
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal(b.StatePath),
})
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
if err != nil {
t.Fatal(err)
}
op.PlanOutBackend = &plans.Backend{
// Just a placeholder so that we can generate a valid plan file.
Type: "local",
Config: cfgRaw,
}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if !p.ReadResourceCalled {
t.Fatal("ReadResource should be called")
}
if run.PlanEmpty {
t.Fatal("plan should not be empty")
}
expectedOutput := `OpenTF used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
OpenTF will perform the following actions:
# test_instance.foo is tainted, so it must be replaced
-/+ resource "test_instance" "foo" {
# (1 unchanged attribute hidden)
# (1 unchanged block hidden)
}
Plan: 1 to add, 0 to change, 1 to destroy.`
if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
t.Fatalf("Unexpected output\ngot\n%s\n\nwant:\n%s", output, expectedOutput)
}
}
func TestLocal_planDeposedOnly(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test", planFixtureSchema())
testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) {
ss.SetResourceInstanceDeposed(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
states.DeposedKey("00000000"),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{
"ami": "bar",
"network_interface": [{
"device_index": 0,
"description": "Main network interface"
}]
}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
}))
outDir := t.TempDir()
planPath := filepath.Join(outDir, "plan.tfplan")
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal(b.StatePath),
})
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
if err != nil {
t.Fatal(err)
}
op.PlanOutBackend = &plans.Backend{
// Just a placeholder so that we can generate a valid plan file.
Type: "local",
Config: cfgRaw,
}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if !p.ReadResourceCalled {
t.Fatal("ReadResource should've been called to refresh the deposed object")
}
if run.PlanEmpty {
t.Fatal("plan should not be empty")
}
// The deposed object and the current object are distinct, so our
// plan includes separate actions for each of them. This strange situation
// is not common: it should arise only if OpenTF fails during
// a create-before-destroy when the "create" hasn't completed yet but
// in a severe way that prevents the previous object from being restored
// as "current".
//
// However, that situation was more common in some earlier OpenTF
// versions where deposed objects were not managed properly, so this
// can arise when upgrading from an older version with deposed objects
// already in the state.
//
// This is one of the few cases where we expose the idea of "deposed" in
// the UI, including the user-unfriendly "deposed key" (00000000 in this
// case) just so that users can correlate this with what they might
// see in `opentf show` and in the subsequent apply output, because
// it's also possible for there to be _multiple_ deposed objects, in the
// unlikely event that create_before_destroy _keeps_ crashing across
// subsequent runs.
expectedOutput := `OpenTF used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
- destroy
OpenTF will perform the following actions:
# test_instance.foo will be created
+ resource "test_instance" "foo" {
+ ami = "bar"
+ network_interface {
+ description = "Main network interface"
+ device_index = 0
}
}
# test_instance.foo (deposed object 00000000) will be destroyed
# (left over from a partially-failed replacement of this instance)
- resource "test_instance" "foo" {
- ami = "bar" -> null
- network_interface {
- description = "Main network interface" -> null
- device_index = 0 -> null
}
}
Plan: 1 to add, 0 to change, 1 to destroy.`
if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
t.Fatalf("Unexpected output\ngot\n%s\n\nwant:\n%s", output, expectedOutput)
}
}
func TestLocal_planTainted_createBeforeDestroy(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test", planFixtureSchema())
testStateFile(t, b.StatePath, testPlanState_tainted())
outDir := t.TempDir()
planPath := filepath.Join(outDir, "plan.tfplan")
op, configCleanup, done := testOperationPlan(t, "./testdata/plan-cbd")
defer configCleanup()
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal(b.StatePath),
})
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
if err != nil {
t.Fatal(err)
}
op.PlanOutBackend = &plans.Backend{
// Just a placeholder so that we can generate a valid plan file.
Type: "local",
Config: cfgRaw,
}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if !p.ReadResourceCalled {
t.Fatal("ReadResource should be called")
}
if run.PlanEmpty {
t.Fatal("plan should not be empty")
}
expectedOutput := `OpenTF used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+/- create replacement and then destroy
OpenTF will perform the following actions:
# test_instance.foo is tainted, so it must be replaced
+/- resource "test_instance" "foo" {
# (1 unchanged attribute hidden)
# (1 unchanged block hidden)
}
Plan: 1 to add, 0 to change, 1 to destroy.`
if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
t.Fatalf("Unexpected output\ngot\n%s\n\nwant:\n%s", output, expectedOutput)
}
}
func TestLocal_planRefreshFalse(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test", planFixtureSchema())
testStateFile(t, b.StatePath, testPlanState())
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if p.ReadResourceCalled {
t.Fatal("ReadResource should not be called")
}
if !run.PlanEmpty {
t.Fatal("plan should be empty")
}
if errOutput := done(t).Stderr(); errOutput != "" {
t.Fatalf("unexpected error output:\n%s", errOutput)
}
}
func TestLocal_planDestroy(t *testing.T) {
b := TestLocal(t)
TestLocalProvider(t, b, "test", planFixtureSchema())
testStateFile(t, b.StatePath, testPlanState())
outDir := t.TempDir()
planPath := filepath.Join(outDir, "plan.tfplan")
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.PlanMode = plans.DestroyMode
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal(b.StatePath),
})
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
if err != nil {
t.Fatal(err)
}
op.PlanOutBackend = &plans.Backend{
// Just a placeholder so that we can generate a valid plan file.
Type: "local",
Config: cfgRaw,
}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if run.PlanEmpty {
t.Fatal("plan should not be empty")
}
plan := testReadPlan(t, planPath)
for _, r := range plan.Changes.Resources {
if r.Action.String() != "Delete" {
t.Fatalf("bad: %#v", r.Action.String())
}
}
if errOutput := done(t).Stderr(); errOutput != "" {
t.Fatalf("unexpected error output:\n%s", errOutput)
}
}
func TestLocal_planDestroy_withDataSources(t *testing.T) {
b := TestLocal(t)
TestLocalProvider(t, b, "test", planFixtureSchema())
testStateFile(t, b.StatePath, testPlanState_withDataSource())
outDir := t.TempDir()
planPath := filepath.Join(outDir, "plan.tfplan")
op, configCleanup, done := testOperationPlan(t, "./testdata/destroy-with-ds")
defer configCleanup()
op.PlanMode = plans.DestroyMode
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal(b.StatePath),
})
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
if err != nil {
t.Fatal(err)
}
op.PlanOutBackend = &plans.Backend{
// Just a placeholder so that we can generate a valid plan file.
Type: "local",
Config: cfgRaw,
}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if run.PlanEmpty {
t.Fatal("plan should not be empty")
}
// Data source should still exist in the the plan file
plan := testReadPlan(t, planPath)
if len(plan.Changes.Resources) != 2 {
t.Fatalf("Expected exactly 1 resource for destruction, %d given: %q",
len(plan.Changes.Resources), getAddrs(plan.Changes.Resources))
}
// Data source should not be rendered in the output
expectedOutput := `OpenTF will perform the following actions:
# test_instance.foo[0] will be destroyed
- resource "test_instance" "foo" {
- ami = "bar" -> null
- network_interface {
- description = "Main network interface" -> null
- device_index = 0 -> null
}
}
Plan: 0 to add, 0 to change, 1 to destroy.`
if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
t.Fatalf("Unexpected output:\n%s", output)
}
}
func getAddrs(resources []*plans.ResourceInstanceChangeSrc) []string {
addrs := make([]string, len(resources))
for i, r := range resources {
addrs[i] = r.Addr.String()
}
return addrs
}
func TestLocal_planOutPathNoChange(t *testing.T) {
b := TestLocal(t)
TestLocalProvider(t, b, "test", planFixtureSchema())
testStateFile(t, b.StatePath, testPlanState())
outDir := t.TempDir()
planPath := filepath.Join(outDir, "plan.tfplan")
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal(b.StatePath),
})
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
if err != nil {
t.Fatal(err)
}
op.PlanOutBackend = &plans.Backend{
// Just a placeholder so that we can generate a valid plan file.
Type: "local",
Config: cfgRaw,
}
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
plan := testReadPlan(t, planPath)
if !plan.Changes.Empty() {
t.Fatalf("expected empty plan to be written")
}
if errOutput := done(t).Stderr(); errOutput != "" {
t.Fatalf("unexpected error output:\n%s", errOutput)
}
}
func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
streams, done := terminal.StreamsForTesting(t)
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
// Many of our tests use an overridden "test" provider that's just in-memory
// inside the test process, not a separate plugin on disk.
depLocks := depsfile.NewLocks()
depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test"))
return &backend.Operation{
Type: backend.OperationTypePlan,
ConfigDir: configDir,
ConfigLoader: configLoader,
StateLocker: clistate.NewNoopLocker(),
View: view,
DependencyLocks: depLocks,
}, configCleanup, done
}
// testPlanState is just a common state that we use for testing plan.
func testPlanState() *states.State {
state := states.NewState()
rootModule := state.RootModule()
rootModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{
"ami": "bar",
"network_interface": [{
"device_index": 0,
"description": "Main network interface"
}]
}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
return state
}
func testPlanState_withDataSource() *states.State {
state := states.NewState()
rootModule := state.RootModule()
rootModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.IntKey(0)),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{
"ami": "bar",
"network_interface": [{
"device_index": 0,
"description": "Main network interface"
}]
}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
rootModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "test_ds",
Name: "bar",
}.Instance(addrs.IntKey(0)),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{
"filter": "foo"
}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
return state
}
func testPlanState_tainted() *states.State {
state := states.NewState()
rootModule := state.RootModule()
rootModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{
"ami": "bar",
"network_interface": [{
"device_index": 0,
"description": "Main network interface"
}]
}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
return state
}
func testReadPlan(t *testing.T, path string) *plans.Plan {
t.Helper()
p, err := planfile.Open(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer p.Close()
plan, err := p.ReadPlan()
if err != nil {
t.Fatalf("err: %s", err)
}
return plan
}
// planFixtureSchema returns a schema suitable for processing the
// configuration in testdata/plan . This schema should be
// assigned to a mock provider named "test".
func planFixtureSchema() providers.ProviderSchema {
return providers.ProviderSchema{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"ami": {Type: cty.String, Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"network_interface": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"device_index": {Type: cty.Number, Optional: true},
"description": {Type: cty.String, Optional: true},
},
},
},
},
},
},
},
DataSources: map[string]providers.Schema{
"test_ds": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"filter": {Type: cty.String, Required: true},
},
},
},
},
}
}
func TestLocal_invalidOptions(t *testing.T) {
b := TestLocal(t)
TestLocalProvider(t, b, "test", planFixtureSchema())
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.PlanRefresh = true
op.PlanMode = plans.RefreshOnlyMode
op.ForceReplace = []addrs.AbsResourceInstance{mustResourceInstanceAddr("test_instance.foo")}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
<-run.Done()
if run.Result == backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if errOutput := done(t).Stderr(); errOutput == "" {
t.Fatal("expected error output")
}
}