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.
This commit is contained in:
Mitchell Hashimoto 2017-01-18 20:47:18 -08:00
parent 7b342100d0
commit 8a070ddef0
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
7 changed files with 302 additions and 0 deletions

127
backend/backend.go Normal file
View File

@ -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
}

70
backend/cli.go Normal file
View File

@ -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
}

31
backend/nil.go Normal file
View File

@ -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
}

9
backend/nil_test.go Normal file
View File

@ -0,0 +1,9 @@
package backend
import (
"testing"
)
func TestNil_impl(t *testing.T) {
var _ Backend = new(Nil)
}

14
backend/operation_type.go Normal file
View File

@ -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
)

View File

@ -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]]
}

35
backend/testing.go Normal file
View File

@ -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
}