github.com/pdecat/terraform@v0.11.9-beta1/backend/remote/backend.go (about)

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