github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/cloud/backend_apply.go (about) 1 package cloud 2 3 import ( 4 "bufio" 5 "context" 6 "encoding/json" 7 "io" 8 "log" 9 10 tfe "github.com/hashicorp/go-tfe" 11 "github.com/hashicorp/terraform/internal/backend" 12 "github.com/hashicorp/terraform/internal/command/jsonformat" 13 "github.com/hashicorp/terraform/internal/plans" 14 "github.com/hashicorp/terraform/internal/terraform" 15 "github.com/hashicorp/terraform/internal/tfdiags" 16 ) 17 18 func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { 19 log.Printf("[INFO] cloud: starting Apply operation") 20 21 var diags tfdiags.Diagnostics 22 23 // We should remove the `CanUpdate` part of this test, but for now 24 // (to remain compatible with tfe.v2.1) we'll leave it in here. 25 if !w.Permissions.CanUpdate && !w.Permissions.CanQueueApply { 26 diags = diags.Append(tfdiags.Sourceless( 27 tfdiags.Error, 28 "Insufficient rights to apply changes", 29 "The provided credentials have insufficient rights to apply changes. In order "+ 30 "to apply changes at least write permissions on the workspace are required.", 31 )) 32 return nil, diags.Err() 33 } 34 35 if w.VCSRepo != nil { 36 diags = diags.Append(tfdiags.Sourceless( 37 tfdiags.Error, 38 "Apply not allowed for workspaces with a VCS connection", 39 "A workspace that is connected to a VCS requires the VCS-driven workflow "+ 40 "to ensure that the VCS remains the single source of truth.", 41 )) 42 return nil, diags.Err() 43 } 44 45 if b.ContextOpts != nil && b.ContextOpts.Parallelism != defaultParallelism { 46 diags = diags.Append(tfdiags.Sourceless( 47 tfdiags.Error, 48 "Custom parallelism values are currently not supported", 49 `Terraform Cloud does not support setting a custom parallelism `+ 50 `value at this time.`, 51 )) 52 } 53 54 if op.PlanFile != nil { 55 diags = diags.Append(tfdiags.Sourceless( 56 tfdiags.Error, 57 "Applying a saved plan is currently not supported", 58 `Terraform Cloud currently requires configuration to be present and `+ 59 `does not accept an existing saved plan as an argument at this time.`, 60 )) 61 } 62 63 if !op.HasConfig() && op.PlanMode != plans.DestroyMode { 64 diags = diags.Append(tfdiags.Sourceless( 65 tfdiags.Error, 66 "No configuration files found", 67 `Apply requires configuration to be present. Applying without a configuration `+ 68 `would mark everything for destruction, which is normally not what is desired. `+ 69 `If you would like to destroy everything, please run 'terraform destroy' which `+ 70 `does not require any configuration files.`, 71 )) 72 } 73 74 // Return if there are any errors. 75 if diags.HasErrors() { 76 return nil, diags.Err() 77 } 78 79 // Run the plan phase. 80 r, err := b.plan(stopCtx, cancelCtx, op, w) 81 if err != nil { 82 return r, err 83 } 84 85 // This check is also performed in the plan method to determine if 86 // the policies should be checked, but we need to check the values 87 // here again to determine if we are done and should return. 88 if !r.HasChanges || r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored { 89 return r, nil 90 } 91 92 // Retrieve the run to get its current status. 93 r, err = b.client.Runs.Read(stopCtx, r.ID) 94 if err != nil { 95 return r, generalError("Failed to retrieve run", err) 96 } 97 98 // Return if the run cannot be confirmed. 99 if !op.AutoApprove && !r.Actions.IsConfirmable { 100 return r, nil 101 } 102 103 mustConfirm := (op.UIIn != nil && op.UIOut != nil) && !op.AutoApprove 104 105 if mustConfirm && b.input { 106 opts := &terraform.InputOpts{Id: "approve"} 107 108 if op.PlanMode == plans.DestroyMode { 109 opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" 110 opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" + 111 "There is no undo. Only 'yes' will be accepted to confirm." 112 } else { 113 opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?" 114 opts.Description = "Terraform will perform the actions described above.\n" + 115 "Only 'yes' will be accepted to approve." 116 } 117 118 err = b.confirm(stopCtx, op, opts, r, "yes") 119 if err != nil && err != errRunApproved { 120 return r, err 121 } 122 } else if mustConfirm && !b.input { 123 return r, errApplyNeedsUIConfirmation 124 } else { 125 // If we don't need to ask for confirmation, insert a blank 126 // line to separate the ouputs. 127 if b.CLI != nil { 128 b.CLI.Output("") 129 } 130 } 131 132 if !op.AutoApprove && err != errRunApproved { 133 if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil { 134 return r, generalError("Failed to approve the apply command", err) 135 } 136 } 137 138 // Retrieve the run to get task stages. 139 // Task Stages are calculated upfront so we only need to call this once for the run. 140 taskStages, err := b.runTaskStages(stopCtx, b.client, r.ID) 141 if err != nil { 142 return r, err 143 } 144 145 if stage, ok := taskStages[tfe.PreApply]; ok { 146 if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, stage.ID, "Pre-apply Tasks"); err != nil { 147 return r, err 148 } 149 } 150 151 r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w) 152 if err != nil { 153 return r, err 154 } 155 156 err = b.renderApplyLogs(stopCtx, r) 157 if err != nil { 158 return r, err 159 } 160 161 return r, nil 162 } 163 164 func (b *Cloud) renderApplyLogs(ctx context.Context, run *tfe.Run) error { 165 logs, err := b.client.Applies.Logs(ctx, run.Apply.ID) 166 if err != nil { 167 return err 168 } 169 170 if b.CLI != nil { 171 reader := bufio.NewReaderSize(logs, 64*1024) 172 skip := 0 173 174 for next := true; next; { 175 var l, line []byte 176 var err error 177 178 for isPrefix := true; isPrefix; { 179 l, isPrefix, err = reader.ReadLine() 180 if err != nil { 181 if err != io.EOF { 182 return generalError("Failed to read logs", err) 183 } 184 next = false 185 } 186 187 line = append(line, l...) 188 } 189 190 // Apply logs show the same Terraform info logs as shown in the plan logs 191 // (which contain version and os/arch information), we therefore skip to prevent duplicate output. 192 if skip < 3 { 193 skip++ 194 continue 195 } 196 197 if next || len(line) > 0 { 198 log := &jsonformat.JSONLog{} 199 if err := json.Unmarshal(line, log); err != nil { 200 // If we can not parse the line as JSON, we will simply 201 // print the line. This maintains backwards compatibility for 202 // users who do not wish to enable structured output in their 203 // workspace. 204 b.CLI.Output(string(line)) 205 continue 206 } 207 208 if b.renderer != nil { 209 // Otherwise, we will print the log 210 err := b.renderer.RenderLog(log) 211 if err != nil { 212 return err 213 } 214 } 215 } 216 } 217 } 218 219 return nil 220 } 221 222 const applyDefaultHeader = ` 223 [reset][yellow]Running apply in Terraform Cloud. Output will stream here. Pressing Ctrl-C 224 will cancel the remote apply if it's still pending. If the apply started it 225 will stop streaming the logs, but will not stop the apply running remotely.[reset] 226 227 Preparing the remote apply... 228 `