github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote/backend_common.go (about) 1 package remote 2 3 import ( 4 "bufio" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "math" 10 "strconv" 11 "strings" 12 "time" 13 14 tfe "github.com/hashicorp/go-tfe" 15 "github.com/hashicorp/terraform/backend" 16 "github.com/hashicorp/terraform/terraform" 17 ) 18 19 var ( 20 errApplyDiscarded = errors.New("Apply discarded.") 21 errDestroyDiscarded = errors.New("Destroy discarded.") 22 errRunApproved = errors.New("approved using the UI or API") 23 errRunDiscarded = errors.New("discarded using the UI or API") 24 errRunOverridden = errors.New("overridden using the UI or API") 25 ) 26 27 // backoff will perform exponential backoff based on the iteration and 28 // limited by the provided min and max (in milliseconds) durations. 29 func backoff(min, max float64, iter int) time.Duration { 30 backoff := math.Pow(2, float64(iter)/5) * min 31 if backoff > max { 32 backoff = max 33 } 34 return time.Duration(backoff) * time.Millisecond 35 } 36 37 func (b *Remote) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Operation, opType string, r *tfe.Run, w *tfe.Workspace) (*tfe.Run, error) { 38 started := time.Now() 39 updated := started 40 for i := 0; ; i++ { 41 select { 42 case <-stopCtx.Done(): 43 return r, stopCtx.Err() 44 case <-cancelCtx.Done(): 45 return r, cancelCtx.Err() 46 case <-time.After(backoff(1000, 3000, i)): 47 // Timer up, show status 48 } 49 50 // Retrieve the run to get its current status. 51 r, err := b.client.Runs.Read(stopCtx, r.ID) 52 if err != nil { 53 return r, generalError("Failed to retrieve run", err) 54 } 55 56 // Return if the run is no longer pending. 57 if r.Status != tfe.RunPending && r.Status != tfe.RunConfirmed { 58 if i == 0 && opType == "plan" && b.CLI != nil { 59 b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Waiting for the %s to start...\n", opType))) 60 } 61 if i > 0 && b.CLI != nil { 62 // Insert a blank line to separate the ouputs. 63 b.CLI.Output("") 64 } 65 return r, nil 66 } 67 68 // Check if 30 seconds have passed since the last update. 69 current := time.Now() 70 if b.CLI != nil && (i == 0 || current.Sub(updated).Seconds() > 30) { 71 updated = current 72 position := 0 73 elapsed := "" 74 75 // Calculate and set the elapsed time. 76 if i > 0 { 77 elapsed = fmt.Sprintf( 78 " (%s elapsed)", current.Sub(started).Truncate(30*time.Second)) 79 } 80 81 // Retrieve the workspace used to run this operation in. 82 w, err = b.client.Workspaces.Read(stopCtx, b.organization, w.Name) 83 if err != nil { 84 return nil, generalError("Failed to retrieve workspace", err) 85 } 86 87 // If the workspace is locked the run will not be queued and we can 88 // update the status without making any expensive calls. 89 if w.Locked && w.CurrentRun != nil { 90 cr, err := b.client.Runs.Read(stopCtx, w.CurrentRun.ID) 91 if err != nil { 92 return r, generalError("Failed to retrieve current run", err) 93 } 94 if cr.Status == tfe.RunPending { 95 b.CLI.Output(b.Colorize().Color( 96 "Waiting for the manually locked workspace to be unlocked..." + elapsed)) 97 continue 98 } 99 } 100 101 // Skip checking the workspace queue when we are the current run. 102 if w.CurrentRun == nil || w.CurrentRun.ID != r.ID { 103 found := false 104 options := tfe.RunListOptions{} 105 runlist: 106 for { 107 rl, err := b.client.Runs.List(stopCtx, w.ID, options) 108 if err != nil { 109 return r, generalError("Failed to retrieve run list", err) 110 } 111 112 // Loop through all runs to calculate the workspace queue position. 113 for _, item := range rl.Items { 114 if !found { 115 if r.ID == item.ID { 116 found = true 117 } 118 continue 119 } 120 121 // If the run is in a final state, ignore it and continue. 122 switch item.Status { 123 case tfe.RunApplied, tfe.RunCanceled, tfe.RunDiscarded, tfe.RunErrored: 124 continue 125 case tfe.RunPlanned: 126 if op.Type == backend.OperationTypePlan { 127 continue 128 } 129 } 130 131 // Increase the workspace queue position. 132 position++ 133 134 // Stop searching when we reached the current run. 135 if w.CurrentRun != nil && w.CurrentRun.ID == item.ID { 136 break runlist 137 } 138 } 139 140 // Exit the loop when we've seen all pages. 141 if rl.CurrentPage >= rl.TotalPages { 142 break 143 } 144 145 // Update the page number to get the next page. 146 options.PageNumber = rl.NextPage 147 } 148 149 if position > 0 { 150 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 151 "Waiting for %d run(s) to finish before being queued...%s", 152 position, 153 elapsed, 154 ))) 155 continue 156 } 157 } 158 159 options := tfe.RunQueueOptions{} 160 search: 161 for { 162 rq, err := b.client.Organizations.RunQueue(stopCtx, b.organization, options) 163 if err != nil { 164 return r, generalError("Failed to retrieve queue", err) 165 } 166 167 // Search through all queued items to find our run. 168 for _, item := range rq.Items { 169 if r.ID == item.ID { 170 position = item.PositionInQueue 171 break search 172 } 173 } 174 175 // Exit the loop when we've seen all pages. 176 if rq.CurrentPage >= rq.TotalPages { 177 break 178 } 179 180 // Update the page number to get the next page. 181 options.PageNumber = rq.NextPage 182 } 183 184 if position > 0 { 185 c, err := b.client.Organizations.Capacity(stopCtx, b.organization) 186 if err != nil { 187 return r, generalError("Failed to retrieve capacity", err) 188 } 189 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 190 "Waiting for %d queued run(s) to finish before starting...%s", 191 position-c.Running, 192 elapsed, 193 ))) 194 continue 195 } 196 197 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 198 "Waiting for the %s to start...%s", opType, elapsed))) 199 } 200 } 201 } 202 203 // hasExplicitVariableValues is a best-effort check to determine whether the 204 // user has provided -var or -var-file arguments to a remote operation. 205 // 206 // The results may be inaccurate if the configuration is invalid or if 207 // individual variable values are invalid. That's okay because we only use this 208 // result to hint the user to set variables a different way. It's always the 209 // remote system's responsibility to do final validation of the input. 210 func (b *Remote) hasExplicitVariableValues(op *backend.Operation) bool { 211 // Load the configuration using the caller-provided configuration loader. 212 config, _, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir) 213 if configDiags.HasErrors() { 214 // If we can't load the configuration then we'll assume no explicit 215 // variable values just to let the remote operation start and let 216 // the remote system return the same set of configuration errors. 217 return false 218 } 219 220 // We're intentionally ignoring the diagnostics here because validation 221 // of the variable values is the responsibilty of the remote system. Our 222 // goal here is just to make a best effort count of how many variable 223 // values are coming from -var or -var-file CLI arguments so that we can 224 // hint the user that those are not supported for remote operations. 225 variables, _ := backend.ParseVariableValues(op.Variables, config.Module.Variables) 226 227 // Check for explicitly-defined (-var and -var-file) variables, which the 228 // remote backend does not support. All other source types are okay, 229 // because they are implicit from the execution context anyway and so 230 // their final values will come from the _remote_ execution context. 231 for _, v := range variables { 232 switch v.SourceType { 233 case terraform.ValueFromCLIArg, terraform.ValueFromNamedFile: 234 return true 235 } 236 } 237 238 return false 239 } 240 241 func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { 242 if r.CostEstimate == nil { 243 return nil 244 } 245 246 if b.CLI != nil { 247 b.CLI.Output("\n------------------------------------------------------------------------\n") 248 } 249 250 msgPrefix := "Cost estimation" 251 if b.CLI != nil { 252 b.CLI.Output(b.Colorize().Color(msgPrefix + ":\n")) 253 } 254 255 started := time.Now() 256 updated := started 257 for i := 0; ; i++ { 258 select { 259 case <-stopCtx.Done(): 260 return stopCtx.Err() 261 case <-cancelCtx.Done(): 262 return cancelCtx.Err() 263 case <-time.After(1 * time.Second): 264 } 265 266 // Retrieve the cost estimate to get its current status. 267 ce, err := b.client.CostEstimates.Read(stopCtx, r.CostEstimate.ID) 268 if err != nil { 269 return generalError("Failed to retrieve cost estimate", err) 270 } 271 272 switch ce.Status { 273 case tfe.CostEstimateFinished: 274 delta, err := strconv.ParseFloat(ce.DeltaMonthlyCost, 64) 275 if err != nil { 276 return generalError("Unexpected error", err) 277 } 278 279 sign := "+" 280 if delta < 0 { 281 sign = "-" 282 } 283 284 deltaRepr := strings.Replace(ce.DeltaMonthlyCost, "-", "", 1) 285 286 if b.CLI != nil { 287 b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Resources: %d of %d estimated", ce.MatchedResourcesCount, ce.ResourcesCount))) 288 b.CLI.Output(b.Colorize().Color(fmt.Sprintf(" $%s/mo %s$%s", ce.ProposedMonthlyCost, sign, deltaRepr))) 289 290 if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backend.OperationTypeApply { 291 b.CLI.Output("\n------------------------------------------------------------------------") 292 } 293 } 294 295 return nil 296 case tfe.CostEstimatePending, tfe.CostEstimateQueued: 297 // Check if 30 seconds have passed since the last update. 298 current := time.Now() 299 if b.CLI != nil && (i == 0 || current.Sub(updated).Seconds() > 30) { 300 updated = current 301 elapsed := "" 302 303 // Calculate and set the elapsed time. 304 if i > 0 { 305 elapsed = fmt.Sprintf( 306 " (%s elapsed)", current.Sub(started).Truncate(30*time.Second)) 307 } 308 b.CLI.Output(b.Colorize().Color("Waiting for cost estimate to complete..." + elapsed + "\n")) 309 } 310 continue 311 case tfe.CostEstimateSkippedDueToTargeting: 312 b.CLI.Output("Not available for this plan, because it was created with the -target option.") 313 b.CLI.Output("\n------------------------------------------------------------------------") 314 return nil 315 case tfe.CostEstimateErrored: 316 return fmt.Errorf(msgPrefix + " errored.") 317 case tfe.CostEstimateCanceled: 318 return fmt.Errorf(msgPrefix + " canceled.") 319 default: 320 return fmt.Errorf("Unknown or unexpected cost estimate state: %s", ce.Status) 321 } 322 } 323 return nil 324 } 325 326 func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { 327 if b.CLI != nil { 328 b.CLI.Output("\n------------------------------------------------------------------------\n") 329 } 330 for i, pc := range r.PolicyChecks { 331 logs, err := b.client.PolicyChecks.Logs(stopCtx, pc.ID) 332 if err != nil { 333 return generalError("Failed to retrieve policy check logs", err) 334 } 335 reader := bufio.NewReaderSize(logs, 64*1024) 336 337 // Retrieve the policy check to get its current status. 338 pc, err := b.client.PolicyChecks.Read(stopCtx, pc.ID) 339 if err != nil { 340 return generalError("Failed to retrieve policy check", err) 341 } 342 343 var msgPrefix string 344 switch pc.Scope { 345 case tfe.PolicyScopeOrganization: 346 msgPrefix = "Organization policy check" 347 case tfe.PolicyScopeWorkspace: 348 msgPrefix = "Workspace policy check" 349 default: 350 msgPrefix = fmt.Sprintf("Unknown policy check (%s)", pc.Scope) 351 } 352 353 if b.CLI != nil { 354 b.CLI.Output(b.Colorize().Color(msgPrefix + ":\n")) 355 } 356 357 if b.CLI != nil { 358 for next := true; next; { 359 var l, line []byte 360 361 for isPrefix := true; isPrefix; { 362 l, isPrefix, err = reader.ReadLine() 363 if err != nil { 364 if err != io.EOF { 365 return generalError("Failed to read logs", err) 366 } 367 next = false 368 } 369 line = append(line, l...) 370 } 371 372 if next || len(line) > 0 { 373 b.CLI.Output(b.Colorize().Color(string(line))) 374 } 375 } 376 } 377 378 switch pc.Status { 379 case tfe.PolicyPasses: 380 if (r.HasChanges && op.Type == backend.OperationTypeApply || i < len(r.PolicyChecks)-1) && b.CLI != nil { 381 b.CLI.Output("\n------------------------------------------------------------------------") 382 } 383 continue 384 case tfe.PolicyErrored: 385 return fmt.Errorf(msgPrefix + " errored.") 386 case tfe.PolicyHardFailed: 387 return fmt.Errorf(msgPrefix + " hard failed.") 388 case tfe.PolicySoftFailed: 389 if op.Type == backend.OperationTypePlan || op.UIOut == nil || op.UIIn == nil || 390 op.AutoApprove || !pc.Actions.IsOverridable || !pc.Permissions.CanOverride { 391 return fmt.Errorf(msgPrefix + " soft failed.") 392 } 393 default: 394 return fmt.Errorf("Unknown or unexpected policy state: %s", pc.Status) 395 } 396 397 opts := &terraform.InputOpts{ 398 Id: "override", 399 Query: "\nDo you want to override the soft failed policy check?", 400 Description: "Only 'override' will be accepted to override.", 401 } 402 403 err = b.confirm(stopCtx, op, opts, r, "override") 404 if err != nil && err != errRunOverridden { 405 return err 406 } 407 408 if err != errRunOverridden { 409 if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil { 410 return generalError("Failed to override policy check", err) 411 } 412 } 413 414 if b.CLI != nil { 415 b.CLI.Output("------------------------------------------------------------------------") 416 } 417 } 418 419 return nil 420 } 421 422 func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error { 423 doneCtx, cancel := context.WithCancel(stopCtx) 424 result := make(chan error, 2) 425 426 go func() { 427 // Make sure we cancel doneCtx before we return 428 // so the input command is also canceled. 429 defer cancel() 430 431 for { 432 select { 433 case <-doneCtx.Done(): 434 return 435 case <-stopCtx.Done(): 436 return 437 case <-time.After(3 * time.Second): 438 // Retrieve the run again to get its current status. 439 r, err := b.client.Runs.Read(stopCtx, r.ID) 440 if err != nil { 441 result <- generalError("Failed to retrieve run", err) 442 return 443 } 444 445 switch keyword { 446 case "override": 447 if r.Status != tfe.RunPolicyOverride { 448 if r.Status == tfe.RunDiscarded { 449 err = errRunDiscarded 450 } else { 451 err = errRunOverridden 452 } 453 } 454 case "yes": 455 if !r.Actions.IsConfirmable { 456 if r.Status == tfe.RunDiscarded { 457 err = errRunDiscarded 458 } else { 459 err = errRunApproved 460 } 461 } 462 } 463 464 if err != nil { 465 if b.CLI != nil { 466 b.CLI.Output(b.Colorize().Color( 467 fmt.Sprintf("[reset][yellow]%s[reset]", err.Error()))) 468 } 469 470 if err == errRunDiscarded { 471 if op.Destroy { 472 err = errDestroyDiscarded 473 } 474 err = errApplyDiscarded 475 } 476 477 result <- err 478 return 479 } 480 } 481 } 482 }() 483 484 result <- func() error { 485 v, err := op.UIIn.Input(doneCtx, opts) 486 if err != nil && err != context.Canceled && stopCtx.Err() != context.Canceled { 487 return fmt.Errorf("Error asking %s: %v", opts.Id, err) 488 } 489 490 // We return the error of our parent channel as we don't 491 // care about the error of the doneCtx which is only used 492 // within this function. So if the doneCtx was canceled 493 // because stopCtx was canceled, this will properly return 494 // a context.Canceled error and otherwise it returns nil. 495 if doneCtx.Err() == context.Canceled || stopCtx.Err() == context.Canceled { 496 return stopCtx.Err() 497 } 498 499 // Make sure we cancel the context here so the loop that 500 // checks for external changes to the run is ended before 501 // we start to make changes ourselves. 502 cancel() 503 504 if v != keyword { 505 // Retrieve the run again to get its current status. 506 r, err = b.client.Runs.Read(stopCtx, r.ID) 507 if err != nil { 508 return generalError("Failed to retrieve run", err) 509 } 510 511 // Make sure we discard the run if possible. 512 if r.Actions.IsDiscardable { 513 err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{}) 514 if err != nil { 515 if op.Destroy { 516 return generalError("Failed to discard destroy", err) 517 } 518 return generalError("Failed to discard apply", err) 519 } 520 } 521 522 // Even if the run was discarded successfully, we still 523 // return an error as the apply command was canceled. 524 if op.Destroy { 525 return errDestroyDiscarded 526 } 527 return errApplyDiscarded 528 } 529 530 return nil 531 }() 532 533 return <-result 534 }