github.com/jzbruno/terraform@v0.10.3-0.20180104230435-18975d727047/backend/local/backend.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"sort"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/hashicorp/terraform/backend"
    15  	"github.com/hashicorp/terraform/helper/schema"
    16  	"github.com/hashicorp/terraform/state"
    17  	"github.com/hashicorp/terraform/terraform"
    18  	"github.com/mitchellh/cli"
    19  	"github.com/mitchellh/colorstring"
    20  )
    21  
    22  const (
    23  	DefaultWorkspaceDir    = "terraform.tfstate.d"
    24  	DefaultWorkspaceFile   = "environment"
    25  	DefaultStateFilename   = "terraform.tfstate"
    26  	DefaultBackupExtension = ".backup"
    27  )
    28  
    29  // Local is an implementation of EnhancedBackend that performs all operations
    30  // locally. This is the "default" backend and implements normal Terraform
    31  // behavior as it is well known.
    32  type Local struct {
    33  	// CLI and Colorize control the CLI output. If CLI is nil then no CLI
    34  	// output will be done. If CLIColor is nil then no coloring will be done.
    35  	CLI      cli.Ui
    36  	CLIColor *colorstring.Colorize
    37  
    38  	// The State* paths are set from the backend config, and may be left blank
    39  	// to use the defaults. If the actual paths for the local backend state are
    40  	// needed, use the StatePaths method.
    41  	//
    42  	// StatePath is the local path where state is read from.
    43  	//
    44  	// StateOutPath is the local path where the state will be written.
    45  	// If this is empty, it will default to StatePath.
    46  	//
    47  	// StateBackupPath is the local path where a backup file will be written.
    48  	// Set this to "-" to disable state backup.
    49  	//
    50  	// StateWorkspaceDir is the path to the folder containing data for
    51  	// non-default workspaces. This defaults to DefaultWorkspaceDir if not set.
    52  	StatePath         string
    53  	StateOutPath      string
    54  	StateBackupPath   string
    55  	StateWorkspaceDir string
    56  
    57  	// We only want to create a single instance of a local state, so store them
    58  	// here as they're loaded.
    59  	states map[string]state.State
    60  
    61  	// Terraform context. Many of these will be overridden or merged by
    62  	// Operation. See Operation for more details.
    63  	ContextOpts *terraform.ContextOpts
    64  
    65  	// OpInput will ask for necessary input prior to performing any operations.
    66  	//
    67  	// OpValidation will perform validation prior to running an operation. The
    68  	// variable naming doesn't match the style of others since we have a func
    69  	// Validate.
    70  	OpInput      bool
    71  	OpValidation bool
    72  
    73  	// Backend, if non-nil, will use this backend for non-enhanced behavior.
    74  	// This allows local behavior with remote state storage. It is a way to
    75  	// "upgrade" a non-enhanced backend to an enhanced backend with typical
    76  	// behavior.
    77  	//
    78  	// If this is nil, local performs normal state loading and storage.
    79  	Backend backend.Backend
    80  
    81  	// RunningInAutomation indicates that commands are being run by an
    82  	// automated system rather than directly at a command prompt.
    83  	//
    84  	// This is a hint not to produce messages that expect that a user can
    85  	// run a follow-up command, perhaps because Terraform is running in
    86  	// some sort of workflow automation tool that abstracts away the
    87  	// exact commands that are being run.
    88  	RunningInAutomation bool
    89  
    90  	schema *schema.Backend
    91  	opLock sync.Mutex
    92  	once   sync.Once
    93  }
    94  
    95  func (b *Local) Input(
    96  	ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
    97  	b.once.Do(b.init)
    98  
    99  	f := b.schema.Input
   100  	if b.Backend != nil {
   101  		f = b.Backend.Input
   102  	}
   103  
   104  	return f(ui, c)
   105  }
   106  
   107  func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) {
   108  	b.once.Do(b.init)
   109  
   110  	f := b.schema.Validate
   111  	if b.Backend != nil {
   112  		f = b.Backend.Validate
   113  	}
   114  
   115  	return f(c)
   116  }
   117  
   118  func (b *Local) Configure(c *terraform.ResourceConfig) error {
   119  	b.once.Do(b.init)
   120  
   121  	f := b.schema.Configure
   122  	if b.Backend != nil {
   123  		f = b.Backend.Configure
   124  	}
   125  
   126  	return f(c)
   127  }
   128  
   129  func (b *Local) States() ([]string, error) {
   130  	// If we have a backend handling state, defer to that.
   131  	if b.Backend != nil {
   132  		return b.Backend.States()
   133  	}
   134  
   135  	// the listing always start with "default"
   136  	envs := []string{backend.DefaultStateName}
   137  
   138  	entries, err := ioutil.ReadDir(b.stateWorkspaceDir())
   139  	// no error if there's no envs configured
   140  	if os.IsNotExist(err) {
   141  		return envs, nil
   142  	}
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	var listed []string
   148  	for _, entry := range entries {
   149  		if entry.IsDir() {
   150  			listed = append(listed, filepath.Base(entry.Name()))
   151  		}
   152  	}
   153  
   154  	sort.Strings(listed)
   155  	envs = append(envs, listed...)
   156  
   157  	return envs, nil
   158  }
   159  
   160  // DeleteState removes a named state.
   161  // The "default" state cannot be removed.
   162  func (b *Local) DeleteState(name string) error {
   163  	// If we have a backend handling state, defer to that.
   164  	if b.Backend != nil {
   165  		return b.Backend.DeleteState(name)
   166  	}
   167  
   168  	if name == "" {
   169  		return errors.New("empty state name")
   170  	}
   171  
   172  	if name == backend.DefaultStateName {
   173  		return errors.New("cannot delete default state")
   174  	}
   175  
   176  	delete(b.states, name)
   177  	return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name))
   178  }
   179  
   180  func (b *Local) State(name string) (state.State, error) {
   181  	statePath, stateOutPath, backupPath := b.StatePaths(name)
   182  
   183  	// If we have a backend handling state, delegate to that.
   184  	if b.Backend != nil {
   185  		return b.Backend.State(name)
   186  	}
   187  
   188  	if s, ok := b.states[name]; ok {
   189  		return s, nil
   190  	}
   191  
   192  	if err := b.createState(name); err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	// Otherwise, we need to load the state.
   197  	var s state.State = &state.LocalState{
   198  		Path:    statePath,
   199  		PathOut: stateOutPath,
   200  	}
   201  
   202  	// If we are backing up the state, wrap it
   203  	if backupPath != "" {
   204  		s = &state.BackupState{
   205  			Real: s,
   206  			Path: backupPath,
   207  		}
   208  	}
   209  
   210  	if b.states == nil {
   211  		b.states = map[string]state.State{}
   212  	}
   213  	b.states[name] = s
   214  	return s, nil
   215  }
   216  
   217  // Operation implements backend.Enhanced
   218  //
   219  // This will initialize an in-memory terraform.Context to perform the
   220  // operation within this process.
   221  //
   222  // The given operation parameter will be merged with the ContextOpts on
   223  // the structure with the following rules. If a rule isn't specified and the
   224  // name conflicts, assume that the field is overwritten if set.
   225  func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
   226  	// Determine the function to call for our operation
   227  	var f func(context.Context, *backend.Operation, *backend.RunningOperation)
   228  	switch op.Type {
   229  	case backend.OperationTypeRefresh:
   230  		f = b.opRefresh
   231  	case backend.OperationTypePlan:
   232  		f = b.opPlan
   233  	case backend.OperationTypeApply:
   234  		f = b.opApply
   235  	default:
   236  		return nil, fmt.Errorf(
   237  			"Unsupported operation type: %s\n\n"+
   238  				"This is a bug in Terraform and should be reported. The local backend\n"+
   239  				"is built-in to Terraform and should always support all operations.",
   240  			op.Type)
   241  	}
   242  
   243  	// Lock
   244  	b.opLock.Lock()
   245  
   246  	// Build our running operation
   247  	runningCtx, runningCtxCancel := context.WithCancel(context.Background())
   248  	runningOp := &backend.RunningOperation{Context: runningCtx}
   249  
   250  	// Do it
   251  	go func() {
   252  		defer b.opLock.Unlock()
   253  		defer runningCtxCancel()
   254  		f(ctx, op, runningOp)
   255  	}()
   256  
   257  	// Return
   258  	return runningOp, nil
   259  }
   260  
   261  // Colorize returns the Colorize structure that can be used for colorizing
   262  // output. This is gauranteed to always return a non-nil value and so is useful
   263  // as a helper to wrap any potentially colored strings.
   264  func (b *Local) Colorize() *colorstring.Colorize {
   265  	if b.CLIColor != nil {
   266  		return b.CLIColor
   267  	}
   268  
   269  	return &colorstring.Colorize{
   270  		Colors:  colorstring.DefaultColors,
   271  		Disable: true,
   272  	}
   273  }
   274  
   275  func (b *Local) init() {
   276  	b.schema = &schema.Backend{
   277  		Schema: map[string]*schema.Schema{
   278  			"path": &schema.Schema{
   279  				Type:     schema.TypeString,
   280  				Optional: true,
   281  				Default:  "",
   282  			},
   283  
   284  			"workspace_dir": &schema.Schema{
   285  				Type:     schema.TypeString,
   286  				Optional: true,
   287  				Default:  "",
   288  			},
   289  
   290  			"environment_dir": &schema.Schema{
   291  				Type:          schema.TypeString,
   292  				Optional:      true,
   293  				Default:       "",
   294  				ConflictsWith: []string{"workspace_dir"},
   295  
   296  				Deprecated: "workspace_dir should be used instead, with the same meaning",
   297  			},
   298  		},
   299  
   300  		ConfigureFunc: b.schemaConfigure,
   301  	}
   302  }
   303  
   304  func (b *Local) schemaConfigure(ctx context.Context) error {
   305  	d := schema.FromContextBackendConfig(ctx)
   306  
   307  	// Set the path if it is set
   308  	pathRaw, ok := d.GetOk("path")
   309  	if ok {
   310  		path := pathRaw.(string)
   311  		if path == "" {
   312  			return fmt.Errorf("configured path is empty")
   313  		}
   314  
   315  		b.StatePath = path
   316  		b.StateOutPath = path
   317  	}
   318  
   319  	if raw, ok := d.GetOk("workspace_dir"); ok {
   320  		path := raw.(string)
   321  		if path != "" {
   322  			b.StateWorkspaceDir = path
   323  		}
   324  	}
   325  
   326  	// Legacy name, which ConflictsWith workspace_dir
   327  	if raw, ok := d.GetOk("environment_dir"); ok {
   328  		path := raw.(string)
   329  		if path != "" {
   330  			b.StateWorkspaceDir = path
   331  		}
   332  	}
   333  
   334  	return nil
   335  }
   336  
   337  // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
   338  // configured from the CLI.
   339  func (b *Local) StatePaths(name string) (string, string, string) {
   340  	statePath := b.StatePath
   341  	stateOutPath := b.StateOutPath
   342  	backupPath := b.StateBackupPath
   343  
   344  	if name == "" {
   345  		name = backend.DefaultStateName
   346  	}
   347  
   348  	if name == backend.DefaultStateName {
   349  		if statePath == "" {
   350  			statePath = DefaultStateFilename
   351  		}
   352  	} else {
   353  		statePath = filepath.Join(b.stateWorkspaceDir(), name, DefaultStateFilename)
   354  	}
   355  
   356  	if stateOutPath == "" {
   357  		stateOutPath = statePath
   358  	}
   359  
   360  	switch backupPath {
   361  	case "-":
   362  		backupPath = ""
   363  	case "":
   364  		backupPath = stateOutPath + DefaultBackupExtension
   365  	}
   366  
   367  	return statePath, stateOutPath, backupPath
   368  }
   369  
   370  // this only ensures that the named directory exists
   371  func (b *Local) createState(name string) error {
   372  	if name == backend.DefaultStateName {
   373  		return nil
   374  	}
   375  
   376  	stateDir := filepath.Join(b.stateWorkspaceDir(), name)
   377  	s, err := os.Stat(stateDir)
   378  	if err == nil && s.IsDir() {
   379  		// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
   380  		// which will catch the other possible errors as well.
   381  		return nil
   382  	}
   383  
   384  	err = os.MkdirAll(stateDir, 0755)
   385  	if err != nil {
   386  		return err
   387  	}
   388  
   389  	return nil
   390  }
   391  
   392  // stateWorkspaceDir returns the directory where state environments are stored.
   393  func (b *Local) stateWorkspaceDir() string {
   394  	if b.StateWorkspaceDir != "" {
   395  		return b.StateWorkspaceDir
   396  	}
   397  
   398  	return DefaultWorkspaceDir
   399  }
   400  
   401  func (b *Local) pluginInitRequired(providerErr *terraform.ResourceProviderError) {
   402  	b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   403  		strings.TrimSpace(errPluginInit)+"\n",
   404  		providerErr)))
   405  }
   406  
   407  // this relies on multierror to format the plugin errors below the copy
   408  const errPluginInit = `
   409  [reset][bold][yellow]Plugin reinitialization required. Please run "terraform init".[reset]
   410  [yellow]Reason: Could not satisfy plugin requirements.
   411  
   412  Plugins are external binaries that Terraform uses to access and manipulate
   413  resources. The configuration provided requires plugins which can't be located,
   414  don't satisfy the version constraints, or are otherwise incompatible.
   415  
   416  [reset][red]%s
   417  
   418  [reset][yellow]Terraform automatically discovers provider requirements from your
   419  configuration, including providers used in child modules. To see the
   420  requirements and constraints from each module, run "terraform providers".
   421  `