github.com/kevinklinger/open_terraform@v1.3.6/noninternal/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  	"sync"
    13  
    14  	"github.com/kevinklinger/open_terraform/noninternal/backend"
    15  	"github.com/kevinklinger/open_terraform/noninternal/command/views"
    16  	"github.com/kevinklinger/open_terraform/noninternal/configs/configschema"
    17  	"github.com/kevinklinger/open_terraform/noninternal/logging"
    18  	"github.com/kevinklinger/open_terraform/noninternal/states/statemgr"
    19  	"github.com/kevinklinger/open_terraform/noninternal/terraform"
    20  	"github.com/kevinklinger/open_terraform/noninternal/tfdiags"
    21  	"github.com/zclconf/go-cty/cty"
    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  	// The State* paths are set from the backend config, and may be left blank
    36  	// to use the defaults. If the actual paths for the local backend state are
    37  	// needed, use the StatePaths method.
    38  	//
    39  	// StatePath is the local path where state is read from.
    40  	//
    41  	// StateOutPath is the local path where the state will be written.
    42  	// If this is empty, it will default to StatePath.
    43  	//
    44  	// StateBackupPath is the local path where a backup file will be written.
    45  	// Set this to "-" to disable state backup.
    46  	//
    47  	// StateWorkspaceDir is the path to the folder containing data for
    48  	// non-default workspaces. This defaults to DefaultWorkspaceDir if not set.
    49  	StatePath         string
    50  	StateOutPath      string
    51  	StateBackupPath   string
    52  	StateWorkspaceDir string
    53  
    54  	// The OverrideState* paths are set based on per-operation CLI arguments
    55  	// and will override what'd be built from the State* fields if non-empty.
    56  	// While the interpretation of the State* fields depends on the active
    57  	// workspace, the OverrideState* fields are always used literally.
    58  	OverrideStatePath       string
    59  	OverrideStateOutPath    string
    60  	OverrideStateBackupPath string
    61  
    62  	// We only want to create a single instance of a local state, so store them
    63  	// here as they're loaded.
    64  	states map[string]statemgr.Full
    65  
    66  	// Terraform context. Many of these will be overridden or merged by
    67  	// Operation. See Operation for more details.
    68  	ContextOpts *terraform.ContextOpts
    69  
    70  	// OpInput will ask for necessary input prior to performing any operations.
    71  	//
    72  	// OpValidation will perform validation prior to running an operation. The
    73  	// variable naming doesn't match the style of others since we have a func
    74  	// Validate.
    75  	OpInput      bool
    76  	OpValidation bool
    77  
    78  	// Backend, if non-nil, will use this backend for non-enhanced behavior.
    79  	// This allows local behavior with remote state storage. It is a way to
    80  	// "upgrade" a non-enhanced backend to an enhanced backend with typical
    81  	// behavior.
    82  	//
    83  	// If this is nil, local performs normal state loading and storage.
    84  	Backend backend.Backend
    85  
    86  	// opLock locks operations
    87  	opLock sync.Mutex
    88  }
    89  
    90  var _ backend.Backend = (*Local)(nil)
    91  
    92  // New returns a new initialized local backend.
    93  func New() *Local {
    94  	return NewWithBackend(nil)
    95  }
    96  
    97  // NewWithBackend returns a new local backend initialized with a
    98  // dedicated backend for non-enhanced behavior.
    99  func NewWithBackend(backend backend.Backend) *Local {
   100  	return &Local{
   101  		Backend: backend,
   102  	}
   103  }
   104  
   105  func (b *Local) ConfigSchema() *configschema.Block {
   106  	if b.Backend != nil {
   107  		return b.Backend.ConfigSchema()
   108  	}
   109  	return &configschema.Block{
   110  		Attributes: map[string]*configschema.Attribute{
   111  			"path": {
   112  				Type:     cty.String,
   113  				Optional: true,
   114  			},
   115  			"workspace_dir": {
   116  				Type:     cty.String,
   117  				Optional: true,
   118  			},
   119  		},
   120  	}
   121  }
   122  
   123  func (b *Local) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) {
   124  	if b.Backend != nil {
   125  		return b.Backend.PrepareConfig(obj)
   126  	}
   127  
   128  	var diags tfdiags.Diagnostics
   129  
   130  	if val := obj.GetAttr("path"); !val.IsNull() {
   131  		p := val.AsString()
   132  		if p == "" {
   133  			diags = diags.Append(tfdiags.AttributeValue(
   134  				tfdiags.Error,
   135  				"Invalid local state file path",
   136  				`The "path" attribute value must not be empty.`,
   137  				cty.Path{cty.GetAttrStep{Name: "path"}},
   138  			))
   139  		}
   140  	}
   141  
   142  	if val := obj.GetAttr("workspace_dir"); !val.IsNull() {
   143  		p := val.AsString()
   144  		if p == "" {
   145  			diags = diags.Append(tfdiags.AttributeValue(
   146  				tfdiags.Error,
   147  				"Invalid local workspace directory path",
   148  				`The "workspace_dir" attribute value must not be empty.`,
   149  				cty.Path{cty.GetAttrStep{Name: "workspace_dir"}},
   150  			))
   151  		}
   152  	}
   153  
   154  	return obj, diags
   155  }
   156  
   157  func (b *Local) Configure(obj cty.Value) tfdiags.Diagnostics {
   158  	if b.Backend != nil {
   159  		return b.Backend.Configure(obj)
   160  	}
   161  
   162  	var diags tfdiags.Diagnostics
   163  
   164  	if val := obj.GetAttr("path"); !val.IsNull() {
   165  		p := val.AsString()
   166  		b.StatePath = p
   167  		b.StateOutPath = p
   168  	} else {
   169  		b.StatePath = DefaultStateFilename
   170  		b.StateOutPath = DefaultStateFilename
   171  	}
   172  
   173  	if val := obj.GetAttr("workspace_dir"); !val.IsNull() {
   174  		p := val.AsString()
   175  		b.StateWorkspaceDir = p
   176  	} else {
   177  		b.StateWorkspaceDir = DefaultWorkspaceDir
   178  	}
   179  
   180  	return diags
   181  }
   182  
   183  func (b *Local) Workspaces() ([]string, error) {
   184  	// If we have a backend handling state, defer to that.
   185  	if b.Backend != nil {
   186  		return b.Backend.Workspaces()
   187  	}
   188  
   189  	// the listing always start with "default"
   190  	envs := []string{backend.DefaultStateName}
   191  
   192  	entries, err := ioutil.ReadDir(b.stateWorkspaceDir())
   193  	// no error if there's no envs configured
   194  	if os.IsNotExist(err) {
   195  		return envs, nil
   196  	}
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	var listed []string
   202  	for _, entry := range entries {
   203  		if entry.IsDir() {
   204  			listed = append(listed, filepath.Base(entry.Name()))
   205  		}
   206  	}
   207  
   208  	sort.Strings(listed)
   209  	envs = append(envs, listed...)
   210  
   211  	return envs, nil
   212  }
   213  
   214  // DeleteWorkspace removes a workspace.
   215  //
   216  // The "default" workspace cannot be removed.
   217  func (b *Local) DeleteWorkspace(name string) error {
   218  	// If we have a backend handling state, defer to that.
   219  	if b.Backend != nil {
   220  		return b.Backend.DeleteWorkspace(name)
   221  	}
   222  
   223  	if name == "" {
   224  		return errors.New("empty state name")
   225  	}
   226  
   227  	if name == backend.DefaultStateName {
   228  		return errors.New("cannot delete default state")
   229  	}
   230  
   231  	delete(b.states, name)
   232  	return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name))
   233  }
   234  
   235  func (b *Local) StateMgr(name string) (statemgr.Full, error) {
   236  	// If we have a backend handling state, delegate to that.
   237  	if b.Backend != nil {
   238  		return b.Backend.StateMgr(name)
   239  	}
   240  
   241  	if s, ok := b.states[name]; ok {
   242  		return s, nil
   243  	}
   244  
   245  	if err := b.createState(name); err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	statePath, stateOutPath, backupPath := b.StatePaths(name)
   250  	log.Printf("[TRACE] backend/local: state manager for workspace %q will:\n - read initial snapshot from %s\n - write new snapshots to %s\n - create any backup at %s", name, statePath, stateOutPath, backupPath)
   251  
   252  	s := statemgr.NewFilesystemBetweenPaths(statePath, stateOutPath)
   253  	if backupPath != "" {
   254  		s.SetBackupPath(backupPath)
   255  	}
   256  
   257  	if b.states == nil {
   258  		b.states = map[string]statemgr.Full{}
   259  	}
   260  	b.states[name] = s
   261  	return s, nil
   262  }
   263  
   264  // Operation implements backend.Enhanced
   265  //
   266  // This will initialize an in-memory terraform.Context to perform the
   267  // operation within this process.
   268  //
   269  // The given operation parameter will be merged with the ContextOpts on
   270  // the structure with the following rules. If a rule isn't specified and the
   271  // name conflicts, assume that the field is overwritten if set.
   272  func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
   273  	if op.View == nil {
   274  		panic("Operation called with nil View")
   275  	}
   276  
   277  	// Determine the function to call for our operation
   278  	var f func(context.Context, context.Context, *backend.Operation, *backend.RunningOperation)
   279  	switch op.Type {
   280  	case backend.OperationTypeRefresh:
   281  		f = b.opRefresh
   282  	case backend.OperationTypePlan:
   283  		f = b.opPlan
   284  	case backend.OperationTypeApply:
   285  		f = b.opApply
   286  	default:
   287  		return nil, fmt.Errorf(
   288  			"unsupported operation type: %s\n\n"+
   289  				"This is a bug in Terraform and should be reported. The local backend\n"+
   290  				"is built-in to Terraform and should always support all operations.",
   291  			op.Type)
   292  	}
   293  
   294  	// Lock
   295  	b.opLock.Lock()
   296  
   297  	// Build our running operation
   298  	// the runninCtx is only used to block until the operation returns.
   299  	runningCtx, done := context.WithCancel(context.Background())
   300  	runningOp := &backend.RunningOperation{
   301  		Context: runningCtx,
   302  	}
   303  
   304  	// stopCtx wraps the context passed in, and is used to signal a graceful Stop.
   305  	stopCtx, stop := context.WithCancel(ctx)
   306  	runningOp.Stop = stop
   307  
   308  	// cancelCtx is used to cancel the operation immediately, usually
   309  	// indicating that the process is exiting.
   310  	cancelCtx, cancel := context.WithCancel(context.Background())
   311  	runningOp.Cancel = cancel
   312  
   313  	op.StateLocker = op.StateLocker.WithContext(stopCtx)
   314  
   315  	// Do it
   316  	go func() {
   317  		defer logging.PanicHandler()
   318  		defer done()
   319  		defer stop()
   320  		defer cancel()
   321  
   322  		defer b.opLock.Unlock()
   323  		f(stopCtx, cancelCtx, op, runningOp)
   324  	}()
   325  
   326  	// Return
   327  	return runningOp, nil
   328  }
   329  
   330  // opWait waits for the operation to complete, and a stop signal or a
   331  // cancelation signal.
   332  func (b *Local) opWait(
   333  	doneCh <-chan struct{},
   334  	stopCtx context.Context,
   335  	cancelCtx context.Context,
   336  	tfCtx *terraform.Context,
   337  	opStateMgr statemgr.Persister,
   338  	view views.Operation) (canceled bool) {
   339  	// Wait for the operation to finish or for us to be interrupted so
   340  	// we can handle it properly.
   341  	select {
   342  	case <-stopCtx.Done():
   343  		view.Stopping()
   344  
   345  		// try to force a PersistState just in case the process is terminated
   346  		// before we can complete.
   347  		if err := opStateMgr.PersistState(nil); err != nil {
   348  			// We can't error out from here, but warn the user if there was an error.
   349  			// If this isn't transient, we will catch it again below, and
   350  			// attempt to save the state another way.
   351  			var diags tfdiags.Diagnostics
   352  			diags = diags.Append(tfdiags.Sourceless(
   353  				tfdiags.Error,
   354  				"Error saving current state",
   355  				fmt.Sprintf(earlyStateWriteErrorFmt, err),
   356  			))
   357  			view.Diagnostics(diags)
   358  		}
   359  
   360  		// Stop execution
   361  		log.Println("[TRACE] backend/local: waiting for the running operation to stop")
   362  		go tfCtx.Stop()
   363  
   364  		select {
   365  		case <-cancelCtx.Done():
   366  			log.Println("[WARN] running operation was forcefully canceled")
   367  			// if the operation was canceled, we need to return immediately
   368  			canceled = true
   369  		case <-doneCh:
   370  			log.Println("[TRACE] backend/local: graceful stop has completed")
   371  		}
   372  	case <-cancelCtx.Done():
   373  		// this should not be called without first attempting to stop the
   374  		// operation
   375  		log.Println("[ERROR] running operation canceled without Stop")
   376  		canceled = true
   377  	case <-doneCh:
   378  	}
   379  	return
   380  }
   381  
   382  // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
   383  // configured from the CLI.
   384  func (b *Local) StatePaths(name string) (stateIn, stateOut, backupOut string) {
   385  	statePath := b.OverrideStatePath
   386  	stateOutPath := b.OverrideStateOutPath
   387  	backupPath := b.OverrideStateBackupPath
   388  
   389  	isDefault := name == backend.DefaultStateName || name == ""
   390  
   391  	baseDir := ""
   392  	if !isDefault {
   393  		baseDir = filepath.Join(b.stateWorkspaceDir(), name)
   394  	}
   395  
   396  	if statePath == "" {
   397  		if isDefault {
   398  			statePath = b.StatePath // s.StatePath applies only to the default workspace, since StateWorkspaceDir is used otherwise
   399  		}
   400  		if statePath == "" {
   401  			statePath = filepath.Join(baseDir, DefaultStateFilename)
   402  		}
   403  	}
   404  	if stateOutPath == "" {
   405  		stateOutPath = statePath
   406  	}
   407  	if backupPath == "" {
   408  		backupPath = b.StateBackupPath
   409  	}
   410  	switch backupPath {
   411  	case "-":
   412  		backupPath = ""
   413  	case "":
   414  		backupPath = stateOutPath + DefaultBackupExtension
   415  	}
   416  
   417  	return statePath, stateOutPath, backupPath
   418  }
   419  
   420  // PathsConflictWith returns true if any state path used by a workspace in
   421  // the receiver is the same as any state path used by the other given
   422  // local backend instance.
   423  //
   424  // This should be used when "migrating" from one local backend configuration to
   425  // another in order to avoid deleting the "old" state snapshots if they are
   426  // in the same files as the "new" state snapshots.
   427  func (b *Local) PathsConflictWith(other *Local) bool {
   428  	otherPaths := map[string]struct{}{}
   429  	otherWorkspaces, err := other.Workspaces()
   430  	if err != nil {
   431  		// If we can't enumerate the workspaces then we'll conservatively
   432  		// assume that paths _do_ overlap, since we can't be certain.
   433  		return true
   434  	}
   435  	for _, name := range otherWorkspaces {
   436  		p, _, _ := other.StatePaths(name)
   437  		otherPaths[p] = struct{}{}
   438  	}
   439  
   440  	ourWorkspaces, err := other.Workspaces()
   441  	if err != nil {
   442  		// If we can't enumerate the workspaces then we'll conservatively
   443  		// assume that paths _do_ overlap, since we can't be certain.
   444  		return true
   445  	}
   446  
   447  	for _, name := range ourWorkspaces {
   448  		p, _, _ := b.StatePaths(name)
   449  		if _, exists := otherPaths[p]; exists {
   450  			return true
   451  		}
   452  	}
   453  	return false
   454  }
   455  
   456  // this only ensures that the named directory exists
   457  func (b *Local) createState(name string) error {
   458  	if name == backend.DefaultStateName {
   459  		return nil
   460  	}
   461  
   462  	stateDir := filepath.Join(b.stateWorkspaceDir(), name)
   463  	s, err := os.Stat(stateDir)
   464  	if err == nil && s.IsDir() {
   465  		// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
   466  		// which will catch the other possible errors as well.
   467  		return nil
   468  	}
   469  
   470  	err = os.MkdirAll(stateDir, 0755)
   471  	if err != nil {
   472  		return err
   473  	}
   474  
   475  	return nil
   476  }
   477  
   478  // stateWorkspaceDir returns the directory where state environments are stored.
   479  func (b *Local) stateWorkspaceDir() string {
   480  	if b.StateWorkspaceDir != "" {
   481  		return b.StateWorkspaceDir
   482  	}
   483  
   484  	return DefaultWorkspaceDir
   485  }
   486  
   487  const earlyStateWriteErrorFmt = `Error: %s
   488  
   489  Terraform encountered an error attempting to save the state before cancelling the current operation. Once the operation is complete another attempt will be made to save the final state.`