Skip to content

Commit

Permalink
backend: introduce the backend set of interfaces
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mitchellh committed Jan 26, 2017
1 parent 7b34210 commit 8a070dd
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 0 deletions.
127 changes: 127 additions & 0 deletions backend/backend.go
Original file line number Diff line number Diff line change
@@ -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 changes: 70 additions & 0 deletions backend/cli.go
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 0 deletions backend/nil.go
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions backend/nil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package backend

import (
"testing"
)

func TestNil_impl(t *testing.T) {
var _ Backend = new(Nil)
}
14 changes: 14 additions & 0 deletions backend/operation_type.go
Original file line number Diff line number Diff line change
@@ -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
)
16 changes: 16 additions & 0 deletions backend/operationtype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions backend/testing.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 8a070dd

Please sign in to comment.