github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/cloud/backend_common.go (about) 1 package cloud 2 3 import ( 4 "bufio" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "math" 11 "net/http" 12 "net/url" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/hashicorp/go-retryablehttp" 18 tfe "github.com/hashicorp/go-tfe" 19 "github.com/hashicorp/jsonapi" 20 "github.com/hashicorp/terraform/internal/backend" 21 "github.com/hashicorp/terraform/internal/command/jsonformat" 22 "github.com/hashicorp/terraform/internal/logging" 23 "github.com/hashicorp/terraform/internal/plans" 24 "github.com/hashicorp/terraform/internal/terraform" 25 ) 26 27 var ( 28 backoffMin = 1000.0 29 backoffMax = 3000.0 30 31 runPollInterval = 3 * time.Second 32 ) 33 34 // backoff will perform exponential backoff based on the iteration and 35 // limited by the provided min and max (in milliseconds) durations. 36 func backoff(min, max float64, iter int) time.Duration { 37 backoff := math.Pow(2, float64(iter)/5) * min 38 if backoff > max { 39 backoff = max 40 } 41 return time.Duration(backoff) * time.Millisecond 42 } 43 44 func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Operation, opType string, r *tfe.Run, w *tfe.Workspace) (*tfe.Run, error) { 45 started := time.Now() 46 updated := started 47 for i := 0; ; i++ { 48 select { 49 case <-stopCtx.Done(): 50 return r, stopCtx.Err() 51 case <-cancelCtx.Done(): 52 return r, cancelCtx.Err() 53 case <-time.After(backoff(backoffMin, backoffMax, i)): 54 // Timer up, show status 55 } 56 57 // Retrieve the run to get its current status. 58 r, err := b.client.Runs.Read(stopCtx, r.ID) 59 if err != nil { 60 return r, generalError("Failed to retrieve run", err) 61 } 62 63 // Return if the run is no longer pending. 64 if r.Status != tfe.RunPending && r.Status != tfe.RunConfirmed { 65 if i == 0 && opType == "plan" && b.CLI != nil { 66 b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Waiting for the %s to start...\n", opType))) 67 } 68 if i > 0 && b.CLI != nil { 69 // Insert a blank line to separate the ouputs. 70 b.CLI.Output("") 71 } 72 return r, nil 73 } 74 75 // Check if 30 seconds have passed since the last update. 76 current := time.Now() 77 if b.CLI != nil && (i == 0 || current.Sub(updated).Seconds() > 30) { 78 updated = current 79 position := 0 80 elapsed := "" 81 82 // Calculate and set the elapsed time. 83 if i > 0 { 84 elapsed = fmt.Sprintf( 85 " (%s elapsed)", current.Sub(started).Truncate(30*time.Second)) 86 } 87 88 // Retrieve the workspace used to run this operation in. 89 w, err = b.client.Workspaces.Read(stopCtx, b.organization, w.Name) 90 if err != nil { 91 return nil, generalError("Failed to retrieve workspace", err) 92 } 93 94 // If the workspace is locked the run will not be queued and we can 95 // update the status without making any expensive calls. 96 if w.Locked && w.CurrentRun != nil { 97 cr, err := b.client.Runs.Read(stopCtx, w.CurrentRun.ID) 98 if err != nil { 99 return r, generalError("Failed to retrieve current run", err) 100 } 101 if cr.Status == tfe.RunPending { 102 b.CLI.Output(b.Colorize().Color( 103 "Waiting for the manually locked workspace to be unlocked..." + elapsed)) 104 continue 105 } 106 } 107 108 // Skip checking the workspace queue when we are the current run. 109 if w.CurrentRun == nil || w.CurrentRun.ID != r.ID { 110 found := false 111 options := &tfe.RunListOptions{} 112 runlist: 113 for { 114 rl, err := b.client.Runs.List(stopCtx, w.ID, options) 115 if err != nil { 116 return r, generalError("Failed to retrieve run list", err) 117 } 118 119 // Loop through all runs to calculate the workspace queue position. 120 for _, item := range rl.Items { 121 if !found { 122 if r.ID == item.ID { 123 found = true 124 } 125 continue 126 } 127 128 // If the run is in a final state, ignore it and continue. 129 switch item.Status { 130 case tfe.RunApplied, tfe.RunCanceled, tfe.RunDiscarded, tfe.RunErrored: 131 continue 132 case tfe.RunPlanned: 133 if op.Type == backend.OperationTypePlan { 134 continue 135 } 136 } 137 138 // Increase the workspace queue position. 139 position++ 140 141 // Stop searching when we reached the current run. 142 if w.CurrentRun != nil && w.CurrentRun.ID == item.ID { 143 break runlist 144 } 145 } 146 147 // Exit the loop when we've seen all pages. 148 if rl.CurrentPage >= rl.TotalPages { 149 break 150 } 151 152 // Update the page number to get the next page. 153 options.PageNumber = rl.NextPage 154 } 155 156 if position > 0 { 157 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 158 "Waiting for %d run(s) to finish before being queued...%s", 159 position, 160 elapsed, 161 ))) 162 continue 163 } 164 } 165 166 options := tfe.ReadRunQueueOptions{} 167 search: 168 for { 169 rq, err := b.client.Organizations.ReadRunQueue(stopCtx, b.organization, options) 170 if err != nil { 171 return r, generalError("Failed to retrieve queue", err) 172 } 173 174 // Search through all queued items to find our run. 175 for _, item := range rq.Items { 176 if r.ID == item.ID { 177 position = item.PositionInQueue 178 break search 179 } 180 } 181 182 // Exit the loop when we've seen all pages. 183 if rq.CurrentPage >= rq.TotalPages { 184 break 185 } 186 187 // Update the page number to get the next page. 188 options.PageNumber = rq.NextPage 189 } 190 191 if position > 0 { 192 c, err := b.client.Organizations.ReadCapacity(stopCtx, b.organization) 193 if err != nil { 194 return r, generalError("Failed to retrieve capacity", err) 195 } 196 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 197 "Waiting for %d queued run(s) to finish before starting...%s", 198 position-c.Running, 199 elapsed, 200 ))) 201 continue 202 } 203 204 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 205 "Waiting for the %s to start...%s", opType, elapsed))) 206 } 207 } 208 } 209 210 func (b *Cloud) waitTaskStage(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run, stageID string, outputTitle string) error { 211 integration := &IntegrationContext{ 212 B: b, 213 StopContext: stopCtx, 214 CancelContext: cancelCtx, 215 Op: op, 216 Run: r, 217 } 218 return b.runTaskStage(integration, integration.BeginOutput(outputTitle), stageID) 219 } 220 221 func (b *Cloud) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { 222 if r.CostEstimate == nil { 223 return nil 224 } 225 226 msgPrefix := "Cost Estimation" 227 started := time.Now() 228 updated := started 229 for i := 0; ; i++ { 230 select { 231 case <-stopCtx.Done(): 232 return stopCtx.Err() 233 case <-cancelCtx.Done(): 234 return cancelCtx.Err() 235 case <-time.After(backoff(backoffMin, backoffMax, i)): 236 } 237 238 // Retrieve the cost estimate to get its current status. 239 ce, err := b.client.CostEstimates.Read(stopCtx, r.CostEstimate.ID) 240 if err != nil { 241 return generalError("Failed to retrieve cost estimate", err) 242 } 243 244 // If the run is canceled or errored, but the cost-estimate still has 245 // no result, there is nothing further to render. 246 if ce.Status != tfe.CostEstimateFinished { 247 if r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored { 248 return nil 249 } 250 } 251 252 // checking if i == 0 so as to avoid printing this starting horizontal-rule 253 // every retry, and that it only prints it on the first (i=0) attempt. 254 if b.CLI != nil && i == 0 { 255 b.CLI.Output("\n------------------------------------------------------------------------\n") 256 } 257 258 switch ce.Status { 259 case tfe.CostEstimateFinished: 260 delta, err := strconv.ParseFloat(ce.DeltaMonthlyCost, 64) 261 if err != nil { 262 return generalError("Unexpected error", err) 263 } 264 265 sign := "+" 266 if delta < 0 { 267 sign = "-" 268 } 269 270 deltaRepr := strings.Replace(ce.DeltaMonthlyCost, "-", "", 1) 271 272 if b.CLI != nil { 273 b.CLI.Output(b.Colorize().Color("[bold]" + msgPrefix + ":\n")) 274 b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Resources: %d of %d estimated", ce.MatchedResourcesCount, ce.ResourcesCount))) 275 b.CLI.Output(b.Colorize().Color(fmt.Sprintf(" $%s/mo %s$%s", ce.ProposedMonthlyCost, sign, deltaRepr))) 276 277 if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backend.OperationTypeApply { 278 b.CLI.Output("\n------------------------------------------------------------------------") 279 } 280 } 281 282 return nil 283 case tfe.CostEstimatePending, tfe.CostEstimateQueued: 284 // Check if 30 seconds have passed since the last update. 285 current := time.Now() 286 if b.CLI != nil && (i == 0 || current.Sub(updated).Seconds() > 30) { 287 updated = current 288 elapsed := "" 289 290 // Calculate and set the elapsed time. 291 if i > 0 { 292 elapsed = fmt.Sprintf( 293 " (%s elapsed)", current.Sub(started).Truncate(30*time.Second)) 294 } 295 b.CLI.Output(b.Colorize().Color("[bold]" + msgPrefix + ":\n")) 296 b.CLI.Output(b.Colorize().Color("Waiting for cost estimate to complete..." + elapsed + "\n")) 297 } 298 continue 299 case tfe.CostEstimateSkippedDueToTargeting: 300 b.CLI.Output(b.Colorize().Color("[bold]" + msgPrefix + ":\n")) 301 b.CLI.Output("Not available for this plan, because it was created with the -target option.") 302 b.CLI.Output("\n------------------------------------------------------------------------") 303 return nil 304 case tfe.CostEstimateErrored: 305 b.CLI.Output(msgPrefix + " errored.\n") 306 b.CLI.Output("\n------------------------------------------------------------------------") 307 return nil 308 case tfe.CostEstimateCanceled: 309 return fmt.Errorf(msgPrefix + " canceled.") 310 default: 311 return fmt.Errorf("Unknown or unexpected cost estimate state: %s", ce.Status) 312 } 313 } 314 } 315 316 func (b *Cloud) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { 317 if b.CLI != nil { 318 b.CLI.Output("\n------------------------------------------------------------------------\n") 319 } 320 for i, pc := range r.PolicyChecks { 321 // Read the policy check logs. This is a blocking call that will only 322 // return once the policy check is complete. 323 logs, err := b.client.PolicyChecks.Logs(stopCtx, pc.ID) 324 if err != nil { 325 return generalError("Failed to retrieve policy check logs", err) 326 } 327 reader := bufio.NewReaderSize(logs, 64*1024) 328 329 // Retrieve the policy check to get its current status. 330 pc, err := b.client.PolicyChecks.Read(stopCtx, pc.ID) 331 if err != nil { 332 return generalError("Failed to retrieve policy check", err) 333 } 334 335 // If the run is canceled or errored, but the policy check still has 336 // no result, there is nothing further to render. 337 if r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored { 338 switch pc.Status { 339 case tfe.PolicyPending, tfe.PolicyQueued, tfe.PolicyUnreachable: 340 continue 341 } 342 } 343 344 var msgPrefix string 345 switch pc.Scope { 346 case tfe.PolicyScopeOrganization: 347 msgPrefix = "Organization Policy Check" 348 case tfe.PolicyScopeWorkspace: 349 msgPrefix = "Workspace Policy Check" 350 default: 351 msgPrefix = fmt.Sprintf("Unknown policy check (%s)", pc.Scope) 352 } 353 354 if b.CLI != nil { 355 b.CLI.Output(b.Colorize().Color("[bold]" + msgPrefix + ":\n")) 356 } 357 358 if b.CLI != nil { 359 for next := true; next; { 360 var l, line []byte 361 362 for isPrefix := true; isPrefix; { 363 l, isPrefix, err = reader.ReadLine() 364 if err != nil { 365 if err != io.EOF { 366 return generalError("Failed to read logs", err) 367 } 368 next = false 369 } 370 line = append(line, l...) 371 } 372 373 if next || len(line) > 0 { 374 b.CLI.Output(b.Colorize().Color(string(line))) 375 } 376 } 377 } 378 379 switch pc.Status { 380 case tfe.PolicyPasses: 381 if (r.HasChanges && op.Type == backend.OperationTypeApply || i < len(r.PolicyChecks)-1) && b.CLI != nil { 382 b.CLI.Output("\n------------------------------------------------------------------------") 383 } 384 continue 385 case tfe.PolicyErrored: 386 return fmt.Errorf(msgPrefix + " errored.") 387 case tfe.PolicyHardFailed: 388 return fmt.Errorf(msgPrefix + " hard failed.") 389 case tfe.PolicySoftFailed: 390 runUrl := fmt.Sprintf(runHeader, b.hostname, b.organization, op.Workspace, r.ID) 391 392 if op.Type == backend.OperationTypePlan || op.UIOut == nil || op.UIIn == nil || 393 !pc.Actions.IsOverridable || !pc.Permissions.CanOverride { 394 return fmt.Errorf(msgPrefix + " soft failed.\n" + runUrl) 395 } 396 397 if op.AutoApprove { 398 if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil { 399 return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runUrl), err) 400 } 401 } else if !b.input { 402 return errPolicyOverrideNeedsUIConfirmation 403 } else { 404 opts := &terraform.InputOpts{ 405 Id: "override", 406 Query: "\nDo you want to override the soft failed policy check?", 407 Description: "Only 'override' will be accepted to override.", 408 } 409 err = b.confirm(stopCtx, op, opts, r, "override") 410 if err != nil && err != errRunOverridden { 411 return fmt.Errorf( 412 fmt.Sprintf("Failed to override: %s\n%s\n", err.Error(), runUrl), 413 ) 414 } 415 416 if err != errRunOverridden { 417 if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil { 418 return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runUrl), err) 419 } 420 } else { 421 b.CLI.Output(fmt.Sprintf("The run needs to be manually overridden or discarded.\n%s\n", runUrl)) 422 } 423 } 424 425 if b.CLI != nil { 426 b.CLI.Output("------------------------------------------------------------------------") 427 } 428 default: 429 return fmt.Errorf("Unknown or unexpected policy state: %s", pc.Status) 430 } 431 } 432 433 return nil 434 } 435 436 func (b *Cloud) confirm(stopCtx context.Context, op *backend.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error { 437 doneCtx, cancel := context.WithCancel(stopCtx) 438 result := make(chan error, 2) 439 440 go func() { 441 // Make sure we cancel doneCtx before we return 442 // so the input command is also canceled. 443 defer cancel() 444 445 for { 446 select { 447 case <-doneCtx.Done(): 448 return 449 case <-stopCtx.Done(): 450 return 451 case <-time.After(runPollInterval): 452 // Retrieve the run again to get its current status. 453 r, err := b.client.Runs.Read(stopCtx, r.ID) 454 if err != nil { 455 result <- generalError("Failed to retrieve run", err) 456 return 457 } 458 459 switch keyword { 460 case "override": 461 if r.Status != tfe.RunPolicyOverride && r.Status != tfe.RunPostPlanAwaitingDecision { 462 if r.Status == tfe.RunDiscarded { 463 err = errRunDiscarded 464 } else { 465 err = errRunOverridden 466 } 467 } 468 case "yes": 469 if !r.Actions.IsConfirmable { 470 if r.Status == tfe.RunDiscarded { 471 err = errRunDiscarded 472 } else { 473 err = errRunApproved 474 } 475 } 476 } 477 478 if err != nil { 479 if b.CLI != nil { 480 b.CLI.Output(b.Colorize().Color( 481 fmt.Sprintf("[reset][yellow]%s[reset]", err.Error()))) 482 } 483 484 if err == errRunDiscarded { 485 err = errApplyDiscarded 486 if op.PlanMode == plans.DestroyMode { 487 err = errDestroyDiscarded 488 } 489 } 490 491 result <- err 492 return 493 } 494 } 495 } 496 }() 497 498 result <- func() error { 499 v, err := op.UIIn.Input(doneCtx, opts) 500 if err != nil && err != context.Canceled && stopCtx.Err() != context.Canceled { 501 return fmt.Errorf("Error asking %s: %v", opts.Id, err) 502 } 503 504 // We return the error of our parent channel as we don't 505 // care about the error of the doneCtx which is only used 506 // within this function. So if the doneCtx was canceled 507 // because stopCtx was canceled, this will properly return 508 // a context.Canceled error and otherwise it returns nil. 509 if doneCtx.Err() == context.Canceled || stopCtx.Err() == context.Canceled { 510 return stopCtx.Err() 511 } 512 513 // Make sure we cancel the context here so the loop that 514 // checks for external changes to the run is ended before 515 // we start to make changes ourselves. 516 cancel() 517 518 if v != keyword { 519 // Retrieve the run again to get its current status. 520 r, err = b.client.Runs.Read(stopCtx, r.ID) 521 if err != nil { 522 return generalError("Failed to retrieve run", err) 523 } 524 525 // Make sure we discard the run if possible. 526 if r.Actions.IsDiscardable { 527 err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{}) 528 if err != nil { 529 if op.PlanMode == plans.DestroyMode { 530 return generalError("Failed to discard destroy", err) 531 } 532 return generalError("Failed to discard apply", err) 533 } 534 } 535 536 // Even if the run was discarded successfully, we still 537 // return an error as the apply command was canceled. 538 if op.PlanMode == plans.DestroyMode { 539 return errDestroyDiscarded 540 } 541 return errApplyDiscarded 542 } 543 544 return nil 545 }() 546 547 return <-result 548 } 549 550 // This method will fetch the redacted plan output and marshal the response into 551 // a struct the jsonformat.Renderer expects. 552 // 553 // Note: Apologies for the lengthy definition, this is a result of not being able to mock receiver methods 554 var readRedactedPlan func(context.Context, url.URL, string, string) (*jsonformat.Plan, error) = func(ctx context.Context, baseURL url.URL, token string, planID string) (*jsonformat.Plan, error) { 555 client := retryablehttp.NewClient() 556 client.RetryMax = 10 557 client.RetryWaitMin = 100 * time.Millisecond 558 client.RetryWaitMax = 400 * time.Millisecond 559 client.Logger = logging.HCLogger() 560 561 u, err := baseURL.Parse(fmt.Sprintf( 562 "plans/%s/json-output-redacted", url.QueryEscape(planID))) 563 if err != nil { 564 return nil, err 565 } 566 567 req, err := retryablehttp.NewRequest("GET", u.String(), nil) 568 if err != nil { 569 return nil, err 570 } 571 572 req.Header.Set("Authorization", "Bearer "+token) 573 req.Header.Set("Accept", "application/json") 574 575 p := &jsonformat.Plan{} 576 resp, err := client.Do(req) 577 if err != nil { 578 return nil, err 579 } 580 defer resp.Body.Close() 581 582 if err = checkResponseCode(resp); err != nil { 583 return nil, err 584 } 585 586 if err := json.NewDecoder(resp.Body).Decode(p); err != nil { 587 return nil, err 588 } 589 590 return p, nil 591 } 592 593 func checkResponseCode(r *http.Response) error { 594 if r.StatusCode >= 200 && r.StatusCode <= 299 { 595 return nil 596 } 597 598 var errs []string 599 var err error 600 601 switch r.StatusCode { 602 case 401: 603 return tfe.ErrUnauthorized 604 case 404: 605 return tfe.ErrResourceNotFound 606 } 607 608 errs, err = decodeErrorPayload(r) 609 if err != nil { 610 return err 611 } 612 613 return errors.New(strings.Join(errs, "\n")) 614 } 615 616 func decodeErrorPayload(r *http.Response) ([]string, error) { 617 // Decode the error payload. 618 var errs []string 619 errPayload := &jsonapi.ErrorsPayload{} 620 err := json.NewDecoder(r.Body).Decode(errPayload) 621 if err != nil || len(errPayload.Errors) == 0 { 622 return errs, errors.New(r.Status) 623 } 624 625 // Parse and format the errors. 626 for _, e := range errPayload.Errors { 627 if e.Detail == "" { 628 errs = append(errs, e.Title) 629 } else { 630 errs = append(errs, fmt.Sprintf("%s\n\n%s", e.Title, e.Detail)) 631 } 632 } 633 634 return errs, nil 635 }