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()