github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/backend/remote/backend.go (about)

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