From 6243a6307ab8182e3a1141660b2ac0366ca740db Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 26 Jun 2020 17:30:00 -0400 Subject: [PATCH] don't evaluate destroy instances Orphaned instances that are create_before_destroy will still be in the state when their references are evaluated. We need to skip instances that are planned to be destroyed altogether, as they can't be part of an evaluation. --- terraform/context_apply_test.go | 82 +++++++++++++++++++++++++++++++++ terraform/evaluate.go | 12 ++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index e06773bb12..7b2e23b187 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -11357,3 +11357,85 @@ output "myoutput" { t.Fatal("expected empty state, got:", state) } } + +func TestContext2Apply_scaleInCBD(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_instance" "a" { + count = 1 + lifecycle { + create_before_destroy = true + } +} + +resource "test_instance" "b" { + foo = join(".", test_instance.a[*].id) +} + +output "out" { + value = join(".", test_instance.a[*].id) +} +`}) + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_instance.a[0]").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"a0"}`), + Dependencies: []addrs.ConfigResource{mustResourceAddr("module.child.aws_instance.child")}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_instance.a[1]").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"a1"}`), + Dependencies: []addrs.ConfigResource{mustResourceAddr("module.child.aws_instance.child")}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_instance.b").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"b", "foo":"old.old"}`), + Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.a")}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + p := testProvider("test") + p.ApplyFn = func(info *InstanceInfo, s *InstanceState, d *InstanceDiff) (*InstanceState, error) { + return testApplyFn(info, s, d) + } + + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Config: m, + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + State: state, + }) + + _, diags := ctx.Plan() + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + // if resource b isn't going to apply correctly, we will get an error about + // an invalid plan value + state, diags = ctx.Apply() + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + // check the output, as those can't cause an error planning the value + out := state.RootModule().OutputValues["out"].Value.AsString() + if out != "a0" { + t.Fatalf(`expected output "new", got: %q`, out) + } +} diff --git a/terraform/evaluate.go b/terraform/evaluate.go index 128dd7a4cc..fad95b4726 100644 --- a/terraform/evaluate.go +++ b/terraform/evaluate.go @@ -655,10 +655,20 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc instAddr := addr.Instance(key).Absolute(d.ModulePath) + change := d.Evaluator.Changes.GetResourceInstanceChange(instAddr, states.CurrentGen) + if change != nil { + // Don't take any resources that are yet to be deleted into account. + // If the referenced resource is CreateBeforeDestroy, then orphaned + // instances will be in the state, as they are not destroyed until + // after their dependants are updated. + if change.Action == plans.Delete { + continue + } + } + // Planned resources are temporarily stored in state with empty values, // and need to be replaced bu the planned value here. if is.Current.Status == states.ObjectPlanned { - change := d.Evaluator.Changes.GetResourceInstanceChange(instAddr, states.CurrentGen) if change == nil { // If the object is in planned status then we should not get // here, since we should have found a pending value in the plan