diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index 648082472..58eee8a28 100644 --- a/backend/local/backend_local.go +++ b/backend/local/backend_local.go @@ -66,7 +66,7 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload. // Load the latest state. If we enter contextFromPlanFile below then the // state snapshot in the plan file must match this, or else it'll return // error diagnostics. - log.Printf("[TRACE] backend/local: retrieving the local state snapshot for workspace %q", op.Workspace) + log.Printf("[TRACE] backend/local: retrieving local state snapshot for workspace %q", op.Workspace) opts.State = s.State() var tfCtx *terraform.Context diff --git a/backend/remote/backend.go b/backend/remote/backend.go index 1a48b39c3..86812137a 100644 --- a/backend/remote/backend.go +++ b/backend/remote/backend.go @@ -328,6 +328,84 @@ func (b *Remote) token(hostname string) (string, error) { return "", nil } +// Workspaces implements backend.Enhanced. +func (b *Remote) Workspaces() ([]string, error) { + if b.prefix == "" { + return nil, backend.ErrWorkspacesNotSupported + } + return b.workspaces() +} + +// workspaces returns a filtered list of remote workspace names. +func (b *Remote) workspaces() ([]string, error) { + options := tfe.WorkspaceListOptions{} + switch { + case b.workspace != "": + options.Search = tfe.String(b.workspace) + case b.prefix != "": + options.Search = tfe.String(b.prefix) + } + + // Create a slice to contain all the names. + var names []string + + for { + wl, err := b.client.Workspaces.List(context.Background(), b.organization, options) + if err != nil { + return nil, err + } + + for _, w := range wl.Items { + if b.workspace != "" && w.Name == b.workspace { + names = append(names, backend.DefaultStateName) + continue + } + if b.prefix != "" && strings.HasPrefix(w.Name, b.prefix) { + names = append(names, strings.TrimPrefix(w.Name, b.prefix)) + } + } + + // Exit the loop when we've seen all pages. + if wl.CurrentPage >= wl.TotalPages { + break + } + + // Update the page number to get the next page. + options.PageNumber = wl.NextPage + } + + // Sort the result so we have consistent output. + sort.StringSlice(names).Sort() + + return names, nil +} + +// DeleteWorkspace implements backend.Enhanced. +func (b *Remote) DeleteWorkspace(name string) error { + if b.workspace == "" && name == backend.DefaultStateName { + return backend.ErrDefaultWorkspaceNotSupported + } + if b.prefix == "" && name != backend.DefaultStateName { + return backend.ErrWorkspacesNotSupported + } + + // Configure the remote workspace name. + switch { + case name == backend.DefaultStateName: + name = b.workspace + case b.prefix != "" && !strings.HasPrefix(name, b.prefix): + name = b.prefix + name + } + + client := &remoteClient{ + client: b.client, + organization: b.organization, + workspace: name, + } + + return client.Delete() +} + // StateMgr implements backend.Enhanced. func (b *Remote) StateMgr(name string) (state.State, error) { if b.workspace == "" && name == backend.DefaultStateName { @@ -387,84 +465,6 @@ func (b *Remote) StateMgr(name string) (state.State, error) { return &remote.State{Client: client}, nil } -// DeleteWorkspace implements backend.Enhanced. -func (b *Remote) DeleteWorkspace(name string) error { - if b.workspace == "" && name == backend.DefaultStateName { - return backend.ErrDefaultWorkspaceNotSupported - } - if b.prefix == "" && name != backend.DefaultStateName { - return backend.ErrWorkspacesNotSupported - } - - // Configure the remote workspace name. - switch { - case name == backend.DefaultStateName: - name = b.workspace - case b.prefix != "" && !strings.HasPrefix(name, b.prefix): - name = b.prefix + name - } - - client := &remoteClient{ - client: b.client, - organization: b.organization, - workspace: name, - } - - return client.Delete() -} - -// Workspaces implements backend.Enhanced. -func (b *Remote) Workspaces() ([]string, error) { - if b.prefix == "" { - return nil, backend.ErrWorkspacesNotSupported - } - return b.workspaces() -} - -// workspaces returns a filtered list of remote workspace names. -func (b *Remote) workspaces() ([]string, error) { - options := tfe.WorkspaceListOptions{} - switch { - case b.workspace != "": - options.Search = tfe.String(b.workspace) - case b.prefix != "": - options.Search = tfe.String(b.prefix) - } - - // Create a slice to contain all the names. - var names []string - - for { - wl, err := b.client.Workspaces.List(context.Background(), b.organization, options) - if err != nil { - return nil, err - } - - for _, w := range wl.Items { - if b.workspace != "" && w.Name == b.workspace { - names = append(names, backend.DefaultStateName) - continue - } - if b.prefix != "" && strings.HasPrefix(w.Name, b.prefix) { - names = append(names, strings.TrimPrefix(w.Name, b.prefix)) - } - } - - // Exit the loop when we've seen all pages. - if wl.CurrentPage >= wl.TotalPages { - break - } - - // Update the page number to get the next page. - options.PageNumber = wl.NextPage - } - - // Sort the result so we have consistent output. - sort.StringSlice(names).Sort() - - return names, nil -} - // Operation implements backend.Enhanced. func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) { // Get the remote workspace name. @@ -645,16 +645,18 @@ func (b *Remote) ReportResult(op *backend.RunningOperation, err error) { // Colorize returns the Colorize structure that can be used for colorizing // output. This is guaranteed to always return a non-nil value and so useful // as a helper to wrap any potentially colored strings. -// func (b *Remote) Colorize() *colorstring.Colorize { -// if b.CLIColor != nil { -// return b.CLIColor -// } +// +// TODO SvH: Rename this back to Colorize as soon as we can pass -no-color. +func (b *Remote) cliColorize() *colorstring.Colorize { + if b.CLIColor != nil { + return b.CLIColor + } -// return &colorstring.Colorize{ -// Colors: colorstring.DefaultColors, -// Disable: true, -// } -// } + return &colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Disable: true, + } +} func generalError(msg string, err error) error { var diags tfdiags.Diagnostics diff --git a/backend/remote/backend_context.go b/backend/remote/backend_context.go new file mode 100644 index 000000000..044d24c45 --- /dev/null +++ b/backend/remote/backend_context.go @@ -0,0 +1,108 @@ +package remote + +import ( + "context" + "log" + + "github.com/hashicorp/errwrap" + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/states/statemgr" + "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" +) + +// Context implements backend.Enhanced. +func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Full, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if op.LockState { + op.StateLocker = clistate.NewLocker(context.Background(), op.StateLockTimeout, b.CLI, b.cliColorize()) + } else { + op.StateLocker = clistate.NewNoopLocker() + } + + // Get the latest state. + log.Printf("[TRACE] backend/remote: requesting state manager for workspace %q", op.Workspace) + stateMgr, err := b.StateMgr(op.Workspace) + if err != nil { + diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) + return nil, nil, diags + } + + log.Printf("[TRACE] backend/remote: requesting state lock for workspace %q", op.Workspace) + if err := op.StateLocker.Lock(stateMgr, op.Type.String()); err != nil { + diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err)) + return nil, nil, diags + } + + log.Printf("[TRACE] backend/remote: reading remote state for workspace %q", op.Workspace) + if err := stateMgr.RefreshState(); err != nil { + diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) + return nil, nil, diags + } + + // Initialize our context options + var opts terraform.ContextOpts + if v := b.ContextOpts; v != nil { + opts = *v + } + + // Copy set options from the operation + opts.Destroy = op.Destroy + opts.Targets = op.Targets + opts.UIInput = op.UIIn + + // Load the latest state. If we enter contextFromPlanFile below then the + // state snapshot in the plan file must match this, or else it'll return + // error diagnostics. + log.Printf("[TRACE] backend/remote: retrieving remote state snapshot for workspace %q", op.Workspace) + opts.State = stateMgr.State() + + log.Printf("[TRACE] backend/remote: loading configuration for the current working directory") + config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return nil, nil, diags + } + opts.Config = config + + log.Printf("[TRACE] backend/remote: retrieving variables from workspace %q", op.Workspace) + tfeVariables, err := b.client.Variables.List(context.Background(), tfe.VariableListOptions{ + Organization: tfe.String(b.organization), + Workspace: tfe.String(op.Workspace), + }) + if err != nil && err != tfe.ErrResourceNotFound { + diags = diags.Append(errwrap.Wrapf("Error loading variables: {{err}}", err)) + return nil, nil, diags + } + + if tfeVariables != nil { + for _, v := range tfeVariables.Items { + if v.Sensitive { + v.Value = "" + } + op.Variables[v.Key] = &unparsedVariableValue{ + value: v.Value, + source: terraform.ValueFromCLIArg, + } + } + } + + variables, varDiags := backend.ParseVariableValues(op.Variables, config.Module.Variables) + diags = diags.Append(varDiags) + if diags.HasErrors() { + return nil, nil, diags + } + if op.Variables != nil { + opts.Variables = variables + } + + tfCtx, ctxDiags := terraform.NewContext(&opts) + diags = diags.Append(ctxDiags) + + log.Printf("[TRACE] backend/remote: finished building terraform.Context") + + return tfCtx, stateMgr, diags +} diff --git a/backend/remote/colorize.go b/backend/remote/colorize.go index 0f877c007..97e24e32c 100644 --- a/backend/remote/colorize.go +++ b/backend/remote/colorize.go @@ -6,6 +6,9 @@ import ( "github.com/mitchellh/colorstring" ) +// TODO SvH: This file should be deleted and the type cliColorize should be +// renamed back to Colorize as soon as we can pass -no-color to the backend. + // colorsRe is used to find ANSI escaped color codes. var colorsRe = regexp.MustCompile("\033\\[\\d{1,3}m")