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