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