From 8a070ddef0659652636fcf570988c33eee19ec6b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jan 2017 20:47:18 -0800 Subject: [PATCH] backend: introduce the backend set of interfaces Backends are a mechanism that allow abstracting the behavior of Terraform CLI from the actual core. This allows us to slip in special behavior such as state loading, remote operations, etc. --- backend/backend.go | 127 ++++++++++++++++++++++++++++++++ backend/cli.go | 70 ++++++++++++++++++ backend/nil.go | 31 ++++++++ backend/nil_test.go | 9 +++ backend/operation_type.go | 14 ++++ backend/operationtype_string.go | 16 ++++ backend/testing.go | 35 +++++++++ 7 files changed, 302 insertions(+) create mode 100644 backend/backend.go create mode 100644 backend/cli.go create mode 100644 backend/nil.go create mode 100644 backend/nil_test.go create mode 100644 backend/operation_type.go create mode 100644 backend/operationtype_string.go create mode 100644 backend/testing.go diff --git a/backend/backend.go b/backend/backend.go new file mode 100644 index 000000000..16b8a2ba4 --- /dev/null +++ b/backend/backend.go @@ -0,0 +1,127 @@ +// 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" + + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/terraform" +) + +// Backend is the minimal interface that must be implemented to enable Terraform. +type Backend interface { + // Ask for input and configure the backend. Similar to + // terraform.ResourceProvider. + Input(terraform.UIInput, *terraform.ResourceConfig) (*terraform.ResourceConfig, error) + Validate(*terraform.ResourceConfig) ([]string, []error) + Configure(*terraform.ResourceConfig) error + + // State returns the current state for this environment. This state may + // not be loaded locally: the proper APIs should be called on state.State + // to load the state. + State() (state.State, error) +} + +// Enhanced implements additional behavior on top of a normal backend. +// +// Enhanced backends allow customizing the behavior of Terraform operations. +// This allows Terraform to potentially run operations remotely, load +// configurations from external sources, etc. +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. + 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 { + // Context returns a runnable terraform Context. The operation parameter + // doesn't need a Type set but it needs other options set such as Module. + Context(*Operation) (*terraform.Context, state.State, error) +} + +// 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 *terraform.BackendState + + // Module settings specify the root module to use for operations. + Module *module.Tree + + // 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. + Plan *terraform.Plan + + // The options below are more self-explanatory and affect the runtime + // behavior of the operation. + Destroy bool + Targets []string + Variables map[string]interface{} + + // Input/output/control options. + UIIn terraform.UIInput + UIOut terraform.UIOutput +} + +// RunningOperation is the result of starting an operation. +type RunningOperation struct { + // Context should be used to track Done and Err for errors. + // + // For implementers of a backend, this context should not wrap the + // passed in context. Otherwise, canceling 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 + + // Err is the error of the operation. This is populated after + // the operation has completed. + Err error + + // 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 *terraform.State +} diff --git a/backend/cli.go b/backend/cli.go new file mode 100644 index 000000000..b23616629 --- /dev/null +++ b/backend/cli.go @@ -0,0 +1,70 @@ +package backend + +import ( + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" + "github.com/mitchellh/colorstring" +) + +// CLI is an optional interface that can be implemented to be initialized +// with information from the Terraform CLI. If this is implemented, this +// initialization function will be called with data to help interact better +// with a CLI. +// +// This interface was created to improve backend interaction with the +// official Terraform CLI while making it optional for API users to have +// to provide full CLI interaction to every backend. +// +// If you're implementing a Backend, it is acceptable to require CLI +// initialization. In this case, your backend should be coded to error +// on other methods (such as State, Operation) if CLI initialization was not +// done with all required fields. +type CLI interface { + Backend + + // CLIIinit is called once with options. The options passed to this + // function may not be modified after calling this since they can be + // read/written at any time by the Backend implementation. + CLIIinit(*CLIOpts) error +} + +// CLIOpts are the options passed into CLIInit for the CLI interface. +// +// These options represent the functionality the CLI exposes and often +// maps to meta-flags available on every CLI (such as -input). +// +// When implementing a backend, it isn't expected that every option applies. +// Your backend should be documented clearly to explain to end users what +// options have an affect and what won't. In some cases, it may even make sense +// to error in your backend when an option is set so that users don't make +// a critically incorrect assumption about behavior. +type CLIOpts struct { + // CLI and Colorize control the CLI output. If CLI is nil then no CLI + // output will be done. If CLIColor is nil then no coloring will be done. + CLI cli.Ui + CLIColor *colorstring.Colorize + + // StatePath is the local path where state is read from. + // + // StateOutPath is the local path where the state will be written. + // If this is empty, it will default to StatePath. + // + // StateBackupPath is the local path where a backup file will be written. + // If this is empty, no backup will be taken. + StatePath string + StateOutPath string + StateBackupPath string + + // ContextOpts are the base context options to set when initializing a + // Terraform context. Many of these will be overridden or merged by + // Operation. See Operation for more details. + ContextOpts *terraform.ContextOpts + + // Input will ask for necessary input prior to performing any operations. + // + // Validation will perform validation prior to running an operation. The + // variable naming doesn't match the style of others since we have a func + // Validate. + Input bool + Validation bool +} diff --git a/backend/nil.go b/backend/nil.go new file mode 100644 index 000000000..120ee4123 --- /dev/null +++ b/backend/nil.go @@ -0,0 +1,31 @@ +package backend + +import ( + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/terraform" +) + +// Nil is a no-op implementation of Backend. +// +// This is useful to embed within another struct to implement all of the +// backend interface for testing. +type Nil struct{} + +func (Nil) Input( + ui terraform.UIInput, + c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { + return c, nil +} + +func (Nil) Validate(*terraform.ResourceConfig) ([]string, []error) { + return nil, nil +} + +func (Nil) Configure(*terraform.ResourceConfig) error { + return nil +} + +func (Nil) State() (state.State, error) { + // We have to return a non-nil state to adhere to the interface + return &state.InmemState{}, nil +} diff --git a/backend/nil_test.go b/backend/nil_test.go new file mode 100644 index 000000000..c20dc9012 --- /dev/null +++ b/backend/nil_test.go @@ -0,0 +1,9 @@ +package backend + +import ( + "testing" +) + +func TestNil_impl(t *testing.T) { + var _ Backend = new(Nil) +} diff --git a/backend/operation_type.go b/backend/operation_type.go new file mode 100644 index 000000000..1739dc7fc --- /dev/null +++ b/backend/operation_type.go @@ -0,0 +1,14 @@ +package backend + +//go:generate stringer -type=OperationType operation_type.go + +// OperationType is an enum used with Operation to specify the operation +// type to perform for Terraform. +type OperationType uint + +const ( + OperationTypeInvalid OperationType = iota + OperationTypeRefresh + OperationTypePlan + OperationTypeApply +) diff --git a/backend/operationtype_string.go b/backend/operationtype_string.go new file mode 100644 index 000000000..6edadb919 --- /dev/null +++ b/backend/operationtype_string.go @@ -0,0 +1,16 @@ +// Code generated by "stringer -type=OperationType operation_type.go"; DO NOT EDIT + +package backend + +import "fmt" + +const _OperationType_name = "OperationTypeInvalidOperationTypeRefreshOperationTypePlanOperationTypeApply" + +var _OperationType_index = [...]uint8{0, 20, 40, 57, 75} + +func (i OperationType) String() string { + if i >= OperationType(len(_OperationType_index)-1) { + return fmt.Sprintf("OperationType(%d)", i) + } + return _OperationType_name[_OperationType_index[i]:_OperationType_index[i+1]] +} diff --git a/backend/testing.go b/backend/testing.go new file mode 100644 index 000000000..09c80fd18 --- /dev/null +++ b/backend/testing.go @@ -0,0 +1,35 @@ +package backend + +import ( + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" +) + +// TestBackendConfig validates and configures the backend with the +// given configuration. +func TestBackendConfig(t *testing.T, b Backend, c map[string]interface{}) Backend { + // Get the proper config structure + rc, err := config.NewRawConfig(c) + if err != nil { + t.Fatalf("bad: %s", err) + } + conf := terraform.NewResourceConfig(rc) + + // Validate + warns, errs := b.Validate(conf) + if len(warns) > 0 { + t.Fatalf("warnings: %s", warns) + } + if len(errs) > 0 { + t.Fatalf("errors: %s", errs) + } + + // Configure + if err := b.Configure(conf); err != nil { + t.Fatalf("err: %s", err) + } + + return b +}