Skip to content

Commit 873f86a

Browse files
authored
Merge pull request hashicorp#11286 from hashicorp/f-remote-backend
core: introduce "backends" to replace "remote state" (superset) and fix UX
2 parents e3c89da + 09e0727 commit 873f86a

185 files changed

Lines changed: 10205 additions & 2598 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ website/node_modules
2525
*.iml
2626

2727
website/vendor
28+
29+
# Test exclusions
30+
!command/test-fixtures/**/*.tfstate
31+
!command/test-fixtures/**/.terraform/

Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ plugin-dev: generate
3838
mv $(GOPATH)/bin/$(PLUGIN) $(GOPATH)/bin/terraform-$(PLUGIN)
3939

4040
# test runs the unit tests
41-
test: fmtcheck errcheck generate
42-
TF_ACC= go test $(TEST) $(TESTARGS) -timeout=30s -parallel=4
41+
test:# fmtcheck errcheck generate
42+
go test -i $(TEST) || exit 1
43+
echo $(TEST) | \
44+
xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4
4345

4446
# testacc runs acceptance tests
4547
testacc: fmtcheck generate

backend/backend.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Package backend provides interfaces that the CLI uses to interact with
2+
// Terraform. A backend provides the abstraction that allows the same CLI
3+
// to simultaneously support both local and remote operations for seamlessly
4+
// using Terraform in a team environment.
5+
package backend
6+
7+
import (
8+
"context"
9+
10+
"github.com/hashicorp/terraform/config/module"
11+
"github.com/hashicorp/terraform/state"
12+
"github.com/hashicorp/terraform/terraform"
13+
)
14+
15+
// Backend is the minimal interface that must be implemented to enable Terraform.
16+
type Backend interface {
17+
// Ask for input and configure the backend. Similar to
18+
// terraform.ResourceProvider.
19+
Input(terraform.UIInput, *terraform.ResourceConfig) (*terraform.ResourceConfig, error)
20+
Validate(*terraform.ResourceConfig) ([]string, []error)
21+
Configure(*terraform.ResourceConfig) error
22+
23+
// State returns the current state for this environment. This state may
24+
// not be loaded locally: the proper APIs should be called on state.State
25+
// to load the state.
26+
State() (state.State, error)
27+
}
28+
29+
// Enhanced implements additional behavior on top of a normal backend.
30+
//
31+
// Enhanced backends allow customizing the behavior of Terraform operations.
32+
// This allows Terraform to potentially run operations remotely, load
33+
// configurations from external sources, etc.
34+
type Enhanced interface {
35+
Backend
36+
37+
// Operation performs a Terraform operation such as refresh, plan, apply.
38+
// It is up to the implementation to determine what "performing" means.
39+
// This DOES NOT BLOCK. The context returned as part of RunningOperation
40+
// should be used to block for completion.
41+
Operation(context.Context, *Operation) (*RunningOperation, error)
42+
}
43+
44+
// Local implements additional behavior on a Backend that allows local
45+
// operations in addition to remote operations.
46+
//
47+
// This enables more behaviors of Terraform that require more data such
48+
// as `console`, `import`, `graph`. These require direct access to
49+
// configurations, variables, and more. Not all backends may support this
50+
// so we separate it out into its own optional interface.
51+
type Local interface {
52+
// Context returns a runnable terraform Context. The operation parameter
53+
// doesn't need a Type set but it needs other options set such as Module.
54+
Context(*Operation) (*terraform.Context, state.State, error)
55+
}
56+
57+
// An operation represents an operation for Terraform to execute.
58+
//
59+
// Note that not all fields are supported by all backends and can result
60+
// in an error if set. All backend implementations should show user-friendly
61+
// errors explaining any incorrectly set values. For example, the local
62+
// backend doesn't support a PlanId being set.
63+
//
64+
// The operation options are purposely designed to have maximal compatibility
65+
// between Terraform and Terraform Servers (a commercial product offered by
66+
// HashiCorp). Therefore, it isn't expected that other implementation support
67+
// every possible option. The struct here is generalized in order to allow
68+
// even partial implementations to exist in the open, without walling off
69+
// remote functionality 100% behind a commercial wall. Anyone can implement
70+
// against this interface and have Terraform interact with it just as it
71+
// would with HashiCorp-provided Terraform Servers.
72+
type Operation struct {
73+
// Type is the operation to perform.
74+
Type OperationType
75+
76+
// PlanId is an opaque value that backends can use to execute a specific
77+
// plan for an apply operation.
78+
//
79+
// PlanOutBackend is the backend to store with the plan. This is the
80+
// backend that will be used when applying the plan.
81+
PlanId string
82+
PlanRefresh bool // PlanRefresh will do a refresh before a plan
83+
PlanOutPath string // PlanOutPath is the path to save the plan
84+
PlanOutBackend *terraform.BackendState
85+
86+
// Module settings specify the root module to use for operations.
87+
Module *module.Tree
88+
89+
// Plan is a plan that was passed as an argument. This is valid for
90+
// plan and apply arguments but may not work for all backends.
91+
Plan *terraform.Plan
92+
93+
// The options below are more self-explanatory and affect the runtime
94+
// behavior of the operation.
95+
Destroy bool
96+
Targets []string
97+
Variables map[string]interface{}
98+
99+
// Input/output/control options.
100+
UIIn terraform.UIInput
101+
UIOut terraform.UIOutput
102+
}
103+
104+
// RunningOperation is the result of starting an operation.
105+
type RunningOperation struct {
106+
// Context should be used to track Done and Err for errors.
107+
//
108+
// For implementers of a backend, this context should not wrap the
109+
// passed in context. Otherwise, canceling the parent context will
110+
// immediately mark this context as "done" but those aren't the semantics
111+
// we want: we want this context to be done only when the operation itself
112+
// is fully done.
113+
context.Context
114+
115+
// Err is the error of the operation. This is populated after
116+
// the operation has completed.
117+
Err error
118+
119+
// PlanEmpty is populated after a Plan operation completes without error
120+
// to note whether a plan is empty or has changes.
121+
PlanEmpty bool
122+
123+
// State is the final state after the operation completed. Persisting
124+
// this state is managed by the backend. This should only be read
125+
// after the operation completes to avoid read/write races.
126+
State *terraform.State
127+
}

