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.
This commit is contained in:
James Bardin
2022-05-02 13:55:32 -04:00
parent 488853be9c
commit df0a70bfb6
2 changed files with 53 additions and 0 deletions

View File

@@ -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 {

View File

@@ -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")
}
}