github.com/lymingtonprecision/terraform@v0.9.9-0.20170613092852-62acef9611a9/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  	"sync"
    12  
    13  	"github.com/hashicorp/terraform/backend"
    14  	"github.com/hashicorp/terraform/helper/schema"
    15  	"github.com/hashicorp/terraform/state"
    16  	"github.com/hashicorp/terraform/terraform"
    17  	"github.com/mitchellh/cli"
    18  	"github.com/mitchellh/colorstring"
    19  )
    20  
    21  const (
    22  	DefaultWorkspaceDir    = "terraform.tfstate.d"
    23  	DefaultWorkspaceFile   = "environment"
    24  	DefaultStateFilename   = "terraform.tfstate"
    25  	DefaultDataDir         = ".terraform"
    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  	schema *schema.Backend
    82  	opLock sync.Mutex
    83  	once   sync.Once
    84  }
    85  
    86  func (b *Local) Input(
    87  	ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
    88  	b.once.Do(b.init)
    89  
    90  	f := b.schema.Input
    91  	if b.Backend != nil {
    92  		f = b.Backend.Input
    93  	}
    94  
    95  	return f(ui, c)
    96  }
    97  
    98  func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) {
    99  	b.once.Do(b.init)
   100  
   101  	f := b.schema.Validate
   102  	if b.Backend != nil {
   103  		f = b.Backend.Validate
   104  	}
   105  
   106  	return f(c)
   107  }
   108  
   109  func (b *Local) Configure(c *terraform.ResourceConfig) error {
   110  	b.once.Do(b.init)
   111  
   112  	f := b.schema.Configure
   113  	if b.Backend != nil {
   114  		f = b.Backend.Configure
   115  	}
   116  
   117  	return f(c)
   118  }
   119  
   120  func (b *Local) States() ([]string, error) {
   121  	// If we have a backend handling state, defer to that.
   122  	if b.Backend != nil {
   123  		return b.Backend.States()
   124  	}
   125  
   126  	// the listing always start with "default"
   127  	envs := []string{backend.DefaultStateName}
   128  
   129  	entries, err := ioutil.ReadDir(b.stateWorkspaceDir())
   130  	// no error if there's no envs configured
   131  	if os.IsNotExist(err) {
   132  		return envs, nil
   133  	}
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	var listed []string
   139  	for _, entry := range entries {
   140  		if entry.IsDir() {
   141  			listed = append(listed, filepath.Base(entry.Name()))
   142  		}
   143  	}
   144  
   145  	sort.Strings(listed)
   146  	envs = append(envs, listed...)
   147  
   148  	return envs, nil
   149  }
   150  
   151  // DeleteState removes a named state.
   152  // The "default" state cannot be removed.
   153  func (b *Local) DeleteState(name string) error {
   154  	// If we have a backend handling state, defer to that.
   155  	if b.Backend != nil {
   156  		return b.Backend.DeleteState(name)
   157  	}
   158  
   159  	if name == "" {
   160  		return errors.New("empty state name")
   161  	}
   162  
   163  	if name == backend.DefaultStateName {
   164  		return errors.New("cannot delete default state")
   165  	}
   166  
   167  	delete(b.states, name)
   168  	return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name))
   169  }
   170  
   171  func (b *Local) State(name string) (state.State, error) {
   172  	statePath, stateOutPath, backupPath := b.StatePaths(name)
   173  
   174  	// If we have a backend handling state, defer to that.
   175  	if b.Backend != nil {
   176  		s, err := b.Backend.State(name)
   177  		if err != nil {
   178  			return nil, err
   179  		}
   180  
   181  		// make sure we always have a backup state, unless it disabled
   182  		if backupPath == "" {
   183  			return s, nil
   184  		}
   185  
   186  		// see if the delegated backend returned a BackupState of its own
   187  		if s, ok := s.(*state.BackupState); ok {
   188  			return s, nil
   189  		}
   190  
   191  		s = &state.BackupState{
   192  			Real: s,
   193  			Path: backupPath,
   194  		}
   195  		return s, nil
   196  	}
   197  
   198  	if s, ok := b.states[name]; ok {
   199  		return s, nil
   200  	}
   201  
   202  	if err := b.createState(name); err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	// Otherwise, we need to load the state.
   207  	var s state.State = &state.LocalState{
   208  		Path:    statePath,
   209  		PathOut: stateOutPath,
   210  	}
   211  
   212  	// If we are backing up the state, wrap it
   213  	if backupPath != "" {
   214  		s = &state.BackupState{
   215  			Real: s,
   216  			Path: backupPath,
   217  		}
   218  	}
   219  
   220  	if b.states == nil {
   221  		b.states = map[string]state.State{}
   222  	}
   223  	b.states[name] = s
   224  	return s, nil
   225  }
   226  
   227  // Operation implements backend.Enhanced
   228  //
   229  // This will initialize an in-memory terraform.Context to perform the
   230  // operation within this process.
   231  //
   232  // The given operation parameter will be merged with the ContextOpts on
   233  // the structure with the following rules. If a rule isn't specified and the
   234  // name conflicts, assume that the field is overwritten if set.
   235  func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
   236  	// Determine the function to call for our operation
   237  	var f func(context.Context, *backend.Operation, *backend.RunningOperation)
   238  	switch op.Type {
   239  	case backend.OperationTypeRefresh:
   240  		f = b.opRefresh
   241  	case backend.OperationTypePlan:
   242  		f = b.opPlan
   243  	case backend.OperationTypeApply:
   244  		f = b.opApply
   245  	default:
   246  		return nil, fmt.Errorf(
   247  			"Unsupported operation type: %s\n\n"+
   248  				"This is a bug in Terraform and should be reported. The local backend\n"+
   249  				"is built-in to Terraform and should always support all operations.",
   250  			op.Type)
   251  	}
   252  
   253  	// Lock
   254  	b.opLock.Lock()
   255  
   256  	// Build our running operation
   257  	runningCtx, runningCtxCancel := context.WithCancel(context.Background())
   258  	runningOp := &backend.RunningOperation{Context: runningCtx}
   259  
   260  	// Do it
   261  	go func() {
   262  		defer b.opLock.Unlock()
   263  		defer runningCtxCancel()
   264  		f(ctx, op, runningOp)
   265  	}()
   266  
   267  	// Return
   268  	return runningOp, nil
   269  }
   270  
   271  // Colorize returns the Colorize structure that can be used for colorizing
   272  // output. This is gauranteed to always return a non-nil value and so is useful
   273  // as a helper to wrap any potentially colored strings.
   274  func (b *Local) Colorize() *colorstring.Colorize {
   275  	if b.CLIColor != nil {
   276  		return b.CLIColor
   277  	}
   278  
   279  	return &colorstring.Colorize{
   280  		Colors:  colorstring.DefaultColors,
   281  		Disable: true,
   282  	}
   283  }
   284  
   285  func (b *Local) init() {
   286  	b.schema = &schema.Backend{
   287  		Schema: map[string]*schema.Schema{
   288  			"path": &schema.Schema{
   289  				Type:     schema.TypeString,
   290  				Optional: true,
   291  				Default:  "",
   292  			},
   293  
   294  			"workspace_dir": &schema.Schema{
   295  				Type:     schema.TypeString,
   296  				Optional: true,
   297  				Default:  "",
   298  			},
   299  
   300  			"environment_dir": &schema.Schema{
   301  				Type:          schema.TypeString,
   302  				Optional:      true,
   303  				Default:       "",
   304  				ConflictsWith: []string{"workspace_dir"},
   305  
   306  				Deprecated: "workspace_dir should be used instead, with the same meaning",
   307  			},
   308  		},
   309  
   310  		ConfigureFunc: b.schemaConfigure,
   311  	}
   312  }
   313  
   314  func (b *Local) schemaConfigure(ctx context.Context) error {
   315  	d := schema.FromContextBackendConfig(ctx)
   316  
   317  	// Set the path if it is set
   318  	pathRaw, ok := d.GetOk("path")
   319  	if ok {
   320  		path := pathRaw.(string)
   321  		if path == "" {
   322  			return fmt.Errorf("configured path is empty")
   323  		}
   324  
   325  		b.StatePath = path
   326  		b.StateOutPath = path
   327  	}
   328  
   329  	if raw, ok := d.GetOk("workspace_dir"); ok {
   330  		path := raw.(string)
   331  		if path != "" {
   332  			b.StateWorkspaceDir = path
   333  		}
   334  	}
   335  
   336  	// Legacy name, which ConflictsWith workspace_dir
   337  	if raw, ok := d.GetOk("environment_dir"); ok {
   338  		path := raw.(string)
   339  		if path != "" {
   340  			b.StateWorkspaceDir = path
   341  		}
   342  	}
   343  
   344  	return nil
   345  }
   346  
   347  // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
   348  // configured from the CLI.
   349  func (b *Local) StatePaths(name string) (string, string, string) {
   350  	statePath := b.StatePath
   351  	stateOutPath := b.StateOutPath
   352  	backupPath := b.StateBackupPath
   353  
   354  	if name == "" {
   355  		name = backend.DefaultStateName
   356  	}
   357  
   358  	if name == backend.DefaultStateName {
   359  		if statePath == "" {
   360  			statePath = DefaultStateFilename
   361  		}
   362  	} else {
   363  		statePath = filepath.Join(b.stateWorkspaceDir(), name, DefaultStateFilename)
   364  	}
   365  
   366  	if stateOutPath == "" {
   367  		stateOutPath = statePath
   368  	}
   369  
   370  	switch backupPath {
   371  	case "-":
   372  		backupPath = ""
   373  	case "":
   374  		backupPath = stateOutPath + DefaultBackupExtension
   375  	}
   376  
   377  	return statePath, stateOutPath, backupPath
   378  }
   379  
   380  // this only ensures that the named directory exists
   381  func (b *Local) createState(name string) error {
   382  	if name == backend.DefaultStateName {
   383  		return nil
   384  	}
   385  
   386  	stateDir := filepath.Join(b.stateWorkspaceDir(), name)
   387  	s, err := os.Stat(stateDir)
   388  	if err == nil && s.IsDir() {
   389  		// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
   390  		// which will catch the other possible errors as well.
   391  		return nil
   392  	}
   393  
   394  	err = os.MkdirAll(stateDir, 0755)
   395  	if err != nil {
   396  		return err
   397  	}
   398  
   399  	return nil
   400  }
   401  
   402  // stateWorkspaceDir returns the directory where state environments are stored.
   403  func (b *Local) stateWorkspaceDir() string {
   404  	if b.StateWorkspaceDir != "" {
   405  		return b.StateWorkspaceDir
   406  	}
   407  
   408  	return DefaultWorkspaceDir
   409  }