github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/cloud/backend.go (about)

     1  package cloud
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"log"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"sort"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	tfe "github.com/hashicorp/go-tfe"
    17  	version "github.com/hashicorp/go-version"
    18  	svchost "github.com/hashicorp/terraform-svchost"
    19  	"github.com/hashicorp/terraform-svchost/disco"
    20  	"github.com/mitchellh/cli"
    21  	"github.com/mitchellh/colorstring"
    22  	"github.com/zclconf/go-cty/cty"
    23  	"github.com/zclconf/go-cty/cty/gocty"
    24  
    25  	"github.com/hashicorp/terraform/internal/backend"
    26  	"github.com/hashicorp/terraform/internal/command/jsonformat"
    27  	"github.com/hashicorp/terraform/internal/configs/configschema"
    28  	"github.com/hashicorp/terraform/internal/plans"
    29  	"github.com/hashicorp/terraform/internal/states/statemgr"
    30  	"github.com/hashicorp/terraform/internal/terraform"
    31  	"github.com/hashicorp/terraform/internal/tfdiags"
    32  	tfversion "github.com/hashicorp/terraform/version"
    33  
    34  	backendLocal "github.com/hashicorp/terraform/internal/backend/local"
    35  )
    36  
    37  const (
    38  	defaultHostname    = "app.terraform.io"
    39  	defaultParallelism = 10
    40  	tfeServiceID       = "tfe.v2"
    41  	headerSourceKey    = "X-Terraform-Integration"
    42  	headerSourceValue  = "cloud"
    43  	genericHostname    = "localterraform.com"
    44  )
    45  
    46  // Cloud is an implementation of EnhancedBackend in service of the Terraform Cloud/Enterprise
    47  // integration for Terraform CLI. This backend is not intended to be surfaced at the user level and
    48  // is instead an implementation detail of cloud.Cloud.
    49  type Cloud struct {
    50  	// CLI and Colorize control the CLI output. If CLI is nil then no CLI
    51  	// output will be done. If CLIColor is nil then no coloring will be done.
    52  	CLI      cli.Ui
    53  	CLIColor *colorstring.Colorize
    54  
    55  	// ContextOpts are the base context options to set when initializing a
    56  	// new Terraform context. Many of these will be overridden or merged by
    57  	// Operation. See Operation for more details.
    58  	ContextOpts *terraform.ContextOpts
    59  
    60  	// client is the Terraform Cloud/Enterprise API client.
    61  	client *tfe.Client
    62  
    63  	// lastRetry is set to the last time a request was retried.
    64  	lastRetry time.Time
    65  
    66  	// hostname of Terraform Cloud or Terraform Enterprise
    67  	hostname string
    68  
    69  	// token for Terraform Cloud or Terraform Enterprise
    70  	token string
    71  
    72  	// organization is the organization that contains the target workspaces.
    73  	organization string
    74  
    75  	// WorkspaceMapping contains strategies for mapping CLI workspaces in the working directory
    76  	// to remote Terraform Cloud workspaces.
    77  	WorkspaceMapping WorkspaceMapping
    78  
    79  	// services is used for service discovery
    80  	services *disco.Disco
    81  
    82  	// renderer is used for rendering JSON plan output and streamed logs.
    83  	renderer *jsonformat.Renderer
    84  
    85  	// local allows local operations, where Terraform Cloud serves as a state storage backend.
    86  	local backend.Enhanced
    87  
    88  	// forceLocal, if true, will force the use of the local backend.
    89  	forceLocal bool
    90  
    91  	// opLock locks operations
    92  	opLock sync.Mutex
    93  
    94  	// ignoreVersionConflict, if true, will disable the requirement that the
    95  	// local Terraform version matches the remote workspace's configured
    96  	// version. This will also cause VerifyWorkspaceTerraformVersion to return
    97  	// a warning diagnostic instead of an error.
    98  	ignoreVersionConflict bool
    99  
   100  	runningInAutomation bool
   101  
   102  	// input stores the value of the -input flag, since it will be used
   103  	// to determine whether or not to ask the user for approval of a run.
   104  	input bool
   105  }
   106  
   107  var _ backend.Backend = (*Cloud)(nil)
   108  var _ backend.Enhanced = (*Cloud)(nil)
   109  var _ backend.Local = (*Cloud)(nil)
   110  
   111  // New creates a new initialized cloud backend.
   112  func New(services *disco.Disco) *Cloud {
   113  	return &Cloud{
   114  		services: services,
   115  	}
   116  }
   117  
   118  // ConfigSchema implements backend.Enhanced.
   119  func (b *Cloud) ConfigSchema() *configschema.Block {
   120  	return &configschema.Block{
   121  		Attributes: map[string]*configschema.Attribute{
   122  			"hostname": {
   123  				Type:        cty.String,
   124  				Optional:    true,
   125  				Description: schemaDescriptionHostname,
   126  			},
   127  			"organization": {
   128  				Type:        cty.String,
   129  				Optional:    true,
   130  				Description: schemaDescriptionOrganization,
   131  			},
   132  			"token": {
   133  				Type:        cty.String,
   134  				Optional:    true,
   135  				Description: schemaDescriptionToken,
   136  			},
   137  		},
   138  
   139  		BlockTypes: map[string]*configschema.NestedBlock{
   140  			"workspaces": {
   141  				Block: configschema.Block{
   142  					Attributes: map[string]*configschema.Attribute{
   143  						"name": {
   144  							Type:        cty.String,
   145  							Optional:    true,
   146  							Description: schemaDescriptionName,
   147  						},
   148  						"tags": {
   149  							Type:        cty.Set(cty.String),
   150  							Optional:    true,
   151  							Description: schemaDescriptionTags,
   152  						},
   153  					},
   154  				},
   155  				Nesting: configschema.NestingSingle,
   156  			},
   157  		},
   158  	}
   159  }
   160  
   161  // PrepareConfig implements backend.Backend.
   162  func (b *Cloud) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) {
   163  	var diags tfdiags.Diagnostics
   164  	if obj.IsNull() {
   165  		return obj, diags
   166  	}
   167  
   168  	// check if organization is specified in the config.
   169  	if val := obj.GetAttr("organization"); val.IsNull() || val.AsString() == "" {
   170  		// organization is specified in the config but is invalid, so
   171  		// we'll fallback on TF_CLOUD_ORGANIZATION
   172  		if val := os.Getenv("TF_CLOUD_ORGANIZATION"); val == "" {
   173  			diags = diags.Append(missingConfigAttributeAndEnvVar("organization", "TF_CLOUD_ORGANIZATION"))
   174  		}
   175  	}
   176  
   177  	WorkspaceMapping := WorkspaceMapping{}
   178  	if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() {
   179  		if val := workspaces.GetAttr("name"); !val.IsNull() {
   180  			WorkspaceMapping.Name = val.AsString()
   181  		}
   182  		if val := workspaces.GetAttr("tags"); !val.IsNull() {
   183  			err := gocty.FromCtyValue(val, &WorkspaceMapping.Tags)
   184  			if err != nil {
   185  				log.Panicf("An unxpected error occurred: %s", err)
   186  			}
   187  		}
   188  	} else {
   189  		WorkspaceMapping.Name = os.Getenv("TF_WORKSPACE")
   190  	}
   191  
   192  	switch WorkspaceMapping.Strategy() {
   193  	// Make sure have a workspace mapping strategy present
   194  	case WorkspaceNoneStrategy:
   195  		diags = diags.Append(invalidWorkspaceConfigMissingValues)
   196  	// Make sure that a workspace name is configured.
   197  	case WorkspaceInvalidStrategy:
   198  		diags = diags.Append(invalidWorkspaceConfigMisconfiguration)
   199  	}
   200  
   201  	return obj, diags
   202  }
   203  
   204  // configureGenericHostname aliases the cloud backend hostname configuration
   205  // as a generic "localterraform.com" hostname. This was originally added as a
   206  // Terraform Enterprise feature and is useful for re-using whatever the
   207  // Cloud/Enterprise backend host is in nested module sources in order
   208  // to prevent code churn when re-using config between multiple
   209  // Terraform Enterprise environments.
   210  func (b *Cloud) configureGenericHostname() {
   211  	// This won't be an error for the given constant value
   212  	genericHost, _ := svchost.ForComparison(genericHostname)
   213  
   214  	// This won't be an error because, by this time, the hostname has been parsed and
   215  	// service discovery requests made against it.
   216  	targetHost, _ := svchost.ForComparison(b.hostname)
   217  
   218  	b.services.Alias(genericHost, targetHost)
   219  }
   220  
   221  // Configure implements backend.Enhanced.
   222  func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics {
   223  	var diags tfdiags.Diagnostics
   224  	if obj.IsNull() {
   225  		return diags
   226  	}
   227  
   228  	diagErr := b.setConfigurationFields(obj)
   229  	if diagErr.HasErrors() {
   230  		return diagErr
   231  	}
   232  
   233  	// Discover the service URL to confirm that it provides the Terraform Cloud/Enterprise API
   234  	service, err := b.discover()
   235  
   236  	// Check for errors before we continue.
   237  	if err != nil {
   238  		diags = diags.Append(tfdiags.AttributeValue(
   239  			tfdiags.Error,
   240  			strings.ToUpper(err.Error()[:1])+err.Error()[1:],
   241  			"", // no description is needed here, the error is clear
   242  			cty.Path{cty.GetAttrStep{Name: "hostname"}},
   243  		))
   244  		return diags
   245  	}
   246  
   247  	// First we'll retrieve the token from the configuration
   248  	var token string
   249  	if val := obj.GetAttr("token"); !val.IsNull() {
   250  		token = val.AsString()
   251  	}
   252  
   253  	// Get the token from the CLI Config File in the credentials section
   254  	// if no token was not set in the configuration
   255  	if token == "" {
   256  		token, err = b.cliConfigToken()
   257  		if err != nil {
   258  			diags = diags.Append(tfdiags.AttributeValue(
   259  				tfdiags.Error,
   260  				strings.ToUpper(err.Error()[:1])+err.Error()[1:],
   261  				"", // no description is needed here, the error is clear
   262  				cty.Path{cty.GetAttrStep{Name: "hostname"}},
   263  			))
   264  			return diags
   265  		}
   266  	}
   267  
   268  	// Return an error if we still don't have a token at this point.
   269  	if token == "" {
   270  		loginCommand := "terraform login"
   271  		if b.hostname != defaultHostname {
   272  			loginCommand = loginCommand + " " + b.hostname
   273  		}
   274  		diags = diags.Append(tfdiags.Sourceless(
   275  			tfdiags.Error,
   276  			"Required token could not be found",
   277  			fmt.Sprintf(
   278  				"Run the following command to generate a token for %s:\n    %s",
   279  				b.hostname,
   280  				loginCommand,
   281  			),
   282  		))
   283  		return diags
   284  	}
   285  
   286  	b.token = token
   287  	b.configureGenericHostname()
   288  
   289  	if b.client == nil {
   290  		cfg := &tfe.Config{
   291  			Address:      service.String(),
   292  			BasePath:     service.Path,
   293  			Token:        token,
   294  			Headers:      make(http.Header),
   295  			RetryLogHook: b.retryLogHook,
   296  		}
   297  
   298  		// Set the version header to the current version.
   299  		cfg.Headers.Set(tfversion.Header, tfversion.Version)
   300  		cfg.Headers.Set(headerSourceKey, headerSourceValue)
   301  
   302  		// Create the TFC/E API client.
   303  		b.client, err = tfe.NewClient(cfg)
   304  		if err != nil {
   305  			diags = diags.Append(tfdiags.Sourceless(
   306  				tfdiags.Error,
   307  				"Failed to create the Terraform Cloud/Enterprise client",
   308  				fmt.Sprintf(
   309  					`Encountered an unexpected error while creating the `+
   310  						`Terraform Cloud/Enterprise client: %s.`, err,
   311  				),
   312  			))
   313  			return diags
   314  		}
   315  	}
   316  
   317  	// Check if the organization exists by reading its entitlements.
   318  	entitlements, err := b.client.Organizations.ReadEntitlements(context.Background(), b.organization)
   319  	if err != nil {
   320  		if err == tfe.ErrResourceNotFound {
   321  			err = fmt.Errorf("organization %q at host %s not found.\n\n"+
   322  				"Please ensure that the organization and hostname are correct "+
   323  				"and that your API token for %s is valid.",
   324  				b.organization, b.hostname, b.hostname)
   325  		}
   326  		diags = diags.Append(tfdiags.AttributeValue(
   327  			tfdiags.Error,
   328  			fmt.Sprintf("Failed to read organization %q at host %s", b.organization, b.hostname),
   329  			fmt.Sprintf("Encountered an unexpected error while reading the "+
   330  				"organization settings: %s", err),
   331  			cty.Path{cty.GetAttrStep{Name: "organization"}},
   332  		))
   333  		return diags
   334  	}
   335  
   336  	if ws, ok := os.LookupEnv("TF_WORKSPACE"); ok {
   337  		if ws == b.WorkspaceMapping.Name || b.WorkspaceMapping.Strategy() == WorkspaceTagsStrategy {
   338  			diag := b.validWorkspaceEnvVar(context.Background(), b.organization, ws)
   339  			if diag != nil {
   340  				diags = diags.Append(diag)
   341  				return diags
   342  			}
   343  		}
   344  	}
   345  
   346  	// Check for the minimum version of Terraform Enterprise required.
   347  	//
   348  	// For API versions prior to 2.3, RemoteAPIVersion will return an empty string,
   349  	// so if there's an error when parsing the RemoteAPIVersion, it's handled as
   350  	// equivalent to an API version < 2.3.
   351  	currentAPIVersion, parseErr := version.NewVersion(b.client.RemoteAPIVersion())
   352  	desiredAPIVersion, _ := version.NewVersion("2.5")
   353  
   354  	if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) {
   355  		log.Printf("[TRACE] API version check failed; want: >= %s, got: %s", desiredAPIVersion.Original(), currentAPIVersion)
   356  		if b.runningInAutomation {
   357  			// It should never be possible for this Terraform process to be mistakenly
   358  			// used internally within an unsupported Terraform Enterprise install - but
   359  			// just in case it happens, give an actionable error.
   360  			diags = diags.Append(
   361  				tfdiags.Sourceless(
   362  					tfdiags.Error,
   363  					"Unsupported Terraform Enterprise version",
   364  					cloudIntegrationUsedInUnsupportedTFE,
   365  				),
   366  			)
   367  		} else {
   368  			diags = diags.Append(tfdiags.Sourceless(
   369  				tfdiags.Error,
   370  				"Unsupported Terraform Enterprise version",
   371  				`The 'cloud' option is not supported with this version of Terraform Enterprise.`,
   372  			),
   373  			)
   374  		}
   375  	}
   376  
   377  	// Configure a local backend for when we need to run operations locally.
   378  	b.local = backendLocal.NewWithBackend(b)
   379  	b.forceLocal = b.forceLocal || !entitlements.Operations
   380  
   381  	// Enable retries for server errors as the backend is now fully configured.
   382  	b.client.RetryServerErrors(true)
   383  
   384  	return diags
   385  }
   386  
   387  func (b *Cloud) setConfigurationFields(obj cty.Value) tfdiags.Diagnostics {
   388  	var diags tfdiags.Diagnostics
   389  
   390  	// Get the hostname.
   391  	b.hostname = os.Getenv("TF_CLOUD_HOSTNAME")
   392  	if val := obj.GetAttr("hostname"); !val.IsNull() && val.AsString() != "" {
   393  		b.hostname = val.AsString()
   394  	} else if b.hostname == "" {
   395  		b.hostname = defaultHostname
   396  	}
   397  
   398  	// We can have two options, setting the organization via the config
   399  	// or using TF_CLOUD_ORGANIZATION. Since PrepareConfig() validates that one of these
   400  	// values must exist, we'll initially set it to the env var and override it if
   401  	// specified in the configuration.
   402  	b.organization = os.Getenv("TF_CLOUD_ORGANIZATION")
   403  
   404  	// Check if the organization is present and valid in the config.
   405  	if val := obj.GetAttr("organization"); !val.IsNull() && val.AsString() != "" {
   406  		b.organization = val.AsString()
   407  	}
   408  
   409  	// Get the workspaces configuration block and retrieve the
   410  	// default workspace name.
   411  	if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() {
   412  
   413  		// PrepareConfig checks that you cannot set both of these.
   414  		if val := workspaces.GetAttr("name"); !val.IsNull() {
   415  			b.WorkspaceMapping.Name = val.AsString()
   416  		}
   417  		if val := workspaces.GetAttr("tags"); !val.IsNull() {
   418  			var tags []string
   419  			err := gocty.FromCtyValue(val, &tags)
   420  			if err != nil {
   421  				log.Panicf("An unexpected error occurred: %s", err)
   422  			}
   423  
   424  			b.WorkspaceMapping.Tags = tags
   425  		}
   426  	} else {
   427  		b.WorkspaceMapping.Name = os.Getenv("TF_WORKSPACE")
   428  	}
   429  
   430  	// Determine if we are forced to use the local backend.
   431  	b.forceLocal = os.Getenv("TF_FORCE_LOCAL_BACKEND") != ""
   432  
   433  	return diags
   434  }
   435  
   436  // discover the TFC/E API service URL and version constraints.
   437  func (b *Cloud) discover() (*url.URL, error) {
   438  	hostname, err := svchost.ForComparison(b.hostname)
   439  	if err != nil {
   440  		return nil, err
   441  	}
   442  
   443  	host, err := b.services.Discover(hostname)
   444  	if err != nil {
   445  		var serviceDiscoErr *disco.ErrServiceDiscoveryNetworkRequest
   446  
   447  		switch {
   448  		case errors.As(err, &serviceDiscoErr):
   449  			err = fmt.Errorf("a network issue prevented cloud configuration; %w", err)
   450  			return nil, err
   451  		default:
   452  			return nil, err
   453  		}
   454  	}
   455  
   456  	service, err := host.ServiceURL(tfeServiceID)
   457  	// Return the error, unless its a disco.ErrVersionNotSupported error.
   458  	if _, ok := err.(*disco.ErrVersionNotSupported); !ok && err != nil {
   459  		return nil, err
   460  	}
   461  
   462  	return service, err
   463  }
   464  
   465  // cliConfigToken returns the token for this host as configured in the credentials
   466  // section of the CLI Config File. If no token was configured, an empty
   467  // string will be returned instead.
   468  func (b *Cloud) cliConfigToken() (string, error) {
   469  	hostname, err := svchost.ForComparison(b.hostname)
   470  	if err != nil {
   471  		return "", err
   472  	}
   473  	creds, err := b.services.CredentialsForHost(hostname)
   474  	if err != nil {
   475  		log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err)
   476  		return "", nil
   477  	}
   478  	if creds != nil {
   479  		return creds.Token(), nil
   480  	}
   481  	return "", nil
   482  }
   483  
   484  // retryLogHook is invoked each time a request is retried allowing the
   485  // backend to log any connection issues to prevent data loss.
   486  func (b *Cloud) retryLogHook(attemptNum int, resp *http.Response) {
   487  	if b.CLI != nil {
   488  		// Ignore the first retry to make sure any delayed output will
   489  		// be written to the console before we start logging retries.
   490  		//
   491  		// The retry logic in the TFE client will retry both rate limited
   492  		// requests and server errors, but in the cloud backend we only
   493  		// care about server errors so we ignore rate limit (429) errors.
   494  		if attemptNum == 0 || (resp != nil && resp.StatusCode == 429) {
   495  			// Reset the last retry time.
   496  			b.lastRetry = time.Now()
   497  			return
   498  		}
   499  
   500  		if attemptNum == 1 {
   501  			b.CLI.Output(b.Colorize().Color(strings.TrimSpace(initialRetryError)))
   502  		} else {
   503  			b.CLI.Output(b.Colorize().Color(strings.TrimSpace(
   504  				fmt.Sprintf(repeatedRetryError, time.Since(b.lastRetry).Round(time.Second)))))
   505  		}
   506  	}
   507  }
   508  
   509  // Workspaces implements backend.Enhanced, returning a filtered list of workspace names according to
   510  // the workspace mapping strategy configured.
   511  func (b *Cloud) Workspaces() ([]string, error) {
   512  	// Create a slice to contain all the names.
   513  	var names []string
   514  
   515  	// If configured for a single workspace, return that exact name only.  The StateMgr for this
   516  	// backend will automatically create the remote workspace if it does not yet exist.
   517  	if b.WorkspaceMapping.Strategy() == WorkspaceNameStrategy {
   518  		names = append(names, b.WorkspaceMapping.Name)
   519  		return names, nil
   520  	}
   521  
   522  	// Otherwise, multiple workspaces are being mapped. Query Terraform Cloud for all the remote
   523  	// workspaces by the provided mapping strategy.
   524  	options := &tfe.WorkspaceListOptions{}
   525  	if b.WorkspaceMapping.Strategy() == WorkspaceTagsStrategy {
   526  		taglist := strings.Join(b.WorkspaceMapping.Tags, ",")
   527  		options.Tags = taglist
   528  	}
   529  
   530  	for {
   531  		wl, err := b.client.Workspaces.List(context.Background(), b.organization, options)
   532  		if err != nil {
   533  			return nil, err
   534  		}
   535  
   536  		for _, w := range wl.Items {
   537  			names = append(names, w.Name)
   538  		}
   539  
   540  		// Exit the loop when we've seen all pages.
   541  		if wl.CurrentPage >= wl.TotalPages {
   542  			break
   543  		}
   544  
   545  		// Update the page number to get the next page.
   546  		options.PageNumber = wl.NextPage
   547  	}
   548  
   549  	// Sort the result so we have consistent output.
   550  	sort.StringSlice(names).Sort()
   551  
   552  	return names, nil
   553  }
   554  
   555  // DeleteWorkspace implements backend.Enhanced.
   556  func (b *Cloud) DeleteWorkspace(name string, force bool) error {
   557  	if name == backend.DefaultStateName {
   558  		return backend.ErrDefaultWorkspaceNotSupported
   559  	}
   560  
   561  	if b.WorkspaceMapping.Strategy() == WorkspaceNameStrategy {
   562  		return backend.ErrWorkspacesNotSupported
   563  	}
   564  
   565  	workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, name)
   566  	if err == tfe.ErrResourceNotFound {
   567  		return nil // If the workspace does not exist, succeed
   568  	}
   569  
   570  	if err != nil {
   571  		return fmt.Errorf("failed to retrieve workspace %s: %v", name, err)
   572  	}
   573  
   574  	// Configure the remote workspace name.
   575  	State := &State{tfeClient: b.client, organization: b.organization, workspace: workspace}
   576  	return State.Delete(force)
   577  }
   578  
   579  // StateMgr implements backend.Enhanced.
   580  func (b *Cloud) StateMgr(name string) (statemgr.Full, error) {
   581  	var remoteTFVersion string
   582  
   583  	if name == backend.DefaultStateName {
   584  		return nil, backend.ErrDefaultWorkspaceNotSupported
   585  	}
   586  
   587  	if b.WorkspaceMapping.Strategy() == WorkspaceNameStrategy && name != b.WorkspaceMapping.Name {
   588  		return nil, backend.ErrWorkspacesNotSupported
   589  	}
   590  
   591  	workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, name)
   592  	if err != nil && err != tfe.ErrResourceNotFound {
   593  		return nil, fmt.Errorf("Failed to retrieve workspace %s: %v", name, err)
   594  	}
   595  	if workspace != nil {
   596  		remoteTFVersion = workspace.TerraformVersion
   597  	}
   598  
   599  	if err == tfe.ErrResourceNotFound {
   600  		// Create a workspace
   601  		options := tfe.WorkspaceCreateOptions{
   602  			Name: tfe.String(name),
   603  			Tags: b.WorkspaceMapping.tfeTags(),
   604  		}
   605  
   606  		log.Printf("[TRACE] cloud: Creating Terraform Cloud workspace %s/%s", b.organization, name)
   607  		workspace, err = b.client.Workspaces.Create(context.Background(), b.organization, options)
   608  		if err != nil {
   609  			return nil, fmt.Errorf("Error creating workspace %s: %v", name, err)
   610  		}
   611  
   612  		remoteTFVersion = workspace.TerraformVersion
   613  
   614  		// Attempt to set the new workspace to use this version of Terraform. This
   615  		// can fail if there's no enabled tool_version whose name matches our
   616  		// version string, but that's expected sometimes -- just warn and continue.
   617  		versionOptions := tfe.WorkspaceUpdateOptions{
   618  			TerraformVersion: tfe.String(tfversion.String()),
   619  		}
   620  		_, err := b.client.Workspaces.UpdateByID(context.Background(), workspace.ID, versionOptions)
   621  		if err == nil {
   622  			remoteTFVersion = tfversion.String()
   623  		} else {
   624  			// TODO: Ideally we could rely on the client to tell us what the actual
   625  			// problem was, but we currently can't get enough context from the error
   626  			// object to do a nicely formatted message, so we're just assuming the
   627  			// issue was that the version wasn't available since that's probably what
   628  			// happened.
   629  			log.Printf("[TRACE] cloud: Attempted to select version %s for TFC workspace; unavailable, so %s will be used instead.", tfversion.String(), workspace.TerraformVersion)
   630  			if b.CLI != nil {
   631  				versionUnavailable := fmt.Sprintf(unavailableTerraformVersion, tfversion.String(), workspace.TerraformVersion)
   632  				b.CLI.Output(b.Colorize().Color(versionUnavailable))
   633  			}
   634  		}
   635  	}
   636  
   637  	if b.workspaceTagsRequireUpdate(workspace, b.WorkspaceMapping) {
   638  		options := tfe.WorkspaceAddTagsOptions{
   639  			Tags: b.WorkspaceMapping.tfeTags(),
   640  		}
   641  		log.Printf("[TRACE] cloud: Adding tags for Terraform Cloud workspace %s/%s", b.organization, name)
   642  		err = b.client.Workspaces.AddTags(context.Background(), workspace.ID, options)
   643  		if err != nil {
   644  			return nil, fmt.Errorf("Error updating workspace %s: %v", name, err)
   645  		}
   646  	}
   647  
   648  	// This is a fallback error check. Most code paths should use other
   649  	// mechanisms to check the version, then set the ignoreVersionConflict
   650  	// field to true. This check is only in place to ensure that we don't
   651  	// accidentally upgrade state with a new code path, and the version check
   652  	// logic is coarser and simpler.
   653  	if !b.ignoreVersionConflict {
   654  		// Explicitly ignore the pseudo-version "latest" here, as it will cause
   655  		// plan and apply to always fail.
   656  		if remoteTFVersion != tfversion.String() && remoteTFVersion != "latest" {
   657  			return nil, fmt.Errorf("Remote workspace Terraform version %q does not match local Terraform version %q", remoteTFVersion, tfversion.String())
   658  		}
   659  	}
   660  
   661  	return &State{tfeClient: b.client, organization: b.organization, workspace: workspace}, nil
   662  }
   663  
   664  // Operation implements backend.Enhanced.
   665  func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
   666  	// Retrieve the workspace for this operation.
   667  	w, err := b.fetchWorkspace(ctx, b.organization, op.Workspace)
   668  	if err != nil {
   669  		return nil, err
   670  	}
   671  
   672  	// Terraform remote version conflicts are not a concern for operations. We
   673  	// are in one of three states:
   674  	//
   675  	// - Running remotely, in which case the local version is irrelevant;
   676  	// - Workspace configured for local operations, in which case the remote
   677  	//   version is meaningless;
   678  	// - Forcing local operations, which should only happen in the Terraform Cloud worker, in
   679  	//   which case the Terraform versions by definition match.
   680  	b.IgnoreVersionConflict()
   681  
   682  	// Check if we need to use the local backend to run the operation.
   683  	if b.forceLocal || isLocalExecutionMode(w.ExecutionMode) {
   684  		// Record that we're forced to run operations locally to allow the
   685  		// command package UI to operate correctly
   686  		b.forceLocal = true
   687  		return b.local.Operation(ctx, op)
   688  	}
   689  
   690  	// Set the remote workspace name.
   691  	op.Workspace = w.Name
   692  
   693  	// Determine the function to call for our operation
   694  	var f func(context.Context, context.Context, *backend.Operation, *tfe.Workspace) (*tfe.Run, error)
   695  	switch op.Type {
   696  	case backend.OperationTypePlan:
   697  		f = b.opPlan
   698  	case backend.OperationTypeApply:
   699  		f = b.opApply
   700  	case backend.OperationTypeRefresh:
   701  		// The `terraform refresh` command has been deprecated in favor of `terraform apply -refresh-state`.
   702  		// Rather than respond with an error telling the user to run the other command we can just run
   703  		// that command instead. We will tell the user what we are doing, and then do it.
   704  		if b.CLI != nil {
   705  			b.CLI.Output(b.Colorize().Color(strings.TrimSpace(refreshToApplyRefresh) + "\n"))
   706  		}
   707  		op.PlanMode = plans.RefreshOnlyMode
   708  		op.PlanRefresh = true
   709  		op.AutoApprove = true
   710  		f = b.opApply
   711  	default:
   712  		return nil, fmt.Errorf(
   713  			"\n\nTerraform Cloud does not support the %q operation.", op.Type)
   714  	}
   715  
   716  	// Lock
   717  	b.opLock.Lock()
   718  
   719  	// Build our running operation
   720  	// the runninCtx is only used to block until the operation returns.
   721  	runningCtx, done := context.WithCancel(context.Background())
   722  	runningOp := &backend.RunningOperation{
   723  		Context:   runningCtx,
   724  		PlanEmpty: true,
   725  	}
   726  
   727  	// stopCtx wraps the context passed in, and is used to signal a graceful Stop.
   728  	stopCtx, stop := context.WithCancel(ctx)
   729  	runningOp.Stop = stop
   730  
   731  	// cancelCtx is used to cancel the operation immediately, usually
   732  	// indicating that the process is exiting.
   733  	cancelCtx, cancel := context.WithCancel(context.Background())
   734  	runningOp.Cancel = cancel
   735  
   736  	// Do it.
   737  	go func() {
   738  		defer done()
   739  		defer stop()
   740  		defer cancel()
   741  
   742  		defer b.opLock.Unlock()
   743  
   744  		r, opErr := f(stopCtx, cancelCtx, op, w)
   745  		if opErr != nil && opErr != context.Canceled {
   746  			var diags tfdiags.Diagnostics
   747  			diags = diags.Append(opErr)
   748  			op.ReportResult(runningOp, diags)
   749  			return
   750  		}
   751  
   752  		if r == nil && opErr == context.Canceled {
   753  			runningOp.Result = backend.OperationFailure
   754  			return
   755  		}
   756  
   757  		if r != nil {
   758  			// Retrieve the run to get its current status.
   759  			r, err := b.client.Runs.Read(cancelCtx, r.ID)
   760  			if err != nil {
   761  				var diags tfdiags.Diagnostics
   762  				diags = diags.Append(generalError("Failed to retrieve run", err))
   763  				op.ReportResult(runningOp, diags)
   764  				return
   765  			}
   766  
   767  			// Record if there are any changes.
   768  			runningOp.PlanEmpty = !r.HasChanges
   769  
   770  			if opErr == context.Canceled {
   771  				if err := b.cancel(cancelCtx, op, r); err != nil {
   772  					var diags tfdiags.Diagnostics
   773  					diags = diags.Append(generalError("Failed to retrieve run", err))
   774  					op.ReportResult(runningOp, diags)
   775  					return
   776  				}
   777  			}
   778  
   779  			if r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored {
   780  				runningOp.Result = backend.OperationFailure
   781  			}
   782  		}
   783  	}()
   784  
   785  	// Return the running operation.
   786  	return runningOp, nil
   787  }
   788  
   789  func (b *Cloud) cancel(cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
   790  	if r.Actions.IsCancelable {
   791  		// Only ask if the remote operation should be canceled
   792  		// if the auto approve flag is not set.
   793  		if !op.AutoApprove {
   794  			v, err := op.UIIn.Input(cancelCtx, &terraform.InputOpts{
   795  				Id:          "cancel",
   796  				Query:       "\nDo you want to cancel the remote operation?",
   797  				Description: "Only 'yes' will be accepted to cancel.",
   798  			})
   799  			if err != nil {
   800  				return generalError("Failed asking to cancel", err)
   801  			}
   802  			if v != "yes" {
   803  				if b.CLI != nil {
   804  					b.CLI.Output(b.Colorize().Color(strings.TrimSpace(operationNotCanceled)))
   805  				}
   806  				return nil
   807  			}
   808  		} else {
   809  			if b.CLI != nil {
   810  				// Insert a blank line to separate the ouputs.
   811  				b.CLI.Output("")
   812  			}
   813  		}
   814  
   815  		// Try to cancel the remote operation.
   816  		err := b.client.Runs.Cancel(cancelCtx, r.ID, tfe.RunCancelOptions{})
   817  		if err != nil {
   818  			return generalError("Failed to cancel run", err)
   819  		}
   820  		if b.CLI != nil {
   821  			b.CLI.Output(b.Colorize().Color(strings.TrimSpace(operationCanceled)))
   822  		}
   823  	}
   824  
   825  	return nil
   826  }
   827  
   828  // IgnoreVersionConflict allows commands to disable the fall-back check that
   829  // the local Terraform version matches the remote workspace's configured
   830  // Terraform version. This should be called by commands where this check is
   831  // unnecessary, such as those performing remote operations, or read-only
   832  // operations. It will also be called if the user uses a command-line flag to
   833  // override this check.
   834  func (b *Cloud) IgnoreVersionConflict() {
   835  	b.ignoreVersionConflict = true
   836  }
   837  
   838  // VerifyWorkspaceTerraformVersion compares the local Terraform version against
   839  // the workspace's configured Terraform version. If they are compatible, this
   840  // means that there are no state compatibility concerns, so it returns no
   841  // diagnostics.
   842  //
   843  // If the versions aren't compatible, it returns an error (or, if
   844  // b.ignoreVersionConflict is set, a warning).
   845  func (b *Cloud) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Diagnostics {
   846  	var diags tfdiags.Diagnostics
   847  
   848  	workspace, err := b.getRemoteWorkspace(context.Background(), workspaceName)
   849  	if err != nil {
   850  		// If the workspace doesn't exist, there can be no compatibility
   851  		// problem, so we can return. This is most likely to happen when
   852  		// migrating state from a local backend to a new workspace.
   853  		if err == tfe.ErrResourceNotFound {
   854  			return nil
   855  		}
   856  
   857  		diags = diags.Append(tfdiags.Sourceless(
   858  			tfdiags.Error,
   859  			"Error looking up workspace",
   860  			fmt.Sprintf("Workspace read failed: %s", err),
   861  		))
   862  		return diags
   863  	}
   864  
   865  	// If the workspace has the pseudo-version "latest", all bets are off. We
   866  	// cannot reasonably determine what the intended Terraform version is, so
   867  	// we'll skip version verification.
   868  	if workspace.TerraformVersion == "latest" {
   869  		return nil
   870  	}
   871  
   872  	// If the workspace has execution-mode set to local, the remote Terraform
   873  	// version is effectively meaningless, so we'll skip version verification.
   874  	if isLocalExecutionMode(workspace.ExecutionMode) {
   875  		return nil
   876  	}
   877  
   878  	remoteConstraint, err := version.NewConstraint(workspace.TerraformVersion)
   879  	if err != nil {
   880  		message := fmt.Sprintf(
   881  			"The remote workspace specified an invalid Terraform version or constraint (%s), "+
   882  				"and it isn't possible to determine whether the local Terraform version (%s) is compatible.",
   883  			workspace.TerraformVersion,
   884  			tfversion.String(),
   885  		)
   886  		diags = diags.Append(incompatibleWorkspaceTerraformVersion(message, b.ignoreVersionConflict))
   887  		return diags
   888  	}
   889  
   890  	remoteVersion, _ := version.NewSemver(workspace.TerraformVersion)
   891  
   892  	// We can use a looser version constraint if the workspace specifies a
   893  	// literal Terraform version, and it is not a prerelease. The latter
   894  	// restriction is because we cannot compare prerelease versions with any
   895  	// operator other than simple equality.
   896  	if remoteVersion != nil && remoteVersion.Prerelease() == "" {
   897  		v014 := version.Must(version.NewSemver("0.14.0"))
   898  		v130 := version.Must(version.NewSemver("1.3.0"))
   899  
   900  		// Versions from 0.14 through the early 1.x series should be compatible
   901  		// (though we don't know about 1.3 yet).
   902  		if remoteVersion.GreaterThanOrEqual(v014) && remoteVersion.LessThan(v130) {
   903  			early1xCompatible, err := version.NewConstraint(fmt.Sprintf(">= 0.14.0, < %s", v130.String()))
   904  			if err != nil {
   905  				panic(err)
   906  			}
   907  			remoteConstraint = early1xCompatible
   908  		}
   909  
   910  		// Any future new state format will require at least a minor version
   911  		// increment, so x.y.* will always be compatible with each other.
   912  		if remoteVersion.GreaterThanOrEqual(v130) {
   913  			rwvs := remoteVersion.Segments64()
   914  			if len(rwvs) >= 3 {
   915  				// ~> x.y.0
   916  				minorVersionCompatible, err := version.NewConstraint(fmt.Sprintf("~> %d.%d.0", rwvs[0], rwvs[1]))
   917  				if err != nil {
   918  					panic(err)
   919  				}
   920  				remoteConstraint = minorVersionCompatible
   921  			}
   922  		}
   923  	}
   924  
   925  	// Re-parsing tfversion.String because tfversion.SemVer omits the prerelease
   926  	// prefix, and we want to allow constraints like `~> 1.2.0-beta1`.
   927  	fullTfversion := version.Must(version.NewSemver(tfversion.String()))
   928  
   929  	if remoteConstraint.Check(fullTfversion) {
   930  		return diags
   931  	}
   932  
   933  	message := fmt.Sprintf(
   934  		"The local Terraform version (%s) does not meet the version requirements for remote workspace %s/%s (%s).",
   935  		tfversion.String(),
   936  		b.organization,
   937  		workspace.Name,
   938  		remoteConstraint,
   939  	)
   940  	diags = diags.Append(incompatibleWorkspaceTerraformVersion(message, b.ignoreVersionConflict))
   941  	return diags
   942  }
   943  
   944  func (b *Cloud) IsLocalOperations() bool {
   945  	return b.forceLocal
   946  }
   947  
   948  // Colorize returns the Colorize structure that can be used for colorizing
   949  // output. This is guaranteed to always return a non-nil value and so useful
   950  // as a helper to wrap any potentially colored strings.
   951  //
   952  // TODO SvH: Rename this back to Colorize as soon as we can pass -no-color.
   953  //
   954  //lint:ignore U1000 see above todo
   955  func (b *Cloud) cliColorize() *colorstring.Colorize {
   956  	if b.CLIColor != nil {
   957  		return b.CLIColor
   958  	}
   959  
   960  	return &colorstring.Colorize{
   961  		Colors:  colorstring.DefaultColors,
   962  		Disable: true,
   963  	}
   964  }
   965  
   966  func (b *Cloud) workspaceTagsRequireUpdate(workspace *tfe.Workspace, workspaceMapping WorkspaceMapping) bool {
   967  	if workspaceMapping.Strategy() != WorkspaceTagsStrategy {
   968  		return false
   969  	}
   970  
   971  	existingTags := map[string]struct{}{}
   972  	for _, t := range workspace.TagNames {
   973  		existingTags[t] = struct{}{}
   974  	}
   975  
   976  	for _, tag := range workspaceMapping.Tags {
   977  		if _, ok := existingTags[tag]; !ok {
   978  			return true
   979  		}
   980  	}
   981  
   982  	return false
   983  }
   984  
   985  type WorkspaceMapping struct {
   986  	Name string
   987  	Tags []string
   988  }
   989  
   990  type workspaceStrategy string
   991  
   992  const (
   993  	WorkspaceTagsStrategy    workspaceStrategy = "tags"
   994  	WorkspaceNameStrategy    workspaceStrategy = "name"
   995  	WorkspaceNoneStrategy    workspaceStrategy = "none"
   996  	WorkspaceInvalidStrategy workspaceStrategy = "invalid"
   997  )
   998  
   999  func (wm WorkspaceMapping) Strategy() workspaceStrategy {
  1000  	switch {
  1001  	case len(wm.Tags) > 0 && wm.Name == "":
  1002  		return WorkspaceTagsStrategy
  1003  	case len(wm.Tags) == 0 && wm.Name != "":
  1004  		return WorkspaceNameStrategy
  1005  	case len(wm.Tags) == 0 && wm.Name == "":
  1006  		return WorkspaceNoneStrategy
  1007  	default:
  1008  		// Any other combination is invalid as each strategy is mutually exclusive
  1009  		return WorkspaceInvalidStrategy
  1010  	}
  1011  }
  1012  
  1013  func isLocalExecutionMode(execMode string) bool {
  1014  	return execMode == "local"
  1015  }
  1016  
  1017  func (b *Cloud) fetchWorkspace(ctx context.Context, organization string, workspace string) (*tfe.Workspace, error) {
  1018  	// Retrieve the workspace for this operation.
  1019  	w, err := b.client.Workspaces.Read(ctx, organization, workspace)
  1020  	if err != nil {
  1021  		switch err {
  1022  		case context.Canceled:
  1023  			return nil, err
  1024  		case tfe.ErrResourceNotFound:
  1025  			return nil, fmt.Errorf(
  1026  				"workspace %s not found\n\n"+
  1027  					"For security, Terraform Cloud returns '404 Not Found' responses for resources\n"+
  1028  					"for resources that a user doesn't have access to, in addition to resources that\n"+
  1029  					"do not exist. If the resource does exist, please check the permissions of the provided token.",
  1030  				workspace,
  1031  			)
  1032  		default:
  1033  			err := fmt.Errorf(
  1034  				"Terraform Cloud returned an unexpected error:\n\n%s",
  1035  				err,
  1036  			)
  1037  			return nil, err
  1038  		}
  1039  	}
  1040  
  1041  	return w, nil
  1042  }
  1043  
  1044  // validWorkspaceEnvVar ensures we have selected a valid workspace using TF_WORKSPACE:
  1045  // First, it ensures the workspace specified by TF_WORKSPACE exists in the organization
  1046  // Second, if tags are specified in the configuration, it ensures TF_WORKSPACE belongs to the set
  1047  // of available workspaces with those given tags.
  1048  func (b *Cloud) validWorkspaceEnvVar(ctx context.Context, organization, workspace string) tfdiags.Diagnostic {
  1049  	// first ensure the workspace exists
  1050  	_, err := b.client.Workspaces.Read(ctx, organization, workspace)
  1051  	if err != nil && err != tfe.ErrResourceNotFound {
  1052  		return tfdiags.Sourceless(
  1053  			tfdiags.Error,
  1054  			"Terraform Cloud returned an unexpected error",
  1055  			err.Error(),
  1056  		)
  1057  	}
  1058  
  1059  	if err == tfe.ErrResourceNotFound {
  1060  		return tfdiags.Sourceless(
  1061  			tfdiags.Error,
  1062  			"Invalid workspace selection",
  1063  			fmt.Sprintf(`Terraform failed to find workspace %q in organization %s.`, workspace, organization),
  1064  		)
  1065  	}
  1066  
  1067  	// if the configuration has specified tags, we need to ensure TF_WORKSPACE
  1068  	// is a valid member
  1069  	if b.WorkspaceMapping.Strategy() == WorkspaceTagsStrategy {
  1070  		opts := &tfe.WorkspaceListOptions{}
  1071  		opts.Tags = strings.Join(b.WorkspaceMapping.Tags, ",")
  1072  
  1073  		for {
  1074  			wl, err := b.client.Workspaces.List(ctx, b.organization, opts)
  1075  			if err != nil {
  1076  				return tfdiags.Sourceless(
  1077  					tfdiags.Error,
  1078  					"Terraform Cloud returned an unexpected error",
  1079  					err.Error(),
  1080  				)
  1081  			}
  1082  
  1083  			for _, ws := range wl.Items {
  1084  				if ws.Name == workspace {
  1085  					return nil
  1086  				}
  1087  			}
  1088  
  1089  			if wl.CurrentPage >= wl.TotalPages {
  1090  				break
  1091  			}
  1092  
  1093  			opts.PageNumber = wl.NextPage
  1094  		}
  1095  
  1096  		return tfdiags.Sourceless(
  1097  			tfdiags.Error,
  1098  			"Invalid workspace selection",
  1099  			fmt.Sprintf(
  1100  				"Terraform failed to find workspace %q with the tags specified in your configuration:\n[%s]",
  1101  				workspace,
  1102  				strings.ReplaceAll(opts.Tags, ",", ", "),
  1103  			),
  1104  		)
  1105  	}
  1106  
  1107  	return nil
  1108  }
  1109  
  1110  func (wm WorkspaceMapping) tfeTags() []*tfe.Tag {
  1111  	var tags []*tfe.Tag
  1112  
  1113  	if wm.Strategy() != WorkspaceTagsStrategy {
  1114  		return tags
  1115  	}
  1116  
  1117  	for _, tag := range wm.Tags {
  1118  		t := tfe.Tag{Name: tag}
  1119  		tags = append(tags, &t)
  1120  	}
  1121  
  1122  	return tags
  1123  }
  1124  
  1125  func generalError(msg string, err error) error {
  1126  	var diags tfdiags.Diagnostics
  1127  
  1128  	if urlErr, ok := err.(*url.Error); ok {
  1129  		err = urlErr.Err
  1130  	}
  1131  
  1132  	switch err {
  1133  	case context.Canceled:
  1134  		return err
  1135  	case tfe.ErrResourceNotFound:
  1136  		diags = diags.Append(tfdiags.Sourceless(
  1137  			tfdiags.Error,
  1138  			fmt.Sprintf("%s: %v", msg, err),
  1139  			"For security, Terraform Cloud returns '404 Not Found' responses for resources\n"+
  1140  				"for resources that a user doesn't have access to, in addition to resources that\n"+
  1141  				"do not exist. If the resource does exist, please check the permissions of the provided token.",
  1142  		))
  1143  		return diags.Err()
  1144  	default:
  1145  		diags = diags.Append(tfdiags.Sourceless(
  1146  			tfdiags.Error,
  1147  			fmt.Sprintf("%s: %v", msg, err),
  1148  			`Terraform Cloud returned an unexpected error. Sometimes `+
  1149  				`this is caused by network connection problems, in which case you could retry `+
  1150  				`the command. If the issue persists please open a support ticket to get help `+
  1151  				`resolving the problem.`,
  1152  		))
  1153  		return diags.Err()
  1154  	}
  1155  }
  1156  
  1157  // The newline in this error is to make it look good in the CLI!
  1158  const initialRetryError = `
  1159  [reset][yellow]There was an error connecting to Terraform Cloud. Please do not exit
  1160  Terraform to prevent data loss! Trying to restore the connection...
  1161  [reset]
  1162  `
  1163  
  1164  const repeatedRetryError = `
  1165  [reset][yellow]Still trying to restore the connection... (%s elapsed)[reset]
  1166  `
  1167  
  1168  const operationCanceled = `
  1169  [reset][red]The remote operation was successfully cancelled.[reset]
  1170  `
  1171  
  1172  const operationNotCanceled = `
  1173  [reset][red]The remote operation was not cancelled.[reset]
  1174  `
  1175  
  1176  const refreshToApplyRefresh = `[bold][yellow]Proceeding with 'terraform apply -refresh-only -auto-approve'.[reset]`
  1177  
  1178  const unavailableTerraformVersion = `
  1179  [reset][yellow]The local Terraform version (%s) is not available in Terraform Cloud, or your
  1180  organization does not have access to it. The new workspace will use %s. You can
  1181  change this later in the workspace settings.[reset]`
  1182  
  1183  const cloudIntegrationUsedInUnsupportedTFE = `
  1184  This version of Terraform Cloud/Enterprise does not support the state mechanism
  1185  attempting to be used by the platform. This should never happen.
  1186  
  1187  Please reach out to HashiCorp Support to resolve this issue.`
  1188  
  1189  var (
  1190  	workspaceConfigurationHelp = fmt.Sprintf(
  1191  		`The 'workspaces' block configures how Terraform CLI maps its workspaces for this single
  1192  configuration to workspaces within a Terraform Cloud organization. Two strategies are available:
  1193  
  1194  [bold]tags[reset] - %s
  1195  
  1196  [bold]name[reset] - %s`, schemaDescriptionTags, schemaDescriptionName)
  1197  
  1198  	schemaDescriptionHostname = `The Terraform Enterprise hostname to connect to. This optional argument defaults to app.terraform.io
  1199  for use with Terraform Cloud.`
  1200  
  1201  	schemaDescriptionOrganization = `The name of the organization containing the targeted workspace(s).`
  1202  
  1203  	schemaDescriptionToken = `The token used to authenticate with Terraform Cloud/Enterprise. Typically this argument should not
  1204  be set, and 'terraform login' used instead; your credentials will then be fetched from your CLI
  1205  configuration file or configured credential helper.`
  1206  
  1207  	schemaDescriptionTags = `A set of tags used to select remote Terraform Cloud workspaces to be used for this single
  1208  configuration. New workspaces will automatically be tagged with these tag values. Generally, this
  1209  is the primary and recommended strategy to use.  This option conflicts with "name".`
  1210  
  1211  	schemaDescriptionName = `The name of a single Terraform Cloud workspace to be used with this configuration.
  1212  When configured, only the specified workspace can be used. This option conflicts with "tags".`
  1213  )