mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
916 lines
27 KiB
Go
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")
|
|
}
|
|
}
|