github.com/hartzell/terraform@v0.8.6-0.20180503104400-0cc9e050ecd4/backend/local/backend.go (about)

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