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