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