github.com/opentofu/opentofu@v1.7.1/internal/backend/local/backend.go (about)

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