github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/local/backend.go (about)

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