backend/cli.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package backend
2+
3+
import (
4+
"github.com/hashicorp/terraform/terraform"
5+
"github.com/mitchellh/cli"
6+
"github.com/mitchellh/colorstring"
7+
)
8+
9+
// CLI is an optional interface that can be implemented to be initialized
10+
// with information from the Terraform CLI. If this is implemented, this
11+
// initialization function will be called with data to help interact better
12+
// with a CLI.
13+
//
14+
// This interface was created to improve backend interaction with the
15+
// official Terraform CLI while making it optional for API users to have
16+
// to provide full CLI interaction to every backend.
17+
//
18+
// If you're implementing a Backend, it is acceptable to require CLI
19+
// initialization. In this case, your backend should be coded to error
20+
// on other methods (such as State, Operation) if CLI initialization was not
21+
// done with all required fields.
22+
type CLI interface {
23+
Backend
24+
25+
// CLIIinit is called once with options. The options passed to this
26+
// function may not be modified after calling this since they can be
27+
// read/written at any time by the Backend implementation.
28+
CLIIinit(*CLIOpts) error
29+
}
30+
31+
// CLIOpts are the options passed into CLIInit for the CLI interface.
32+
//
33+
// These options represent the functionality the CLI exposes and often
34+
// maps to meta-flags available on every CLI (such as -input).
35+
//
36+
// When implementing a backend, it isn't expected that every option applies.
37+
// Your backend should be documented clearly to explain to end users what
38+
// options have an affect and what won't. In some cases, it may even make sense
39+
// to error in your backend when an option is set so that users don't make
40+
// a critically incorrect assumption about behavior.
41+
type CLIOpts struct {
42+
// CLI and Colorize control the CLI output. If CLI is nil then no CLI
43+
// output will be done. If CLIColor is nil then no coloring will be done.
44+
CLI cli.Ui
45+
CLIColor *colorstring.Colorize
46+
47+
// StatePath is the local path where state is read from.
48+
//
49+
// StateOutPath is the local path where the state will be written.
50+
// If this is empty, it will default to StatePath.
51+
//
52+
// StateBackupPath is the local path where a backup file will be written.
53+
// If this is empty, no backup will be taken.
54+
StatePath string
55+
StateOutPath string
56+
StateBackupPath string
57+
58+
// ContextOpts are the base context options to set when initializing a
59+
// Terraform context. Many of these will be overridden or merged by
60+
// Operation. See Operation for more details.
61+
ContextOpts *terraform.ContextOpts
62+
63+
// Input will ask for necessary input prior to performing any operations.
64+
//
65+
// Validation will perform validation prior to running an operation. The
66+
// variable naming doesn't match the style of others since we have a func
67+
// Validate.
68+
Input bool
69+
Validation bool
70+
}

