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