backend/remote: implement the Local interface

This commit is contained in:
Sander van Harmelen 2018-11-26 14:06:27 +01:00
parent e68377b1f8
commit 35d9ce3f92
4 changed files with 201 additions and 88 deletions

View File

@ -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

View File

@ -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

View File

@ -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 = "<sensitive>"
}
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
}

View File

@ -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")