diff --git a/internal/backend/local/backend_apply.go b/internal/backend/local/backend_apply.go index 897c36e40..42e78d940 100644 --- a/internal/backend/local/backend_apply.go +++ b/internal/backend/local/backend_apply.go @@ -168,6 +168,14 @@ func (b *Local) opApply( } diags = diags.Append(applyDiags) + // Even on error with an empty state, the state value should not be nil. + // Return early here to prevent corrupting any existing state. + if diags.HasErrors() && applyState == nil { + log.Printf("[ERROR] backend/local: apply returned nil state") + op.ReportResult(runningOp, diags) + return + } + // Store the final state runningOp.State = applyState err := statemgr.WriteAndPersist(opState, applyState) diff --git a/internal/terraform/context_apply.go b/internal/terraform/context_apply.go index e94449b72..e5d2702bc 100644 --- a/internal/terraform/context_apply.go +++ b/internal/terraform/context_apply.go @@ -22,12 +22,11 @@ import ( // resulting state which is likely to have been partially-updated. func (c *Context) Apply(plan *plans.Plan, config *configs.Config) (*states.State, tfdiags.Diagnostics) { defer c.acquireRun("apply")() - var diags tfdiags.Diagnostics log.Printf("[DEBUG] Building and walking apply graph for %s plan", plan.UIMode) - graph, operation, moreDiags := c.applyGraph(plan, config, true) - if moreDiags.HasErrors() { + graph, operation, diags := c.applyGraph(plan, config, true) + if diags.HasErrors() { return nil, diags } diff --git a/internal/terraform/context_apply2_test.go b/internal/terraform/context_apply2_test.go index e02c73df1..6b87d409d 100644 --- a/internal/terraform/context_apply2_test.go +++ b/internal/terraform/context_apply2_test.go @@ -678,3 +678,61 @@ resource "test_object" "s" { }) assertNoErrors(t, diags) } + +func TestContext2Apply_graphError(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + test_string = "ok" +} + +resource "test_object" "b" { + test_string = test_object.a.test_string +} +`, + }) + + p := simpleMockProvider() + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.a").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, + AttrsJSON: []byte(`{"test_string":"ok"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.b").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, + AttrsJSON: []byte(`{"test_string":"ok"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + }) + if diags.HasErrors() { + t.Fatalf("plan: %s", diags.Err()) + } + + // We're going to corrupt the stored state so that the dependencies will + // cause a cycle when building the apply graph. + testObjA := plan.PriorState.Modules[""].Resources["test_object.a"].Instances[addrs.NoKey].Current + testObjA.Dependencies = append(testObjA.Dependencies, mustResourceInstanceAddr("test_object.b").ContainingResource().Config()) + + _, diags = ctx.Apply(plan, m) + if !diags.HasErrors() { + t.Fatal("expected cycle error from apply") + } +}