github.com/pdecat/terraform@v0.11.9-beta1/backend/remote/backend_apply.go (about) 1 package remote 2 3 import ( 4 "bufio" 5 "context" 6 "errors" 7 "fmt" 8 "log" 9 "strings" 10 11 tfe "github.com/hashicorp/go-tfe" 12 "github.com/hashicorp/terraform/backend" 13 "github.com/hashicorp/terraform/terraform" 14 ) 15 16 func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation) (*tfe.Run, error) { 17 log.Printf("[INFO] backend/remote: starting Apply operation") 18 19 // Retrieve the workspace used to run this operation in. 20 w, err := b.client.Workspaces.Read(stopCtx, b.organization, op.Workspace) 21 if err != nil { 22 return nil, generalError("error retrieving workspace", err) 23 } 24 25 if !w.Permissions.CanUpdate { 26 return nil, fmt.Errorf(strings.TrimSpace(applyErrNoUpdateRights)) 27 } 28 29 if w.VCSRepo != nil { 30 return nil, fmt.Errorf(strings.TrimSpace(applyErrVCSNotSupported)) 31 } 32 33 if op.Parallelism != defaultParallelism { 34 return nil, fmt.Errorf(strings.TrimSpace(applyErrParallelismNotSupported)) 35 } 36 37 if op.Plan != nil { 38 return nil, fmt.Errorf(strings.TrimSpace(applyErrPlanNotSupported)) 39 } 40 41 if !op.PlanRefresh { 42 return nil, fmt.Errorf(strings.TrimSpace(applyErrNoRefreshNotSupported)) 43 } 44 45 if op.Targets != nil { 46 return nil, fmt.Errorf(strings.TrimSpace(applyErrTargetsNotSupported)) 47 } 48 49 if op.Variables != nil { 50 return nil, fmt.Errorf(strings.TrimSpace( 51 fmt.Sprintf(applyErrVariablesNotSupported, b.hostname, b.organization, op.Workspace))) 52 } 53 54 if (op.Module == nil || op.Module.Config().Dir == "") && !op.Destroy { 55 return nil, fmt.Errorf(strings.TrimSpace(applyErrNoConfig)) 56 } 57 58 // Run the plan phase. 59 r, err := b.plan(stopCtx, cancelCtx, op, w) 60 if err != nil { 61 return r, err 62 } 63 64 // Retrieve the run to get its current status. 65 r, err = b.client.Runs.Read(stopCtx, r.ID) 66 if err != nil { 67 return r, generalError("error retrieving run", err) 68 } 69 70 // Return if there are no changes or the run errored. We return 71 // without an error, even if the run errored, as the error is 72 // already displayed by the output of the remote run. 73 if !r.HasChanges || r.Status == tfe.RunErrored { 74 return r, nil 75 } 76 77 // Check any configured sentinel policies. 78 if len(r.PolicyChecks) > 0 { 79 err = b.checkPolicy(stopCtx, cancelCtx, op, r) 80 if err != nil { 81 return r, err 82 } 83 } 84 85 // Retrieve the run to get its current status. 86 r, err = b.client.Runs.Read(stopCtx, r.ID) 87 if err != nil { 88 return r, generalError("error retrieving run", err) 89 } 90 91 // Return if the run cannot be confirmed. 92 if !w.AutoApply && !r.Actions.IsConfirmable { 93 return r, nil 94 } 95 96 // Since we already checked the permissions before creating the run 97 // this should never happen. But it doesn't hurt to keep this in as 98 // a safeguard for any unexpected situations. 99 if !w.AutoApply && !r.Permissions.CanApply { 100 // Make sure we discard the run if possible. 101 if r.Actions.IsDiscardable { 102 err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{}) 103 if err != nil { 104 if op.Destroy { 105 return r, generalError("error disarding destroy", err) 106 } 107 return r, generalError("error disarding apply", err) 108 } 109 } 110 return r, fmt.Errorf(strings.TrimSpace( 111 fmt.Sprintf(applyErrNoApplyRights, b.hostname, b.organization, op.Workspace))) 112 } 113 114 mustConfirm := (op.UIIn != nil && op.UIOut != nil) && 115 ((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove)) 116 117 if !w.AutoApply { 118 if mustConfirm { 119 opts := &terraform.InputOpts{Id: "approve"} 120 121 if op.Destroy { 122 opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" 123 opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" + 124 "There is no undo. Only 'yes' will be accepted to confirm." 125 } else { 126 opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?" 127 opts.Description = "Terraform will perform the actions described above.\n" + 128 "Only 'yes' will be accepted to approve." 129 } 130 131 if err = b.confirm(stopCtx, op, opts, r, "yes"); err != nil { 132 return r, err 133 } 134 } 135 136 err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}) 137 if err != nil { 138 return r, generalError("error approving the apply command", err) 139 } 140 } 141 142 // If we don't need to ask for confirmation, insert a blank 143 // line to separate the ouputs. 144 if w.AutoApply || !mustConfirm { 145 if b.CLI != nil { 146 b.CLI.Output("") 147 } 148 } 149 150 r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w) 151 if err != nil { 152 return r, err 153 } 154 155 logs, err := b.client.Applies.Logs(stopCtx, r.Apply.ID) 156 if err != nil { 157 return r, generalError("error retrieving logs", err) 158 } 159 scanner := bufio.NewScanner(logs) 160 161 skip := 0 162 for scanner.Scan() { 163 // Skip the first 3 lines to prevent duplicate output. 164 if skip < 3 { 165 skip++ 166 continue 167 } 168 if b.CLI != nil { 169 b.CLI.Output(b.Colorize().Color(scanner.Text())) 170 } 171 } 172 if err := scanner.Err(); err != nil { 173 return r, generalError("error reading logs", err) 174 } 175 176 return r, nil 177 } 178 179 func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { 180 if b.CLI != nil { 181 b.CLI.Output("\n------------------------------------------------------------------------\n") 182 } 183 for _, pc := range r.PolicyChecks { 184 logs, err := b.client.PolicyChecks.Logs(stopCtx, pc.ID) 185 if err != nil { 186 return generalError("error retrieving policy check logs", err) 187 } 188 scanner := bufio.NewScanner(logs) 189 190 // Retrieve the policy check to get its current status. 191 pc, err := b.client.PolicyChecks.Read(stopCtx, pc.ID) 192 if err != nil { 193 return generalError("error retrieving policy check", err) 194 } 195 196 var msgPrefix string 197 switch pc.Scope { 198 case tfe.PolicyScopeOrganization: 199 msgPrefix = "Organization policy check" 200 case tfe.PolicyScopeWorkspace: 201 msgPrefix = "Workspace policy check" 202 default: 203 msgPrefix = fmt.Sprintf("Unknown policy check (%s)", pc.Scope) 204 } 205 206 if b.CLI != nil { 207 b.CLI.Output(b.Colorize().Color(msgPrefix + ":\n")) 208 } 209 210 for scanner.Scan() { 211 if b.CLI != nil { 212 b.CLI.Output(b.Colorize().Color(scanner.Text())) 213 } 214 } 215 if err := scanner.Err(); err != nil { 216 return generalError("error reading logs", err) 217 } 218 219 switch pc.Status { 220 case tfe.PolicyPasses: 221 if b.CLI != nil { 222 b.CLI.Output("\n------------------------------------------------------------------------") 223 } 224 continue 225 case tfe.PolicyErrored: 226 return fmt.Errorf(msgPrefix + " errored.") 227 case tfe.PolicyHardFailed: 228 return fmt.Errorf(msgPrefix + " hard failed.") 229 case tfe.PolicySoftFailed: 230 if op.UIOut == nil || op.UIIn == nil || op.AutoApprove || 231 !pc.Actions.IsOverridable || !pc.Permissions.CanOverride { 232 return fmt.Errorf(msgPrefix + " soft failed.") 233 } 234 default: 235 return fmt.Errorf("Unknown or unexpected policy state: %s", pc.Status) 236 } 237 238 opts := &terraform.InputOpts{ 239 Id: "override", 240 Query: "\nDo you want to override the soft failed policy check?", 241 Description: "Only 'override' will be accepted to override.", 242 } 243 244 if err = b.confirm(stopCtx, op, opts, r, "override"); err != nil { 245 return err 246 } 247 248 if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil { 249 return generalError("error overriding policy check", err) 250 } 251 252 if b.CLI != nil { 253 b.CLI.Output("------------------------------------------------------------------------") 254 } 255 } 256 257 return nil 258 } 259 260 func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error { 261 v, err := op.UIIn.Input(opts) 262 if err != nil { 263 return fmt.Errorf("Error asking %s: %v", opts.Id, err) 264 } 265 if v != keyword { 266 // Retrieve the run again to get its current status. 267 r, err = b.client.Runs.Read(stopCtx, r.ID) 268 if err != nil { 269 return generalError("error retrieving run", err) 270 } 271 272 // Make sure we discard the run if possible. 273 if r.Actions.IsDiscardable { 274 err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{}) 275 if err != nil { 276 if op.Destroy { 277 return generalError("error disarding destroy", err) 278 } 279 return generalError("error disarding apply", err) 280 } 281 } 282 283 // Even if the run was disarding successfully, we still 284 // return an error as the apply command was cancelled. 285 if op.Destroy { 286 return errors.New("Destroy discarded.") 287 } 288 return errors.New("Apply discarded.") 289 } 290 291 return nil 292 } 293 294 const applyErrNoUpdateRights = ` 295 Insufficient rights to apply changes! 296 297 [reset][yellow]The provided credentials have insufficient rights to apply changes. In order 298 to apply changes at least write permissions on the workspace are required.[reset] 299 ` 300 301 const applyErrVCSNotSupported = ` 302 Apply not allowed for workspaces with a VCS connection. 303 304 A workspace that is connected to a VCS requires the VCS-driven workflow 305 to ensure that the VCS remains the single source of truth. 306 ` 307 308 const applyErrParallelismNotSupported = ` 309 Custom parallelism values are currently not supported! 310 311 The "remote" backend does not support setting a custom parallelism 312 value at this time. 313 ` 314 315 const applyErrPlanNotSupported = ` 316 Applying a saved plan is currently not supported! 317 318 The "remote" backend currently requires configuration to be present and 319 does not accept an existing saved plan as an argument at this time. 320 ` 321 322 const applyErrNoRefreshNotSupported = ` 323 Applying without refresh is currently not supported! 324 325 Currently the "remote" backend will always do an in-memory refresh of 326 the Terraform state prior to generating the plan. 327 ` 328 329 const applyErrTargetsNotSupported = ` 330 Resource targeting is currently not supported! 331 332 The "remote" backend does not support resource targeting at this time. 333 ` 334 335 const applyErrVariablesNotSupported = ` 336 Run variables are currently not supported! 337 338 The "remote" backend does not support setting run variables at this time. 339 Currently the only to way to pass variables to the remote backend is by 340 creating a '*.auto.tfvars' variables file. This file will automatically 341 be loaded by the "remote" backend when the workspace is configured to use 342 Terraform v0.10.0 or later. 343 344 Additionally you can also set variables on the workspace in the web UI: 345 https://%s/app/%s/%s/variables 346 ` 347 348 const applyErrNoConfig = ` 349 No configuration files found! 350 351 Apply requires configuration to be present. Applying without a configuration 352 would mark everything for destruction, which is normally not what is desired. 353 If you would like to destroy everything, please run 'terraform destroy' which 354 does not require any configuration files. 355 ` 356 357 const applyErrNoApplyRights = ` 358 Insufficient rights to approve the pending changes! 359 360 [reset][yellow]There are pending changes, but the provided credentials have insufficient rights 361 to approve them. The run will be discarded to prevent it from blocking the queue 362 waiting for external approval. To queue a run that can be approved by someone 363 else, please use the 'Queue Plan' button in the web UI: 364 https://%s/app/%s/%s/runs[reset] 365 ` 366 367 const applyDefaultHeader = ` 368 [reset][yellow]Running apply in the remote backend. Output will stream here. Pressing Ctrl-C 369 will cancel the remote apply if its still pending. If the apply started it 370 will stop streaming the logs, but will not stop the apply running remotely. 371 To view this run in a browser, visit: 372 https://%s/app/%s/%s/runs/%s[reset] 373 `