github.com/svenhamers/terraform@v0.11.12-beta1/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  
    14  	tfe "github.com/hashicorp/go-tfe"
    15  	version "github.com/hashicorp/go-version"
    16  	"github.com/hashicorp/terraform/backend"
    17  	"github.com/hashicorp/terraform/helper/schema"
    18  	"github.com/hashicorp/terraform/state"
    19  	"github.com/hashicorp/terraform/state/remote"
    20  	"github.com/hashicorp/terraform/svchost"
    21  	"github.com/hashicorp/terraform/svchost/disco"
    22  	"github.com/hashicorp/terraform/terraform"
    23  	tfversion "github.com/hashicorp/terraform/version"
    24  	"github.com/mitchellh/cli"
    25  	"github.com/mitchellh/colorstring"
    26  
    27  	backendLocal "github.com/hashicorp/terraform/backend/local"
    28  )
    29  
    30  const (
    31  	defaultHostname    = "app.terraform.io"
    32  	defaultModuleDepth = -1
    33  	defaultParallelism = 10
    34  	tfeServiceID       = "tfe.v2.1"
    35  )
    36  
    37  // Remote is an implementation of EnhancedBackend that performs all
    38  // operations in a remote backend.
    39  type Remote struct {
    40  	// CLI and Colorize control the CLI output. If CLI is nil then no CLI
    41  	// output will be done. If CLIColor is nil then no coloring will be done.
    42  	CLI      cli.Ui
    43  	CLIColor *colorstring.Colorize
    44  
    45  	// ContextOpts are the base context options to set when initializing a
    46  	// new Terraform context. Many of these will be overridden or merged by
    47  	// Operation. See Operation for more details.
    48  	ContextOpts *terraform.ContextOpts
    49  
    50  	// client is the remote backend API client
    51  	client *tfe.Client
    52  
    53  	// hostname of the remote backend server
    54  	hostname string
    55  
    56  	// organization is the organization that contains the target workspaces
    57  	organization string
    58  
    59  	// workspace is used to map the default workspace to a remote workspace
    60  	workspace string
    61  
    62  	// prefix is used to filter down a set of workspaces that use a single
    63  	// configuration
    64  	prefix string
    65  
    66  	// schema defines the configuration for the backend
    67  	schema *schema.Backend
    68  
    69  	// services is used for service discovery
    70  	services *disco.Disco
    71  
    72  	// local, if non-nil, will be used for all enhanced behavior. This
    73  	// allows local behavior with the remote backend functioning as remote
    74  	// state storage backend.
    75  	local backend.Enhanced
    76  
    77  	// forceLocal, if true, will force the use of the local backend.
    78  	forceLocal bool
    79  
    80  	// opLock locks operations
    81  	opLock sync.Mutex
    82  }
    83  
    84  // New creates a new initialized remote backend.
    85  func New(services *disco.Disco) *Remote {
    86  	b := &Remote{
    87  		services: services,
    88  	}
    89  
    90  	b.schema = &schema.Backend{
    91  		Schema: map[string]*schema.Schema{
    92  			"hostname": &schema.Schema{
    93  				Type:        schema.TypeString,
    94  				Optional:    true,
    95  				Description: schemaDescriptions["hostname"],
    96  				Default:     defaultHostname,
    97  			},
    98  
    99  			"organization": &schema.Schema{
   100  				Type:        schema.TypeString,
   101  				Required:    true,
   102  				Description: schemaDescriptions["organization"],
   103  			},
   104  
   105  			"token": &schema.Schema{
   106  				Type:        schema.TypeString,
   107  				Optional:    true,
   108  				Description: schemaDescriptions["token"],
   109  			},
   110  
   111  			"workspaces": &schema.Schema{
   112  				Type:        schema.TypeSet,
   113  				Required:    true,
   114  				Description: schemaDescriptions["workspaces"],
   115  				MinItems:    1,
   116  				MaxItems:    1,
   117  				Elem: &schema.Resource{
   118  					Schema: map[string]*schema.Schema{
   119  						"name": &schema.Schema{
   120  							Type:        schema.TypeString,
   121  							Optional:    true,
   122  							Description: schemaDescriptions["name"],
   123  						},
   124  
   125  						"prefix": &schema.Schema{
   126  							Type:        schema.TypeString,
   127  							Optional:    true,
   128  							Description: schemaDescriptions["prefix"],
   129  						},
   130  					},
   131  				},
   132  			},
   133  		},
   134  
   135  		ConfigureFunc: b.configure,
   136  	}
   137  
   138  	return b
   139  }
   140  
   141  func (b *Remote) configure(ctx context.Context) error {
   142  	d := schema.FromContextBackendConfig(ctx)
   143  
   144  	// Get the hostname and organization.
   145  	b.hostname = d.Get("hostname").(string)
   146  	b.organization = d.Get("organization").(string)
   147  
   148  	// Get and assert the workspaces configuration block.
   149  	workspace := d.Get("workspaces").(*schema.Set).List()[0].(map[string]interface{})
   150  
   151  	// Get the default workspace name and prefix.
   152  	b.workspace = workspace["name"].(string)
   153  	b.prefix = workspace["prefix"].(string)
   154  
   155  	// Make sure that we have either a workspace name or a prefix.
   156  	if b.workspace == "" && b.prefix == "" {
   157  		return fmt.Errorf("either workspace 'name' or 'prefix' is required")
   158  	}
   159  
   160  	// Make sure that only one of workspace name or a prefix is configured.
   161  	if b.workspace != "" && b.prefix != "" {
   162  		return fmt.Errorf("only one of workspace 'name' or 'prefix' is allowed")
   163  	}
   164  
   165  	// Discover the service URL for this host to confirm that it provides
   166  	// a remote backend API and to get the version constraints.
   167  	service, constraints, err := b.discover()
   168  
   169  	// First check any contraints we might have received.<Paste>
   170  	if constraints != nil {
   171  		if err := b.checkConstraints(constraints); err != nil {
   172  			return err
   173  		}
   174  	}
   175  
   176  	// When we don't have any constraints errors, also check for discovery
   177  	// errors before we continue.
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	// Retrieve the token for this host as configured in the credentials
   183  	// section of the CLI Config File.
   184  	token, err := b.token()
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	// Get the token from the config if no token was configured for this
   190  	// host in credentials section of the CLI Config File.
   191  	if token == "" {
   192  		token = d.Get("token").(string)
   193  	}
   194  
   195  	// Return an error if we still don't have a token at this point.
   196  	if token == "" {
   197  		return fmt.Errorf("required token could not be found")
   198  	}
   199  
   200  	cfg := &tfe.Config{
   201  		Address:  service.String(),
   202  		BasePath: service.Path,
   203  		Token:    token,
   204  		Headers:  make(http.Header),
   205  	}
   206  
   207  	// Set the version header to the current version.
   208  	cfg.Headers.Set(tfversion.Header, tfversion.Version)
   209  
   210  	// Create the remote backend API client.
   211  	b.client, err = tfe.NewClient(cfg)
   212  	if err != nil {
   213  		return err
   214  	}
   215  
   216  	// Check if the organization exists by reading its entitlements.
   217  	entitlements, err := b.client.Organizations.Entitlements(context.Background(), b.organization)
   218  	if err != nil {
   219  		if err == tfe.ErrResourceNotFound {
   220  			return fmt.Errorf("organization %s does not exist", b.organization)
   221  		}
   222  		return fmt.Errorf("failed to read organization entitlements: %v", err)
   223  	}
   224  
   225  	// Configure a local backend for when we need to run operations locally.
   226  	b.local = backendLocal.NewWithBackend(b)
   227  	b.forceLocal = !entitlements.Operations || os.Getenv("TF_FORCE_LOCAL_BACKEND") != ""
   228  
   229  	return nil
   230  }
   231  
   232  // discover the remote backend API service URL and version constraints.
   233  func (b *Remote) discover() (*url.URL, *disco.Constraints, error) {
   234  	hostname, err := svchost.ForComparison(b.hostname)
   235  	if err != nil {
   236  		return nil, nil, err
   237  	}
   238  
   239  	host, err := b.services.Discover(hostname)
   240  	if err != nil {
   241  		return nil, nil, err
   242  	}
   243  
   244  	service, err := host.ServiceURL(tfeServiceID)
   245  	// Return the error, unless its a disco.ErrVersionNotSupported error.
   246  	if _, ok := err.(*disco.ErrVersionNotSupported); !ok && err != nil {
   247  		return nil, nil, err
   248  	}
   249  
   250  	// Return early if we are a development build.
   251  	if tfversion.Prerelease == "dev" {
   252  		return service, nil, err
   253  	}
   254  
   255  	// We purposefully ignore the error and return the previous error, as
   256  	// checking for version constraints is considered optional.
   257  	constraints, _ := host.VersionConstraints(tfeServiceID, "terraform")
   258  
   259  	return service, constraints, err
   260  }
   261  
   262  // checkConstraints checks service version constrains against our own
   263  // version and returns rich and informational diagnostics in case any
   264  // incompatibilities are detected.
   265  func (b *Remote) checkConstraints(c *disco.Constraints) error {
   266  	if c == nil || c.Minimum == "" || c.Maximum == "" {
   267  		return nil
   268  	}
   269  
   270  	// Generate a parsable constraints string.
   271  	excluding := ""
   272  	if len(c.Excluding) > 0 {
   273  		excluding = fmt.Sprintf(", != %s", strings.Join(c.Excluding, ", != "))
   274  	}
   275  	constStr := fmt.Sprintf(">= %s%s, <= %s", c.Minimum, excluding, c.Maximum)
   276  
   277  	// Create the constraints to check against.
   278  	constraints, err := version.NewConstraint(constStr)
   279  	if err != nil {
   280  		return checkConstraintsWarning(err)
   281  	}
   282  
   283  	// Create the version to check.
   284  	v, err := version.NewVersion(tfversion.Version)
   285  	if err != nil {
   286  		return checkConstraintsWarning(err)
   287  	}
   288  
   289  	// Return if we satisfy all constraints.
   290  	if constraints.Check(v) {
   291  		return nil
   292  	}
   293  
   294  	// Find out what action (upgrade/downgrade) we should advice.
   295  	minimum, err := version.NewVersion(c.Minimum)
   296  	if err != nil {
   297  		return checkConstraintsWarning(err)
   298  	}
   299  
   300  	maximum, err := version.NewVersion(c.Maximum)
   301  	if err != nil {
   302  		return checkConstraintsWarning(err)
   303  	}
   304  
   305  	var excludes []*version.Version
   306  	for _, exclude := range c.Excluding {
   307  		v, err := version.NewVersion(exclude)
   308  		if err != nil {
   309  			return checkConstraintsWarning(err)
   310  		}
   311  		excludes = append(excludes, v)
   312  	}
   313  
   314  	// Sort all the excludes.
   315  	sort.Sort(version.Collection(excludes))
   316  
   317  	var action, toVersion string
   318  	switch {
   319  	case minimum.GreaterThan(v):
   320  		action = "upgrade"
   321  		toVersion = ">= " + minimum.String()
   322  	case maximum.LessThan(v):
   323  		action = "downgrade"
   324  		toVersion = "<= " + maximum.String()
   325  	case len(excludes) > 0:
   326  		// Get the latest excluded version.
   327  		action = "upgrade"
   328  		toVersion = "> " + excludes[len(excludes)-1].String()
   329  	}
   330  
   331  	switch {
   332  	case len(excludes) == 1:
   333  		excluding = fmt.Sprintf(", excluding version %s", excludes[0].String())
   334  	case len(excludes) > 1:
   335  		var vs []string
   336  		for _, v := range excludes {
   337  			vs = append(vs, v.String())
   338  		}
   339  		excluding = fmt.Sprintf(", excluding versions %s", strings.Join(vs, ", "))
   340  	default:
   341  		excluding = ""
   342  	}
   343  
   344  	summary := fmt.Sprintf("Incompatible Terraform version v%s", v.String())
   345  	details := fmt.Sprintf(
   346  		"The configured Terraform Enterprise backend is compatible with Terraform\n"+
   347  			"versions >= %s, <= %s%s.", c.Minimum, c.Maximum, excluding,
   348  	)
   349  
   350  	if action != "" && toVersion != "" {
   351  		summary = fmt.Sprintf("Please %s Terraform to %s", action, toVersion)
   352  	}
   353  
   354  	// Return the customized and informational error message.
   355  	return fmt.Errorf("%s\n\n%s", summary, details)
   356  }
   357  
   358  // token returns the token for this host as configured in the credentials
   359  // section of the CLI Config File. If no token was configured, an empty
   360  // string will be returned instead.
   361  func (b *Remote) token() (string, error) {
   362  	hostname, err := svchost.ForComparison(b.hostname)
   363  	if err != nil {
   364  		return "", err
   365  	}
   366  	creds, err := b.services.CredentialsForHost(hostname)
   367  	if err != nil {
   368  		log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err)
   369  		return "", nil
   370  	}
   371  	if creds != nil {
   372  		return creds.Token(), nil
   373  	}
   374  	return "", nil
   375  }
   376  
   377  // Input is called to ask the user for input for completing the configuration.
   378  func (b *Remote) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
   379  	return b.schema.Input(ui, c)
   380  }
   381  
   382  // Validate is called once at the beginning with the raw configuration and
   383  // can return a list of warnings and/or errors.
   384  func (b *Remote) Validate(c *terraform.ResourceConfig) ([]string, []error) {
   385  	return b.schema.Validate(c)
   386  }
   387  
   388  // Configure configures the backend itself with the configuration given.
   389  func (b *Remote) Configure(c *terraform.ResourceConfig) error {
   390  	return b.schema.Configure(c)
   391  }
   392  
   393  // State returns the latest state of the given remote workspace. The workspace
   394  // will be created if it doesn't exist.
   395  func (b *Remote) State(name string) (state.State, error) {
   396  	if b.workspace == "" && name == backend.DefaultStateName {
   397  		return nil, backend.ErrDefaultStateNotSupported
   398  	}
   399  	if b.prefix == "" && name != backend.DefaultStateName {
   400  		return nil, backend.ErrNamedStatesNotSupported
   401  	}
   402  
   403  	// Configure the remote workspace name.
   404  	switch {
   405  	case name == backend.DefaultStateName:
   406  		name = b.workspace
   407  	case b.prefix != "" && !strings.HasPrefix(name, b.prefix):
   408  		name = b.prefix + name
   409  	}
   410  
   411  	workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, name)
   412  	if err != nil && err != tfe.ErrResourceNotFound {
   413  		return nil, fmt.Errorf("Failed to retrieve workspace %s: %v", name, err)
   414  	}
   415  
   416  	if err == tfe.ErrResourceNotFound {
   417  		options := tfe.WorkspaceCreateOptions{
   418  			Name: tfe.String(name),
   419  		}
   420  
   421  		// We only set the Terraform Version for the new workspace if this is
   422  		// a release candidate or a final release.
   423  		if tfversion.Prerelease == "" || strings.HasPrefix(tfversion.Prerelease, "rc") {
   424  			options.TerraformVersion = tfe.String(tfversion.String())
   425  		}
   426  
   427  		workspace, err = b.client.Workspaces.Create(context.Background(), b.organization, options)
   428  		if err != nil {
   429  			return nil, fmt.Errorf("Error creating workspace %s: %v", name, err)
   430  		}
   431  	}
   432  
   433  	client := &remoteClient{
   434  		client:       b.client,
   435  		organization: b.organization,
   436  		workspace:    workspace,
   437  
   438  		// This is optionally set during Terraform Enterprise runs.
   439  		runID: os.Getenv("TFE_RUN_ID"),
   440  	}
   441  
   442  	return &remote.State{Client: client}, nil
   443  }
   444  
   445  // DeleteState removes the remote workspace if it exists.
   446  func (b *Remote) DeleteState(name string) error {
   447  	if b.workspace == "" && name == backend.DefaultStateName {
   448  		return backend.ErrDefaultStateNotSupported
   449  	}
   450  	if b.prefix == "" && name != backend.DefaultStateName {
   451  		return backend.ErrNamedStatesNotSupported
   452  	}
   453  
   454  	// Configure the remote workspace name.
   455  	switch {
   456  	case name == backend.DefaultStateName:
   457  		name = b.workspace
   458  	case b.prefix != "" && !strings.HasPrefix(name, b.prefix):
   459  		name = b.prefix + name
   460  	}
   461  
   462  	client := &remoteClient{
   463  		client:       b.client,
   464  		organization: b.organization,
   465  		workspace: &tfe.Workspace{
   466  			Name: name,
   467  		},
   468  	}
   469  
   470  	return client.Delete()
   471  }
   472  
   473  // States returns a filtered list of remote workspace names.
   474  func (b *Remote) States() ([]string, error) {
   475  	if b.prefix == "" {
   476  		return nil, backend.ErrNamedStatesNotSupported
   477  	}
   478  	return b.states()
   479  }
   480  
   481  func (b *Remote) states() ([]string, error) {
   482  	options := tfe.WorkspaceListOptions{}
   483  	switch {
   484  	case b.workspace != "":
   485  		options.Search = tfe.String(b.workspace)
   486  	case b.prefix != "":
   487  		options.Search = tfe.String(b.prefix)
   488  	}
   489  
   490  	// Create a slice to contain all the names.
   491  	var names []string
   492  
   493  	for {
   494  		wl, err := b.client.Workspaces.List(context.Background(), b.organization, options)
   495  		if err != nil {
   496  			return nil, err
   497  		}
   498  
   499  		for _, w := range wl.Items {
   500  			if b.workspace != "" && w.Name == b.workspace {
   501  				names = append(names, backend.DefaultStateName)
   502  				continue
   503  			}
   504  			if b.prefix != "" && strings.HasPrefix(w.Name, b.prefix) {
   505  				names = append(names, strings.TrimPrefix(w.Name, b.prefix))
   506  			}
   507  		}
   508  
   509  		// Exit the loop when we've seen all pages.
   510  		if wl.CurrentPage >= wl.TotalPages {
   511  			break
   512  		}
   513  
   514  		// Update the page number to get the next page.
   515  		options.PageNumber = wl.NextPage
   516  	}
   517  
   518  	// Sort the result so we have consistent output.
   519  	sort.StringSlice(names).Sort()
   520  
   521  	return names, nil
   522  }
   523  
   524  // Operation implements backend.Enhanced
   525  func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
   526  	// Configure the remote workspace name.
   527  	name := op.Workspace
   528  	switch {
   529  	case op.Workspace == backend.DefaultStateName:
   530  		name = b.workspace
   531  	case b.prefix != "" && !strings.HasPrefix(op.Workspace, b.prefix):
   532  		name = b.prefix + op.Workspace
   533  	}
   534  
   535  	// Retrieve the workspace for this operation.
   536  	w, err := b.client.Workspaces.Read(ctx, b.organization, name)
   537  	if err != nil {
   538  		switch err {
   539  		case context.Canceled:
   540  			return nil, err
   541  		case tfe.ErrResourceNotFound:
   542  			return nil, fmt.Errorf(
   543  				"workspace %s not found\n\n"+
   544  					"The configured \"remote\" backend returns '404 Not Found' errors for resources\n"+
   545  					"that do not exist, as well as for resources that a user doesn't have access\n"+
   546  					"to. If the resource does exists, please check the rights for the used token.",
   547  				name,
   548  			)
   549  		default:
   550  			return nil, fmt.Errorf(
   551  				"%s\n\n"+
   552  					"The configured \"remote\" backend encountered an unexpected error. Sometimes\n"+
   553  					"this is caused by network connection problems, in which case you could retry\n"+
   554  					"the command. If the issue persists please open a support ticket to get help\n"+
   555  					"resolving the problem.",
   556  				err,
   557  			)
   558  		}
   559  	}
   560  
   561  	// Check if we need to use the local backend to run the operation.
   562  	if b.forceLocal || !w.Operations {
   563  		return b.local.Operation(ctx, op)
   564  	}
   565  
   566  	// Set the remote workspace name.
   567  	op.Workspace = w.Name
   568  
   569  	// Determine the function to call for our operation
   570  	var f func(context.Context, context.Context, *backend.Operation, *tfe.Workspace) (*tfe.Run, error)
   571  	switch op.Type {
   572  	case backend.OperationTypePlan:
   573  		f = b.opPlan
   574  	case backend.OperationTypeApply:
   575  		f = b.opApply
   576  	default:
   577  		return nil, fmt.Errorf(
   578  			"\n\nThe \"remote\" backend does not support the %q operation.", op.Type)
   579  	}
   580  
   581  	// Lock
   582  	b.opLock.Lock()
   583  
   584  	// Build our running operation
   585  	// the runninCtx is only used to block until the operation returns.
   586  	runningCtx, done := context.WithCancel(context.Background())
   587  	runningOp := &backend.RunningOperation{
   588  		Context:   runningCtx,
   589  		PlanEmpty: true,
   590  	}
   591  
   592  	// stopCtx wraps the context passed in, and is used to signal a graceful Stop.
   593  	stopCtx, stop := context.WithCancel(ctx)
   594  	runningOp.Stop = stop
   595  
   596  	// cancelCtx is used to cancel the operation immediately, usually
   597  	// indicating that the process is exiting.
   598  	cancelCtx, cancel := context.WithCancel(context.Background())
   599  	runningOp.Cancel = cancel
   600  
   601  	// Do it.
   602  	go func() {
   603  		defer done()
   604  		defer stop()
   605  		defer cancel()
   606  
   607  		defer b.opLock.Unlock()
   608  
   609  		r, opErr := f(stopCtx, cancelCtx, op, w)
   610  		if opErr != nil && opErr != context.Canceled {
   611  			runningOp.Err = opErr
   612  			return
   613  		}
   614  
   615  		if r != nil {
   616  			// Retrieve the run to get its current status.
   617  			r, err := b.client.Runs.Read(cancelCtx, r.ID)
   618  			if err != nil {
   619  				runningOp.Err = generalError("error retrieving run", err)
   620  				return
   621  			}
   622  
   623  			// Record if there are any changes.
   624  			runningOp.PlanEmpty = !r.HasChanges
   625  
   626  			if opErr == context.Canceled {
   627  				runningOp.Err = b.cancel(cancelCtx, op, r)
   628  			}
   629  
   630  			if runningOp.Err == nil && r.Status == tfe.RunErrored {
   631  				runningOp.ExitCode = 1
   632  			}
   633  		}
   634  	}()
   635  
   636  	// Return the running operation.
   637  	return runningOp, nil
   638  }
   639  
   640  func (b *Remote) cancel(cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
   641  	if r.Status == tfe.RunPending && r.Actions.IsCancelable {
   642  		// Only ask if the remote operation should be canceled
   643  		// if the auto approve flag is not set.
   644  		if !op.AutoApprove {
   645  			v, err := op.UIIn.Input(&terraform.InputOpts{
   646  				Id:          "cancel",
   647  				Query:       "\nDo you want to cancel the pending remote operation?",
   648  				Description: "Only 'yes' will be accepted to cancel.",
   649  			})
   650  			if err != nil {
   651  				return generalError("error asking to cancel", err)
   652  			}
   653  			if v != "yes" {
   654  				if b.CLI != nil {
   655  					b.CLI.Output(b.Colorize().Color(strings.TrimSpace(operationNotCanceled)))
   656  				}
   657  				return nil
   658  			}
   659  		} else {
   660  			if b.CLI != nil {
   661  				// Insert a blank line to separate the ouputs.
   662  				b.CLI.Output("")
   663  			}
   664  		}
   665  
   666  		// Try to cancel the remote operation.
   667  		err := b.client.Runs.Cancel(cancelCtx, r.ID, tfe.RunCancelOptions{})
   668  		if err != nil {
   669  			return generalError("error cancelling run", err)
   670  		}
   671  		if b.CLI != nil {
   672  			b.CLI.Output(b.Colorize().Color(strings.TrimSpace(operationCanceled)))
   673  		}
   674  	}
   675  
   676  	return nil
   677  }
   678  
   679  // Colorize returns the Colorize structure that can be used for colorizing
   680  // output. This is guaranteed to always return a non-nil value and so useful
   681  // as a helper to wrap any potentially colored strings.
   682  //
   683  // TODO SvH: Rename this back to Colorize as soon as we can pass -no-color.
   684  func (b *Remote) cliColorize() *colorstring.Colorize {
   685  	if b.CLIColor != nil {
   686  		return b.CLIColor
   687  	}
   688  
   689  	return &colorstring.Colorize{
   690  		Colors:  colorstring.DefaultColors,
   691  		Disable: true,
   692  	}
   693  }
   694  
   695  func generalError(msg string, err error) error {
   696  	if urlErr, ok := err.(*url.Error); ok {
   697  		err = urlErr.Err
   698  	}
   699  	switch err {
   700  	case context.Canceled:
   701  		return err
   702  	case tfe.ErrResourceNotFound:
   703  		return fmt.Errorf(strings.TrimSpace(fmt.Sprintf(notFoundErr, msg, err)))
   704  	default:
   705  		return fmt.Errorf(strings.TrimSpace(fmt.Sprintf(generalErr, msg, err)))
   706  	}
   707  }
   708  
   709  func checkConstraintsWarning(err error) error {
   710  	return fmt.Errorf(
   711  		"Failed to check version constraints: %v\n\n"+
   712  			"Checking version constraints is considered optional, but this is an\n"+
   713  			"unexpected error which should be reported.",
   714  		err,
   715  	)
   716  }
   717  
   718  const generalErr = `
   719  %s: %v
   720  
   721  The configured "remote" backend encountered an unexpected error. Sometimes
   722  this is caused by network connection problems, in which case you could retry
   723  the command. If the issue persists please open a support ticket to get help
   724  resolving the problem.
   725  `
   726  
   727  const notFoundErr = `
   728  %s: %v
   729  
   730  The configured "remote" backend returns '404 Not Found' errors for resources
   731  that do not exist, as well as for resources that a user doesn't have access
   732  to. When the resource does exists, please check the rights for the used token.
   733  `
   734  
   735  const operationCanceled = `
   736  [reset][red]The remote operation was successfully cancelled.[reset]
   737  `
   738  
   739  const operationNotCanceled = `
   740  [reset][red]The remote operation was not cancelled.[reset]
   741  `
   742  
   743  var schemaDescriptions = map[string]string{
   744  	"hostname":     "The remote backend hostname to connect to (defaults to app.terraform.io).",
   745  	"organization": "The name of the organization containing the targeted workspace(s).",
   746  	"token": "The token used to authenticate with the remote backend. If credentials for the\n" +
   747  		"host are configured in the CLI Config File, then those will be used instead.",
   748  	"workspaces": "Workspaces contains arguments used to filter down to a set of workspaces\n" +
   749  		"to work on.",
   750  	"name": "A workspace name used to map the default workspace to a named remote workspace.\n" +
   751  		"When configured only the default workspace can be used. This option conflicts\n" +
   752  		"with \"prefix\"",
   753  	"prefix": "A prefix used to filter workspaces using a single configuration. New workspaces\n" +
   754  		"will automatically be prefixed with this prefix. If omitted only the default\n" +
   755  		"workspace can be used. This option conflicts with \"name\"",
   756  }