@@ -7,8 +7,10 @@ import (
77 "bufio"
88 "context"
99 "encoding/json"
10+ "fmt"
1011 "io"
1112 "log"
13+ "strings"
1214
1315 tfe "github.com/hashicorp/go-tfe"
1416 "github.com/hashicorp/terraform/internal/backend"
@@ -54,12 +56,12 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio
5456 ))
5557 }
5658
57- if op .PlanFile != nil {
59+ if op .PlanFile . IsLocal () {
5860 diags = diags .Append (tfdiags .Sourceless (
5961 tfdiags .Error ,
60- "Applying a saved plan is currently not supported" ,
61- `Terraform Cloud currently requires configuration to be present and ` +
62- `does not accept an existing saved plan as an argument at this time .` ,
62+ "Applying a saved local plan is not supported" ,
63+ `Terraform Cloud can apply a saved cloud plan, or create a new plan when ` +
64+ `configuration is present. It cannot apply a saved local plan .` ,
6365 ))
6466 }
6567
@@ -79,59 +81,107 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio
7981 return nil , diags .Err ()
8082 }
8183
82- // Run the plan phase.
83- r , err := b .plan (stopCtx , cancelCtx , op , w )
84- if err != nil {
85- return r , err
86- }
84+ var r * tfe.Run
85+ var err error
86+
87+ if cp , ok := op .PlanFile .Cloud (); ok {
88+ log .Printf ("[TRACE] Loading saved cloud plan for apply" )
89+ // Check hostname first, for a more actionable error than a generic 404 later
90+ if cp .Hostname != b .hostname {
91+ diags = diags .Append (tfdiags .Sourceless (
92+ tfdiags .Error ,
93+ "Saved plan is for a different hostname" ,
94+ fmt .Sprintf ("The given saved plan refers to a run on %s, but the currently configured Terraform Cloud or Terraform Enterprise instance is %s." , cp .Hostname , b .hostname ),
95+ ))
96+ return r , diags .Err ()
97+ }
98+ // Fetch the run referenced in the saved plan bookmark.
99+ r , err = b .client .Runs .ReadWithOptions (stopCtx , cp .RunID , & tfe.RunReadOptions {
100+ Include : []tfe.RunIncludeOpt {tfe .RunWorkspace },
101+ })
87102
88- // This check is also performed in the plan method to determine if
89- // the policies should be checked, but we need to check the values
90- // here again to determine if we are done and should return.
91- if ! r .HasChanges || r .Status == tfe .RunCanceled || r .Status == tfe .RunErrored {
92- return r , nil
93- }
103+ if err != nil {
104+ return r , err
105+ }
94106
95- // Retrieve the run to get its current status.
96- r , err = b .client .Runs .Read (stopCtx , r .ID )
97- if err != nil {
98- return r , generalError ("Failed to retrieve run" , err )
99- }
107+ if r .Workspace .ID != w .ID {
108+ diags = diags .Append (tfdiags .Sourceless (
109+ tfdiags .Error ,
110+ "Saved plan is for a different workspace" ,
111+ fmt .Sprintf ("The given saved plan does not refer to a run in the current workspace (%s/%s), so it cannot currently be applied. For more details, view this run in a browser at:\n %s" , w .Organization .Name , w .Name , runURL (b .hostname , r .Workspace .Organization .Name , r .Workspace .Name , r .ID )),
112+ ))
113+ return r , diags .Err ()
114+ }
100115
101- // Return if the run cannot be confirmed.
102- if ! op . AutoApprove && ! r . Actions . IsConfirmable {
103- return r , nil
104- }
116+ if ! r . Actions . IsConfirmable {
117+ url := runURL ( b . hostname , b . organization , op . Workspace , r . ID )
118+ return r , unusableSavedPlanError ( r . Status , url )
119+ }
105120
106- mustConfirm := (op .UIIn != nil && op .UIOut != nil ) && ! op .AutoApprove
121+ // Since we're not calling plan(), we need to print a run header ourselves:
122+ if b .CLI != nil {
123+ b .CLI .Output (b .Colorize ().Color (strings .TrimSpace (applySavedHeader ) + "\n " ))
124+ b .CLI .Output (b .Colorize ().Color (strings .TrimSpace (fmt .Sprintf (
125+ runHeader , b .hostname , b .organization , r .Workspace .Name , r .ID )) + "\n " ))
126+ }
127+ } else {
128+ log .Printf ("[TRACE] Running new cloud plan for apply" )
129+ // Run the plan phase.
130+ r , err = b .plan (stopCtx , cancelCtx , op , w )
107131
108- if mustConfirm && b .input {
109- opts := & terraform.InputOpts {Id : "approve" }
132+ if err != nil {
133+ return r , err
134+ }
110135
111- if op .PlanMode == plans .DestroyMode {
112- opts .Query = "\n Do you really want to destroy all resources in workspace \" " + op .Workspace + "\" ?"
113- opts .Description = "Terraform will destroy all your managed infrastructure, as shown above.\n " +
114- "There is no undo. Only 'yes' will be accepted to confirm."
115- } else {
116- opts .Query = "\n Do you want to perform these actions in workspace \" " + op .Workspace + "\" ?"
117- opts .Description = "Terraform will perform the actions described above.\n " +
118- "Only 'yes' will be accepted to approve."
136+ // This check is also performed in the plan method to determine if
137+ // the policies should be checked, but we need to check the values
138+ // here again to determine if we are done and should return.
139+ if ! r .HasChanges || r .Status == tfe .RunCanceled || r .Status == tfe .RunErrored {
140+ return r , nil
119141 }
120142
121- err = b .confirm (stopCtx , op , opts , r , "yes" )
122- if err != nil && err != errRunApproved {
123- return r , err
143+ // Retrieve the run to get its current status.
144+ r , err = b .client .Runs .Read (stopCtx , r .ID )
145+ if err != nil {
146+ return r , generalError ("Failed to retrieve run" , err )
124147 }
125- } else if mustConfirm && ! b .input {
126- return r , errApplyNeedsUIConfirmation
127- } else {
128- // If we don't need to ask for confirmation, insert a blank
129- // line to separate the ouputs.
130- if b .CLI != nil {
131- b .CLI .Output ("" )
148+
149+ // Return if the run cannot be confirmed.
150+ if ! op .AutoApprove && ! r .Actions .IsConfirmable {
151+ return r , nil
152+ }
153+
154+ mustConfirm := (op .UIIn != nil && op .UIOut != nil ) && ! op .AutoApprove
155+
156+ if mustConfirm && b .input {
157+ opts := & terraform.InputOpts {Id : "approve" }
158+
159+ if op .PlanMode == plans .DestroyMode {
160+ opts .Query = "\n Do you really want to destroy all resources in workspace \" " + op .Workspace + "\" ?"
161+ opts .Description = "Terraform will destroy all your managed infrastructure, as shown above.\n " +
162+ "There is no undo. Only 'yes' will be accepted to confirm."
163+ } else {
164+ opts .Query = "\n Do you want to perform these actions in workspace \" " + op .Workspace + "\" ?"
165+ opts .Description = "Terraform will perform the actions described above.\n " +
166+ "Only 'yes' will be accepted to approve."
167+ }
168+
169+ err = b .confirm (stopCtx , op , opts , r , "yes" )
170+ if err != nil && err != errRunApproved {
171+ return r , err
172+ }
173+ } else if mustConfirm && ! b .input {
174+ return r , errApplyNeedsUIConfirmation
175+ } else {
176+ // If we don't need to ask for confirmation, insert a blank
177+ // line to separate the ouputs.
178+ if b .CLI != nil {
179+ b .CLI .Output ("" )
180+ }
132181 }
133182 }
134183
184+ // Do the apply!
135185 if ! op .AutoApprove && err != errRunApproved {
136186 if err = b .client .Runs .Apply (stopCtx , r .ID , tfe.RunApplyOptions {}); err != nil {
137187 return r , generalError ("Failed to approve the apply command" , err )
@@ -222,10 +272,63 @@ func (b *Cloud) renderApplyLogs(ctx context.Context, run *tfe.Run) error {
222272 return nil
223273}
224274
275+ func runURL (hostname , orgName , wsName , runID string ) string {
276+ return fmt .Sprintf ("https://%s/app/%s/%s/runs/%s" , hostname , orgName , wsName , runID )
277+ }
278+
279+ func unusableSavedPlanError (status tfe.RunStatus , url string ) error {
280+ var diags tfdiags.Diagnostics
281+ var summary , reason string
282+
283+ switch status {
284+ case tfe .RunApplied :
285+ summary = "Saved plan is already applied"
286+ reason = "The given plan file was already successfully applied, and cannot be applied again."
287+ case tfe .RunApplying , tfe .RunApplyQueued , tfe .RunConfirmed :
288+ summary = "Saved plan is already confirmed"
289+ reason = "The given plan file is already being applied, and cannot be applied again."
290+ case tfe .RunCanceled :
291+ summary = "Saved plan is canceled"
292+ reason = "The given plan file can no longer be applied because the run was canceled via the Terraform Cloud UI or API."
293+ case tfe .RunDiscarded :
294+ summary = "Saved plan is discarded"
295+ reason = "The given plan file can no longer be applied; either another run was applied first, or a user discarded it via the Terraform Cloud UI or API."
296+ case tfe .RunErrored :
297+ summary = "Saved plan is errored"
298+ reason = "The given plan file refers to a plan that had errors and did not complete successfully. It cannot be applied."
299+ case tfe .RunPlannedAndFinished :
300+ // Note: planned and finished can also indicate a plan-only run, but
301+ // terraform plan can't create a saved plan for a plan-only run, so we
302+ // know it's no-changes in this case.
303+ summary = "Saved plan has no changes"
304+ reason = "The given plan file contains no changes, so it cannot be applied."
305+ case tfe .RunPolicyOverride :
306+ summary = "Saved plan requires policy override"
307+ reason = "The given plan file has soft policy failures, and cannot be applied until a user with appropriate permissions overrides the policy check."
308+ default :
309+ summary = "Saved plan cannot be applied"
310+ reason = "Terraform Cloud cannot apply the given plan file. This may mean the plan and checks have not yet completed, or may indicate another problem."
311+ }
312+
313+ diags = diags .Append (tfdiags .Sourceless (
314+ tfdiags .Error ,
315+ summary ,
316+ fmt .Sprintf ("%s For more details, view this run in a browser at:\n %s" , reason , url ),
317+ ))
318+ return diags .Err ()
319+ }
320+
225321const applyDefaultHeader = `
226322[reset][yellow]Running apply in Terraform Cloud. Output will stream here. Pressing Ctrl-C
227323will cancel the remote apply if it's still pending. If the apply started it
228324will stop streaming the logs, but will not stop the apply running remotely.[reset]
229325
230326Preparing the remote apply...
231327`
328+
329+ const applySavedHeader = `
330+ [reset][yellow]Running apply in Terraform Cloud. Output will stream here. Pressing Ctrl-C
331+ will stop streaming the logs, but will not stop the apply running remotely.[reset]
332+
333+ Preparing the remote apply...
334+ `
0 commit comments