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  }