backend/legacy/backend.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package legacy
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/terraform/state"
7+
"github.com/hashicorp/terraform/state/remote"
8+
"github.com/hashicorp/terraform/terraform"
9+
"github.com/mitchellh/mapstructure"
10+
)
11+
12+
// Backend is an implementation of backend.Backend for legacy remote state
13+
// clients.
14+
type Backend struct {
15+
// Type is the type of remote state client to support
16+
Type string
17+
18+
// client is set after Configure is called and client is initialized.
19+
client remote.Client
20+
}
21+
22+
func (b *Backend) Input(
23+
ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
24+
// Return the config as-is, legacy doesn't support input
25+
return c, nil
26+
}
27+
28+
func (b *Backend) Validate(*terraform.ResourceConfig) ([]string, []error) {
29+
// No validation was supported for old clients
30+
return nil, nil
31+
}
32+
33+
func (b *Backend) Configure(c *terraform.ResourceConfig) error {
34+
// Legacy remote state was only map[string]string config
35+
var conf map[string]string
36+
if err := mapstructure.Decode(c.Raw, &conf); err != nil {
37+
return fmt.Errorf(
38+
"Failed to decode %q configuration: %s\n\n"+
39+
"This backend expects all configuration keys and values to be\n"+
40+
"strings. Please verify your configuration and try again.",
41+
b.Type, err)
42+
}
43+
44+
client, err := remote.NewClient(b.Type, conf)
45+
if err != nil {
46+
return fmt.Errorf(
47+
"Failed to configure remote backend %q: %s",
48+
b.Type, err)
49+
}
50+
51+
// Set our client
52+
b.client = client
53+
return nil
54+
}
55+
56+
func (b *Backend) State() (state.State, error) {
57+
if b.client == nil {
58+
panic("State called with nil remote state client")
59+
}
60+
61+
return &remote.State{Client: b.client}, nil
62+
}

backend/legacy/backend_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package legacy
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/hashicorp/terraform/backend"
10+
"github.com/hashicorp/terraform/config"
11+
"github.com/hashicorp/terraform/state"
12+
"github.com/hashicorp/terraform/terraform"
13+
)
14+
15+
func TestBackend_impl(t *testing.T) {
16+
var _ backend.Backend = new(Backend)
17+
}
18+
19+
func TestBackend(t *testing.T) {
20+
td, err := ioutil.TempDir("", "tf")
21+
if err != nil {
22+
t.Fatalf("err: %s", err)
23+
}
24+
defer os.RemoveAll(td)
25+
26+
b := &Backend{Type: "local"}
27+
conf := terraform.NewResourceConfig(config.TestRawConfig(t, map[string]interface{}{
28+
"path": filepath.Join(td, "data"),
29+
}))
30+
31+
// Config
32+
if err := b.Configure(conf); err != nil {
33+
t.Fatalf("err: %s", err)
34+
}
35+
36+
// Grab state
37+
s, err := b.State()
38+
if err != nil {
39+
t.Fatalf("err: %s", err)
40+
}
41+
if s == nil {
42+
t.Fatalf("state is nil")
43+
}
44+
45+
// Test it
46+
s.WriteState(state.TestStateInitial())
47+
s.PersistState()
48+
state.TestState(t, s)
49+
}

backend/legacy/legacy.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Package legacy contains a backend implementation that can be used
2+
// with the legacy remote state clients.
3+
package legacy
4+
5+
import (
6+
"github.com/hashicorp/terraform/backend"
7+
"github.com/hashicorp/terraform/state/remote"
8+
)
9+
10+
// Init updates the backend/init package map of initializers to support
11+
// all the remote state types.
12+
//
13+
// If a type is already in the map, it will not be added. This will allow
14+
// us to slowly convert the legacy types to first-class backends.
15+
func Init(m map[string]func() backend.Backend) {
16+
for k, _ := range remote.BuiltinClients {
17+
if _, ok := m[k]; !ok {
18+
// Copy the "k" value since the variable "k" is reused for
19+
// each key (address doesn't change).
20+
typ := k
21+
22+
// Build the factory function to return a backend of typ
23+
m[k] = func() backend.Backend {
24+
return &Backend{Type: typ}
25+
}
26+
}
27+
}
28+
}

backend/legacy/legacy_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package legacy
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/terraform/backend"
7+
"github.com/hashicorp/terraform/state/remote"
8+
)
9+
10+
func TestInit(t *testing.T) {
11+
m := make(map[string]func() backend.Backend)
12+
Init(m)
13+
14+
for k, _ := range remote.BuiltinClients {
15+
b, ok := m[k]
16+
if !ok {
17+
t.Fatalf("missing: %s", k)
18+
}
19+
20+
if typ := b().(*Backend).Type; typ != k {
21+
t.Fatalf("bad type: %s", typ)
22+
}
23+
}
24+
}
25+
26+
func TestInit_ignoreExisting(t *testing.T) {
27+
m := make(map[string]func() backend.Backend)
28+
m["local"] = nil
29+
Init(m)
30+
31+
if v, ok := m["local"]; !ok || v != nil {
32+
t.Fatalf("bad: %#v", m)
33+
}
34+
}

0 commit comments

Comments
 (0)