Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement plan -out with cloud integration #33418

Merged
merged 9 commits into from
Jul 6, 2023
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,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.4
github.com/hashicorp/go-tfe v1.28.0
github.com/hashicorp/go-tfe v1.29.0
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hcl v1.0.0
Expand Down
5 changes: 3 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -621,8 +621,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-tfe v1.28.0 h1:YQNfHz5UPMiOD2idad4GCjzG3R2ExPww741PBPqMOIU=
github.com/hashicorp/go-tfe v1.28.0/go.mod h1:z0182DGE/63AKUaWblUVBIrt+xdSmsuuXg5AoxGqDF4=
github.com/hashicorp/go-tfe v1.29.0 h1:hVvgoKtLAWTkXl9p/8WnItCaW65VJwqpjLZkXe8R2AM=
github.com/hashicorp/go-tfe v1.29.0/go.mod h1:z0182DGE/63AKUaWblUVBIrt+xdSmsuuXg5AoxGqDF4=
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=
Expand Down Expand Up @@ -1175,6 +1175,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
36 changes: 25 additions & 11 deletions internal/cloud/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
version "github.com/hashicorp/go-version"

"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/cloud/cloudplan"
"github.com/hashicorp/terraform/internal/command/jsonformat"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/genconfig"
Expand Down Expand Up @@ -65,15 +66,6 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation
))
}

if op.PlanOutPath != "" {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Saving a generated plan is currently not supported",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love it when "currently" becomes the past 😆

`Terraform Cloud does not support saving the generated execution `+
`plan locally at this time.`,
))
}

if !op.HasConfig() && op.PlanMode != plans.DestroyMode {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
Expand All @@ -95,7 +87,25 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation
return nil, diags.Err()
}

return b.plan(stopCtx, cancelCtx, op, w)
// If the run errored, exit before checking whether to save a plan file
run, err := b.plan(stopCtx, cancelCtx, op, w)
if err != nil {
return run, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we return an error, we should be make it clear that we are returning nil, err, I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yeah, all right

}

// Maybe save plan file
sebasslash marked this conversation as resolved.
Show resolved Hide resolved
if op.PlanOutPath != "" {
bookmark := cloudplan.NewSavedPlanBookmark(run.ID, b.hostname)
err = bookmark.Save(op.PlanOutPath)
}

// Only display next steps if everything succeeded
if err == nil {
// Cloud currently supports plan -out but not genconfig
op.View.PlanNextStep(op.PlanOutPath, "")
}
sebasslash marked this conversation as resolved.
Show resolved Hide resolved

return run, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestion here. I think we want to be clear about returning nil if we are also returning an error.

}

func (b *Cloud) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) {
Expand All @@ -107,9 +117,12 @@ func (b *Cloud) plan(stopCtx, cancelCtx context.Context, op *backend.Operation,
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(header) + "\n"))
}

// Plan-only means they ran terraform plan without -out.
planOnly := op.Type == backend.OperationTypePlan && op.PlanOutPath == ""

configOptions := tfe.ConfigurationVersionCreateOptions{
AutoQueueRuns: tfe.Bool(false),
Speculative: tfe.Bool(op.Type == backend.OperationTypePlan),
Speculative: tfe.Bool(planOnly),
}

cv, err := b.client.ConfigurationVersions.Create(stopCtx, w.ID, configOptions)
Expand Down Expand Up @@ -206,6 +219,7 @@ in order to capture the filesystem context the remote workspace expects:
Refresh: tfe.Bool(op.PlanRefresh),
Workspace: w,
AutoApply: tfe.Bool(op.AutoApprove),
SavePlan: tfe.Bool(op.PlanOutPath != ""),
}

switch op.PlanMode {
Expand Down
32 changes: 23 additions & 9 deletions internal/cloud/backend_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/cloud/cloudplan"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate"
"github.com/hashicorp/terraform/internal/command/jsonformat"
Expand Down Expand Up @@ -365,8 +366,11 @@ func TestCloud_planWithPath(t *testing.T) {

op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
defer done(t)

op.PlanOutPath = "./testdata/plan"
tmpDir := t.TempDir()
pfPath := tmpDir + "/plan.tfplan"
op.PlanOutPath = pfPath
op.Workspace = testBackendSingleWorkspaceName

run, err := b.Operation(context.Background(), op)
Expand All @@ -375,17 +379,27 @@ func TestCloud_planWithPath(t *testing.T) {
}

<-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail")
if run.Result != backend.OperationSuccess {
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
}
if !run.PlanEmpty {
t.Fatalf("expected plan to be empty")
if run.PlanEmpty {
t.Fatal("expected a non-empty plan")
}

errOutput := output.Stderr()
if !strings.Contains(errOutput, "generated plan is currently not supported") {
t.Fatalf("expected a generated plan error, got: %v", errOutput)
output := b.CLI.(*cli.MockUi).OutputWriter.String()
if !strings.Contains(output, "Running plan in Terraform Cloud") {
t.Fatalf("expected TFC header in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summary in output: %s", output)
}

plan, err := cloudplan.LoadSavedPlanBookmark(pfPath)
if err != nil {
t.Fatalf("error loading cloud plan file: %v", err)
}
if !strings.Contains(plan.RunID, "run-") || plan.Hostname != "app.terraform.io" {
t.Fatalf("unexpected contents in saved cloud plan: %v", plan)
}
}

Expand Down
8 changes: 8 additions & 0 deletions internal/cloud/cloudplan/saved_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ type SavedPlanBookmark struct {
Hostname string `json:"hostname"`
}

func NewSavedPlanBookmark(runID, hostname string) SavedPlanBookmark {
return SavedPlanBookmark{
RemotePlanFormat: 1,
RunID: runID,
Hostname: hostname,
}
}

func LoadSavedPlanBookmark(filepath string) (SavedPlanBookmark, error) {
bookmark := SavedPlanBookmark{}

Expand Down