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