mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge pull request #31821 from glennsarti/gs/TF-707-add-pre-apply
Add support for pre-apply task results in the cloud backend
This commit is contained in:
commit
bc1436af53
2
go.mod
2
go.mod
@ -39,7 +39,7 @@ require (
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-plugin v1.4.3
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1
|
||||
github.com/hashicorp/go-tfe v1.9.0
|
||||
github.com/hashicorp/go-tfe v1.10.0
|
||||
github.com/hashicorp/go-uuid v1.0.3
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
|
||||
|
4
go.sum
4
go.sum
@ -368,8 +368,8 @@ github.com/hashicorp/go-slug v0.10.0/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu4
|
||||
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-tfe v1.9.0 h1:jkmyo7WKNA7gZDegG5imndoC4sojWXhqMufO+KcHqrU=
|
||||
github.com/hashicorp/go-tfe v1.9.0/go.mod h1:uSWi2sPw7tLrqNIiASid9j3SprbbkPSJ/2s3X0mMemg=
|
||||
github.com/hashicorp/go-tfe v1.10.0 h1:mkEge/DSca8VQeBSAQbjEy8fWFHbrJA76M7dny5XlYc=
|
||||
github.com/hashicorp/go-tfe v1.10.0/go.mod h1:uSWi2sPw7tLrqNIiASid9j3SprbbkPSJ/2s3X0mMemg=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
|
@ -133,6 +133,19 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the run to get task stages.
|
||||
// Task Stages are calculated upfront so we only need to call this once for the run.
|
||||
taskStages, err := b.runTaskStages(stopCtx, b.client, r.ID)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
|
||||
if stage, ok := taskStages[tfe.PreApply]; ok {
|
||||
if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, stage.ID, "Pre-apply Tasks"); err != nil {
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
|
||||
r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w)
|
||||
if err != nil {
|
||||
return r, err
|
||||
|
@ -293,22 +293,13 @@ in order to capture the filesystem context the remote workspace expects:
|
||||
|
||||
// Retrieve the run to get task stages.
|
||||
// Task Stages are calculated upfront so we only need to call this once for the run.
|
||||
taskStages := make([]*tfe.TaskStage, 0)
|
||||
result, err := b.client.Runs.ReadWithOptions(stopCtx, r.ID, &tfe.RunReadOptions{
|
||||
Include: []tfe.RunIncludeOpt{tfe.RunTaskStages},
|
||||
})
|
||||
if err == nil {
|
||||
taskStages = result.TaskStages
|
||||
} else {
|
||||
// This error would be expected for older versions of TFE that do not allow
|
||||
// fetching task_stages.
|
||||
if !strings.HasSuffix(err.Error(), "Invalid include parameter") {
|
||||
return r, generalError("Failed to retrieve run", err)
|
||||
}
|
||||
taskStages, err := b.runTaskStages(stopCtx, b.client, r.ID)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
|
||||
if stageID := getTaskStageIDByName(taskStages, tfe.PrePlan); stageID != nil {
|
||||
if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, *stageID, "Pre-plan Tasks"); err != nil {
|
||||
if stage, ok := taskStages[tfe.PrePlan]; ok {
|
||||
if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, stage.ID, "Pre-plan Tasks"); err != nil {
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
@ -357,8 +348,8 @@ in order to capture the filesystem context the remote workspace expects:
|
||||
// status of the run will be "errored", but there is still policy
|
||||
// information which should be shown.
|
||||
|
||||
if stageID := getTaskStageIDByName(taskStages, tfe.PostPlan); stageID != nil {
|
||||
if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, *stageID, "Post-plan Tasks"); err != nil {
|
||||
if stage, ok := taskStages[tfe.PostPlan]; ok {
|
||||
if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, stage.ID, "Post-plan Tasks"); err != nil {
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
@ -382,19 +373,6 @@ in order to capture the filesystem context the remote workspace expects:
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func getTaskStageIDByName(stages []*tfe.TaskStage, stageName tfe.Stage) *string {
|
||||
if len(stages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, stage := range stages {
|
||||
if stage.Stage == stageName {
|
||||
return &stage.ID
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const planDefaultHeader = `
|
||||
[reset][yellow]Running plan in Terraform Cloud. Output will stream here. Pressing Ctrl-C
|
||||
will stop streaming the logs, but will not stop the plan running remotely.[reset]
|
||||
|
32
internal/cloud/backend_taskStages.go
Normal file
32
internal/cloud/backend_taskStages.go
Normal file
@ -0,0 +1,32 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
tfe "github.com/hashicorp/go-tfe"
|
||||
)
|
||||
|
||||
type taskStages map[tfe.Stage]*tfe.TaskStage
|
||||
|
||||
func (b *Cloud) runTaskStages(ctx context.Context, client *tfe.Client, runId string) (taskStages, error) {
|
||||
taskStages := make(taskStages, 0)
|
||||
result, err := client.Runs.ReadWithOptions(ctx, runId, &tfe.RunReadOptions{
|
||||
Include: []tfe.RunIncludeOpt{tfe.RunTaskStages},
|
||||
})
|
||||
if err == nil {
|
||||
for _, t := range result.TaskStages {
|
||||
if t != nil {
|
||||
taskStages[t.Stage] = t
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This error would be expected for older versions of TFE that do not allow
|
||||
// fetching task_stages.
|
||||
if !strings.HasSuffix(err.Error(), "Invalid include parameter") {
|
||||
return taskStages, generalError("Failed to retrieve run", err)
|
||||
}
|
||||
}
|
||||
|
||||
return taskStages, nil
|
||||
}
|
207
internal/cloud/backend_taskStages_test.go
Normal file
207
internal/cloud/backend_taskStages_test.go
Normal file
@ -0,0 +1,207 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/hashicorp/go-tfe"
|
||||
tfemocks "github.com/hashicorp/go-tfe/mocks"
|
||||
)
|
||||
|
||||
func MockAllTaskStages(t *testing.T, client *tfe.Client) (RunID string) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
RunID = "run-all_task_stages"
|
||||
|
||||
mockRunsAPI := tfemocks.NewMockRuns(ctrl)
|
||||
|
||||
goodRun := tfe.Run{
|
||||
TaskStages: []*tfe.TaskStage{
|
||||
{
|
||||
Stage: tfe.PrePlan,
|
||||
},
|
||||
{
|
||||
Stage: tfe.PostPlan,
|
||||
},
|
||||
{
|
||||
Stage: tfe.PreApply,
|
||||
},
|
||||
},
|
||||
}
|
||||
mockRunsAPI.
|
||||
EXPECT().
|
||||
ReadWithOptions(gomock.Any(), RunID, gomock.Any()).
|
||||
Return(&goodRun, nil).
|
||||
AnyTimes()
|
||||
|
||||
// Mock a bad Read response
|
||||
mockRunsAPI.
|
||||
EXPECT().
|
||||
ReadWithOptions(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, tfe.ErrInvalidOrg).
|
||||
AnyTimes()
|
||||
|
||||
// Wire up the mock interfaces
|
||||
client.Runs = mockRunsAPI
|
||||
return
|
||||
}
|
||||
|
||||
func MockPrePlanTaskStage(t *testing.T, client *tfe.Client) (RunID string) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
RunID = "run-pre_plan_task_stage"
|
||||
|
||||
mockRunsAPI := tfemocks.NewMockRuns(ctrl)
|
||||
|
||||
goodRun := tfe.Run{
|
||||
TaskStages: []*tfe.TaskStage{
|
||||
{
|
||||
Stage: tfe.PrePlan,
|
||||
},
|
||||
},
|
||||
}
|
||||
mockRunsAPI.
|
||||
EXPECT().
|
||||
ReadWithOptions(gomock.Any(), RunID, gomock.Any()).
|
||||
Return(&goodRun, nil).
|
||||
AnyTimes()
|
||||
|
||||
// Mock a bad Read response
|
||||
mockRunsAPI.
|
||||
EXPECT().
|
||||
ReadWithOptions(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, tfe.ErrInvalidOrg).
|
||||
AnyTimes()
|
||||
|
||||
// Wire up the mock interfaces
|
||||
client.Runs = mockRunsAPI
|
||||
return
|
||||
}
|
||||
|
||||
func MockTaskStageUnsupported(t *testing.T, client *tfe.Client) (RunID string) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
RunID = "run-unsupported_task_stage"
|
||||
|
||||
mockRunsAPI := tfemocks.NewMockRuns(ctrl)
|
||||
|
||||
mockRunsAPI.
|
||||
EXPECT().
|
||||
ReadWithOptions(gomock.Any(), RunID, gomock.Any()).
|
||||
Return(nil, errors.New("Invalid include parameter")).
|
||||
AnyTimes()
|
||||
|
||||
mockRunsAPI.
|
||||
EXPECT().
|
||||
ReadWithOptions(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, tfe.ErrInvalidOrg).
|
||||
AnyTimes()
|
||||
|
||||
client.Runs = mockRunsAPI
|
||||
return
|
||||
}
|
||||
|
||||
func TestTaskStagesWithAllStages(t *testing.T) {
|
||||
b, bCleanup := testBackendWithName(t)
|
||||
defer bCleanup()
|
||||
|
||||
config := &tfe.Config{
|
||||
Token: "not-a-token",
|
||||
}
|
||||
client, _ := tfe.NewClient(config)
|
||||
runID := MockAllTaskStages(t, client)
|
||||
|
||||
ctx := context.TODO()
|
||||
taskStages, err := b.runTaskStages(ctx, client, runID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected to not error but received %s", err)
|
||||
}
|
||||
|
||||
for _, stageName := range []tfe.Stage{
|
||||
tfe.PrePlan,
|
||||
tfe.PostPlan,
|
||||
tfe.PreApply,
|
||||
} {
|
||||
if stage, ok := taskStages[stageName]; ok {
|
||||
if stage.Stage != stageName {
|
||||
t.Errorf("Expected task stage indexed by %s to find a Task Stage with the same index, but receieved %s", stageName, stage.Stage)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Expected task stage indexed by %s to exist, but it did not", stageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskStagesWithOneStage(t *testing.T) {
|
||||
b, bCleanup := testBackendWithName(t)
|
||||
defer bCleanup()
|
||||
|
||||
config := &tfe.Config{
|
||||
Token: "not-a-token",
|
||||
}
|
||||
client, _ := tfe.NewClient(config)
|
||||
runID := MockPrePlanTaskStage(t, client)
|
||||
|
||||
ctx := context.TODO()
|
||||
taskStages, err := b.runTaskStages(ctx, client, runID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected to not error but received %s", err)
|
||||
}
|
||||
|
||||
if _, ok := taskStages[tfe.PrePlan]; !ok {
|
||||
t.Errorf("Expected task stage indexed by %s to exist, but it did not", tfe.PrePlan)
|
||||
}
|
||||
|
||||
for _, stageName := range []tfe.Stage{
|
||||
tfe.PostPlan,
|
||||
tfe.PreApply,
|
||||
} {
|
||||
if _, ok := taskStages[stageName]; ok {
|
||||
t.Errorf("Expected task stage indexed by %s to not exist, but it did", stageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskStagesWithOldTFC(t *testing.T) {
|
||||
b, bCleanup := testBackendWithName(t)
|
||||
defer bCleanup()
|
||||
|
||||
config := &tfe.Config{
|
||||
Token: "not-a-token",
|
||||
}
|
||||
client, _ := tfe.NewClient(config)
|
||||
runID := MockTaskStageUnsupported(t, client)
|
||||
|
||||
ctx := context.TODO()
|
||||
taskStages, err := b.runTaskStages(ctx, client, runID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected to not error but received %s", err)
|
||||
}
|
||||
|
||||
if len(taskStages) != 0 {
|
||||
t.Errorf("Expected task stage to be empty, but found %d stages", len(taskStages))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskStagesWithErrors(t *testing.T) {
|
||||
b, bCleanup := testBackendWithName(t)
|
||||
defer bCleanup()
|
||||
|
||||
config := &tfe.Config{
|
||||
Token: "not-a-token",
|
||||
}
|
||||
client, _ := tfe.NewClient(config)
|
||||
MockTaskStageUnsupported(t, client)
|
||||
|
||||
ctx := context.TODO()
|
||||
_, err := b.runTaskStages(ctx, client, "this run ID will not exist is invalid anyway")
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected to error but did not")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user