// Package backend provides interfaces that the CLI uses to interact with // Terraform. A backend provides the abstraction that allows the same CLI // to simultaneously support both local and remote operations for seamlessly // using Terraform in a team environment. package backend import ( "context" "errors" "io/ioutil" "log" "os" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planfile" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/mitchellh/go-homedir" "github.com/zclconf/go-cty/cty" ) // DefaultStateName is the name of the default, initial state that every // backend must have. This state cannot be deleted. const DefaultStateName = "default" var ( // ErrDefaultWorkspaceNotSupported is returned when an operation does not // support using the default workspace, but requires a named workspace to // be selected. ErrDefaultWorkspaceNotSupported = errors.New("default workspace not supported\n" + "You can create a new workspace with the \"workspace new\" command.") // ErrWorkspacesNotSupported is an error returned when a caller attempts // to perform an operation on a workspace other than "default" for a // backend that doesn't support multiple workspaces. // // The caller can detect this to do special fallback behavior or produce // a specific, helpful error message. ErrWorkspacesNotSupported = errors.New("workspaces not supported") ) // InitFn is used to initialize a new backend. type InitFn func() Backend // Backend is the minimal interface that must be implemented to enable Terraform. type Backend interface { // ConfigSchema returns a description of the expected configuration // structure for the receiving backend. // // This method does not have any side-effects for the backend and can // be safely used before configuring. ConfigSchema() *configschema.Block // PrepareConfig checks the validity of the values in the given // configuration, and inserts any missing defaults, assuming that its // structure has already been validated per the schema returned by // ConfigSchema. // // This method does not have any side-effects for the backend and can // be safely used before configuring. It also does not consult any // external data such as environment variables, disk files, etc. Validation // that requires such external data should be deferred until the // Configure call. // // If error diagnostics are returned then the configuration is not valid // and must not subsequently be passed to the Configure method. // // This method may return configuration-contextual diagnostics such // as tfdiags.AttributeValue, and so the caller should provide the // necessary context via the diags.InConfigBody method before returning // diagnostics to the user. PrepareConfig(cty.Value) (cty.Value, tfdiags.Diagnostics) // Configure uses the provided configuration to set configuration fields // within the backend. // // The given configuration is assumed to have already been validated // against the schema returned by ConfigSchema and passed validation // via PrepareConfig. // // This method may be called only once per backend instance, and must be // called before all other methods except where otherwise stated. // // If error diagnostics are returned, the internal state of the instance // is undefined and no other methods may be called. Configure(cty.Value) tfdiags.Diagnostics // StateMgr returns the state manager for the given workspace name. // // If the returned state manager also implements statemgr.Locker then // it's the caller's responsibility to call Lock and Unlock as appropriate. // // If the named workspace doesn't exist, or if it has no state, it will // be created either immediately on this call or the first time // PersistState is called, depending on the state manager implementation. StateMgr(workspace string) (statemgr.Full, error) // DeleteWorkspace removes the workspace with the given name if it exists. // // DeleteWorkspace cannot prevent deleting a state that is in use. It is // the responsibility of the caller to hold a Lock for the state manager // belonging to this workspace before calling this method. DeleteWorkspace(name string) error // States returns a list of the names of all of the workspaces that exist // in this backend. Workspaces() ([]string, error) } // Enhanced implements additional behavior on top of a normal backend. // // 'Enhanced' backends are an implementation detail only, and are no longer reflected as an external // 'feature' of backends. In other words, backends refer to plugins for remote state snapshot // storage only, and the Enhanced interface here is a necessary vestige of the 'local' and // remote/cloud backends only. type Enhanced interface { Backend // Operation performs a Terraform operation such as refresh, plan, apply. // It is up to the implementation to determine what "performing" means. // This DOES NOT BLOCK. The context returned as part of RunningOperation // should be used to block for completion. // If the state used in the operation can be locked, it is the // responsibility of the Backend to lock the state for the duration of the // running operation. Operation(context.Context, *Operation) (*RunningOperation, error) } // Local implements additional behavior on a Backend that allows local // operations in addition to remote operations. // // This enables more behaviors of Terraform that require more data such // as `console`, `import`, `graph`. These require direct access to // configurations, variables, and more. Not all backends may support this // so we separate it out into its own optional interface. type Local interface { // LocalRun uses information in the Operation to prepare a set of objects // needed to start running that operation. // // The operation doesn't need a Type set, but it needs various other // options set. This is a rather odd API that tries to treat all // operations as the same when they really aren't; see the local and remote // backend's implementations of this to understand what this actually // does, because this operation has no well-defined contract aside from // "whatever it already does". LocalRun(*Operation) (*LocalRun, statemgr.Full, tfdiags.Diagnostics) } // LocalRun represents the assortment of objects that we can collect or // calculate from an Operation object, which we can then use for local // operations. // // The operation methods on terraform.Context (Plan, Apply, Import, etc) each // generate new artifacts which supersede parts of the LocalRun object that // started the operation, so callers should be careful to use those subsequent // artifacts instead of the fields of LocalRun where appropriate. The LocalRun // data intentionally doesn't update as a result of calling methods on Context, // in order to make data flow explicit. // // This type is a weird architectural wart resulting from the overly-general // way our backend API models operations, whereby we behave as if all // Terraform operations have the same inputs and outputs even though they // are actually all rather different. The exact meaning of the fields in // this type therefore vary depending on which OperationType was passed to // Local.Context in order to create an object of this type. type LocalRun struct { // Core is an already-initialized Terraform Core context, ready to be // used to run operations such as Plan and Apply. Core *terraform.Context // Config is the configuration we're working with, which typically comes // from either config files directly on local disk (when we're creating // a plan, or similar) or from a snapshot embedded in a plan file // (when we're applying a saved plan). Config *configs.Config // InputState is the state that should be used for whatever is the first // method call to a context created with CoreOpts. When creating a plan // this will be the previous run state, but when applying a saved plan // this will be the prior state recorded in that plan. InputState *states.State // PlanOpts are options to pass to a Plan or Plan-like operation. // // This is nil when we're applying a saved plan, because the plan itself // contains enough information about its options to apply it. PlanOpts *terraform.PlanOpts // Plan is a plan loaded from a saved plan file, if our operation is to // apply that saved plan. // // This is nil when we're not applying a saved plan. Plan *plans.Plan } // An operation represents an operation for Terraform to execute. // // Note that not all fields are supported by all backends and can result // in an error if set. All backend implementations should show user-friendly // errors explaining any incorrectly set values. For example, the local // backend doesn't support a PlanId being set. // // The operation options are purposely designed to have maximal compatibility // between Terraform and Terraform Servers (a commercial product offered by // HashiCorp). Therefore, it isn't expected that other implementation support // every possible option. The struct here is generalized in order to allow // even partial implementations to exist in the open, without walling off // remote functionality 100% behind a commercial wall. Anyone can implement // against this interface and have Terraform interact with it just as it // would with HashiCorp-provided Terraform Servers. type Operation struct { // Type is the operation to perform. Type OperationType // PlanId is an opaque value that backends can use to execute a specific // plan for an apply operation. // // PlanOutBackend is the backend to store with the plan. This is the // backend that will be used when applying the plan. PlanId string PlanRefresh bool // PlanRefresh will do a refresh before a plan PlanOutPath string // PlanOutPath is the path to save the plan PlanOutBackend *plans.Backend // ConfigDir is the path to the directory containing the configuration's // root module. ConfigDir string // ConfigLoader is a configuration loader that can be used to load // configuration from ConfigDir. ConfigLoader *configload.Loader // DependencyLocks represents the locked dependencies associated with // the configuration directory given in ConfigDir. // // Note that if field PlanFile is set then the plan file should contain // its own dependency locks. The backend is responsible for correctly // selecting between these two sets of locks depending on whether it // will be using ConfigDir or PlanFile to get the configuration for // this operation. DependencyLocks *depsfile.Locks // Hooks can be used to perform actions triggered by various events during // the operation's lifecycle. Hooks []terraform.Hook // Plan is a plan that was passed as an argument. This is valid for // plan and apply arguments but may not work for all backends. PlanFile *planfile.Reader // The options below are more self-explanatory and affect the runtime // behavior of the operation. PlanMode plans.Mode AutoApprove bool Targets []addrs.Targetable ForceReplace []addrs.AbsResourceInstance Variables map[string]UnparsedVariableValue // Some operations use root module variables only opportunistically or // don't need them at all. If this flag is set, the backend must treat // all variables as optional and provide an unknown value for any required // variables that aren't set in order to allow partial evaluation against // the resulting incomplete context. // // This flag is honored only if PlanFile isn't set. If PlanFile is set then // the variables set in the plan are used instead, and they must be valid. AllowUnsetVariables bool // View implements the logic for all UI interactions. View views.Operation // Input/output/control options. UIIn terraform.UIInput UIOut terraform.UIOutput // StateLocker is used to lock the state while providing UI feedback to the // user. This will be replaced by the Backend to update the context. // // If state locking is not necessary, this should be set to a no-op // implementation of clistate.Locker. StateLocker clistate.Locker // Workspace is the name of the workspace that this operation should run // in, which controls which named state is used. Workspace string } // HasConfig returns true if and only if the operation has a ConfigDir value // that refers to a directory containing at least one Terraform configuration // file. func (o *Operation) HasConfig() bool { return o.ConfigLoader.IsConfigDir(o.ConfigDir) } // Config loads the configuration that the operation applies to, using the // ConfigDir and ConfigLoader fields within the receiving operation. func (o *Operation) Config() (*configs.Config, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics config, hclDiags := o.ConfigLoader.LoadConfig(o.ConfigDir) diags = diags.Append(hclDiags) return config, diags } // ReportResult is a helper for the common chore of setting the status of // a running operation and showing any diagnostics produced during that // operation. // // If the given diagnostics contains errors then the operation's result // will be set to backend.OperationFailure. It will be set to // backend.OperationSuccess otherwise. It will then use o.View.Diagnostics // to show the given diagnostics before returning. // // Callers should feel free to do each of these operations separately in // more complex cases where e.g. diagnostics are interleaved with other // output, but terminating immediately after reporting error diagnostics is // common and can be expressed concisely via this method. func (o *Operation) ReportResult(op *RunningOperation, diags tfdiags.Diagnostics) { if diags.HasErrors() { op.Result = OperationFailure } else { op.Result = OperationSuccess } if o.View != nil { o.View.Diagnostics(diags) } else { // Shouldn't generally happen, but if it does then we'll at least // make some noise in the logs to help us spot it. if len(diags) != 0 { log.Printf( "[ERROR] Backend needs to report diagnostics but View is not set:\n%s", diags.ErrWithWarnings(), ) } } } // RunningOperation is the result of starting an operation. type RunningOperation struct { // For implementers of a backend, this context should not wrap the // passed in context. Otherwise, cancelling the parent context will // immediately mark this context as "done" but those aren't the semantics // we want: we want this context to be done only when the operation itself // is fully done. context.Context // Stop requests the operation to complete early, by calling Stop on all // the plugins. If the process needs to terminate immediately, call Cancel. Stop context.CancelFunc // Cancel is the context.CancelFunc associated with the embedded context, // and can be called to terminate the operation early. // Once Cancel is called, the operation should return as soon as possible // to avoid running operations during process exit. Cancel context.CancelFunc // Result is the exit status of the operation, populated only after the // operation has completed. Result OperationResult // PlanEmpty is populated after a Plan operation completes without error // to note whether a plan is empty or has changes. PlanEmpty bool // State is the final state after the operation completed. Persisting // this state is managed by the backend. This should only be read // after the operation completes to avoid read/write races. State *states.State } // OperationResult describes the result status of an operation. type OperationResult int const ( // OperationSuccess indicates that the operation completed as expected. OperationSuccess OperationResult = 0 // OperationFailure indicates that the operation encountered some sort // of error, and thus may have been only partially performed or not // performed at all. OperationFailure OperationResult = 1 ) func (r OperationResult) ExitStatus() int { return int(r) } // If the argument is a path, Read loads it and returns the contents, // otherwise the argument is assumed to be the desired contents and is simply // returned. func ReadPathOrContents(poc string) (string, error) { if len(poc) == 0 { return poc, nil } path := poc if path[0] == '~' { var err error path, err = homedir.Expand(path) if err != nil { return path, err } } if _, err := os.Stat(path); err == nil { contents, err := ioutil.ReadFile(path) if err != nil { return string(contents), err } return string(contents), nil } return poc, nil }