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