From df0a70bfb680c10a1635fc2a9d60fc615bf6d9e7 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 2 May 2022 13:55:32 -0400 Subject: [PATCH] check for cancellation before apply confirmation When executing an apply with no plan, it's possible for a cancellation to arrive during the final batch of provider operations, resulting in no errors in the plan. The run context was next checked during the confirmation for apply, but in the case of -auto-approve that confirmation is skipped, resulting in the canceled plan being applied. Make sure we directly check for cancellation before confirming the plan. --- internal/backend/local/backend_apply.go | 20 ++++++++++++ internal/backend/local/backend_apply_test.go | 33 ++++++++++++++++++++ 2 files changed, 53 insertions(+) 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") + } + +}