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