From b1213f7f6ca038ed2ea159bfe7c092a8b4ccfd13 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 3 Jun 2019 14:50:32 -0700 Subject: [PATCH] backend/local: don't panic when an instance has only a deposed object This unusual situation isn't supposed to arise in normal use, but it can come up in practice in some edge-case scenarios where Terraform fails in a severe way during a create_before_destroy. Some earlier versions of Terraform also had bugs in their handling of deposed objects, so this may also arise if upgrading from one of those older versions with some leftover deposed objects in the state. --- backend/local/backend_plan.go | 7 +- backend/local/backend_plan_test.go | 115 +++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index e995d2bc821f..0a7f28dac110 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -260,9 +260,10 @@ func (b *Local) renderPlan(plan *plans.Plan, state *states.State, schemas *terra // check if the change is due to a tainted resource tainted := false if !state.Empty() { - rs := state.ResourceInstance(rcs.Addr) - if rs != nil { - tainted = rs.Current.Status == states.ObjectTainted + if is := state.ResourceInstance(rcs.Addr); is != nil { + if obj := is.GetGeneration(rcs.DeposedKey.Generation()); obj != nil { + tainted = obj.Status == states.ObjectTainted + } } } diff --git a/backend/local/backend_plan_test.go b/backend/local/backend_plan_test.go index 68bb5a90a0d5..6554d72b8ad0 100644 --- a/backend/local/backend_plan_test.go +++ b/backend/local/backend_plan_test.go @@ -193,6 +193,121 @@ Plan: 1 to add, 0 to change, 1 to destroy.` } } +func TestLocal_planDeposedOnly(t *testing.T) { + b, cleanup := TestLocal(t) + defer cleanup() + 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.ProviderConfig{ + Type: "test", + }.Absolute(addrs.RootModuleInstance), + ) + })) + b.CLI = cli.NewMockUi() + outDir := testTempDir(t) + defer os.RemoveAll(outDir) + planPath := filepath.Join(outDir, "plan.tfplan") + op, configCleanup := testOperationPlan(t, "./test-fixtures/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") + } + + // 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 Terraform 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 Terraform + // 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 `terraform 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 := `An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + + create + - destroy + +Terraform 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 + - 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.` + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if !strings.Contains(output, expectedOutput) { + t.Fatalf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput) + } +} + func TestLocal_planTainted_createBeforeDestroy(t *testing.T) { b, cleanup := TestLocal(t) defer cleanup()