diff --git a/internal/backend/local/backend_apply.go b/internal/backend/local/backend_apply.go index 42e78d9405d6..a6c6ea9f0b4b 100644 --- a/internal/backend/local/backend_apply.go +++ b/internal/backend/local/backend_apply.go @@ -2,6 +2,7 @@ package local import ( "context" + "errors" "fmt" "log" @@ -16,6 +17,9 @@ import ( "github.com/hashicorp/terraform/internal/tfdiags" ) +// test hook called between plan+apply during opApply +var testHookStopPlanApply func() + func (b *Local) opApply( stopCtx context.Context, cancelCtx context.Context, @@ -88,6 +92,22 @@ func (b *Local) opApply( mustConfirm := hasUI && !op.AutoApprove && !trivialPlan op.View.Plan(plan, schemas) + if testHookStopPlanApply != nil { + testHookStopPlanApply() + } + + // Check if we've been stopped before going through confirmation, or + // skipping confirmation in the case of -auto-approve. + // This can currently happen if a single stop request was received + // during the final batch of resource plan calls, so no operations were + // forced to abort, and no errors were returned from Plan. + if stopCtx.Err() != nil { + diags = diags.Append(errors.New("execution halted")) + runningOp.Result = backend.OperationFailure + op.ReportResult(runningOp, diags) + return + } + if mustConfirm { var desc, query string switch op.PlanMode { diff --git a/internal/backend/local/backend_apply_test.go b/internal/backend/local/backend_apply_test.go index 1f9514ad5410..493000a802b3 100644 --- a/internal/backend/local/backend_apply_test.go +++ b/internal/backend/local/backend_apply_test.go @@ -351,3 +351,36 @@ func applyFixtureSchema() *terraform.ProviderSchema { }, } } + +func TestApply_applyCanceledAutoApprove(t *testing.T) { + b := TestLocal(t) + + TestLocalProvider(t, b, "test", applyFixtureSchema()) + + op, configCleanup, done := testOperationApply(t, "./testdata/apply") + op.AutoApprove = true + defer configCleanup() + defer func() { + output := done(t) + if !strings.Contains(output.Stderr(), "execution halted") { + t.Fatal("expected 'execution halted', got:\n", output.All()) + } + }() + + ctx, cancel := context.WithCancel(context.Background()) + testHookStopPlanApply = cancel + defer func() { + testHookStopPlanApply = nil + }() + + run, err := b.Operation(ctx, op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result == backend.OperationSuccess { + t.Fatal("expected apply operation to fail") + } + +}