Skip to content

Commit 7c91cb2

Browse files
committed
Add polling to wait for post plan tasks to complete in remote backend
1 parent 8e9b44d commit 7c91cb2

File tree

4 files changed

+124
-0
lines changed

4 files changed

+124
-0
lines changed

internal/backend/remote/backend_common.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,33 @@ func backoff(min, max float64, iter int) time.Duration {
4747
return time.Duration(backoff) * time.Millisecond
4848
}
4949

50+
func (b *Remote) waitForPostPlanTasks(stopCtx, cancelCtx context.Context, op *backendrun.Operation, r *tfe.Run) error {
51+
taskStages := make(taskStages, 0)
52+
result, err := b.client.Runs.ReadWithOptions(stopCtx, r.ID, &tfe.RunReadOptions{
53+
Include: []tfe.RunIncludeOpt{tfe.RunTaskStages},
54+
})
55+
if err == nil {
56+
for _, t := range result.TaskStages {
57+
if t != nil {
58+
taskStages[t.Stage] = t
59+
}
60+
}
61+
} else {
62+
// This error would be expected for older versions of TFE that do not allow
63+
// fetching task_stages.
64+
if !strings.HasSuffix(err.Error(), "Invalid include parameter") {
65+
generalError("Failed to retrieve run", err)
66+
}
67+
}
68+
69+
if stage, ok := taskStages[tfe.PostPlan]; ok {
70+
if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, stage.ID); err != nil {
71+
return err
72+
}
73+
}
74+
return nil
75+
}
76+
5077
func (b *Remote) waitForRun(stopCtx, cancelCtx context.Context, op *backendrun.Operation, opType string, r *tfe.Run, w *tfe.Workspace) (*tfe.Run, error) {
5178
started := time.Now()
5279
updated := started

internal/backend/remote/backend_plan.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,15 @@ in order to capture the filesystem context the remote workspace expects:
411411
return r, generalError("Failed to retrieve run", err)
412412
}
413413

414+
// Wait for post plan tasks to complete before proceeding.
415+
// Otherwise, in the case of an apply, if they are still running
416+
// when we check for whether the run is confirmable the CLI will
417+
// uncermoniously exit before the user has a chance to confirm, or for an auto-apply to take place.
418+
err = b.waitForPostPlanTasks(stopCtx, cancelCtx, op, r)
419+
if err != nil {
420+
return r, err
421+
}
422+
414423
// If the run is canceled or errored, we still continue to the
415424
// cost-estimation and policy check phases to ensure we render any
416425
// results available. In the case of a hard-failed policy check, the
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package remote
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
tfe "github.com/hashicorp/go-tfe"
8+
"github.com/hashicorp/terraform/internal/backend/backendrun"
9+
)
10+
11+
type taskStages map[tfe.Stage]*tfe.TaskStage
12+
13+
const (
14+
taskStageBackoffMin = 4000.0
15+
taskStageBackoffMax = 12000.0
16+
)
17+
18+
// waitTaskStage waits for a task stage to complete, only informs the caller if the stage has failed in some way.
19+
func (b *Remote) waitTaskStage(stopCtx, cancelCtx context.Context, op *backendrun.Operation, r *tfe.Run, stageID string) error {
20+
ctx := &IntegrationContext{
21+
StopContext: stopCtx,
22+
CancelContext: cancelCtx,
23+
}
24+
return ctx.Poll(taskStageBackoffMin, taskStageBackoffMax, func(i int) (bool, error) {
25+
options := tfe.TaskStageReadOptions{
26+
Include: []tfe.TaskStageIncludeOpt{tfe.TaskStageTaskResults, tfe.PolicyEvaluationsTaskResults},
27+
}
28+
stage, err := b.client.TaskStages.Read(ctx.StopContext, stageID, &options)
29+
if err != nil {
30+
return false, generalError("Failed to retrieve task stage", err)
31+
}
32+
33+
switch stage.Status {
34+
case tfe.TaskStagePending:
35+
// Waiting for it to start
36+
return true, nil
37+
case tfe.TaskStageRunning:
38+
// not a terminal status so we continue to poll
39+
return true, nil
40+
case tfe.TaskStagePassed:
41+
return false, nil
42+
case tfe.TaskStageCanceled, tfe.TaskStageErrored, tfe.TaskStageFailed:
43+
return false, fmt.Errorf("Task Stage '%s': %s.", stage.ID, stage.Status)
44+
case tfe.TaskStageAwaitingOverride:
45+
return false, fmt.Errorf("Task Stage '%s' awaiting override.", stage.ID, stage.Status)
46+
case tfe.TaskStageUnreachable:
47+
return false, nil
48+
default:
49+
return false, fmt.Errorf("Task stage '%s' has invalid status: %s", stage.ID, stage.Status)
50+
}
51+
})
52+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package remote
5+
6+
import (
7+
"context"
8+
"log"
9+
"time"
10+
)
11+
12+
// IntegrationContext is a set of data that is useful when performing HCP Terraform integration operations
13+
type IntegrationContext struct {
14+
StopContext context.Context
15+
CancelContext context.Context
16+
}
17+
18+
func (s *IntegrationContext) Poll(backoffMinInterval float64, backoffMaxInterval float64, every func(i int) (bool, error)) error {
19+
for i := 0; ; i++ {
20+
select {
21+
case <-s.StopContext.Done():
22+
log.Print("IntegrationContext.Poll: StopContext.Done() called")
23+
return s.StopContext.Err()
24+
case <-s.CancelContext.Done():
25+
log.Print("IntegrationContext.Poll: CancelContext.Done() called")
26+
return s.CancelContext.Err()
27+
case <-time.After(backoff(backoffMinInterval, backoffMaxInterval, i)):
28+
// blocks for a time between min and max
29+
}
30+
31+
cont, err := every(i)
32+
if !cont {
33+
return err
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)