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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  // Package backend provides interfaces that the CLI uses to interact with
     5  // Terraform. A backend provides the abstraction that allows the same CLI
     6  // to simultaneously support both local and remote operations for seamlessly
     7  // using Terraform in a team environment.
     8  package backend
     9  
    10  import (
    11  	"context"
    12  	"errors"
    13  	"io/ioutil"
    14  	"log"
    15  	"os"
    16  
    17  	svchost "github.com/hashicorp/terraform-svchost"
    18  	"github.com/mitchellh/go-homedir"
    19  	"github.com/terramate-io/tf/addrs"
    20  	"github.com/terramate-io/tf/command/clistate"
    21  	"github.com/terramate-io/tf/command/views"
    22  	"github.com/terramate-io/tf/configs"
    23  	"github.com/terramate-io/tf/configs/configload"
    24  	"github.com/terramate-io/tf/configs/configschema"
    25  	"github.com/terramate-io/tf/depsfile"
    26  	"github.com/terramate-io/tf/plans"
    27  	"github.com/terramate-io/tf/plans/planfile"
    28  	"github.com/terramate-io/tf/states"
    29  	"github.com/terramate-io/tf/states/statemgr"
    30  	"github.com/terramate-io/tf/terraform"
    31  	"github.com/terramate-io/tf/tfdiags"
    32  	"github.com/zclconf/go-cty/cty"
    33  )
    34  
    35  // DefaultStateName is the name of the default, initial state that every
    36  // backend must have. This state cannot be deleted.
    37  const DefaultStateName = "default"
    38  
    39  var (
    40  	// ErrDefaultWorkspaceNotSupported is returned when an operation does not
    41  	// support using the default workspace, but requires a named workspace to
    42  	// be selected.
    43  	ErrDefaultWorkspaceNotSupported = errors.New("default workspace not supported\n" +
    44  		"You can create a new workspace with the \"workspace new\" command.")
    45  
    46  	// ErrWorkspacesNotSupported is an error returned when a caller attempts
    47  	// to perform an operation on a workspace other than "default" for a
    48  	// backend that doesn't support multiple workspaces.
    49  	//
    50  	// The caller can detect this to do special fallback behavior or produce
    51  	// a specific, helpful error message.
    52  	ErrWorkspacesNotSupported = errors.New("workspaces not supported")
    53  )
    54  
    55  // InitFn is used to initialize a new backend.
    56  type InitFn func() Backend
    57  
    58  // Backend is the minimal interface that must be implemented to enable Terraform.
    59  type Backend interface {
    60  	// ConfigSchema returns a description of the expected configuration
    61  	// structure for the receiving backend.
    62  	//
    63  	// This method does not have any side-effects for the backend and can
    64  	// be safely used before configuring.
    65  	ConfigSchema() *configschema.Block
    66  
    67  	// PrepareConfig checks the validity of the values in the given
    68  	// configuration, and inserts any missing defaults, assuming that its
    69  	// structure has already been validated per the schema returned by
    70  	// ConfigSchema.
    71  	//
    72  	// This method does not have any side-effects for the backend and can
    73  	// be safely used before configuring. It also does not consult any
    74  	// external data such as environment variables, disk files, etc. Validation
    75  	// that requires such external data should be deferred until the
    76  	// Configure call.
    77  	//
    78  	// If error diagnostics are returned then the configuration is not valid
    79  	// and must not subsequently be passed to the Configure method.
    80  	//
    81  	// This method may return configuration-contextual diagnostics such
    82  	// as tfdiags.AttributeValue, and so the caller should provide the
    83  	// necessary context via the diags.InConfigBody method before returning
    84  	// diagnostics to the user.
    85  	PrepareConfig(cty.Value) (cty.Value, tfdiags.Diagnostics)
    86  
    87  	// Configure uses the provided configuration to set configuration fields
    88  	// within the backend.
    89  	//
    90  	// The given configuration is assumed to have already been validated
    91  	// against the schema returned by ConfigSchema and passed validation
    92  	// via PrepareConfig.
    93  	//
    94  	// This method may be called only once per backend instance, and must be
    95  	// called before all other methods except where otherwise stated.
    96  	//
    97  	// If error diagnostics are returned, the internal state of the instance
    98  	// is undefined and no other methods may be called.
    99  	Configure(cty.Value) tfdiags.Diagnostics
   100  
   101  	// StateMgr returns the state manager for the given workspace name.
   102  	//
   103  	// If the returned state manager also implements statemgr.Locker then
   104  	// it's the caller's responsibility to call Lock and Unlock as appropriate.
   105  	//
   106  	// If the named workspace doesn't exist, or if it has no state, it will
   107  	// be created either immediately on this call or the first time
   108  	// PersistState is called, depending on the state manager implementation.
   109  	StateMgr(workspace string) (statemgr.Full, error)
   110  
   111  	// DeleteWorkspace removes the workspace with the given name if it exists.
   112  	//
   113  	// DeleteWorkspace cannot prevent deleting a state that is in use. It is
   114  	// the responsibility of the caller to hold a Lock for the state manager
   115  	// belonging to this workspace before calling this method.
   116  	DeleteWorkspace(name string, force bool) error
   117  
   118  	// States returns a list of the names of all of the workspaces that exist
   119  	// in this backend.
   120  	Workspaces() ([]string, error)
   121  }
   122  
   123  // HostAlias describes a list of aliases that should be used when initializing an
   124  // Enhanced Backend
   125  type HostAlias struct {
   126  	From svchost.Hostname
   127  	To   svchost.Hostname
   128  }
   129  
   130  // Enhanced implements additional behavior on top of a normal backend.
   131  //
   132  // 'Enhanced' backends are an implementation detail only, and are no longer reflected as an external
   133  // 'feature' of backends. In other words, backends refer to plugins for remote state snapshot
   134  // storage only, and the Enhanced interface here is a necessary vestige of the 'local' and
   135  // remote/cloud backends only.
   136  type Enhanced interface {
   137  	Backend
   138  
   139  	// Operation performs a Terraform operation such as refresh, plan, apply.
   140  	// It is up to the implementation to determine what "performing" means.
   141  	// This DOES NOT BLOCK. The context returned as part of RunningOperation
   142  	// should be used to block for completion.
   143  	// If the state used in the operation can be locked, it is the
   144  	// responsibility of the Backend to lock the state for the duration of the
   145  	// running operation.
   146  	Operation(context.Context, *Operation) (*RunningOperation, error)
   147  
   148  	// ServiceDiscoveryAliases returns a mapping of Alias -> Target hosts to
   149  	// configure.
   150  	ServiceDiscoveryAliases() ([]HostAlias, error)
   151  }
   152  
   153  // Local implements additional behavior on a Backend that allows local
   154  // operations in addition to remote operations.
   155  //
   156  // This enables more behaviors of Terraform that require more data such
   157  // as `console`, `import`, `graph`. These require direct access to
   158  // configurations, variables, and more. Not all backends may support this
   159  // so we separate it out into its own optional interface.
   160  type Local interface {
   161  	// LocalRun uses information in the Operation to prepare a set of objects
   162  	// needed to start running that operation.
   163  	//
   164  	// The operation doesn't need a Type set, but it needs various other
   165  	// options set. This is a rather odd API that tries to treat all
   166  	// operations as the same when they really aren't; see the local and remote
   167  	// backend's implementations of this to understand what this actually
   168  	// does, because this operation has no well-defined contract aside from
   169  	// "whatever it already does".
   170  	LocalRun(*Operation) (*LocalRun, statemgr.Full, tfdiags.Diagnostics)
   171  }
   172  
   173  // LocalRun represents the assortment of objects that we can collect or
   174  // calculate from an Operation object, which we can then use for local
   175  // operations.
   176  //
   177  // The operation methods on terraform.Context (Plan, Apply, Import, etc) each
   178  // generate new artifacts which supersede parts of the LocalRun object that
   179  // started the operation, so callers should be careful to use those subsequent
   180  // artifacts instead of the fields of LocalRun where appropriate. The LocalRun
   181  // data intentionally doesn't update as a result of calling methods on Context,
   182  // in order to make data flow explicit.
   183  //
   184  // This type is a weird architectural wart resulting from the overly-general
   185  // way our backend API models operations, whereby we behave as if all
   186  // Terraform operations have the same inputs and outputs even though they
   187  // are actually all rather different. The exact meaning of the fields in
   188  // this type therefore vary depending on which OperationType was passed to
   189  // Local.Context in order to create an object of this type.
   190  type LocalRun struct {
   191  	// Core is an already-initialized Terraform Core context, ready to be
   192  	// used to run operations such as Plan and Apply.
   193  	Core *terraform.Context
   194  
   195  	// Config is the configuration we're working with, which typically comes
   196  	// from either config files directly on local disk (when we're creating
   197  	// a plan, or similar) or from a snapshot embedded in a plan file
   198  	// (when we're applying a saved plan).
   199  	Config *configs.Config
   200  
   201  	// InputState is the state that should be used for whatever is the first
   202  	// method call to a context created with CoreOpts. When creating a plan
   203  	// this will be the previous run state, but when applying a saved plan
   204  	// this will be the prior state recorded in that plan.
   205  	InputState *states.State
   206  
   207  	// PlanOpts are options to pass to a Plan or Plan-like operation.
   208  	//
   209  	// This is nil when we're applying a saved plan, because the plan itself
   210  	// contains enough information about its options to apply it.
   211  	PlanOpts *terraform.PlanOpts
   212  
   213  	// Plan is a plan loaded from a saved plan file, if our operation is to
   214  	// apply that saved plan.
   215  	//
   216  	// This is nil when we're not applying a saved plan.
   217  	Plan *plans.Plan
   218  }
   219  
   220  // An operation represents an operation for Terraform to execute.
   221  //
   222  // Note that not all fields are supported by all backends and can result
   223  // in an error if set. All backend implementations should show user-friendly
   224  // errors explaining any incorrectly set values. For example, the local
   225  // backend doesn't support a PlanId being set.
   226  //
   227  // The operation options are purposely designed to have maximal compatibility
   228  // between Terraform and Terraform Servers (a commercial product offered by
   229  // HashiCorp). Therefore, it isn't expected that other implementation support
   230  // every possible option. The struct here is generalized in order to allow
   231  // even partial implementations to exist in the open, without walling off
   232  // remote functionality 100% behind a commercial wall. Anyone can implement
   233  // against this interface and have Terraform interact with it just as it
   234  // would with HashiCorp-provided Terraform Servers.
   235  type Operation struct {
   236  	// Type is the operation to perform.
   237  	Type OperationType
   238  
   239  	// PlanId is an opaque value that backends can use to execute a specific
   240  	// plan for an apply operation.
   241  	//
   242  	// PlanOutBackend is the backend to store with the plan. This is the
   243  	// backend that will be used when applying the plan.
   244  	PlanId         string
   245  	PlanRefresh    bool   // PlanRefresh will do a refresh before a plan
   246  	PlanOutPath    string // PlanOutPath is the path to save the plan
   247  	PlanOutBackend *plans.Backend
   248  
   249  	// ConfigDir is the path to the directory containing the configuration's
   250  	// root module.
   251  	ConfigDir string
   252  
   253  	// ConfigLoader is a configuration loader that can be used to load
   254  	// configuration from ConfigDir.
   255  	ConfigLoader *configload.Loader
   256  
   257  	// DependencyLocks represents the locked dependencies associated with
   258  	// the configuration directory given in ConfigDir.
   259  	//
   260  	// Note that if field PlanFile is set then the plan file should contain
   261  	// its own dependency locks. The backend is responsible for correctly
   262  	// selecting between these two sets of locks depending on whether it
   263  	// will be using ConfigDir or PlanFile to get the configuration for
   264  	// this operation.
   265  	DependencyLocks *depsfile.Locks
   266  
   267  	// Hooks can be used to perform actions triggered by various events during
   268  	// the operation's lifecycle.
   269  	Hooks []terraform.Hook
   270  
   271  	// Plan is a plan that was passed as an argument. This is valid for
   272  	// plan and apply arguments but may not work for all backends.
   273  	PlanFile *planfile.WrappedPlanFile
   274  
   275  	// The options below are more self-explanatory and affect the runtime
   276  	// behavior of the operation.
   277  	PlanMode     plans.Mode
   278  	AutoApprove  bool
   279  	Targets      []addrs.Targetable
   280  	ForceReplace []addrs.AbsResourceInstance
   281  	Variables    map[string]UnparsedVariableValue
   282  
   283  	// Some operations use root module variables only opportunistically or
   284  	// don't need them at all. If this flag is set, the backend must treat
   285  	// all variables as optional and provide an unknown value for any required
   286  	// variables that aren't set in order to allow partial evaluation against
   287  	// the resulting incomplete context.
   288  	//
   289  	// This flag is honored only if PlanFile isn't set. If PlanFile is set then
   290  	// the variables set in the plan are used instead, and they must be valid.
   291  	AllowUnsetVariables bool
   292  
   293  	// View implements the logic for all UI interactions.
   294  	View views.Operation
   295  
   296  	// Input/output/control options.
   297  	UIIn  terraform.UIInput
   298  	UIOut terraform.UIOutput
   299  
   300  	// StateLocker is used to lock the state while providing UI feedback to the
   301  	// user. This will be replaced by the Backend to update the context.
   302  	//
   303  	// If state locking is not necessary, this should be set to a no-op
   304  	// implementation of clistate.Locker.
   305  	StateLocker clistate.Locker
   306  
   307  	// Workspace is the name of the workspace that this operation should run
   308  	// in, which controls which named state is used.
   309  	Workspace string
   310  
   311  	// GenerateConfigOut tells the operation both that it should generate config
   312  	// for unmatched import targets and where any generated config should be
   313  	// written to.
   314  	GenerateConfigOut string
   315  }
   316  
   317  // HasConfig returns true if and only if the operation has a ConfigDir value
   318  // that refers to a directory containing at least one Terraform configuration
   319  // file.
   320  func (o *Operation) HasConfig() bool {
   321  	return o.ConfigLoader.IsConfigDir(o.ConfigDir)
   322  }
   323  
   324  // Config loads the configuration that the operation applies to, using the
   325  // ConfigDir and ConfigLoader fields within the receiving operation.
   326  func (o *Operation) Config() (*configs.Config, tfdiags.Diagnostics) {
   327  	var diags tfdiags.Diagnostics
   328  	config, hclDiags := o.ConfigLoader.LoadConfig(o.ConfigDir)
   329  	diags = diags.Append(hclDiags)
   330  	return config, diags
   331  }
   332  
   333  // ReportResult is a helper for the common chore of setting the status of
   334  // a running operation and showing any diagnostics produced during that
   335  // operation.
   336  //
   337  // If the given diagnostics contains errors then the operation's result
   338  // will be set to backend.OperationFailure. It will be set to
   339  // backend.OperationSuccess otherwise. It will then use o.View.Diagnostics
   340  // to show the given diagnostics before returning.
   341  //
   342  // Callers should feel free to do each of these operations separately in
   343  // more complex cases where e.g. diagnostics are interleaved with other
   344  // output, but terminating immediately after reporting error diagnostics is
   345  // common and can be expressed concisely via this method.
   346  func (o *Operation) ReportResult(op *RunningOperation, diags tfdiags.Diagnostics) {
   347  	if diags.HasErrors() {
   348  		op.Result = OperationFailure
   349  	} else {
   350  		op.Result = OperationSuccess
   351  	}
   352  	if o.View != nil {
   353  		o.View.Diagnostics(diags)
   354  	} else {
   355  		// Shouldn't generally happen, but if it does then we'll at least
   356  		// make some noise in the logs to help us spot it.
   357  		if len(diags) != 0 {
   358  			log.Printf(
   359  				"[ERROR] Backend needs to report diagnostics but View is not set:\n%s",
   360  				diags.ErrWithWarnings(),
   361  			)
   362  		}
   363  	}
   364  }
   365  
   366  // RunningOperation is the result of starting an operation.
   367  type RunningOperation struct {
   368  	// For implementers of a backend, this context should not wrap the
   369  	// passed in context. Otherwise, cancelling the parent context will
   370  	// immediately mark this context as "done" but those aren't the semantics
   371  	// we want: we want this context to be done only when the operation itself
   372  	// is fully done.
   373  	context.Context
   374  
   375  	// Stop requests the operation to complete early, by calling Stop on all
   376  	// the plugins. If the process needs to terminate immediately, call Cancel.
   377  	Stop context.CancelFunc
   378  
   379  	// Cancel is the context.CancelFunc associated with the embedded context,
   380  	// and can be called to terminate the operation early.
   381  	// Once Cancel is called, the operation should return as soon as possible
   382  	// to avoid running operations during process exit.
   383  	Cancel context.CancelFunc
   384  
   385  	// Result is the exit status of the operation, populated only after the
   386  	// operation has completed.
   387  	Result OperationResult
   388  
   389  	// PlanEmpty is populated after a Plan operation completes to note whether
   390  	// a plan is empty or has changes. This is only used in the CLI to determine
   391  	// the exit status because the plan value is not available at that point.
   392  	PlanEmpty bool
   393  
   394  	// State is the final state after the operation completed. Persisting
   395  	// this state is managed by the backend. This should only be read
   396  	// after the operation completes to avoid read/write races.
   397  	State *states.State
   398  }
   399  
   400  // OperationResult describes the result status of an operation.
   401  type OperationResult int
   402  
   403  const (
   404  	// OperationSuccess indicates that the operation completed as expected.
   405  	OperationSuccess OperationResult = 0
   406  
   407  	// OperationFailure indicates that the operation encountered some sort
   408  	// of error, and thus may have been only partially performed or not
   409  	// performed at all.
   410  	OperationFailure OperationResult = 1
   411  )
   412  
   413  func (r OperationResult) ExitStatus() int {
   414  	return int(r)
   415  }
   416  
   417  // If the argument is a path, Read loads it and returns the contents,
   418  // otherwise the argument is assumed to be the desired contents and is simply
   419  // returned.
   420  func ReadPathOrContents(poc string) (string, error) {
   421  	if len(poc) == 0 {
   422  		return poc, nil
   423  	}
   424  
   425  	path := poc
   426  	if path[0] == '~' {
   427  		var err error
   428  		path, err = homedir.Expand(path)
   429  		if err != nil {
   430  			return path, err
   431  		}
   432  	}
   433  
   434  	if _, err := os.Stat(path); err == nil {
   435  		contents, err := ioutil.ReadFile(path)
   436  		if err != nil {
   437  			return string(contents), err
   438  		}
   439  		return string(contents), nil
   440  	}
   441  
   442  	return poc, nil
   443  }