github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/backend/remote/backend.go (about) 1 package remote 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "net/http" 8 "net/url" 9 "os" 10 "sort" 11 "strings" 12 "sync" 13 "time" 14 15 tfe "github.com/hashicorp/go-tfe" 16 version "github.com/hashicorp/go-version" 17 svchost "github.com/hashicorp/terraform-svchost" 18 "github.com/hashicorp/terraform-svchost/disco" 19 "github.com/hashicorp/terraform/internal/backend" 20 "github.com/hashicorp/terraform/internal/configs/configschema" 21 "github.com/hashicorp/terraform/internal/logging" 22 "github.com/hashicorp/terraform/internal/states/remote" 23 "github.com/hashicorp/terraform/internal/states/statemgr" 24 "github.com/hashicorp/terraform/internal/terraform" 25 "github.com/hashicorp/terraform/internal/tfdiags" 26 tfversion "github.com/hashicorp/terraform/version" 27 "github.com/mitchellh/cli" 28 "github.com/mitchellh/colorstring" 29 "github.com/zclconf/go-cty/cty" 30 31 backendLocal "github.com/hashicorp/terraform/internal/backend/local" 32 ) 33 34 const ( 35 defaultHostname = "app.terraform.io" 36 defaultParallelism = 10 37 stateServiceID = "state.v2" 38 tfeServiceID = "tfe.v2.1" 39 ) 40 41 // Remote is an implementation of EnhancedBackend that performs all 42 // operations in a remote backend. 43 type Remote struct { 44 // CLI and Colorize control the CLI output. If CLI is nil then no CLI 45 // output will be done. If CLIColor is nil then no coloring will be done. 46 CLI cli.Ui 47 CLIColor *colorstring.Colorize 48 49 // ContextOpts are the base context options to set when initializing a 50 // new Terraform context. Many of these will be overridden or merged by 51 // Operation. See Operation for more details. 52 ContextOpts *terraform.ContextOpts 53 54 // client is the remote backend API client. 55 client *tfe.Client 56 57 // lastRetry is set to the last time a request was retried. 58 lastRetry time.Time 59 60 // hostname of the remote backend server. 61 hostname string 62 63 // organization is the organization that contains the target workspaces. 64 organization string 65 66 // workspace is used to map the default workspace to a remote workspace. 67 workspace string 68 69 // prefix is used to filter down a set of workspaces that use a single 70 // configuration. 71 prefix string 72 73 // services is used for service discovery 74 services *disco.Disco 75 76 // local, if non-nil, will be used for all enhanced behavior. This 77 // allows local behavior with the remote backend functioning as remote 78 // state storage backend. 79 local backend.Enhanced 80 81 // forceLocal, if true, will force the use of the local backend. 82 forceLocal bool 83 84 // opLock locks operations 85 opLock sync.Mutex 86 87 // ignoreVersionConflict, if true, will disable the requirement that the 88 // local Terraform version matches the remote workspace's configured 89 // version. This will also cause VerifyWorkspaceTerraformVersion to return 90 // a warning diagnostic instead of an error. 91 ignoreVersionConflict bool 92 } 93 94 var _ backend.Backend = (*Remote)(nil) 95 var _ backend.Enhanced = (*Remote)(nil) 96 var _ backend.Local = (*Remote)(nil) 97 98 // New creates a new initialized remote backend. 99 func New(services *disco.Disco) *Remote { 100 return &Remote{ 101 services: services, 102 } 103 } 104 105 // ConfigSchema implements backend.Enhanced. 106 func (b *Remote) ConfigSchema() *configschema.Block { 107 return &configschema.Block{ 108 Attributes: map[string]*configschema.Attribute{ 109 "hostname": { 110 Type: cty.String, 111 Optional: true, 112 Description: schemaDescriptions["hostname"], 113 }, 114 "organization": { 115 Type: cty.String, 116 Required: true, 117 Description: schemaDescriptions["organization"], 118 }, 119 "token": { 120 Type: cty.String, 121 Optional: true, 122 Description: schemaDescriptions["token"], 123 }, 124 }, 125 126 BlockTypes: map[string]*configschema.NestedBlock{ 127 "workspaces": { 128 Block: configschema.Block{ 129 Attributes: map[string]*configschema.Attribute{ 130 "name": { 131 Type: cty.String, 132 Optional: true, 133 Description: schemaDescriptions["name"], 134 }, 135 "prefix": { 136 Type: cty.String, 137 Optional: true, 138 Description: schemaDescriptions["prefix"], 139 }, 140 }, 141 }, 142 Nesting: configschema.NestingSingle, 143 }, 144 }, 145 } 146 } 147 148 // PrepareConfig implements backend.Backend. 149 func (b *Remote) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { 150 var diags tfdiags.Diagnostics 151 if obj.IsNull() { 152 return obj, diags 153 } 154 155 if val := obj.GetAttr("organization"); val.IsNull() || val.AsString() == "" { 156 diags = diags.Append(tfdiags.AttributeValue( 157 tfdiags.Error, 158 "Invalid organization value", 159 `The "organization" attribute value must not be empty.`, 160 cty.Path{cty.GetAttrStep{Name: "organization"}}, 161 )) 162 } 163 164 var name, prefix string 165 if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() { 166 if val := workspaces.GetAttr("name"); !val.IsNull() { 167 name = val.AsString() 168 } 169 if val := workspaces.GetAttr("prefix"); !val.IsNull() { 170 prefix = val.AsString() 171 } 172 } 173 174 // Make sure that we have either a workspace name or a prefix. 175 if name == "" && prefix == "" { 176 diags = diags.Append(tfdiags.AttributeValue( 177 tfdiags.Error, 178 "Invalid workspaces configuration", 179 `Either workspace "name" or "prefix" is required.`, 180 cty.Path{cty.GetAttrStep{Name: "workspaces"}}, 181 )) 182 } 183 184 // Make sure that only one of workspace name or a prefix is configured. 185 if name != "" && prefix != "" { 186 diags = diags.Append(tfdiags.AttributeValue( 187 tfdiags.Error, 188 "Invalid workspaces configuration", 189 `Only one of workspace "name" or "prefix" is allowed.`, 190 cty.Path{cty.GetAttrStep{Name: "workspaces"}}, 191 )) 192 } 193 194 return obj, diags 195 } 196 197 // Configure implements backend.Enhanced. 198 func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics { 199 var diags tfdiags.Diagnostics 200 if obj.IsNull() { 201 return diags 202 } 203 204 // Get the hostname. 205 if val := obj.GetAttr("hostname"); !val.IsNull() && val.AsString() != "" { 206 b.hostname = val.AsString() 207 } else { 208 b.hostname = defaultHostname 209 } 210 211 // Get the organization. 212 if val := obj.GetAttr("organization"); !val.IsNull() { 213 b.organization = val.AsString() 214 } 215 216 // Get the workspaces configuration block and retrieve the 217 // default workspace name and prefix. 218 if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() { 219 if val := workspaces.GetAttr("name"); !val.IsNull() { 220 b.workspace = val.AsString() 221 } 222 if val := workspaces.GetAttr("prefix"); !val.IsNull() { 223 b.prefix = val.AsString() 224 } 225 } 226 227 // Determine if we are forced to use the local backend. 228 b.forceLocal = os.Getenv("TF_FORCE_LOCAL_BACKEND") != "" 229 230 serviceID := tfeServiceID 231 if b.forceLocal { 232 serviceID = stateServiceID 233 } 234 235 // Discover the service URL for this host to confirm that it provides 236 // a remote backend API and to get the version constraints. 237 service, constraints, err := b.discover(serviceID) 238 239 // First check any contraints we might have received. 240 if constraints != nil { 241 diags = diags.Append(b.checkConstraints(constraints)) 242 if diags.HasErrors() { 243 return diags 244 } 245 } 246 247 // When we don't have any constraints errors, also check for discovery 248 // errors before we continue. 249 if err != nil { 250 diags = diags.Append(tfdiags.AttributeValue( 251 tfdiags.Error, 252 strings.ToUpper(err.Error()[:1])+err.Error()[1:], 253 "", // no description is needed here, the error is clear 254 cty.Path{cty.GetAttrStep{Name: "hostname"}}, 255 )) 256 return diags 257 } 258 259 // Get the token from the config. 260 var token string 261 if val := obj.GetAttr("token"); !val.IsNull() { 262 token = val.AsString() 263 } 264 265 // Retrieve the token for this host as configured in the credentials 266 // section of the CLI Config File if no token was configured for this 267 // host in the config. 268 if token == "" { 269 token, err = b.token() 270 if err != nil { 271 diags = diags.Append(tfdiags.AttributeValue( 272 tfdiags.Error, 273 strings.ToUpper(err.Error()[:1])+err.Error()[1:], 274 "", // no description is needed here, the error is clear 275 cty.Path{cty.GetAttrStep{Name: "hostname"}}, 276 )) 277 return diags 278 } 279 } 280 281 // Return an error if we still don't have a token at this point. 282 if token == "" { 283 loginCommand := "terraform login" 284 if b.hostname != defaultHostname { 285 loginCommand = loginCommand + " " + b.hostname 286 } 287 diags = diags.Append(tfdiags.Sourceless( 288 tfdiags.Error, 289 "Required token could not be found", 290 fmt.Sprintf( 291 "Run the following command to generate a token for %s:\n %s", 292 b.hostname, 293 loginCommand, 294 ), 295 )) 296 return diags 297 } 298 299 cfg := &tfe.Config{ 300 Address: service.String(), 301 BasePath: service.Path, 302 Token: token, 303 Headers: make(http.Header), 304 RetryLogHook: b.retryLogHook, 305 } 306 307 // Set the version header to the current version. 308 cfg.Headers.Set(tfversion.Header, tfversion.Version) 309 310 // Create the remote backend API client. 311 b.client, err = tfe.NewClient(cfg) 312 if err != nil { 313 diags = diags.Append(tfdiags.Sourceless( 314 tfdiags.Error, 315 "Failed to create the Terraform Enterprise client", 316 fmt.Sprintf( 317 `The "remote" backend encountered an unexpected error while creating the `+ 318 `Terraform Enterprise client: %s.`, err, 319 ), 320 )) 321 return diags 322 } 323 324 // Check if the organization exists by reading its entitlements. 325 entitlements, err := b.client.Organizations.ReadEntitlements(context.Background(), b.organization) 326 if err != nil { 327 if err == tfe.ErrResourceNotFound { 328 err = fmt.Errorf("organization %q at host %s not found.\n\n"+ 329 "Please ensure that the organization and hostname are correct "+ 330 "and that your API token for %s is valid.", 331 b.organization, b.hostname, b.hostname) 332 } 333 diags = diags.Append(tfdiags.AttributeValue( 334 tfdiags.Error, 335 fmt.Sprintf("Failed to read organization %q at host %s", b.organization, b.hostname), 336 fmt.Sprintf("The \"remote\" backend encountered an unexpected error while reading the "+ 337 "organization settings: %s", err), 338 cty.Path{cty.GetAttrStep{Name: "organization"}}, 339 )) 340 return diags 341 } 342 343 // Configure a local backend for when we need to run operations locally. 344 b.local = backendLocal.NewWithBackend(b) 345 b.forceLocal = b.forceLocal || !entitlements.Operations 346 347 // Enable retries for server errors as the backend is now fully configured. 348 b.client.RetryServerErrors(true) 349 350 return diags 351 } 352 353 // discover the remote backend API service URL and version constraints. 354 func (b *Remote) discover(serviceID string) (*url.URL, *disco.Constraints, error) { 355 hostname, err := svchost.ForComparison(b.hostname) 356 if err != nil { 357 return nil, nil, err 358 } 359 360 host, err := b.services.Discover(hostname) 361 if err != nil { 362 return nil, nil, err 363 } 364 365 service, err := host.ServiceURL(serviceID) 366 // Return the error, unless its a disco.ErrVersionNotSupported error. 367 if _, ok := err.(*disco.ErrVersionNotSupported); !ok && err != nil { 368 return nil, nil, err 369 } 370 371 // We purposefully ignore the error and return the previous error, as 372 // checking for version constraints is considered optional. 373 constraints, _ := host.VersionConstraints(serviceID, "terraform") 374 375 return service, constraints, err 376 } 377 378 // checkConstraints checks service version constrains against our own 379 // version and returns rich and informational diagnostics in case any 380 // incompatibilities are detected. 381 func (b *Remote) checkConstraints(c *disco.Constraints) tfdiags.Diagnostics { 382 var diags tfdiags.Diagnostics 383 384 if c == nil || c.Minimum == "" || c.Maximum == "" { 385 return diags 386 } 387 388 // Generate a parsable constraints string. 389 excluding := "" 390 if len(c.Excluding) > 0 { 391 excluding = fmt.Sprintf(", != %s", strings.Join(c.Excluding, ", != ")) 392 } 393 constStr := fmt.Sprintf(">= %s%s, <= %s", c.Minimum, excluding, c.Maximum) 394 395 // Create the constraints to check against. 396 constraints, err := version.NewConstraint(constStr) 397 if err != nil { 398 return diags.Append(checkConstraintsWarning(err)) 399 } 400 401 // Create the version to check. 402 v, err := version.NewVersion(tfversion.Version) 403 if err != nil { 404 return diags.Append(checkConstraintsWarning(err)) 405 } 406 407 // Return if we satisfy all constraints. 408 if constraints.Check(v) { 409 return diags 410 } 411 412 // Find out what action (upgrade/downgrade) we should advice. 413 minimum, err := version.NewVersion(c.Minimum) 414 if err != nil { 415 return diags.Append(checkConstraintsWarning(err)) 416 } 417 418 maximum, err := version.NewVersion(c.Maximum) 419 if err != nil { 420 return diags.Append(checkConstraintsWarning(err)) 421 } 422 423 var excludes []*version.Version 424 for _, exclude := range c.Excluding { 425 v, err := version.NewVersion(exclude) 426 if err != nil { 427 return diags.Append(checkConstraintsWarning(err)) 428 } 429 excludes = append(excludes, v) 430 } 431 432 // Sort all the excludes. 433 sort.Sort(version.Collection(excludes)) 434 435 var action, toVersion string 436 switch { 437 case minimum.GreaterThan(v): 438 action = "upgrade" 439 toVersion = ">= " + minimum.String() 440 case maximum.LessThan(v): 441 action = "downgrade" 442 toVersion = "<= " + maximum.String() 443 case len(excludes) > 0: 444 // Get the latest excluded version. 445 action = "upgrade" 446 toVersion = "> " + excludes[len(excludes)-1].String() 447 } 448 449 switch { 450 case len(excludes) == 1: 451 excluding = fmt.Sprintf(", excluding version %s", excludes[0].String()) 452 case len(excludes) > 1: 453 var vs []string 454 for _, v := range excludes { 455 vs = append(vs, v.String()) 456 } 457 excluding = fmt.Sprintf(", excluding versions %s", strings.Join(vs, ", ")) 458 default: 459 excluding = "" 460 } 461 462 summary := fmt.Sprintf("Incompatible Terraform version v%s", v.String()) 463 details := fmt.Sprintf( 464 "The configured Terraform Enterprise backend is compatible with Terraform "+ 465 "versions >= %s, <= %s%s.", c.Minimum, c.Maximum, excluding, 466 ) 467 468 if action != "" && toVersion != "" { 469 summary = fmt.Sprintf("Please %s Terraform to %s", action, toVersion) 470 details += fmt.Sprintf(" Please %s to a supported version and try again.", action) 471 } 472 473 // Return the customized and informational error message. 474 return diags.Append(tfdiags.Sourceless(tfdiags.Error, summary, details)) 475 } 476 477 // token returns the token for this host as configured in the credentials 478 // section of the CLI Config File. If no token was configured, an empty 479 // string will be returned instead. 480 func (b *Remote) token() (string, error) { 481 hostname, err := svchost.ForComparison(b.hostname) 482 if err != nil { 483 return "", err 484 } 485 creds, err := b.services.CredentialsForHost(hostname) 486 if err != nil { 487 log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err) 488 return "", nil 489 } 490 if creds != nil { 491 return creds.Token(), nil 492 } 493 return "", nil 494 } 495 496 // retryLogHook is invoked each time a request is retried allowing the 497 // backend to log any connection issues to prevent data loss. 498 func (b *Remote) retryLogHook(attemptNum int, resp *http.Response) { 499 if b.CLI != nil { 500 // Ignore the first retry to make sure any delayed output will 501 // be written to the console before we start logging retries. 502 // 503 // The retry logic in the TFE client will retry both rate limited 504 // requests and server errors, but in the remote backend we only 505 // care about server errors so we ignore rate limit (429) errors. 506 if attemptNum == 0 || (resp != nil && resp.StatusCode == 429) { 507 // Reset the last retry time. 508 b.lastRetry = time.Now() 509 return 510 } 511 512 if attemptNum == 1 { 513 b.CLI.Output(b.Colorize().Color(strings.TrimSpace(initialRetryError))) 514 } else { 515 b.CLI.Output(b.Colorize().Color(strings.TrimSpace( 516 fmt.Sprintf(repeatedRetryError, time.Since(b.lastRetry).Round(time.Second))))) 517 } 518 } 519 } 520 521 // Workspaces implements backend.Enhanced. 522 func (b *Remote) Workspaces() ([]string, error) { 523 if b.prefix == "" { 524 return nil, backend.ErrWorkspacesNotSupported 525 } 526 return b.workspaces() 527 } 528 529 // workspaces returns a filtered list of remote workspace names. 530 func (b *Remote) workspaces() ([]string, error) { 531 options := &tfe.WorkspaceListOptions{} 532 switch { 533 case b.workspace != "": 534 options.Search = b.workspace 535 case b.prefix != "": 536 options.Search = b.prefix 537 } 538 539 // Create a slice to contain all the names. 540 var names []string 541 542 for { 543 wl, err := b.client.Workspaces.List(context.Background(), b.organization, options) 544 if err != nil { 545 return nil, err 546 } 547 548 for _, w := range wl.Items { 549 if b.workspace != "" && w.Name == b.workspace { 550 names = append(names, backend.DefaultStateName) 551 continue 552 } 553 if b.prefix != "" && strings.HasPrefix(w.Name, b.prefix) { 554 names = append(names, strings.TrimPrefix(w.Name, b.prefix)) 555 } 556 } 557 558 // Exit the loop when we've seen all pages. 559 if wl.CurrentPage >= wl.TotalPages { 560 break 561 } 562 563 // Update the page number to get the next page. 564 options.PageNumber = wl.NextPage 565 } 566 567 // Sort the result so we have consistent output. 568 sort.StringSlice(names).Sort() 569 570 return names, nil 571 } 572 573 // WorkspaceNamePattern provides an appropriate workspace renaming pattern for backend migration 574 // purposes (handled outside of this package), based on previous usage of this backend with the 575 // 'prefix' workspace functionality. As of this writing, see meta_backend.migrate.go 576 func (b *Remote) WorkspaceNamePattern() string { 577 if b.prefix != "" { 578 return b.prefix + "*" 579 } 580 581 return "" 582 } 583 584 // DeleteWorkspace implements backend.Enhanced. 585 func (b *Remote) DeleteWorkspace(name string) error { 586 if b.workspace == "" && name == backend.DefaultStateName { 587 return backend.ErrDefaultWorkspaceNotSupported 588 } 589 if b.prefix == "" && name != backend.DefaultStateName { 590 return backend.ErrWorkspacesNotSupported 591 } 592 593 // Configure the remote workspace name. 594 switch { 595 case name == backend.DefaultStateName: 596 name = b.workspace 597 case b.prefix != "" && !strings.HasPrefix(name, b.prefix): 598 name = b.prefix + name 599 } 600 601 client := &remoteClient{ 602 client: b.client, 603 organization: b.organization, 604 workspace: &tfe.Workspace{ 605 Name: name, 606 }, 607 } 608 609 return client.Delete() 610 } 611 612 // StateMgr implements backend.Enhanced. 613 func (b *Remote) StateMgr(name string) (statemgr.Full, error) { 614 if b.workspace == "" && name == backend.DefaultStateName { 615 return nil, backend.ErrDefaultWorkspaceNotSupported 616 } 617 if b.prefix == "" && name != backend.DefaultStateName { 618 return nil, backend.ErrWorkspacesNotSupported 619 } 620 621 // Configure the remote workspace name. 622 switch { 623 case name == backend.DefaultStateName: 624 name = b.workspace 625 case b.prefix != "" && !strings.HasPrefix(name, b.prefix): 626 name = b.prefix + name 627 } 628 629 workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, name) 630 if err != nil && err != tfe.ErrResourceNotFound { 631 return nil, fmt.Errorf("Failed to retrieve workspace %s: %v", name, err) 632 } 633 634 if err == tfe.ErrResourceNotFound { 635 options := tfe.WorkspaceCreateOptions{ 636 Name: tfe.String(name), 637 } 638 639 // We only set the Terraform Version for the new workspace if this is 640 // a release candidate or a final release. 641 if tfversion.Prerelease == "" || strings.HasPrefix(tfversion.Prerelease, "rc") { 642 options.TerraformVersion = tfe.String(tfversion.String()) 643 } 644 645 workspace, err = b.client.Workspaces.Create(context.Background(), b.organization, options) 646 if err != nil { 647 return nil, fmt.Errorf("Error creating workspace %s: %v", name, err) 648 } 649 } 650 651 // This is a fallback error check. Most code paths should use other 652 // mechanisms to check the version, then set the ignoreVersionConflict 653 // field to true. This check is only in place to ensure that we don't 654 // accidentally upgrade state with a new code path, and the version check 655 // logic is coarser and simpler. 656 if !b.ignoreVersionConflict { 657 wsv := workspace.TerraformVersion 658 // Explicitly ignore the pseudo-version "latest" here, as it will cause 659 // plan and apply to always fail. 660 if wsv != tfversion.String() && wsv != "latest" { 661 return nil, fmt.Errorf("Remote workspace Terraform version %q does not match local Terraform version %q", workspace.TerraformVersion, tfversion.String()) 662 } 663 } 664 665 client := &remoteClient{ 666 client: b.client, 667 organization: b.organization, 668 workspace: workspace, 669 670 // This is optionally set during Terraform Enterprise runs. 671 runID: os.Getenv("TFE_RUN_ID"), 672 } 673 674 return &remote.State{Client: client}, nil 675 } 676 677 func isLocalExecutionMode(execMode string) bool { 678 return execMode == "local" 679 } 680 681 func (b *Remote) fetchWorkspace(ctx context.Context, organization string, name string) (*tfe.Workspace, error) { 682 remoteWorkspaceName := b.getRemoteWorkspaceName(name) 683 // Retrieve the workspace for this operation. 684 w, err := b.client.Workspaces.Read(ctx, b.organization, remoteWorkspaceName) 685 if err != nil { 686 switch err { 687 case context.Canceled: 688 return nil, err 689 case tfe.ErrResourceNotFound: 690 return nil, fmt.Errorf( 691 "workspace %s not found\n\n"+ 692 "The configured \"remote\" backend returns '404 Not Found' errors for resources\n"+ 693 "that do not exist, as well as for resources that a user doesn't have access\n"+ 694 "to. If the resource does exist, please check the rights for the used token", 695 name, 696 ) 697 default: 698 err := fmt.Errorf( 699 "the configured \"remote\" backend encountered an unexpected error:\n\n%s", 700 err, 701 ) 702 return nil, err 703 } 704 } 705 706 return w, nil 707 } 708 709 // Operation implements backend.Enhanced. 710 func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) { 711 w, err := b.fetchWorkspace(ctx, b.organization, op.Workspace) 712 713 if err != nil { 714 return nil, err 715 } 716 717 // Terraform remote version conflicts are not a concern for operations. We 718 // are in one of three states: 719 // 720 // - Running remotely, in which case the local version is irrelevant; 721 // - Workspace configured for local operations, in which case the remote 722 // version is meaningless; 723 // - Forcing local operations with a remote backend, which should only 724 // happen in the Terraform Cloud worker, in which case the Terraform 725 // versions by definition match. 726 b.IgnoreVersionConflict() 727 728 // Check if we need to use the local backend to run the operation. 729 if b.forceLocal || isLocalExecutionMode(w.ExecutionMode) { 730 // Record that we're forced to run operations locally to allow the 731 // command package UI to operate correctly 732 b.forceLocal = true 733 log.Printf("[DEBUG] Remote backend is delegating %s to the local backend", op.Type) 734 return b.local.Operation(ctx, op) 735 } 736 737 // Set the remote workspace name. 738 op.Workspace = w.Name 739 740 // Determine the function to call for our operation 741 var f func(context.Context, context.Context, *backend.Operation, *tfe.Workspace) (*tfe.Run, error) 742 switch op.Type { 743 case backend.OperationTypePlan: 744 f = b.opPlan 745 case backend.OperationTypeApply: 746 f = b.opApply 747 case backend.OperationTypeRefresh: 748 return nil, fmt.Errorf( 749 "\n\nThe \"refresh\" operation is not supported when using the \"remote\" backend. " + 750 "Use \"terraform apply -refresh-only\" instead.") 751 default: 752 return nil, fmt.Errorf( 753 "\n\nThe \"remote\" backend does not support the %q operation.", op.Type) 754 } 755 756 // Lock 757 b.opLock.Lock() 758 759 // Build our running operation 760 // the runninCtx is only used to block until the operation returns. 761 runningCtx, done := context.WithCancel(context.Background()) 762 runningOp := &backend.RunningOperation{ 763 Context: runningCtx, 764 PlanEmpty: true, 765 } 766 767 // stopCtx wraps the context passed in, and is used to signal a graceful Stop. 768 stopCtx, stop := context.WithCancel(ctx) 769 runningOp.Stop = stop 770 771 // cancelCtx is used to cancel the operation immediately, usually 772 // indicating that the process is exiting. 773 cancelCtx, cancel := context.WithCancel(context.Background()) 774 runningOp.Cancel = cancel 775 776 // Do it. 777 go func() { 778 defer logging.PanicHandler() 779 defer done() 780 defer stop() 781 defer cancel() 782 783 defer b.opLock.Unlock() 784 785 r, opErr := f(stopCtx, cancelCtx, op, w) 786 if opErr != nil && opErr != context.Canceled { 787 var diags tfdiags.Diagnostics 788 diags = diags.Append(opErr) 789 op.ReportResult(runningOp, diags) 790 return 791 } 792 793 if r == nil && opErr == context.Canceled { 794 runningOp.Result = backend.OperationFailure 795 return 796 } 797 798 if r != nil { 799 // Retrieve the run to get its current status. 800 r, err := b.client.Runs.Read(cancelCtx, r.ID) 801 if err != nil { 802 var diags tfdiags.Diagnostics 803 diags = diags.Append(generalError("Failed to retrieve run", err)) 804 op.ReportResult(runningOp, diags) 805 return 806 } 807 808 // Record if there are any changes. 809 runningOp.PlanEmpty = !r.HasChanges 810 811 if opErr == context.Canceled { 812 if err := b.cancel(cancelCtx, op, r); err != nil { 813 var diags tfdiags.Diagnostics 814 diags = diags.Append(generalError("Failed to retrieve run", err)) 815 op.ReportResult(runningOp, diags) 816 return 817 } 818 } 819 820 if r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored { 821 runningOp.Result = backend.OperationFailure 822 } 823 } 824 }() 825 826 // Return the running operation. 827 return runningOp, nil 828 } 829 830 func (b *Remote) cancel(cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { 831 if r.Actions.IsCancelable { 832 // Only ask if the remote operation should be canceled 833 // if the auto approve flag is not set. 834 if !op.AutoApprove { 835 v, err := op.UIIn.Input(cancelCtx, &terraform.InputOpts{ 836 Id: "cancel", 837 Query: "\nDo you want to cancel the remote operation?", 838 Description: "Only 'yes' will be accepted to cancel.", 839 }) 840 if err != nil { 841 return generalError("Failed asking to cancel", err) 842 } 843 if v != "yes" { 844 if b.CLI != nil { 845 b.CLI.Output(b.Colorize().Color(strings.TrimSpace(operationNotCanceled))) 846 } 847 return nil 848 } 849 } else { 850 if b.CLI != nil { 851 // Insert a blank line to separate the ouputs. 852 b.CLI.Output("") 853 } 854 } 855 856 // Try to cancel the remote operation. 857 err := b.client.Runs.Cancel(cancelCtx, r.ID, tfe.RunCancelOptions{}) 858 if err != nil { 859 return generalError("Failed to cancel run", err) 860 } 861 if b.CLI != nil { 862 b.CLI.Output(b.Colorize().Color(strings.TrimSpace(operationCanceled))) 863 } 864 } 865 866 return nil 867 } 868 869 // IgnoreVersionConflict allows commands to disable the fall-back check that 870 // the local Terraform version matches the remote workspace's configured 871 // Terraform version. This should be called by commands where this check is 872 // unnecessary, such as those performing remote operations, or read-only 873 // operations. It will also be called if the user uses a command-line flag to 874 // override this check. 875 func (b *Remote) IgnoreVersionConflict() { 876 b.ignoreVersionConflict = true 877 } 878 879 // VerifyWorkspaceTerraformVersion compares the local Terraform version against 880 // the workspace's configured Terraform version. If they are equal, this means 881 // that there are no compatibility concerns, so it returns no diagnostics. 882 // 883 // If the versions differ, 884 func (b *Remote) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Diagnostics { 885 var diags tfdiags.Diagnostics 886 887 workspace, err := b.getRemoteWorkspace(context.Background(), workspaceName) 888 if err != nil { 889 // If the workspace doesn't exist, there can be no compatibility 890 // problem, so we can return. This is most likely to happen when 891 // migrating state from a local backend to a new workspace. 892 if err == tfe.ErrResourceNotFound { 893 return nil 894 } 895 896 diags = diags.Append(tfdiags.Sourceless( 897 tfdiags.Error, 898 "Error looking up workspace", 899 fmt.Sprintf("Workspace read failed: %s", err), 900 )) 901 return diags 902 } 903 904 // If the workspace has the pseudo-version "latest", all bets are off. We 905 // cannot reasonably determine what the intended Terraform version is, so 906 // we'll skip version verification. 907 if workspace.TerraformVersion == "latest" { 908 return nil 909 } 910 911 // If the workspace has remote operations disabled, the remote Terraform 912 // version is effectively meaningless, so we'll skip version verification. 913 if isLocalExecutionMode(workspace.ExecutionMode) { 914 return nil 915 } 916 917 remoteVersion, err := version.NewSemver(workspace.TerraformVersion) 918 if err != nil { 919 diags = diags.Append(tfdiags.Sourceless( 920 tfdiags.Error, 921 "Error looking up workspace", 922 fmt.Sprintf("Invalid Terraform version: %s", err), 923 )) 924 return diags 925 } 926 927 v014 := version.Must(version.NewSemver("0.14.0")) 928 if tfversion.SemVer.LessThan(v014) || remoteVersion.LessThan(v014) { 929 // Versions of Terraform prior to 0.14.0 will refuse to load state files 930 // written by a newer version of Terraform, even if it is only a patch 931 // level difference. As a result we require an exact match. 932 if tfversion.SemVer.Equal(remoteVersion) { 933 return diags 934 } 935 } 936 if tfversion.SemVer.GreaterThanOrEqual(v014) && remoteVersion.GreaterThanOrEqual(v014) { 937 // Versions of Terraform after 0.14.0 should be compatible with each 938 // other. At the time this code was written, the only constraints we 939 // are aware of are: 940 // 941 // - 0.14.0 is guaranteed to be compatible with versions up to but not 942 // including 1.3.0 943 v130 := version.Must(version.NewSemver("1.3.0")) 944 if tfversion.SemVer.LessThan(v130) && remoteVersion.LessThan(v130) { 945 return diags 946 } 947 // - Any new Terraform state version will require at least minor patch 948 // increment, so x.y.* will always be compatible with each other 949 tfvs := tfversion.SemVer.Segments64() 950 rwvs := remoteVersion.Segments64() 951 if len(tfvs) == 3 && len(rwvs) == 3 && tfvs[0] == rwvs[0] && tfvs[1] == rwvs[1] { 952 return diags 953 } 954 } 955 956 // Even if ignoring version conflicts, it may still be useful to call this 957 // method and warn the user about a mismatch between the local and remote 958 // Terraform versions. 959 severity := tfdiags.Error 960 if b.ignoreVersionConflict { 961 severity = tfdiags.Warning 962 } 963 964 suggestion := " If you're sure you want to upgrade the state, you can force Terraform to continue using the -ignore-remote-version flag. This may result in an unusable workspace." 965 if b.ignoreVersionConflict { 966 suggestion = "" 967 } 968 diags = diags.Append(tfdiags.Sourceless( 969 severity, 970 "Terraform version mismatch", 971 fmt.Sprintf( 972 "The local Terraform version (%s) does not match the configured version for remote workspace %s/%s (%s).%s", 973 tfversion.String(), 974 b.organization, 975 workspace.Name, 976 workspace.TerraformVersion, 977 suggestion, 978 ), 979 )) 980 981 return diags 982 } 983 984 func (b *Remote) IsLocalOperations() bool { 985 return b.forceLocal 986 } 987 988 func generalError(msg string, err error) error { 989 var diags tfdiags.Diagnostics 990 991 if urlErr, ok := err.(*url.Error); ok { 992 err = urlErr.Err 993 } 994 995 switch err { 996 case context.Canceled: 997 return err 998 case tfe.ErrResourceNotFound: 999 diags = diags.Append(tfdiags.Sourceless( 1000 tfdiags.Error, 1001 fmt.Sprintf("%s: %v", msg, err), 1002 `The configured "remote" backend returns '404 Not Found' errors for resources `+ 1003 `that do not exist, as well as for resources that a user doesn't have access `+ 1004 `to. If the resource does exist, please check the rights for the used token.`, 1005 )) 1006 return diags.Err() 1007 default: 1008 diags = diags.Append(tfdiags.Sourceless( 1009 tfdiags.Error, 1010 fmt.Sprintf("%s: %v", msg, err), 1011 `The configured "remote" backend encountered an unexpected error. Sometimes `+ 1012 `this is caused by network connection problems, in which case you could retry `+ 1013 `the command. If the issue persists please open a support ticket to get help `+ 1014 `resolving the problem.`, 1015 )) 1016 return diags.Err() 1017 } 1018 } 1019 1020 func checkConstraintsWarning(err error) tfdiags.Diagnostic { 1021 return tfdiags.Sourceless( 1022 tfdiags.Warning, 1023 fmt.Sprintf("Failed to check version constraints: %v", err), 1024 "Checking version constraints is considered optional, but this is an"+ 1025 "unexpected error which should be reported.", 1026 ) 1027 } 1028 1029 // The newline in this error is to make it look good in the CLI! 1030 const initialRetryError = ` 1031 [reset][yellow]There was an error connecting to the remote backend. Please do not exit 1032 Terraform to prevent data loss! Trying to restore the connection... 1033 [reset] 1034 ` 1035 1036 const repeatedRetryError = ` 1037 [reset][yellow]Still trying to restore the connection... (%s elapsed)[reset] 1038 ` 1039 1040 const operationCanceled = ` 1041 [reset][red]The remote operation was successfully cancelled.[reset] 1042 ` 1043 1044 const operationNotCanceled = ` 1045 [reset][red]The remote operation was not cancelled.[reset] 1046 ` 1047 1048 var schemaDescriptions = map[string]string{ 1049 "hostname": "The remote backend hostname to connect to (defaults to app.terraform.io).", 1050 "organization": "The name of the organization containing the targeted workspace(s).", 1051 "token": "The token used to authenticate with the remote backend. If credentials for the\n" + 1052 "host are configured in the CLI Config File, then those will be used instead.", 1053 "name": "A workspace name used to map the default workspace to a named remote workspace.\n" + 1054 "When configured only the default workspace can be used. This option conflicts\n" + 1055 "with \"prefix\"", 1056 "prefix": "A prefix used to filter workspaces using a single configuration. New workspaces\n" + 1057 "will automatically be prefixed with this prefix. If omitted only the default\n" + 1058 "workspace can be used. This option conflicts with \"name\"", 1059 }