From 4b04e3fa59ff80330e298c761e3bbdfa5ee30f1f Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 6 Nov 2019 16:52:34 -0500 Subject: [PATCH] failing tests for destroying tainted resources If a tainted resource fails to destroy, it loses the tainted status --- terraform/context_apply_test.go | 136 ++++++++++++++++++ .../testdata/apply-destroy-tainted/main.tf | 17 +++ 2 files changed, 153 insertions(+) create mode 100644 terraform/testdata/apply-destroy-tainted/main.tf diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 8d50a3e9e..4bc016e9f 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -10963,3 +10963,139 @@ func TestContext2Apply_destroyDataCycle(t *testing.T) { t.Fatalf("diags: %s", diags.Err()) } } + +func TestContext2Apply_taintedDestroyFailure(t *testing.T) { + m := testModule(t, "apply-destroy-tainted") + p := testProvider("test") + p.DiffFn = testDiffFn + p.ApplyFn = func(info *InstanceInfo, s *InstanceState, d *InstanceDiff) (*InstanceState, error) { + // All destroys fail. + // c will also fail to create, meaning the existing tainted instance + // becomes deposed, ans is then promoted back to current. + // only C has a foo attribute + attr := d.Attributes["foo"] + if d.Destroy || (attr != nil && attr.New == "c") { + return nil, errors.New("failure") + } + + return testApplyFn(info, s, d) + } + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "a", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, + AttrsJSON: []byte(`{"id":"a","foo":"a"}`), + }, + addrs.ProviderConfig{ + Type: "test", + }.Absolute(addrs.RootModuleInstance), + ) + root.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "b", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, + AttrsJSON: []byte(`{"id":"b","foo":"b"}`), + }, + addrs.ProviderConfig{ + Type: "test", + }.Absolute(addrs.RootModuleInstance), + ) + root.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "c", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, + AttrsJSON: []byte(`{"id":"c","foo":"old"}`), + }, + addrs.ProviderConfig{ + Type: "test", + }.Absolute(addrs.RootModuleInstance), + ) + + providerResolver := providers.ResolverFixed( + map[string]providers.Factory{ + "test": testProviderFuncFixed(p), + }, + ) + + ctx := testContext2(t, &ContextOpts{ + Config: m, + ProviderResolver: providerResolver, + State: state, + Hooks: []Hook{&testHook{}}, + }) + + _, diags := ctx.Plan() + diags.HasErrors() + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + state, diags = ctx.Apply() + if !diags.HasErrors() { + t.Fatal("expected error") + } + + root = state.Module(addrs.RootModuleInstance) + + // the instance that failed to destroy should remain tainted + a := root.ResourceInstance(addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "a", + }.Instance(addrs.NoKey)) + + if a.Current.Status != states.ObjectTainted { + t.Fatal("test_instance.a should be tainted") + } + + // b is create_before_destroy, and the destroy failed, so there should be 1 + // deposed instance. + b := root.ResourceInstance(addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "b", + }.Instance(addrs.NoKey)) + + if b.Current.Status != states.ObjectReady { + t.Fatal("test_instance.b should be Ready") + } + + if len(b.Deposed) != 1 { + t.Fatal("test_instance.b failed to keep deposed instance") + } + + // the desposed c instance should be promoted back to Current, and remain + // tainted + c := root.ResourceInstance(addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "c", + }.Instance(addrs.NoKey)) + + if c.Current.Status != states.ObjectTainted { + t.Fatal("test_instance.c should be tainted") + } + + if len(c.Deposed) != 0 { + t.Fatal("test_instance.c should have no deposed instances") + } + + if string(c.Current.AttrsJSON) != `{"id":"c","foo":"old"}` { + t.Fatalf("unexpected attrs for c: %q\n", c.Current.AttrsJSON) + } +} diff --git a/terraform/testdata/apply-destroy-tainted/main.tf b/terraform/testdata/apply-destroy-tainted/main.tf new file mode 100644 index 000000000..48f4f1378 --- /dev/null +++ b/terraform/testdata/apply-destroy-tainted/main.tf @@ -0,0 +1,17 @@ +resource "test_instance" "a" { + foo = "a" +} + +resource "test_instance" "b" { + foo = "b" + lifecycle { + create_before_destroy = true + } +} + +resource "test_instance" "c" { + foo = "c" + lifecycle { + create_before_destroy = true + } +}