diff --git a/.changes/v1.11/BUG FIXES-20250206-155025.yaml b/.changes/v1.11/BUG FIXES-20250206-155025.yaml new file mode 100644 index 000000000000..2c6384ba58ae --- /dev/null +++ b/.changes/v1.11/BUG FIXES-20250206-155025.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: Fixes hanging behavior seen when applying a saved plan with -auto-approve using the cloud backend +time: 2025-02-06T15:50:25.767607-05:00 +custom: + Issue: "36453" diff --git a/internal/cloud/backend_apply.go b/internal/cloud/backend_apply.go index 26afd2b13411..27efb6951a45 100644 --- a/internal/cloud/backend_apply.go +++ b/internal/cloud/backend_apply.go @@ -83,8 +83,8 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backendrun.Opera var r *tfe.Run var err error - - if cp, ok := op.PlanFile.Cloud(); ok { + cp, hasSavedPlanFile := op.PlanFile.Cloud() + if hasSavedPlanFile { log.Printf("[TRACE] Loading saved cloud plan for apply") // Check hostname first, for a more actionable error than a generic 404 later if cp.Hostname != b.Hostname { @@ -182,7 +182,9 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backendrun.Opera } // Do the apply! - if !op.AutoApprove && err != errRunApproved { + // If we have a saved plan file, we proceed to apply the run without confirmation + // regardless of the value of AutoApprove. + if (!op.AutoApprove || hasSavedPlanFile) && err != errRunApproved { if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil { return r, b.generalError("Failed to approve the apply command", err) } diff --git a/internal/cloud/backend_apply_test.go b/internal/cloud/backend_apply_test.go index b3b73e5a24b0..969b8df31a44 100644 --- a/internal/cloud/backend_apply_test.go +++ b/internal/cloud/backend_apply_test.go @@ -508,6 +508,73 @@ func TestCloud_applyWithCloudPlan(t *testing.T) { } } +func TestCloud_applyAutoApprove_with_CloudPlan(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + op, configCleanup, done := testOperationApply(t, "./testdata/apply-json") + defer configCleanup() + defer done(t) + + op.AutoApprove = true + op.UIOut = b.CLI + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + ws, err := b.client.Workspaces.Read(context.Background(), b.Organization, b.WorkspaceMapping.Name) + if err != nil { + t.Fatalf("Couldn't read workspace: %s", err) + } + + planRun, err := b.plan(context.Background(), context.Background(), op, ws) + if err != nil { + t.Fatalf("Couldn't perform plan: %s", err) + } + + // Synthesize a cloud plan file with the plan's run ID + pf := &cloudplan.SavedPlanBookmark{ + RemotePlanFormat: 1, + RunID: planRun.ID, + Hostname: b.Hostname, + } + op.PlanFile = planfile.NewWrappedCloud(pf) + + // Start spying on the apply output (now that the plan's done) + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + // Try apply + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + output := close(t) + if run.Result != backendrun.OperationSuccess { + t.Fatal("expected apply operation to succeed") + } + if run.PlanEmpty { + t.Fatalf("expected plan to not be empty") + } + + gotOut := output.Stdout() + if !strings.Contains(gotOut, "1 added, 0 changed, 0 destroyed") { + t.Fatalf("expected apply summary in output: %s", gotOut) + } + + stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) + // An error suggests that the state was not unlocked after apply + if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { + t.Fatalf("unexpected error locking state after apply: %s", err.Error()) + } +} + func TestCloud_applyWithoutRefresh(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup()