github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/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/hashicorp/terraform/backend"
    15  	"github.com/hashicorp/terraform/command/clistate"
    16  	"github.com/hashicorp/terraform/configs/configschema"
    17  	"github.com/hashicorp/terraform/states/statemgr"
    18  	"github.com/hashicorp/terraform/terraform"
    19  	"github.com/hashicorp/terraform/tfdiags"
    20  	"github.com/mitchellh/cli"
    21  	"github.com/mitchellh/colorstring"
    22  	"github.com/zclconf/go-cty/cty"
    23  )
    24  
    25  const (
    26  	DefaultWorkspaceDir    = "terraform.tfstate.d"
    27  	DefaultWorkspaceFile   = "environment"
    28  	DefaultStateFilename   = "terraform.tfstate"
    29  	DefaultBackupExtension = ".backup"
    30  )
    31  
    32  // Local is an implementation of EnhancedBackend that performs all operations
    33  // locally. This is the "default" backend and implements normal Terraform
    34  // behavior as it is well known.
    35  type Local struct {
    36  	// CLI and Colorize control the CLI output. If CLI is nil then no CLI
    37  	// output will be done. If CLIColor is nil then no coloring will be done.
    38  	CLI      cli.Ui
    39  	CLIColor *colorstring.Colorize
    40  
    41  	// ShowDiagnostics prints diagnostic messages to the UI.
    42  	ShowDiagnostics func(vals ...interface{})
    43  
    44  	// The State* paths are set from the backend config, and may be left blank
    45  	// to use the defaults. If the actual paths for the local backend state are
    46  	// needed, use the StatePaths method.
    47  	//
    48  	// StatePath is the local path where state is read from.
    49  	//
    50  	// StateOutPath is the local path where the state will be written.
    51  	// If this is empty, it will default to StatePath.
    52  	//
    53  	// StateBackupPath is the local path where a backup file will be written.
    54  	// Set this to "-" to disable state backup.
    55  	//
    56  	// StateWorkspaceDir is the path to the folder containing data for
    57  	// non-default workspaces. This defaults to DefaultWorkspaceDir if not set.
    58  	StatePath         string
    59  	StateOutPath      string
    60  	StateBackupPath   string
    61  	StateWorkspaceDir string
    62  
    63  	// The OverrideState* paths are set based on per-operation CLI arguments
    64  	// and will override what'd be built from the State* fields if non-empty.
    65  	// While the interpretation of the State* fields depends on the active
    66  	// workspace, the OverrideState* fields are always used literally.
    67  	OverrideStatePath       string
    68  	OverrideStateOutPath    string
    69  	OverrideStateBackupPath string
    70  
    71  	// We only want to create a single instance of a local state, so store them
    72  	// here as they're loaded.
    73  	states map[string]statemgr.Full
    74  
    75  	// Terraform context. Many of these will be overridden or merged by
    76  	// Operation. See Operation for more details.
    77  	ContextOpts *terraform.ContextOpts
    78  
    79  	// OpInput will ask for necessary input prior to performing any operations.
    80  	//
    81  	// OpValidation will perform validation prior to running an operation. The
    82  	// variable naming doesn't match the style of others since we have a func
    83  	// Validate.
    84  	OpInput      bool
    85  	OpValidation bool
    86  
    87  	// Backend, if non-nil, will use this backend for non-enhanced behavior.
    88  	// This allows local behavior with remote state storage. It is a way to
    89  	// "upgrade" a non-enhanced backend to an enhanced backend with typical
    90  	// behavior.
    91  	//
    92  	// If this is nil, local performs normal state loading and storage.
    93  	Backend backend.Backend
    94  
    95  	// RunningInAutomation indicates that commands are being run by an
    96  	// automated system rather than directly at a command prompt.
    97  	//
    98  	// This is a hint not to produce messages that expect that a user can
    99  	// run a follow-up command, perhaps because Terraform is running in
   100  	// some sort of workflow automation tool that abstracts away the
   101  	// exact commands that are being run.
   102  	RunningInAutomation bool
   103  
   104  	// opLock locks operations
   105  	opLock sync.Mutex
   106  }
   107  
   108  var _ backend.Backend = (*Local)(nil)
   109  
   110  // New returns a new initialized local backend.
   111  func New() *Local {
   112  	return NewWithBackend(nil)
   113  }
   114  
   115  // NewWithBackend returns a new local backend initialized with a
   116  // dedicated backend for non-enhanced behavior.
   117  func NewWithBackend(backend backend.Backend) *Local {
   118  	return &Local{
   119  		Backend: backend,
   120  	}
   121  }
   122  
   123  func (b *Local) ConfigSchema() *configschema.Block {
   124  	if b.Backend != nil {
   125  		return b.Backend.ConfigSchema()
   126  	}
   127  	return &configschema.Block{
   128  		Attributes: map[string]*configschema.Attribute{
   129  			"path": {
   130  				Type:     cty.String,
   131  				Optional: true,
   132  			},
   133  			"workspace_dir": {
   134  				Type:     cty.String,
   135  				Optional: true,
   136  			},
   137  		},
   138  	}
   139  }
   140  
   141  func (b *Local) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) {
   142  	if b.Backend != nil {
   143  		return b.Backend.PrepareConfig(obj)
   144  	}
   145  
   146  	var diags tfdiags.Diagnostics
   147  
   148  	if val := obj.GetAttr("path"); !val.IsNull() {
   149  		p := val.AsString()
   150  		if p == "" {
   151  			diags = diags.Append(tfdiags.AttributeValue(
   152  				tfdiags.Error,
   153  				"Invalid local state file path",
   154  				`The "path" attribute value must not be empty.`,
   155  				cty.Path{cty.GetAttrStep{Name: "path"}},
   156  			))
   157  		}
   158  	}
   159  
   160  	if val := obj.GetAttr("workspace_dir"); !val.IsNull() {
   161  		p := val.AsString()
   162  		if p == "" {
   163  			diags = diags.Append(tfdiags.AttributeValue(
   164  				tfdiags.Error,
   165  				"Invalid local workspace directory path",
   166  				`The "workspace_dir" attribute value must not be empty.`,
   167  				cty.Path{cty.GetAttrStep{Name: "workspace_dir"}},
   168  			))
   169  		}
   170  	}
   171  
   172  	return obj, diags
   173  }
   174  
   175  func (b *Local) Configure(obj cty.Value) tfdiags.Diagnostics {
   176  	if b.Backend != nil {
   177  		return b.Backend.Configure(obj)
   178  	}
   179  
   180  	var diags tfdiags.Diagnostics
   181  
   182  	if val := obj.GetAttr("path"); !val.IsNull() {
   183  		p := val.AsString()
   184  		b.StatePath = p
   185  		b.StateOutPath = p
   186  	} else {
   187  		b.StatePath = DefaultStateFilename
   188  		b.StateOutPath = DefaultStateFilename
   189  	}
   190  
   191  	if val := obj.GetAttr("workspace_dir"); !val.IsNull() {
   192  		p := val.AsString()
   193  		b.StateWorkspaceDir = p
   194  	} else {
   195  		b.StateWorkspaceDir = DefaultWorkspaceDir
   196  	}
   197  
   198  	return diags
   199  }
   200  
   201  func (b *Local) Workspaces() ([]string, error) {
   202  	// If we have a backend handling state, defer to that.
   203  	if b.Backend != nil {
   204  		return b.Backend.Workspaces()
   205  	}
   206  
   207  	// the listing always start with "default"
   208  	envs := []string{backend.DefaultStateName}
   209  
   210  	entries, err := ioutil.ReadDir(b.stateWorkspaceDir())
   211  	// no error if there's no envs configured
   212  	if os.IsNotExist(err) {
   213  		return envs, nil
   214  	}
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	var listed []string
   220  	for _, entry := range entries {
   221  		if entry.IsDir() {
   222  			listed = append(listed, filepath.Base(entry.Name()))
   223  		}
   224  	}
   225  
   226  	sort.Strings(listed)
   227  	envs = append(envs, listed...)
   228  
   229  	return envs, nil
   230  }
   231  
   232  // DeleteWorkspace removes a workspace.
   233  //
   234  // The "default" workspace cannot be removed.
   235  func (b *Local) DeleteWorkspace(name string) error {
   236  	// If we have a backend handling state, defer to that.
   237  	if b.Backend != nil {
   238  		return b.Backend.DeleteWorkspace(name)
   239  	}
   240  
   241  	if name == "" {
   242  		return errors.New("empty state name")
   243  	}
   244  
   245  	if name == backend.DefaultStateName {
   246  		return errors.New("cannot delete default state")
   247  	}
   248  
   249  	delete(b.states, name)
   250  	return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name))
   251  }
   252  
   253  func (b *Local) StateMgr(name string) (statemgr.Full, error) {
   254  	// If we have a backend handling state, delegate to that.
   255  	if b.Backend != nil {
   256  		return b.Backend.StateMgr(name)
   257  	}
   258  
   259  	if s, ok := b.states[name]; ok {
   260  		return s, nil
   261  	}
   262  
   263  	if err := b.createState(name); err != nil {
   264  		return nil, err
   265  	}
   266  
   267  	statePath, stateOutPath, backupPath := b.StatePaths(name)
   268  	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)
   269  
   270  	s := statemgr.NewFilesystemBetweenPaths(statePath, stateOutPath)
   271  	if backupPath != "" {
   272  		s.SetBackupPath(backupPath)
   273  	}
   274  
   275  	if b.states == nil {
   276  		b.states = map[string]statemgr.Full{}
   277  	}
   278  	b.states[name] = s
   279  	return s, nil
   280  }
   281  
   282  func (b *Local) StateMgrWithoutCheckVersion(name string) (statemgr.Full, error) {
   283  	return b.StateMgr(name)
   284  }
   285  
   286  // Operation implements backend.Enhanced
   287  //
   288  // This will initialize an in-memory terraform.Context to perform the
   289  // operation within this process.
   290  //
   291  // The given operation parameter will be merged with the ContextOpts on
   292  // the structure with the following rules. If a rule isn't specified and the
   293  // name conflicts, assume that the field is overwritten if set.
   294  func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
   295  	// Determine the function to call for our operation
   296  	var f func(context.Context, context.Context, *backend.Operation, *backend.RunningOperation)
   297  	switch op.Type {
   298  	case backend.OperationTypeRefresh:
   299  		f = b.opRefresh
   300  	case backend.OperationTypePlan:
   301  		f = b.opPlan
   302  	case backend.OperationTypeApply:
   303  		f = b.opApply
   304  	default:
   305  		return nil, fmt.Errorf(
   306  			"Unsupported operation type: %s\n\n"+
   307  				"This is a bug in Terraform and should be reported. The local backend\n"+
   308  				"is built-in to Terraform and should always support all operations.",
   309  			op.Type)
   310  	}
   311  
   312  	// Lock
   313  	b.opLock.Lock()
   314  
   315  	// Build our running operation
   316  	// the runninCtx is only used to block until the operation returns.
   317  	runningCtx, done := context.WithCancel(context.Background())
   318  	runningOp := &backend.RunningOperation{
   319  		Context: runningCtx,
   320  	}
   321  
   322  	// stopCtx wraps the context passed in, and is used to signal a graceful Stop.
   323  	stopCtx, stop := context.WithCancel(ctx)
   324  	runningOp.Stop = stop
   325  
   326  	// cancelCtx is used to cancel the operation immediately, usually
   327  	// indicating that the process is exiting.
   328  	cancelCtx, cancel := context.WithCancel(context.Background())
   329  	runningOp.Cancel = cancel
   330  
   331  	if op.LockState {
   332  		op.StateLocker = clistate.NewLocker(stopCtx, op.StateLockTimeout, b.CLI, b.Colorize())
   333  	} else {
   334  		op.StateLocker = clistate.NewNoopLocker()
   335  	}
   336  
   337  	// Do it
   338  	go func() {
   339  		defer done()
   340  		defer stop()
   341  		defer cancel()
   342  
   343  		// the state was locked during context creation, unlock the state when
   344  		// the operation completes
   345  		defer func() {
   346  			err := op.StateLocker.Unlock(nil)
   347  			if err != nil {
   348  				b.ShowDiagnostics(err)
   349  				runningOp.Result = backend.OperationFailure
   350  			}
   351  		}()
   352  
   353  		defer b.opLock.Unlock()
   354  		f(stopCtx, cancelCtx, op, runningOp)
   355  	}()
   356  
   357  	// Return
   358  	return runningOp, nil
   359  }
   360  
   361  // opWait waits for the operation to complete, and a stop signal or a
   362  // cancelation signal.
   363  func (b *Local) opWait(
   364  	doneCh <-chan struct{},
   365  	stopCtx context.Context,
   366  	cancelCtx context.Context,
   367  	tfCtx *terraform.Context,
   368  	opStateMgr statemgr.Persister) (canceled bool) {
   369  	// Wait for the operation to finish or for us to be interrupted so
   370  	// we can handle it properly.
   371  	select {
   372  	case <-stopCtx.Done():
   373  		if b.CLI != nil {
   374  			b.CLI.Output("Stopping operation...")
   375  		}
   376  
   377  		// try to force a PersistState just in case the process is terminated
   378  		// before we can complete.
   379  		if err := opStateMgr.PersistState(); err != nil {
   380  			// We can't error out from here, but warn the user if there was an error.
   381  			// If this isn't transient, we will catch it again below, and
   382  			// attempt to save the state another way.
   383  			if b.CLI != nil {
   384  				b.CLI.Error(fmt.Sprintf(earlyStateWriteErrorFmt, err))
   385  			}
   386  		}
   387  
   388  		// Stop execution
   389  		log.Println("[TRACE] backend/local: waiting for the running operation to stop")
   390  		go tfCtx.Stop()
   391  
   392  		select {
   393  		case <-cancelCtx.Done():
   394  			log.Println("[WARN] running operation was forcefully canceled")
   395  			// if the operation was canceled, we need to return immediately
   396  			canceled = true
   397  		case <-doneCh:
   398  			log.Println("[TRACE] backend/local: graceful stop has completed")
   399  		}
   400  	case <-cancelCtx.Done():
   401  		// this should not be called without first attempting to stop the
   402  		// operation
   403  		log.Println("[ERROR] running operation canceled without Stop")
   404  		canceled = true
   405  	case <-doneCh:
   406  	}
   407  	return
   408  }
   409  
   410  // ReportResult is a helper for the common chore of setting the status of
   411  // a running operation and showing any diagnostics produced during that
   412  // operation.
   413  //
   414  // If the given diagnostics contains errors then the operation's result
   415  // will be set to backend.OperationFailure. It will be set to
   416  // backend.OperationSuccess otherwise. It will then use b.ShowDiagnostics
   417  // to show the given diagnostics before returning.
   418  //
   419  // Callers should feel free to do each of these operations separately in
   420  // more complex cases where e.g. diagnostics are interleaved with other
   421  // output, but terminating immediately after reporting error diagnostics is
   422  // common and can be expressed concisely via this method.
   423  func (b *Local) ReportResult(op *backend.RunningOperation, diags tfdiags.Diagnostics) {
   424  	if diags.HasErrors() {
   425  		op.Result = backend.OperationFailure
   426  	} else {
   427  		op.Result = backend.OperationSuccess
   428  	}
   429  	if b.ShowDiagnostics != nil {
   430  		b.ShowDiagnostics(diags)
   431  	} else {
   432  		// Shouldn't generally happen, but if it does then we'll at least
   433  		// make some noise in the logs to help us spot it.
   434  		if len(diags) != 0 {
   435  			log.Printf(
   436  				"[ERROR] Local backend needs to report diagnostics but ShowDiagnostics is not set:\n%s",
   437  				diags.ErrWithWarnings(),
   438  			)
   439  		}
   440  	}
   441  }
   442  
   443  // Colorize returns the Colorize structure that can be used for colorizing
   444  // output. This is guaranteed to always return a non-nil value and so is useful
   445  // as a helper to wrap any potentially colored strings.
   446  func (b *Local) Colorize() *colorstring.Colorize {
   447  	if b.CLIColor != nil {
   448  		return b.CLIColor
   449  	}
   450  
   451  	return &colorstring.Colorize{
   452  		Colors:  colorstring.DefaultColors,
   453  		Disable: true,
   454  	}
   455  }
   456  
   457  // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
   458  // configured from the CLI.
   459  func (b *Local) StatePaths(name string) (stateIn, stateOut, backupOut string) {
   460  	statePath := b.OverrideStatePath
   461  	stateOutPath := b.OverrideStateOutPath
   462  	backupPath := b.OverrideStateBackupPath
   463  
   464  	isDefault := name == backend.DefaultStateName || name == ""
   465  
   466  	baseDir := ""
   467  	if !isDefault {
   468  		baseDir = filepath.Join(b.stateWorkspaceDir(), name)
   469  	}
   470  
   471  	if statePath == "" {
   472  		if isDefault {
   473  			statePath = b.StatePath // s.StatePath applies only to the default workspace, since StateWorkspaceDir is used otherwise
   474  		}
   475  		if statePath == "" {
   476  			statePath = filepath.Join(baseDir, DefaultStateFilename)
   477  		}
   478  	}
   479  	if stateOutPath == "" {
   480  		stateOutPath = statePath
   481  	}
   482  	if backupPath == "" {
   483  		backupPath = b.StateBackupPath
   484  	}
   485  	switch backupPath {
   486  	case "-":
   487  		backupPath = ""
   488  	case "":
   489  		backupPath = stateOutPath + DefaultBackupExtension
   490  	}
   491  
   492  	return statePath, stateOutPath, backupPath
   493  }
   494  
   495  // PathsConflictWith returns true if any state path used by a workspace in
   496  // the receiver is the same as any state path used by the other given
   497  // local backend instance.
   498  //
   499  // This should be used when "migrating" from one local backend configuration to
   500  // another in order to avoid deleting the "old" state snapshots if they are
   501  // in the same files as the "new" state snapshots.
   502  func (b *Local) PathsConflictWith(other *Local) bool {
   503  	otherPaths := map[string]struct{}{}
   504  	otherWorkspaces, err := other.Workspaces()
   505  	if err != nil {
   506  		// If we can't enumerate the workspaces then we'll conservatively
   507  		// assume that paths _do_ overlap, since we can't be certain.
   508  		return true
   509  	}
   510  	for _, name := range otherWorkspaces {
   511  		p, _, _ := other.StatePaths(name)
   512  		otherPaths[p] = struct{}{}
   513  	}
   514  
   515  	ourWorkspaces, err := other.Workspaces()
   516  	if err != nil {
   517  		// If we can't enumerate the workspaces then we'll conservatively
   518  		// assume that paths _do_ overlap, since we can't be certain.
   519  		return true
   520  	}
   521  
   522  	for _, name := range ourWorkspaces {
   523  		p, _, _ := b.StatePaths(name)
   524  		if _, exists := otherPaths[p]; exists {
   525  			return true
   526  		}
   527  	}
   528  	return false
   529  }
   530  
   531  // this only ensures that the named directory exists
   532  func (b *Local) createState(name string) error {
   533  	if name == backend.DefaultStateName {
   534  		return nil
   535  	}
   536  
   537  	stateDir := filepath.Join(b.stateWorkspaceDir(), name)
   538  	s, err := os.Stat(stateDir)
   539  	if err == nil && s.IsDir() {
   540  		// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
   541  		// which will catch the other possible errors as well.
   542  		return nil
   543  	}
   544  
   545  	err = os.MkdirAll(stateDir, 0755)
   546  	if err != nil {
   547  		return err
   548  	}
   549  
   550  	return nil
   551  }
   552  
   553  // stateWorkspaceDir returns the directory where state environments are stored.
   554  func (b *Local) stateWorkspaceDir() string {
   555  	if b.StateWorkspaceDir != "" {
   556  		return b.StateWorkspaceDir
   557  	}
   558  
   559  	return DefaultWorkspaceDir
   560  }