github.com/wikibal01/hashicorp-terraform@v0.11.12-beta1/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  }
    95  
    96  // New returns a new initialized local backend.
    97  func New() *Local {
    98  	return NewWithBackend(nil)
    99  }
   100  
   101  // NewWithBackend returns a new local backend initialized with a
   102  // dedicated backend for non-enhanced behavior.
   103  func NewWithBackend(backend backend.Backend) *Local {
   104  	b := &Local{
   105  		Backend: backend,
   106  	}
   107  
   108  	b.schema = &schema.Backend{
   109  		Schema: map[string]*schema.Schema{
   110  			"path": &schema.Schema{
   111  				Type:     schema.TypeString,
   112  				Optional: true,
   113  				Default:  "",
   114  			},
   115  
   116  			"workspace_dir": &schema.Schema{
   117  				Type:     schema.TypeString,
   118  				Optional: true,
   119  				Default:  "",
   120  			},
   121  
   122  			"environment_dir": &schema.Schema{
   123  				Type:          schema.TypeString,
   124  				Optional:      true,
   125  				Default:       "",
   126  				ConflictsWith: []string{"workspace_dir"},
   127  				Deprecated:    "workspace_dir should be used instead, with the same meaning",
   128  			},
   129  		},
   130  
   131  		ConfigureFunc: b.configure,
   132  	}
   133  
   134  	return b
   135  }
   136  
   137  func (b *Local) configure(ctx context.Context) error {
   138  	d := schema.FromContextBackendConfig(ctx)
   139  
   140  	// Set the path if it is set
   141  	pathRaw, ok := d.GetOk("path")
   142  	if ok {
   143  		path := pathRaw.(string)
   144  		if path == "" {
   145  			return fmt.Errorf("configured path is empty")
   146  		}
   147  
   148  		b.StatePath = path
   149  		b.StateOutPath = path
   150  	}
   151  
   152  	if raw, ok := d.GetOk("workspace_dir"); ok {
   153  		path := raw.(string)
   154  		if path != "" {
   155  			b.StateWorkspaceDir = path
   156  		}
   157  	}
   158  
   159  	// Legacy name, which ConflictsWith workspace_dir
   160  	if raw, ok := d.GetOk("environment_dir"); ok {
   161  		path := raw.(string)
   162  		if path != "" {
   163  			b.StateWorkspaceDir = path
   164  		}
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  func (b *Local) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
   171  	f := b.schema.Input
   172  	if b.Backend != nil {
   173  		f = b.Backend.Input
   174  	}
   175  	return f(ui, c)
   176  }
   177  
   178  func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) {
   179  	f := b.schema.Validate
   180  	if b.Backend != nil {
   181  		f = b.Backend.Validate
   182  	}
   183  	return f(c)
   184  }
   185  
   186  func (b *Local) Configure(c *terraform.ResourceConfig) error {
   187  	f := b.schema.Configure
   188  	if b.Backend != nil {
   189  		f = b.Backend.Configure
   190  	}
   191  	return f(c)
   192  }
   193  
   194  func (b *Local) State(name string) (state.State, error) {
   195  	statePath, stateOutPath, backupPath := b.StatePaths(name)
   196  
   197  	// If we have a backend handling state, delegate to that.
   198  	if b.Backend != nil {
   199  		return b.Backend.State(name)
   200  	}
   201  
   202  	if s, ok := b.states[name]; ok {
   203  		return s, nil
   204  	}
   205  
   206  	if err := b.createState(name); err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	// Otherwise, we need to load the state.
   211  	var s state.State = &state.LocalState{
   212  		Path:    statePath,
   213  		PathOut: stateOutPath,
   214  	}
   215  
   216  	// If we are backing up the state, wrap it
   217  	if backupPath != "" {
   218  		s = &state.BackupState{
   219  			Real: s,
   220  			Path: backupPath,
   221  		}
   222  	}
   223  
   224  	if b.states == nil {
   225  		b.states = map[string]state.State{}
   226  	}
   227  	b.states[name] = s
   228  	return s, nil
   229  }
   230  
   231  // DeleteState removes a named state.
   232  // The "default" state cannot be removed.
   233  func (b *Local) DeleteState(name string) error {
   234  	// If we have a backend handling state, defer to that.
   235  	if b.Backend != nil {
   236  		return b.Backend.DeleteState(name)
   237  	}
   238  
   239  	if name == "" {
   240  		return errors.New("empty state name")
   241  	}
   242  
   243  	if name == backend.DefaultStateName {
   244  		return errors.New("cannot delete default state")
   245  	}
   246  
   247  	delete(b.states, name)
   248  	return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name))
   249  }
   250  
   251  func (b *Local) States() ([]string, error) {
   252  	// If we have a backend handling state, defer to that.
   253  	if b.Backend != nil {
   254  		return b.Backend.States()
   255  	}
   256  
   257  	// the listing always start with "default"
   258  	envs := []string{backend.DefaultStateName}
   259  
   260  	entries, err := ioutil.ReadDir(b.stateWorkspaceDir())
   261  	// no error if there's no envs configured
   262  	if os.IsNotExist(err) {
   263  		return envs, nil
   264  	}
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  
   269  	var listed []string
   270  	for _, entry := range entries {
   271  		if entry.IsDir() {
   272  			listed = append(listed, filepath.Base(entry.Name()))
   273  		}
   274  	}
   275  
   276  	sort.Strings(listed)
   277  	envs = append(envs, listed...)
   278  
   279  	return envs, nil
   280  }
   281  
   282  // Operation implements backend.Enhanced
   283  //
   284  // This will initialize an in-memory terraform.Context to perform the
   285  // operation within this process.
   286  //
   287  // The given operation parameter will be merged with the ContextOpts on
   288  // the structure with the following rules. If a rule isn't specified and the
   289  // name conflicts, assume that the field is overwritten if set.
   290  func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
   291  	// Determine the function to call for our operation
   292  	var f func(context.Context, context.Context, *backend.Operation, *backend.RunningOperation)
   293  	switch op.Type {
   294  	case backend.OperationTypeRefresh:
   295  		f = b.opRefresh
   296  	case backend.OperationTypePlan:
   297  		f = b.opPlan
   298  	case backend.OperationTypeApply:
   299  		f = b.opApply
   300  	default:
   301  		return nil, fmt.Errorf(
   302  			"Unsupported operation type: %s\n\n"+
   303  				"This is a bug in Terraform and should be reported. The local backend\n"+
   304  				"is built-in to Terraform and should always support all operations.",
   305  			op.Type)
   306  	}
   307  
   308  	// Lock
   309  	b.opLock.Lock()
   310  
   311  	// Build our running operation
   312  	// the runninCtx is only used to block until the operation returns.
   313  	runningCtx, done := context.WithCancel(context.Background())
   314  	runningOp := &backend.RunningOperation{
   315  		Context: runningCtx,
   316  	}
   317  
   318  	// stopCtx wraps the context passed in, and is used to signal a graceful Stop.
   319  	stopCtx, stop := context.WithCancel(ctx)
   320  	runningOp.Stop = stop
   321  
   322  	// cancelCtx is used to cancel the operation immediately, usually
   323  	// indicating that the process is exiting.
   324  	cancelCtx, cancel := context.WithCancel(context.Background())
   325  	runningOp.Cancel = cancel
   326  
   327  	if op.LockState {
   328  		op.StateLocker = clistate.NewLocker(stopCtx, op.StateLockTimeout, b.CLI, b.Colorize())
   329  	} else {
   330  		op.StateLocker = clistate.NewNoopLocker()
   331  	}
   332  
   333  	// Do it
   334  	go func() {
   335  		defer done()
   336  		defer stop()
   337  		defer cancel()
   338  
   339  		// the state was locked during context creation, unlock the state when
   340  		// the operation completes
   341  		defer func() {
   342  			runningOp.Err = op.StateLocker.Unlock(runningOp.Err)
   343  		}()
   344  
   345  		defer b.opLock.Unlock()
   346  		f(stopCtx, cancelCtx, op, runningOp)
   347  	}()
   348  
   349  	// Return
   350  	return runningOp, nil
   351  }
   352  
   353  // opWait waits for the operation to complete, and a stop signal or a
   354  // cancelation signal.
   355  func (b *Local) opWait(
   356  	doneCh <-chan struct{},
   357  	stopCtx context.Context,
   358  	cancelCtx context.Context,
   359  	tfCtx *terraform.Context,
   360  	opState state.State) (canceled bool) {
   361  	// Wait for the operation to finish or for us to be interrupted so
   362  	// we can handle it properly.
   363  	select {
   364  	case <-stopCtx.Done():
   365  		if b.CLI != nil {
   366  			b.CLI.Output("stopping operation...")
   367  		}
   368  
   369  		// try to force a PersistState just in case the process is terminated
   370  		// before we can complete.
   371  		if err := opState.PersistState(); err != nil {
   372  			// We can't error out from here, but warn the user if there was an error.
   373  			// If this isn't transient, we will catch it again below, and
   374  			// attempt to save the state another way.
   375  			if b.CLI != nil {
   376  				b.CLI.Error(fmt.Sprintf(earlyStateWriteErrorFmt, err))
   377  			}
   378  		}
   379  
   380  		// Stop execution
   381  		go tfCtx.Stop()
   382  
   383  		select {
   384  		case <-cancelCtx.Done():
   385  			log.Println("[WARN] running operation canceled")
   386  			// if the operation was canceled, we need to return immediately
   387  			canceled = true
   388  		case <-doneCh:
   389  		}
   390  	case <-cancelCtx.Done():
   391  		// this should not be called without first attempting to stop the
   392  		// operation
   393  		log.Println("[ERROR] running operation canceled without Stop")
   394  		canceled = true
   395  	case <-doneCh:
   396  	}
   397  	return
   398  }
   399  
   400  // Colorize returns the Colorize structure that can be used for colorizing
   401  // output. This is gauranteed to always return a non-nil value and so is useful
   402  // as a helper to wrap any potentially colored strings.
   403  func (b *Local) Colorize() *colorstring.Colorize {
   404  	if b.CLIColor != nil {
   405  		return b.CLIColor
   406  	}
   407  
   408  	return &colorstring.Colorize{
   409  		Colors:  colorstring.DefaultColors,
   410  		Disable: true,
   411  	}
   412  }
   413  
   414  // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
   415  // configured from the CLI.
   416  func (b *Local) StatePaths(name string) (string, string, string) {
   417  	statePath := b.StatePath
   418  	stateOutPath := b.StateOutPath
   419  	backupPath := b.StateBackupPath
   420  
   421  	if name == "" {
   422  		name = backend.DefaultStateName
   423  	}
   424  
   425  	if name == backend.DefaultStateName {
   426  		if statePath == "" {
   427  			statePath = DefaultStateFilename
   428  		}
   429  	} else {
   430  		statePath = filepath.Join(b.stateWorkspaceDir(), name, DefaultStateFilename)
   431  	}
   432  
   433  	if stateOutPath == "" {
   434  		stateOutPath = statePath
   435  	}
   436  
   437  	switch backupPath {
   438  	case "-":
   439  		backupPath = ""
   440  	case "":
   441  		backupPath = stateOutPath + DefaultBackupExtension
   442  	}
   443  
   444  	return statePath, stateOutPath, backupPath
   445  }
   446  
   447  // this only ensures that the named directory exists
   448  func (b *Local) createState(name string) error {
   449  	if name == backend.DefaultStateName {
   450  		return nil
   451  	}
   452  
   453  	stateDir := filepath.Join(b.stateWorkspaceDir(), name)
   454  	s, err := os.Stat(stateDir)
   455  	if err == nil && s.IsDir() {
   456  		// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
   457  		// which will catch the other possible errors as well.
   458  		return nil
   459  	}
   460  
   461  	err = os.MkdirAll(stateDir, 0755)
   462  	if err != nil {
   463  		return err
   464  	}
   465  
   466  	return nil
   467  }
   468  
   469  // stateWorkspaceDir returns the directory where state environments are stored.
   470  func (b *Local) stateWorkspaceDir() string {
   471  	if b.StateWorkspaceDir != "" {
   472  		return b.StateWorkspaceDir
   473  	}
   474  
   475  	return DefaultWorkspaceDir
   476  }
   477  
   478  func (b *Local) pluginInitRequired(providerErr *terraform.ResourceProviderError) {
   479  	b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   480  		strings.TrimSpace(errPluginInit)+"\n",
   481  		providerErr)))
   482  }
   483  
   484  // this relies on multierror to format the plugin errors below the copy
   485  const errPluginInit = `
   486  [reset][bold][yellow]Plugin reinitialization required. Please run "terraform init".[reset]
   487  [yellow]Reason: Could not satisfy plugin requirements.
   488  
   489  Plugins are external binaries that Terraform uses to access and manipulate
   490  resources. The configuration provided requires plugins which can't be located,
   491  don't satisfy the version constraints, or are otherwise incompatible.
   492  
   493  [reset][red]%s
   494  
   495  [reset][yellow]Terraform automatically discovers provider requirements from your
   496  configuration, including providers used in child modules. To see the
   497  requirements and constraints from each module, run "terraform providers".
   498  `