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