Merge pull request #11286 from hashicorp/f-remote-backend

core: introduce "backends" to replace "remote state" (superset) and fix UX
This commit is contained in:
Mitchell Hashimoto 2017-01-26 14:54:52 -08:00 committed by GitHub
commit 873f86aee9
185 changed files with 10241 additions and 2634 deletions

4
.gitignore vendored
View File

@ -25,3 +25,7 @@ website/node_modules
*.iml
website/vendor
# Test exclusions
!command/test-fixtures/**/*.tfstate
!command/test-fixtures/**/.terraform/

View File

@ -38,8 +38,10 @@ plugin-dev: generate
mv $(GOPATH)/bin/$(PLUGIN) $(GOPATH)/bin/terraform-$(PLUGIN)
# test runs the unit tests
test: fmtcheck errcheck generate
TF_ACC= go test $(TEST) $(TESTARGS) -timeout=30s -parallel=4
test:# fmtcheck errcheck generate
go test -i $(TEST) || exit 1
echo $(TEST) | \
xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4
# testacc runs acceptance tests
testacc: fmtcheck generate

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
}

62
backend/legacy/backend.go Normal file
View File

@ -0,0 +1,62 @@
package legacy
import (
"fmt"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
)
// Backend is an implementation of backend.Backend for legacy remote state
// clients.
type Backend struct {
// Type is the type of remote state client to support
Type string
// client is set after Configure is called and client is initialized.
client remote.Client
}
func (b *Backend) Input(
ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
// Return the config as-is, legacy doesn't support input
return c, nil
}
func (b *Backend) Validate(*terraform.ResourceConfig) ([]string, []error) {
// No validation was supported for old clients
return nil, nil
}
func (b *Backend) Configure(c *terraform.ResourceConfig) error {
// Legacy remote state was only map[string]string config
var conf map[string]string
if err := mapstructure.Decode(c.Raw, &conf); err != nil {
return fmt.Errorf(
"Failed to decode %q configuration: %s\n\n"+
"This backend expects all configuration keys and values to be\n"+
"strings. Please verify your configuration and try again.",
b.Type, err)
}
client, err := remote.NewClient(b.Type, conf)
if err != nil {
return fmt.Errorf(
"Failed to configure remote backend %q: %s",
b.Type, err)
}
// Set our client
b.client = client
return nil
}
func (b *Backend) State() (state.State, error) {
if b.client == nil {
panic("State called with nil remote state client")
}
return &remote.State{Client: b.client}, nil
}

View File

@ -0,0 +1,49 @@
package legacy
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
)
func TestBackend_impl(t *testing.T) {
var _ backend.Backend = new(Backend)
}
func TestBackend(t *testing.T) {
td, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(td)
b := &Backend{Type: "local"}
conf := terraform.NewResourceConfig(config.TestRawConfig(t, map[string]interface{}{
"path": filepath.Join(td, "data"),
}))
// Config
if err := b.Configure(conf); err != nil {
t.Fatalf("err: %s", err)
}
// Grab state
s, err := b.State()
if err != nil {
t.Fatalf("err: %s", err)
}
if s == nil {
t.Fatalf("state is nil")
}
// Test it
s.WriteState(state.TestStateInitial())
s.PersistState()
state.TestState(t, s)
}

28
backend/legacy/legacy.go Normal file
View File

@ -0,0 +1,28 @@
// Package legacy contains a backend implementation that can be used
// with the legacy remote state clients.
package legacy
import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state/remote"
)
// Init updates the backend/init package map of initializers to support
// all the remote state types.
//
// If a type is already in the map, it will not be added. This will allow
// us to slowly convert the legacy types to first-class backends.
func Init(m map[string]func() backend.Backend) {
for k, _ := range remote.BuiltinClients {
if _, ok := m[k]; !ok {
// Copy the "k" value since the variable "k" is reused for
// each key (address doesn't change).
typ := k
// Build the factory function to return a backend of typ
m[k] = func() backend.Backend {
return &Backend{Type: typ}
}
}
}
}

View File

@ -0,0 +1,34 @@
package legacy
import (
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state/remote"
)
func TestInit(t *testing.T) {
m := make(map[string]func() backend.Backend)
Init(m)
for k, _ := range remote.BuiltinClients {
b, ok := m[k]
if !ok {
t.Fatalf("missing: %s", k)
}
if typ := b().(*Backend).Type; typ != k {
t.Fatalf("bad type: %s", typ)
}
}
}
func TestInit_ignoreExisting(t *testing.T) {
m := make(map[string]func() backend.Backend)
m["local"] = nil
Init(m)
if v, ok := m["local"]; !ok || v != nil {
t.Fatalf("bad: %#v", m)
}
}

211
backend/local/backend.go Normal file
View File

@ -0,0 +1,211 @@
package local
import (
"context"
"fmt"
"sync"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)
// Local is an implementation of EnhancedBackend that performs all operations
// locally. This is the "default" backend and implements normal Terraform
// behavior as it is well known.
type Local 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
// OpInput will ask for necessary input prior to performing any operations.
//
// OpValidation will perform validation prior to running an operation. The
// variable naming doesn't match the style of others since we have a func
// Validate.
OpInput bool
OpValidation bool
// Backend, if non-nil, will use this backend for non-enhanced behavior.
// This allows local behavior with remote state storage. It is a way to
// "upgrade" a non-enhanced backend to an enhanced backend with typical
// behavior.
//
// If this is nil, local performs normal state loading and storage.
Backend backend.Backend
schema *schema.Backend
opLock sync.Mutex
once sync.Once
}
func (b *Local) Input(
ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
b.once.Do(b.init)
f := b.schema.Input
if b.Backend != nil {
f = b.Backend.Input
}
return f(ui, c)
}
func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) {
b.once.Do(b.init)
f := b.schema.Validate
if b.Backend != nil {
f = b.Backend.Validate
}
return f(c)
}
func (b *Local) Configure(c *terraform.ResourceConfig) error {
b.once.Do(b.init)
f := b.schema.Configure
if b.Backend != nil {
f = b.Backend.Configure
}
return f(c)
}
func (b *Local) State() (state.State, error) {
// If we have a backend handling state, defer to that.
if b.Backend != nil {
return b.Backend.State()
}
// Otherwise, we need to load the state.
var s state.State = &state.LocalState{
Path: b.StatePath,
PathOut: b.StateOutPath,
}
// Load the state as a sanity check
if err := s.RefreshState(); err != nil {
return nil, errwrap.Wrapf("Error reading local state: {{err}}", err)
}
// If we are backing up the state, wrap it
if path := b.StateBackupPath; path != "" {
s = &state.BackupState{
Real: s,
Path: path,
}
}
return s, nil
}
// Operation implements backend.Enhanced
//
// This will initialize an in-memory terraform.Context to perform the
// operation within this process.
//
// The given operation parameter will be merged with the ContextOpts on
// the structure with the following rules. If a rule isn't specified and the
// name conflicts, assume that the field is overwritten if set.
func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
// Determine the function to call for our operation
var f func(context.Context, *backend.Operation, *backend.RunningOperation)
switch op.Type {
case backend.OperationTypeRefresh:
f = b.opRefresh
case backend.OperationTypePlan:
f = b.opPlan
case backend.OperationTypeApply:
f = b.opApply
default:
return nil, fmt.Errorf(
"Unsupported operation type: %s\n\n"+
"This is a bug in Terraform and should be reported. The local backend\n"+
"is built-in to Terraform and should always support all operations.",
op.Type)
}
// Lock
b.opLock.Lock()
// Build our running operation
runningCtx, runningCtxCancel := context.WithCancel(context.Background())
runningOp := &backend.RunningOperation{Context: runningCtx}
// Do it
go func() {
defer b.opLock.Unlock()
defer runningCtxCancel()
f(ctx, op, runningOp)
}()
// Return
return runningOp, nil
}
// Colorize returns the Colorize structure that can be used for colorizing
// output. This is gauranteed to always return a non-nil value and so is useful
// as a helper to wrap any potentially colored strings.
func (b *Local) Colorize() *colorstring.Colorize {
if b.CLIColor != nil {
return b.CLIColor
}
return &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true,
}
}
func (b *Local) init() {
b.schema = &schema.Backend{
Schema: map[string]*schema.Schema{
"path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
ConfigureFunc: b.schemaConfigure,
}
}
func (b *Local) schemaConfigure(ctx context.Context) error {
d := schema.FromContextBackendConfig(ctx)
// Set the path if it is set
pathRaw, ok := d.GetOk("path")
if ok {
path := pathRaw.(string)
if path == "" {
return fmt.Errorf("configured path is empty")
}
b.StatePath = path
}
return nil
}

View File

@ -0,0 +1,149 @@
package local
import (
"context"
"fmt"
"log"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/terraform"
)
func (b *Local) opApply(
ctx context.Context,
op *backend.Operation,
runningOp *backend.RunningOperation) {
log.Printf("[INFO] backend/local: starting Apply operation")
// Setup our count hook that keeps track of resource changes
countHook := new(CountHook)
stateHook := new(StateHook)
if b.ContextOpts == nil {
b.ContextOpts = new(terraform.ContextOpts)
}
old := b.ContextOpts.Hooks
defer func() { b.ContextOpts.Hooks = old }()
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook)
// Get our context
tfCtx, state, err := b.context(op)
if err != nil {
runningOp.Err = err
return
}
// Setup the state
runningOp.State = tfCtx.State()
// If we weren't given a plan, then we refresh/plan
if op.Plan == nil {
// If we're refreshing before apply, perform that
if op.PlanRefresh {
log.Printf("[INFO] backend/local: apply calling Refresh")
_, err := tfCtx.Refresh()
if err != nil {
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
return
}
}
// Perform the plan
log.Printf("[INFO] backend/local: apply calling Plan")
if _, err := tfCtx.Plan(); err != nil {
runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err)
return
}
}
// Setup our hook for continuous state updates
stateHook.State = state
// Start the apply in a goroutine so that we can be interrupted.
var applyState *terraform.State
var applyErr error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
applyState, applyErr = tfCtx.Apply()
/*
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "apply operation:"))
}
*/
}()
// Wait for the apply to finish or for us to be interrupted so
// we can handle it properly.
err = nil
select {
case <-ctx.Done():
if b.CLI != nil {
b.CLI.Output("Interrupt received. Gracefully shutting down...")
}
// Stop execution
go tfCtx.Stop()
// Wait for completion still
<-doneCh
case <-doneCh:
}
// Store the final state
runningOp.State = applyState
// Persist the state
if err := state.WriteState(applyState); err != nil {
runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
return
}
if err := state.PersistState(); err != nil {
runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
return
}
if applyErr != nil {
runningOp.Err = fmt.Errorf(
"Error applying plan:\n\n"+
"%s\n\n"+
"Terraform does not automatically rollback in the face of errors.\n"+
"Instead, your Terraform state file has been partially updated with\n"+
"any resources that successfully completed. Please address the error\n"+
"above and apply again to incrementally change your infrastructure.",
multierror.Flatten(applyErr))
return
}
// If we have a UI, output the results
if b.CLI != nil {
if op.Destroy {
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Destroy complete! Resources: %d destroyed.",
countHook.Removed)))
} else {
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Apply complete! Resources: %d added, %d changed, %d destroyed.",
countHook.Added,
countHook.Changed,
countHook.Removed)))
}
if countHook.Added > 0 || countHook.Changed > 0 {
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
"[reset]\n"+
"The state of your infrastructure has been saved to the path\n"+
"below. This state is required to modify and destroy your\n"+
"infrastructure, so keep it safe. To inspect the complete state\n"+
"use the `terraform show` command.\n\n"+
"State path: %s",
b.StateOutPath)))
}
}
}

View File

@ -0,0 +1,131 @@
package local
import (
"context"
"fmt"
"sync"
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
)
func TestLocal_applyBasic(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test")
p.ApplyReturn = &terraform.InstanceState{ID: "yes"}
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
defer modCleanup()
op := testOperationApply()
op.Module = mod
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
}
if p.RefreshCalled {
t.Fatal("refresh should not be called")
}
if !p.DiffCalled {
t.Fatal("diff should be called")
}
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
checkState(t, b.StateOutPath, `
test_instance.foo:
ID = yes
`)
}
func TestLocal_applyError(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test")
var lock sync.Mutex
errored := false
p.ApplyFn = func(
info *terraform.InstanceInfo,
s *terraform.InstanceState,
d *terraform.InstanceDiff) (*terraform.InstanceState, error) {
lock.Lock()
defer lock.Unlock()
if !errored && info.Id == "test_instance.bar" {
errored = true
return nil, fmt.Errorf("error")
}
return &terraform.InstanceState{ID: "foo"}, nil
}
p.DiffFn = func(
*terraform.InstanceInfo,
*terraform.InstanceState,
*terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
return &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ami": &terraform.ResourceAttrDiff{
New: "bar",
},
},
}, nil
}
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-error")
defer modCleanup()
op := testOperationApply()
op.Module = mod
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err == nil {
t.Fatal("should error")
}
checkState(t, b.StateOutPath, `
test_instance.foo:
ID = foo
`)
}
func testOperationApply() *backend.Operation {
return &backend.Operation{
Type: backend.OperationTypeApply,
}
}
// testApplyState is just a common state that we use for testing refresh.
func testApplyState() *terraform.State {
return &terraform.State{
Version: 2,
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
},
},
},
},
},
}
}

View File

@ -0,0 +1,85 @@
package local
import (
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
)
// backend.Local implementation.
func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State, error) {
// Make sure the type is invalid. We use this as a way to know not
// to ask for input/validate.
op.Type = backend.OperationTypeInvalid
return b.context(op)
}
func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, error) {
// Get the state.
s, err := b.State()
if err != nil {
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
}
if err := s.RefreshState(); err != nil {
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
}
// Initialize our context options
var opts terraform.ContextOpts
if v := b.ContextOpts; v != nil {
opts = *v
}
// Copy set options from the operation
opts.Destroy = op.Destroy
opts.Module = op.Module
opts.Targets = op.Targets
opts.UIInput = op.UIIn
if op.Variables != nil {
opts.Variables = op.Variables
}
// Load our state
opts.State = s.State()
// Build the context
var tfCtx *terraform.Context
if op.Plan != nil {
tfCtx, err = op.Plan.Context(&opts)
} else {
tfCtx, err = terraform.NewContext(&opts)
}
if err != nil {
return nil, nil, err
}
// If we have an operation, then we automatically do the input/validate
// here since every option requires this.
if op.Type != backend.OperationTypeInvalid {
// If input asking is enabled, then do that
if op.Plan == nil && b.OpInput {
mode := terraform.InputModeProvider
mode |= terraform.InputModeVar
mode |= terraform.InputModeVarUnset
if err := tfCtx.Input(mode); err != nil {
return nil, nil, errwrap.Wrapf("Error asking for user input: {{err}}", err)
}
}
// If validation is enabled, validate
if b.OpValidation {
// We ignore warnings here on purpose. We expect users to be listening
// to the terraform.Hook called after a validation.
_, es := tfCtx.Validate()
if len(es) > 0 {
return nil, nil, multierror.Append(nil, es...)
}
}
}
return tfCtx, s, nil
}

View File

@ -0,0 +1,160 @@
package local
import (
"context"
"fmt"
"log"
"os"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/terraform"
)
func (b *Local) opPlan(
ctx context.Context,
op *backend.Operation,
runningOp *backend.RunningOperation) {
log.Printf("[INFO] backend/local: starting Plan operation")
if b.CLI != nil && op.Plan != nil {
b.CLI.Output(b.Colorize().Color(
"[reset][bold][yellow]" +
"The plan command received a saved plan file as input. This command\n" +
"will output the saved plan. This will not modify the already-existing\n" +
"plan. If you wish to generate a new plan, please pass in a configuration\n" +
"directory as an argument.\n\n"))
}
// Setup our count hook that keeps track of resource changes
countHook := new(CountHook)
if b.ContextOpts == nil {
b.ContextOpts = new(terraform.ContextOpts)
}
old := b.ContextOpts.Hooks
defer func() { b.ContextOpts.Hooks = old }()
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook)
// Get our context
tfCtx, _, err := b.context(op)
if err != nil {
runningOp.Err = err
return
}
// Setup the state
runningOp.State = tfCtx.State()
// If we're refreshing before plan, perform that
if op.PlanRefresh {
log.Printf("[INFO] backend/local: plan calling Refresh")
if b.CLI != nil {
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planRefreshing) + "\n"))
}
_, err := tfCtx.Refresh()
if err != nil {
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
return
}
}
// Perform the plan
log.Printf("[INFO] backend/local: plan calling Plan")
plan, err := tfCtx.Plan()
if err != nil {
runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err)
return
}
// Record state
runningOp.PlanEmpty = plan.Diff.Empty()
// Save the plan to disk
if path := op.PlanOutPath; path != "" {
// Write the backend if we have one
plan.Backend = op.PlanOutBackend
log.Printf("[INFO] backend/local: writing plan output to: %s", path)
f, err := os.Create(path)
if err == nil {
err = terraform.WritePlan(plan, f)
}
f.Close()
if err != nil {
runningOp.Err = fmt.Errorf("Error writing plan file: %s", err)
return
}
}
// Perform some output tasks if we have a CLI to output to.
if b.CLI != nil {
if plan.Diff.Empty() {
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planNoChanges)))
return
}
if path := op.PlanOutPath; path == "" {
b.CLI.Output(strings.TrimSpace(planHeaderNoOutput) + "\n")
} else {
b.CLI.Output(fmt.Sprintf(
strings.TrimSpace(planHeaderYesOutput)+"\n",
path))
}
b.CLI.Output(format.Plan(&format.PlanOpts{
Plan: plan,
Color: b.Colorize(),
ModuleDepth: -1,
}))
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
"[reset][bold]Plan:[reset] "+
"%d to add, %d to change, %d to destroy.",
countHook.ToAdd+countHook.ToRemoveAndAdd,
countHook.ToChange,
countHook.ToRemove+countHook.ToRemoveAndAdd)))
}
}
const planHeaderNoOutput = `
The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed. Cyan entries are data sources to be read.
Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.
`
const planHeaderYesOutput = `
The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed. Cyan entries are data sources to be read.
Your plan was also saved to the path below. Call the "apply" subcommand
with this plan file and Terraform will exactly execute this execution
plan.
Path: %s
`
const planNoChanges = `
[reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green]
This means that Terraform could not detect any differences between your
configuration and real physical resources that exist. As a result, Terraform
doesn't need to do anything.
`
const planRefreshing = `
[reset][bold]Refreshing Terraform state in-memory prior to plan...[reset]
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
`

View File

@ -0,0 +1,183 @@
package local
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
)
func TestLocal_planBasic(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test")
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
op := testOperationPlan()
op.Module = mod
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
}
if !p.DiffCalled {
t.Fatal("diff should be called")
}
}
func TestLocal_planRefreshFalse(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test")
terraform.TestStateFile(t, b.StatePath, testPlanState())
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
op := testOperationPlan()
op.Module = mod
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
}
if p.RefreshCalled {
t.Fatal("refresh should not be called")
}
if !run.PlanEmpty {
t.Fatal("plan should be empty")
}
}
func TestLocal_planDestroy(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test")
terraform.TestStateFile(t, b.StatePath, testPlanState())
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
outDir := testTempDir(t)
defer os.RemoveAll(outDir)
planPath := filepath.Join(outDir, "plan.tfplan")
op := testOperationPlan()
op.Destroy = true
op.PlanRefresh = true
op.Module = mod
op.PlanOutPath = planPath
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
}
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
if run.PlanEmpty {
t.Fatal("plan should not be empty")
}
plan := testReadPlan(t, planPath)
for _, m := range plan.Diff.Modules {
for _, r := range m.Resources {
if !r.Destroy {
t.Fatalf("bad: %#v", r)
}
}
}
}
func TestLocal_planOutPathNoChange(t *testing.T) {
b := TestLocal(t)
TestLocalProvider(t, b, "test")
terraform.TestStateFile(t, b.StatePath, testPlanState())
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
outDir := testTempDir(t)
defer os.RemoveAll(outDir)
planPath := filepath.Join(outDir, "plan.tfplan")
op := testOperationPlan()
op.Module = mod
op.PlanOutPath = planPath
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
}
plan := testReadPlan(t, planPath)
if !plan.Diff.Empty() {
t.Fatalf("expected empty plan to be written")
}
}
func testOperationPlan() *backend.Operation {
return &backend.Operation{
Type: backend.OperationTypePlan,
}
}
// testPlanState is just a common state that we use for testing refresh.
func testPlanState() *terraform.State {
return &terraform.State{
Version: 2,
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
},
},
},
},
},
}
}
func testReadPlan(t *testing.T, path string) *terraform.Plan {
f, err := os.Open(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
p, err := terraform.ReadPlan(f)
if err != nil {
t.Fatalf("err: %s", err)
}
return p
}

View File

@ -0,0 +1,69 @@
package local
import (
"context"
"fmt"
"os"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/backend"
)
func (b *Local) opRefresh(
ctx context.Context,
op *backend.Operation,
runningOp *backend.RunningOperation) {
// Check if our state exists if we're performing a refresh operation. We
// only do this if we're managing state with this backend.
if b.Backend == nil {
if _, err := os.Stat(b.StatePath); err != nil {
if os.IsNotExist(err) {
runningOp.Err = fmt.Errorf(
"The Terraform state file for your infrastructure does not\n"+
"exist. The 'refresh' command only works and only makes sense\n"+
"when there is existing state that Terraform is managing. Please\n"+
"double-check the value given below and try again. If you\n"+
"haven't created infrastructure with Terraform yet, use the\n"+
"'terraform apply' command.\n\n"+
"Path: %s",
b.StatePath)
return
}
runningOp.Err = fmt.Errorf(
"There was an error reading the Terraform state that is needed\n"+
"for refreshing. The path and error are shown below.\n\n"+
"Path: %s\n\nError: %s",
b.StatePath, err)
return
}
}
// Get our context
tfCtx, state, err := b.context(op)
if err != nil {
runningOp.Err = err
return
}
// Set our state
runningOp.State = state.State()
// Perform operation and write the resulting state to the running op
newState, err := tfCtx.Refresh()
runningOp.State = newState
if err != nil {
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
return
}
// Write and persist the state
if err := state.WriteState(newState); err != nil {
runningOp.Err = errwrap.Wrapf("Error writing state: {{err}}", err)
return
}
if err := state.PersistState(); err != nil {
runningOp.Err = errwrap.Wrapf("Error saving state: {{err}}", err)
return
}
}

View File

@ -0,0 +1,144 @@
package local
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
)
func TestLocal_refresh(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test")
terraform.TestStateFile(t, b.StatePath, testRefreshState())
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh")
defer modCleanup()
op := testOperationRefresh()
op.Module = mod
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
checkState(t, b.StateOutPath, `
test_instance.foo:
ID = yes
`)
}
func TestLocal_refreshInput(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test")
terraform.TestStateFile(t, b.StatePath, testRefreshState())
p.ConfigureFn = func(c *terraform.ResourceConfig) error {
if v, ok := c.Get("value"); !ok || v != "bar" {
return fmt.Errorf("no value set")
}
return nil
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh-var-unset")
defer modCleanup()
// Enable input asking since it is normally disabled by default
b.OpInput = true
b.ContextOpts.UIInput = &terraform.MockUIInput{InputReturnString: "bar"}
op := testOperationRefresh()
op.Module = mod
op.UIIn = b.ContextOpts.UIInput
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
checkState(t, b.StateOutPath, `
test_instance.foo:
ID = yes
`)
}
func TestLocal_refreshValidate(t *testing.T) {
b := TestLocal(t)
p := TestLocalProvider(t, b, "test")
terraform.TestStateFile(t, b.StatePath, testRefreshState())
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh")
defer modCleanup()
// Enable validation
b.OpValidation = true
op := testOperationRefresh()
op.Module = mod
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if !p.ValidateCalled {
t.Fatal("validate should be called")
}
checkState(t, b.StateOutPath, `
test_instance.foo:
ID = yes
`)
}
func testOperationRefresh() *backend.Operation {
return &backend.Operation{
Type: backend.OperationTypeRefresh,
}
}
// testRefreshState is just a common state that we use for testing refresh.
func testRefreshState() *terraform.State {
return &terraform.State{
Version: 2,
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
},
},
},
Outputs: map[string]*terraform.OutputState{},
},
},
}
}

View File

@ -0,0 +1,35 @@
package local
import (
"os"
"strings"
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/terraform"
)
func TestLocal_impl(t *testing.T) {
var _ backend.Enhanced = new(Local)
var _ backend.Local = new(Local)
}
func checkState(t *testing.T, path, expected string) {
// Read the state
f, err := os.Open(path)
if err != nil {
t.Fatalf("err: %s", err)
}
state, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(state.String())
expected = strings.TrimSpace(expected)
if actual != expected {
t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected)
}
}

View File

@ -0,0 +1,16 @@
// Code generated by "stringer -type=countHookAction hook_count_action.go"; DO NOT EDIT
package local
import "fmt"
const _countHookAction_name = "countHookActionAddcountHookActionChangecountHookActionRemove"
var _countHookAction_index = [...]uint8{0, 18, 39, 60}
func (i countHookAction) String() string {
if i >= countHookAction(len(_countHookAction_index)-1) {
return fmt.Sprintf("countHookAction(%d)", i)
}
return _countHookAction_name[_countHookAction_index[i]:_countHookAction_index[i+1]]
}

111
backend/local/hook_count.go Normal file
View File

@ -0,0 +1,111 @@
package local
import (
"strings"
"sync"
"github.com/hashicorp/terraform/terraform"
)
// CountHook is a hook that counts the number of resources
// added, removed, changed during the course of an apply.
type CountHook struct {
Added int
Changed int
Removed int
ToAdd int
ToChange int
ToRemove int
ToRemoveAndAdd int
pending map[string]countHookAction
sync.Mutex
terraform.NilHook
}
func (h *CountHook) Reset() {
h.Lock()
defer h.Unlock()
h.pending = nil
h.Added = 0
h.Changed = 0
h.Removed = 0
}
func (h *CountHook) PreApply(
n *terraform.InstanceInfo,
s *terraform.InstanceState,
d *terraform.InstanceDiff) (terraform.HookAction, error) {
h.Lock()
defer h.Unlock()
if h.pending == nil {
h.pending = make(map[string]countHookAction)
}
action := countHookActionChange
if d.GetDestroy() {
action = countHookActionRemove
} else if s.ID == "" {
action = countHookActionAdd
}
h.pending[n.HumanId()] = action
return terraform.HookActionContinue, nil
}
func (h *CountHook) PostApply(
n *terraform.InstanceInfo,
s *terraform.InstanceState,
e error) (terraform.HookAction, error) {
h.Lock()
defer h.Unlock()
if h.pending != nil {
if a, ok := h.pending[n.HumanId()]; ok {
delete(h.pending, n.HumanId())
if e == nil {
switch a {
case countHookActionAdd:
h.Added += 1
case countHookActionChange:
h.Changed += 1
case countHookActionRemove:
h.Removed += 1
}
}
}
}
return terraform.HookActionContinue, nil
}
func (h *CountHook) PostDiff(
n *terraform.InstanceInfo, d *terraform.InstanceDiff) (
terraform.HookAction, error) {
h.Lock()
defer h.Unlock()
// We don't count anything for data sources
if strings.HasPrefix(n.Id, "data.") {
return terraform.HookActionContinue, nil
}
switch d.ChangeType() {
case terraform.DiffDestroyCreate:
h.ToRemoveAndAdd += 1
case terraform.DiffCreate:
h.ToAdd += 1
case terraform.DiffDestroy:
h.ToRemove += 1
case terraform.DiffUpdate:
h.ToChange += 1
}
return terraform.HookActionContinue, nil
}

View File

@ -0,0 +1,11 @@
package local
//go:generate stringer -type=countHookAction hook_count_action.go
type countHookAction byte
const (
countHookActionAdd countHookAction = iota
countHookActionChange
countHookActionRemove
)

View File

@ -0,0 +1,243 @@
package local
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestCountHook_impl(t *testing.T) {
var _ terraform.Hook = new(CountHook)
}
func TestCountHookPostDiff_DestroyDeposed(t *testing.T) {
h := new(CountHook)
resources := map[string]*terraform.InstanceDiff{
"lorem": &terraform.InstanceDiff{DestroyDeposed: true},
}
n := &terraform.InstanceInfo{} // TODO
for _, d := range resources {
h.PostDiff(n, d)
}
expected := new(CountHook)
expected.ToAdd = 0
expected.ToChange = 0
expected.ToRemoveAndAdd = 0
expected.ToRemove = 1
if !reflect.DeepEqual(expected, h) {
t.Fatalf("Expected %#v, got %#v instead.",
expected, h)
}
}
func TestCountHookPostDiff_DestroyOnly(t *testing.T) {
h := new(CountHook)
resources := map[string]*terraform.InstanceDiff{
"foo": &terraform.InstanceDiff{Destroy: true},
"bar": &terraform.InstanceDiff{Destroy: true},
"lorem": &terraform.InstanceDiff{Destroy: true},
"ipsum": &terraform.InstanceDiff{Destroy: true},
}
n := &terraform.InstanceInfo{} // TODO
for _, d := range resources {
h.PostDiff(n, d)
}
expected := new(CountHook)
expected.ToAdd = 0
expected.ToChange = 0
expected.ToRemoveAndAdd = 0
expected.ToRemove = 4
if !reflect.DeepEqual(expected, h) {
t.Fatalf("Expected %#v, got %#v instead.",
expected, h)
}
}
func TestCountHookPostDiff_AddOnly(t *testing.T) {
h := new(CountHook)
resources := map[string]*terraform.InstanceDiff{
"foo": &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{RequiresNew: true},
},
},
"bar": &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{RequiresNew: true},
},
},
"lorem": &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{RequiresNew: true},
},
},
}
n := &terraform.InstanceInfo{}
for _, d := range resources {
h.PostDiff(n, d)
}
expected := new(CountHook)
expected.ToAdd = 3
expected.ToChange = 0
expected.ToRemoveAndAdd = 0
expected.ToRemove = 0
if !reflect.DeepEqual(expected, h) {
t.Fatalf("Expected %#v, got %#v instead.",
expected, h)
}
}
func TestCountHookPostDiff_ChangeOnly(t *testing.T) {
h := new(CountHook)
resources := map[string]*terraform.InstanceDiff{
"foo": &terraform.InstanceDiff{
Destroy: false,
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{},
},
},
"bar": &terraform.InstanceDiff{
Destroy: false,
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{},
},
},
"lorem": &terraform.InstanceDiff{
Destroy: false,
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{},
},
},
}
n := &terraform.InstanceInfo{}
for _, d := range resources {
h.PostDiff(n, d)
}
expected := new(CountHook)
expected.ToAdd = 0
expected.ToChange = 3
expected.ToRemoveAndAdd = 0
expected.ToRemove = 0
if !reflect.DeepEqual(expected, h) {
t.Fatalf("Expected %#v, got %#v instead.",
expected, h)
}
}
func TestCountHookPostDiff_Mixed(t *testing.T) {
h := new(CountHook)
resources := map[string]*terraform.InstanceDiff{
"foo": &terraform.InstanceDiff{
Destroy: true,
},
"bar": &terraform.InstanceDiff{},
"lorem": &terraform.InstanceDiff{
Destroy: false,
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{},
},
},
"ipsum": &terraform.InstanceDiff{Destroy: true},
}
n := &terraform.InstanceInfo{}
for _, d := range resources {
h.PostDiff(n, d)
}
expected := new(CountHook)
expected.ToAdd = 0
expected.ToChange = 1
expected.ToRemoveAndAdd = 0
expected.ToRemove = 2
if !reflect.DeepEqual(expected, h) {
t.Fatalf("Expected %#v, got %#v instead.",
expected, h)
}
}
func TestCountHookPostDiff_NoChange(t *testing.T) {
h := new(CountHook)
resources := map[string]*terraform.InstanceDiff{
"foo": &terraform.InstanceDiff{},
"bar": &terraform.InstanceDiff{},
"lorem": &terraform.InstanceDiff{},
"ipsum": &terraform.InstanceDiff{},
}
n := &terraform.InstanceInfo{}
for _, d := range resources {
h.PostDiff(n, d)
}
expected := new(CountHook)
expected.ToAdd = 0
expected.ToChange = 0
expected.ToRemoveAndAdd = 0
expected.ToRemove = 0
if !reflect.DeepEqual(expected, h) {
t.Fatalf("Expected %#v, got %#v instead.",
expected, h)
}
}
func TestCountHookPostDiff_DataSource(t *testing.T) {
h := new(CountHook)
resources := map[string]*terraform.InstanceDiff{
"data.foo": &terraform.InstanceDiff{
Destroy: true,
},
"data.bar": &terraform.InstanceDiff{},
"data.lorem": &terraform.InstanceDiff{
Destroy: false,
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{},
},
},
"data.ipsum": &terraform.InstanceDiff{Destroy: true},
}
for k, d := range resources {
n := &terraform.InstanceInfo{Id: k}
h.PostDiff(n, d)
}
expected := new(CountHook)
expected.ToAdd = 0
expected.ToChange = 0
expected.ToRemoveAndAdd = 0
expected.ToRemove = 0
if !reflect.DeepEqual(expected, h) {
t.Fatalf("Expected %#v, got %#v instead.",
expected, h)
}
}

View File

@ -0,0 +1,33 @@
package local
import (
"sync"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
)
// StateHook is a hook that continuously updates the state by calling
// WriteState on a state.State.
type StateHook struct {
terraform.NilHook
sync.Mutex
State state.State
}
func (h *StateHook) PostStateUpdate(
s *terraform.State) (terraform.HookAction, error) {
h.Lock()
defer h.Unlock()
if h.State != nil {
// Write the new state
if err := h.State.WriteState(s); err != nil {
return terraform.HookActionHalt, err
}
}
// Continue forth
return terraform.HookActionContinue, nil
}

View File

@ -0,0 +1,29 @@
package local
import (
"testing"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
)
func TestStateHook_impl(t *testing.T) {
var _ terraform.Hook = new(StateHook)
}
func TestStateHook(t *testing.T) {
is := &state.InmemState{}
var hook terraform.Hook = &StateHook{State: is}
s := state.TestStateInitial()
action, err := hook.PostStateUpdate(s)
if err != nil {
t.Fatalf("err: %s", err)
}
if action != terraform.HookActionContinue {
t.Fatalf("bad: %v", action)
}
if !is.State().Equal(s) {
t.Fatalf("bad state: %#v", is.State())
}
}

View File

@ -0,0 +1,25 @@
package local
import (
"flag"
"io/ioutil"
"log"
"os"
"testing"
"github.com/hashicorp/terraform/helper/logging"
)
func TestMain(m *testing.M) {
flag.Parse()
if testing.Verbose() {
// if we're verbose, use the logging requested by TF_LOG
logging.SetOutput()
} else {
// otherwise silence all logs
log.SetOutput(ioutil.Discard)
}
os.Exit(m.Run())
}

View File

@ -0,0 +1,7 @@
resource "test_instance" "foo" {
ami = "bar"
}
resource "test_instance" "bar" {
error = "true"
}

View File

@ -0,0 +1,3 @@
resource "test_instance" "foo" {
ami = "bar"
}

View File

@ -0,0 +1,9 @@
resource "test_instance" "foo" {
ami = "bar"
# This is here because at some point it caused a test failure
network_interface {
device_index = 0
description = "Main network interface"
}
}

View File

@ -0,0 +1,9 @@
variable "should_ask" {}
provider "test" {
value = "${var.should_ask}"
}
resource "test_instance" "foo" {
foo = "bar"
}

View File

@ -0,0 +1,3 @@
resource "test_instance" "foo" {
ami = "bar"
}

66
backend/local/testing.go Normal file
View File

@ -0,0 +1,66 @@
package local
import (
"io/ioutil"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/terraform"
)
// TestLocal returns a configured Local struct with temporary paths and
// in-memory ContextOpts.
//
// No operations will be called on the returned value, so you can still set
// public fields without any locks.
func TestLocal(t *testing.T) *Local {
tempDir := testTempDir(t)
return &Local{
StatePath: filepath.Join(tempDir, "state.tfstate"),
StateOutPath: filepath.Join(tempDir, "state.tfstate"),
StateBackupPath: filepath.Join(tempDir, "state.tfstate.bak"),
ContextOpts: &terraform.ContextOpts{},
}
}
// TestLocalProvider modifies the ContextOpts of the *Local parameter to
// have a provider with the given name.
func TestLocalProvider(t *testing.T, b *Local, name string) *terraform.MockResourceProvider {
// Build a mock resource provider for in-memory operations
p := new(terraform.MockResourceProvider)
p.DiffReturn = &terraform.InstanceDiff{}
p.RefreshFn = func(
info *terraform.InstanceInfo,
s *terraform.InstanceState) (*terraform.InstanceState, error) {
return s, nil
}
p.ResourcesReturn = []terraform.ResourceType{
terraform.ResourceType{
Name: "test_instance",
},
}
// Initialize the opts
if b.ContextOpts == nil {
b.ContextOpts = &terraform.ContextOpts{}
}
if b.ContextOpts.Providers == nil {
b.ContextOpts.Providers = make(map[string]terraform.ResourceProviderFactory)
}
// Setup our provider
b.ContextOpts.Providers[name] = func() (terraform.ResourceProvider, error) {
return p, nil
}
return p
}
func testTempDir(t *testing.T) string {
d, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
return d
}

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

View File

@ -0,0 +1,57 @@
// Package remotestate implements a Backend for remote state implementations
// from the state/remote package that also implement a backend schema for
// configuration.
package remotestate
import (
"context"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
// Backend implements backend.Backend for remote state backends.
//
// All exported fields should be set. This struct should only be used
// by implementers of backends, not by consumers. If you're consuming, please
// use a higher level package such as Consul backends.
type Backend struct {
// Backend should be set to the configuration schema. ConfigureFunc
// should not be set on the schema.
*schema.Backend
// ConfigureFunc takes the ctx from a schema.Backend and returns a
// fully configured remote client to use for state operations.
ConfigureFunc func(ctx context.Context) (remote.Client, error)
client remote.Client
}
func (b *Backend) Configure(rc *terraform.ResourceConfig) error {
// Set our configureFunc manually
b.Backend.ConfigureFunc = func(ctx context.Context) error {
c, err := b.ConfigureFunc(ctx)
if err != nil {
return err
}
// Set the client for later
b.client = c
return nil
}
// Run the normal configuration
return b.Backend.Configure(rc)
}
func (b *Backend) State() (state.State, error) {
// This shouldn't happen
if b.client == nil {
panic("nil remote client")
}
s := &remote.State{Client: b.client}
return s, nil
}

View File

@ -0,0 +1,11 @@
package remotestate
import (
"testing"
"github.com/hashicorp/terraform/backend"
)
func TestBackend_impl(t *testing.T) {
var _ backend.Backend = new(Backend)
}

View File

@ -0,0 +1,111 @@
package consul
import (
"context"
"strings"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/backend/remote-state"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state/remote"
)
// New creates a new backend for Consul remote state.
func New() backend.Backend {
return &remotestate.Backend{
ConfigureFunc: configure,
// Set the schema
Backend: &schema.Backend{
Schema: map[string]*schema.Schema{
"path": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Path to store state in Consul",
},
"access_token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Access token for a Consul ACL",
Default: "", // To prevent input
},
"address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Address to the Consul Cluster",
},
"scheme": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Scheme to communicate to Consul with",
Default: "", // To prevent input
},
"datacenter": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Datacenter to communicate with",
Default: "", // To prevent input
},
"http_auth": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "HTTP Auth in the format of 'username:password'",
Default: "", // To prevent input
},
},
},
}
}
func configure(ctx context.Context) (remote.Client, error) {
// Grab the resource data
data := schema.FromContextBackendConfig(ctx)
// Configure the client
config := consulapi.DefaultConfig()
if v, ok := data.GetOk("access_token"); ok && v.(string) != "" {
config.Token = v.(string)
}
if v, ok := data.GetOk("address"); ok && v.(string) != "" {
config.Address = v.(string)
}
if v, ok := data.GetOk("scheme"); ok && v.(string) != "" {
config.Scheme = v.(string)
}
if v, ok := data.GetOk("datacenter"); ok && v.(string) != "" {
config.Datacenter = v.(string)
}
if v, ok := data.GetOk("http_auth"); ok && v.(string) != "" {
auth := v.(string)
var username, password string
if strings.Contains(auth, ":") {
split := strings.SplitN(auth, ":", 2)
username = split[0]
password = split[1]
} else {
username = auth
}
config.HttpAuth = &consulapi.HttpBasicAuth{
Username: username,
Password: password,
}
}
client, err := consulapi.NewClient(config)
if err != nil {
return nil, err
}
return &RemoteClient{
Client: client,
Path: data.Get("path").(string),
}, nil
}

View File

@ -0,0 +1,45 @@
package consul
import (
"crypto/md5"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/state/remote"
)
// RemoteClient is a remote client that stores data in Consul.
type RemoteClient struct {
Client *consulapi.Client
Path string
}
func (c *RemoteClient) Get() (*remote.Payload, error) {
pair, _, err := c.Client.KV().Get(c.Path, nil)
if err != nil {
return nil, err
}
if pair == nil {
return nil, nil
}
md5 := md5.Sum(pair.Value)
return &remote.Payload{
Data: pair.Value,
MD5: md5[:],
}, nil
}
func (c *RemoteClient) Put(data []byte) error {
kv := c.Client.KV()
_, err := kv.Put(&consulapi.KVPair{
Key: c.Path,
Value: data,
}, nil)
return err
}
func (c *RemoteClient) Delete() error {
kv := c.Client.KV()
_, err := kv.Delete(c.Path, nil)
return err
}

View File

@ -0,0 +1,29 @@
package consul
import (
"fmt"
"testing"
"time"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/backend/remote-state"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/state/remote"
)
func TestRemoteClient_impl(t *testing.T) {
var _ remote.Client = new(RemoteClient)
}
func TestRemoteClient(t *testing.T) {
acctest.RemoteTestPrecheck(t)
// Get the backend
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
"address": "demo.consul.io:80",
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()),
})
// Test
remotestate.TestClient(t, b)
}

View File

@ -0,0 +1,17 @@
package remotestate
import (
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state/remote"
)
func TestClient(t *testing.T, raw backend.Backend) {
b, ok := raw.(*Backend)
if !ok {
t.Fatalf("not Backend: %T", raw)
}
remote.TestClient(t, b.client)
}

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
}

View File

@ -2,15 +2,16 @@ package command
import (
"bytes"
"context"
"fmt"
"os"
"sort"
"strings"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
)
@ -43,7 +44,7 @@ func (c *ApplyCommand) Run(args []string) int {
cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
cmdFlags.IntVar(
&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
@ -51,32 +52,25 @@ func (c *ApplyCommand) Run(args []string) int {
return 1
}
pwd, err := os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
return 1
}
var configPath string
maybeInit := true
// Get the args. The "maybeInit" flag tracks whether we may need to
// initialize the configuration from a remote path. This is true as long
// as we have an argument.
args = cmdFlags.Args()
if len(args) > 1 {
c.Ui.Error("The apply command expects at most one argument.")
cmdFlags.Usage()
maybeInit := len(args) == 1
configPath, err := ModulePath(args)
if err != nil {
c.Ui.Error(err.Error())
return 1
} else if len(args) == 1 {
configPath = args[0]
} else {
configPath = pwd
maybeInit = false
}
// Prepare the extra hooks to count resources
countHook := new(CountHook)
stateHook := new(StateHook)
c.Meta.extraHooks = []terraform.Hook{countHook, stateHook}
if !c.Destroy && maybeInit {
// We need the pwd for the getter operation below
pwd, err := os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
return 1
}
// Do a detect to determine if we need to do an init + apply.
if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil {
c.Ui.Error(fmt.Sprintf(
@ -96,37 +90,58 @@ func (c *ApplyCommand) Run(args []string) int {
}
}
terraform.SetDebugInfo(DefaultDataDir)
// Check for the legacy graph
if experiment.Enabled(experiment.X_legacyGraph) {
c.Ui.Output(c.Colorize().Color(
"[reset][bold][yellow]" +
"Legacy graph enabled! This will use the graph from Terraform 0.7.x\n" +
"to execute this operation. This will be removed in the future so\n" +
"please report any issues causing you to use this to the Terraform\n" +
"project.\n\n"))
}
// This is going to keep track of shadow errors
var shadowErr error
// Build the context based on the arguments given
ctx, planned, err := c.Context(contextOpts{
Destroy: c.Destroy,
Path: configPath,
StatePath: c.Meta.statePath,
Parallelism: c.Meta.parallelism,
})
// Check if the path is a plan
plan, err := c.Plan(configPath)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if c.Destroy && planned {
if c.Destroy && plan != nil {
c.Ui.Error(fmt.Sprintf(
"Destroy can't be called with a plan file."))
return 1
}
if plan != nil {
// Reset the config path for backend loading
configPath = ""
}
// Load the module if we don't have one yet (not running from plan)
var mod *module.Tree
if plan == nil {
mod, err = c.Module(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
}
/*
terraform.SetDebugInfo(DefaultDataDir)
// Check for the legacy graph
if experiment.Enabled(experiment.X_legacyGraph) {
c.Ui.Output(c.Colorize().Color(
"[reset][bold][yellow]" +
"Legacy graph enabled! This will use the graph from Terraform 0.7.x\n" +
"to execute this operation. This will be removed in the future so\n" +
"please report any issues causing you to use this to the Terraform\n" +
"project.\n\n"))
}
*/
// Load the backend
b, err := c.Backend(&BackendOpts{
ConfigPath: configPath,
Plan: plan,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// If we're not forcing and we're destroying, verify with the
// user at this point.
if !destroyForce && c.Destroy {
// Default destroy message
desc := "Terraform will delete all your managed infrastructure.\n" +
@ -159,80 +174,32 @@ func (c *ApplyCommand) Run(args []string) int {
return 1
}
}
if !planned {
if err := ctx.Input(c.InputMode()); err != nil {
c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
return 1
}
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "input operation:"))
}
}
if !validateContext(ctx, c.Ui) {
// Build the operation
opReq := c.Operation()
opReq.Destroy = c.Destroy
opReq.Module = mod
opReq.Plan = plan
opReq.PlanRefresh = refresh
opReq.Type = backend.OperationTypeApply
// Perform the operation
ctx, ctxCancel := context.WithCancel(context.Background())
defer ctxCancel()
op, err := b.Operation(ctx, opReq)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err))
return 1
}
// Plan if we haven't already
if !planned {
if refresh {
if _, err := ctx.Refresh(); err != nil {
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
return 1
}
}
if _, err := ctx.Plan(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error creating plan: %s", err))
return 1
}
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "plan operation:"))
}
}
// Setup the state hook for continuous state updates
{
state, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading state: %s", err))
return 1
}
stateHook.State = state
}
// Start the apply in a goroutine so that we can be interrupted.
var state *terraform.State
var applyErr error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
state, applyErr = ctx.Apply()
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "apply operation:"))
}
}()
// Wait for the apply to finish or for us to be interrupted so
// we can handle it properly.
err = nil
// Wait for the operation to complete or an interrupt to occur
select {
case <-c.ShutdownCh:
c.Ui.Output("Interrupt received. Gracefully shutting down...")
// Cancel our context so we can start gracefully exiting
ctxCancel()
// Stop execution
go ctx.Stop()
// Notify the user
c.Ui.Output("Interrupt received. Gracefully shutting down...")
// Still get the result, since there is still one
select {
@ -241,65 +208,27 @@ func (c *ApplyCommand) Run(args []string) int {
"Two interrupts received. Exiting immediately. Note that data\n" +
"loss may have occurred.")
return 1
case <-doneCh:
case <-op.Done():
}
case <-doneCh:
}
// Persist the state
if state != nil {
if err := c.Meta.PersistState(state); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
case <-op.Done():
if err := op.Err; err != nil {
c.Ui.Error(err.Error())
return 1
}
}
if applyErr != nil {
c.Ui.Error(fmt.Sprintf(
"Error applying plan:\n\n"+
"%s\n\n"+
"Terraform does not automatically rollback in the face of errors.\n"+
"Instead, your Terraform state file has been partially updated with\n"+
"any resources that successfully completed. Please address the error\n"+
"above and apply again to incrementally change your infrastructure.",
multierror.Flatten(applyErr)))
return 1
}
if c.Destroy {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Destroy complete! Resources: %d destroyed.",
countHook.Removed)))
} else {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Apply complete! Resources: %d added, %d changed, %d destroyed.",
countHook.Added,
countHook.Changed,
countHook.Removed)))
}
if countHook.Added > 0 || countHook.Changed > 0 {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset]\n"+
"The state of your infrastructure has been saved to the path\n"+
"below. This state is required to modify and destroy your\n"+
"infrastructure, so keep it safe. To inspect the complete state\n"+
"use the `terraform show` command.\n\n"+
"State path: %s",
c.Meta.StateOutPath())))
}
if !c.Destroy {
if outputs := outputsAsString(state, terraform.RootModulePath, ctx.Module().Config().Outputs, true); outputs != "" {
// Get the right module that we used. If we ran a plan, then use
// that module.
if plan != nil {
mod = plan.Module
}
if outputs := outputsAsString(op.State, terraform.RootModulePath, mod.Config().Outputs, true); outputs != "" {
c.Ui.Output(c.Colorize().Color(outputs))
}
}
// If we have an error in the shadow graph, let the user know.
c.outputShadowError(shadowErr, applyErr == nil)
return 0
}

View File

@ -519,7 +519,7 @@ func TestApply_plan(t *testing.T) {
}
args := []string{
"-state", statePath,
"-state-out", statePath,
planPath,
}
if code := c.Run(args); code != 0 {
@ -564,7 +564,7 @@ func TestApply_plan_backup(t *testing.T) {
}
args := []string{
"-state", statePath,
"-state-out", statePath,
"-backup", backupPath,
planPath,
}
@ -601,7 +601,7 @@ func TestApply_plan_noBackup(t *testing.T) {
}
args := []string{
"-state", statePath,
"-state-out", statePath,
"-backup", "-",
planPath,
}
@ -670,12 +670,13 @@ func TestApply_plan_remoteState(t *testing.T) {
// State file should be not be installed
if _, err := os.Stat(filepath.Join(tmp, DefaultStateFilename)); err == nil {
t.Fatalf("State path should not exist")
data, _ := ioutil.ReadFile(DefaultStateFilename)
t.Fatalf("State path should not exist: %s", string(data))
}
// Check for remote state
if _, err := os.Stat(remoteStatePath); err != nil {
t.Fatalf("missing remote state: %s", err)
// Check that there is no remote state config
if _, err := os.Stat(remoteStatePath); err == nil {
t.Fatalf("has remote state config")
}
}
@ -710,7 +711,7 @@ func TestApply_planWithVarFile(t *testing.T) {
}
args := []string{
"-state", statePath,
"-state-out", statePath,
planPath,
}
if code := c.Run(args); code != 0 {
@ -1489,59 +1490,6 @@ func TestApply_disableBackup(t *testing.T) {
}
}
// -state-out wasn't taking effect when a plan is supplied. GH-7264
func TestApply_stateOutWithPlan(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
tmpDir := testTempDir(t)
defer os.RemoveAll(tmpDir)
statePath := filepath.Join(tmpDir, "state.tfstate")
planPath := filepath.Join(tmpDir, "terraform.tfplan")
args := []string{
"-state", statePath,
"-out", planPath,
testFixturePath("plan"),
}
// Run plan first to get a current plan file
pc := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
if code := pc.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// now run apply with the generated plan
stateOutPath := filepath.Join(tmpDir, "state-new.tfstate")
args = []string{
"-state", statePath,
"-state-out", stateOutPath,
planPath,
}
ac := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
if code := ac.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// now make sure we wrote out our new state
if _, err := os.Stat(stateOutPath); err != nil {
t.Fatalf("missing new state file: %s", err)
}
}
func testHttpServer(t *testing.T) net.Listener {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {

View File

@ -3,6 +3,7 @@ package command
import (
"fmt"
"log"
"os"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
@ -27,6 +28,47 @@ const DefaultBackupExtension = ".backup"
// operations as it walks the dependency graph.
const DefaultParallelism = 10
// ErrUnsupportedLocalOp is the common error message shown for operations
// that require a backend.Local.
const ErrUnsupportedLocalOp = `The configured backend doesn't support this operation.
The "backend" in Terraform defines how Terraform operates. The default
backend performs all operations locally on your machine. Your configuration
is configured to use a non-local backend. This backend doesn't support this
operation.
If you want to use the state from the backend but force all other data
(configuration, variables, etc.) to come locally, you can force local
behavior with the "-local" flag.
`
// ModulePath returns the path to the root module from the CLI args.
//
// This centralizes the logic for any commands that expect a module path
// on their CLI args. This will verify that only one argument is given
// and that it is a path to configuration.
//
// If your command accepts more than one arg, then change the slice bounds
// to pass validation.
func ModulePath(args []string) (string, error) {
// TODO: test
if len(args) > 1 {
return "", fmt.Errorf("Too many command line arguments. Configuration path expected.")
}
if len(args) == 0 {
path, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("Error getting pwd: %s", err)
}
return path, nil
}
return args[0], nil
}
func validateContext(ctx *terraform.Context, ui cli.Ui) bool {
log.Println("[INFO] Validating the context...")
ws, es := ctx.Validate()

View File

@ -1,10 +1,16 @@
package command
import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/json"
"flag"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
@ -164,7 +170,20 @@ func testState() *terraform.State {
},
}
state.Init()
return state
// Write and read the state so that it is properly initialized. We
// do this since we didn't call the normal NewState constructor.
var buf bytes.Buffer
if err := terraform.WriteState(state, &buf); err != nil {
panic(err)
}
result, err := terraform.ReadState(&buf)
if err != nil {
panic(err)
}
return result
}
func testStateFile(t *testing.T, s *terraform.State) string {
@ -220,9 +239,8 @@ func testStateFileRemote(t *testing.T, s *terraform.State) string {
return path
}
// testStateOutput tests that the state at the given path contains
// the expected state string.
func testStateOutput(t *testing.T, path string, expected string) {
// testStateRead reads the state from a file
func testStateRead(t *testing.T, path string) *terraform.State {
f, err := os.Open(path)
if err != nil {
t.Fatalf("err: %s", err)
@ -234,6 +252,13 @@ func testStateOutput(t *testing.T, path string, expected string) {
t.Fatalf("err: %s", err)
}
return newState
}
// testStateOutput tests that the state at the given path contains
// the expected state string.
func testStateOutput(t *testing.T, path string, expected string) {
newState := testStateRead(t, path)
actual := strings.TrimSpace(newState.String())
expected = strings.TrimSpace(expected)
if actual != expected {
@ -401,3 +426,106 @@ func testStdoutCapture(t *testing.T, dst io.Writer) func() {
<-doneCh
}
}
// testInteractiveInput configures tests so that the answers given are sent
// in order to interactive prompts. The returned function must be called
// in a defer to clean up.
func testInteractiveInput(t *testing.T, answers []string) func() {
// Disable test mode so input is called
test = false
// Setup reader/writers
testInputResponse = answers
defaultInputReader = bytes.NewBufferString("")
defaultInputWriter = new(bytes.Buffer)
// Return the cleanup
return func() {
test = true
testInputResponse = nil
}
}
// testBackendState is used to make a test HTTP server to test a configured
// backend. This returns the complete state that can be saved. Use
// `testStateFileRemote` to write the returned state.
func testBackendState(t *testing.T, s *terraform.State, c int) (*terraform.State, *httptest.Server) {
var b64md5 string
buf := bytes.NewBuffer(nil)
cb := func(resp http.ResponseWriter, req *http.Request) {
if req.Method == "PUT" {
resp.WriteHeader(c)
return
}
if s == nil {
resp.WriteHeader(404)
return
}
resp.Header().Set("Content-MD5", b64md5)
resp.Write(buf.Bytes())
}
// If a state was given, make sure we calculate the proper b64md5
if s != nil {
enc := json.NewEncoder(buf)
if err := enc.Encode(s); err != nil {
t.Fatalf("err: %v", err)
}
md5 := md5.Sum(buf.Bytes())
b64md5 = base64.StdEncoding.EncodeToString(md5[:16])
}
srv := httptest.NewServer(http.HandlerFunc(cb))
state := terraform.NewState()
state.Backend = &terraform.BackendState{
Type: "http",
Config: map[string]interface{}{"address": srv.URL},
Hash: 2529831861221416334,
}
return state, srv
}
// testRemoteState is used to make a test HTTP server to return a given
// state file that can be used for testing legacy remote state.
func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) {
var b64md5 string
buf := bytes.NewBuffer(nil)
cb := func(resp http.ResponseWriter, req *http.Request) {
if req.Method == "PUT" {
resp.WriteHeader(c)
return
}
if s == nil {
resp.WriteHeader(404)
return
}
resp.Header().Set("Content-MD5", b64md5)
resp.Write(buf.Bytes())
}
srv := httptest.NewServer(http.HandlerFunc(cb))
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{"address": srv.URL},
}
if s != nil {
// Set the remote data
s.Remote = remote
enc := json.NewEncoder(buf)
if err := enc.Encode(s); err != nil {
t.Fatalf("err: %v", err)
}
md5 := md5.Sum(buf.Bytes())
b64md5 = base64.StdEncoding.EncodeToString(md5[:16])
}
return remote, srv
}

View File

@ -3,9 +3,9 @@ package command
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/wrappedstreams"
"github.com/hashicorp/terraform/repl"
@ -30,30 +30,39 @@ func (c *ConsoleCommand) Run(args []string) int {
return 1
}
pwd, err := os.Getwd()
configPath, err := ModulePath(cmdFlags.Args())
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
c.Ui.Error(err.Error())
return 1
}
var configPath string
args = cmdFlags.Args()
if len(args) > 1 {
c.Ui.Error("The console command expects at most one argument.")
cmdFlags.Usage()
// Load the module
mod, err := c.Module(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
} else if len(args) == 1 {
configPath = args[0]
} else {
configPath = pwd
}
// Build the context based on the arguments given
ctx, _, err := c.Context(contextOpts{
Path: configPath,
PathEmptyOk: true,
StatePath: c.Meta.statePath,
})
// Load the backend
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// We require a local backend
local, ok := b.(backend.Local)
if !ok {
c.Ui.Error(ErrUnsupportedLocalOp)
return 1
}
// Build the operation
opReq := c.Operation()
opReq.Module = mod
// Get the context
ctx, _, err := local.Context(opReq)
if err != nil {
c.Ui.Error(err.Error())
return 1

8
command/format/format.go Normal file
View File

@ -0,0 +1,8 @@
// Package format contains helpers for formatting various Terraform
// structures for human-readabout output.
//
// This package is used by the official Terraform CLI in formatting any
// output and is exported to encourage non-official frontends to mimic the
// output formatting as much as possible so that text formats of Terraform
// structures have a consistent look and feel.
package format

View File

@ -1,4 +1,4 @@
package command
package format
import (
"bytes"
@ -10,8 +10,8 @@ import (
"github.com/mitchellh/colorstring"
)
// FormatPlanOpts are the options for formatting a plan.
type FormatPlanOpts struct {
// PlanOpts are the options for formatting a plan.
type PlanOpts struct {
// Plan is the plan to format. This is required.
Plan *terraform.Plan
@ -23,8 +23,8 @@ type FormatPlanOpts struct {
ModuleDepth int
}
// FormatPlan takes a plan and returns a
func FormatPlan(opts *FormatPlanOpts) string {
// Plan takes a plan and returns a
func Plan(opts *PlanOpts) string {
p := opts.Plan
if p.Diff == nil || p.Diff.Empty() {
return "This plan does nothing."
@ -52,7 +52,7 @@ func FormatPlan(opts *FormatPlanOpts) string {
// formatPlanModuleExpand will output the given module and all of its
// resources.
func formatPlanModuleExpand(
buf *bytes.Buffer, m *terraform.ModuleDiff, opts *FormatPlanOpts) {
buf *bytes.Buffer, m *terraform.ModuleDiff, opts *PlanOpts) {
// Ignore empty diffs
if m.Empty() {
return
@ -199,7 +199,7 @@ func formatPlanModuleExpand(
// formatPlanModuleSingle will output the given module and all of its
// resources.
func formatPlanModuleSingle(
buf *bytes.Buffer, m *terraform.ModuleDiff, opts *FormatPlanOpts) {
buf *bytes.Buffer, m *terraform.ModuleDiff, opts *PlanOpts) {
// Ignore empty diffs
if m.Empty() {
return

View File

@ -1,4 +1,4 @@
package command
package format
import (
"strings"
@ -9,7 +9,7 @@ import (
)
// Test that a root level data source gets a special plan output on create
func TestFormatPlan_destroyDeposed(t *testing.T) {
func TestPlan_destroyDeposed(t *testing.T) {
plan := &terraform.Plan{
Diff: &terraform.Diff{
Modules: []*terraform.ModuleDiff{
@ -24,7 +24,7 @@ func TestFormatPlan_destroyDeposed(t *testing.T) {
},
},
}
opts := &FormatPlanOpts{
opts := &PlanOpts{
Plan: plan,
Color: &colorstring.Colorize{
Colors: colorstring.DefaultColors,
@ -33,7 +33,7 @@ func TestFormatPlan_destroyDeposed(t *testing.T) {
ModuleDepth: 1,
}
actual := FormatPlan(opts)
actual := Plan(opts)
expected := strings.TrimSpace(`
- aws_instance.foo (deposed)
@ -44,7 +44,7 @@ func TestFormatPlan_destroyDeposed(t *testing.T) {
}
// Test that computed fields with an interpolation string get displayed
func TestFormatPlan_displayInterpolations(t *testing.T) {
func TestPlan_displayInterpolations(t *testing.T) {
plan := &terraform.Plan{
Diff: &terraform.Diff{
Modules: []*terraform.ModuleDiff{
@ -64,7 +64,7 @@ func TestFormatPlan_displayInterpolations(t *testing.T) {
},
},
}
opts := &FormatPlanOpts{
opts := &PlanOpts{
Plan: plan,
Color: &colorstring.Colorize{
Colors: colorstring.DefaultColors,
@ -73,7 +73,7 @@ func TestFormatPlan_displayInterpolations(t *testing.T) {
ModuleDepth: 1,
}
out := FormatPlan(opts)
out := Plan(opts)
lines := strings.Split(out, "\n")
if len(lines) != 2 {
t.Fatal("expected 2 lines of output, got:\n", out)
@ -88,7 +88,7 @@ func TestFormatPlan_displayInterpolations(t *testing.T) {
}
// Test that a root level data source gets a special plan output on create
func TestFormatPlan_rootDataSource(t *testing.T) {
func TestPlan_rootDataSource(t *testing.T) {
plan := &terraform.Plan{
Diff: &terraform.Diff{
Modules: []*terraform.ModuleDiff{
@ -108,7 +108,7 @@ func TestFormatPlan_rootDataSource(t *testing.T) {
},
},
}
opts := &FormatPlanOpts{
opts := &PlanOpts{
Plan: plan,
Color: &colorstring.Colorize{
Colors: colorstring.DefaultColors,
@ -117,7 +117,7 @@ func TestFormatPlan_rootDataSource(t *testing.T) {
ModuleDepth: 1,
}
actual := FormatPlan(opts)
actual := Plan(opts)
expected := strings.TrimSpace(`
<= data.type.name
@ -129,7 +129,7 @@ func TestFormatPlan_rootDataSource(t *testing.T) {
}
// Test that data sources nested in modules get the same plan output
func TestFormatPlan_nestedDataSource(t *testing.T) {
func TestPlan_nestedDataSource(t *testing.T) {
plan := &terraform.Plan{
Diff: &terraform.Diff{
Modules: []*terraform.ModuleDiff{
@ -149,7 +149,7 @@ func TestFormatPlan_nestedDataSource(t *testing.T) {
},
},
}
opts := &FormatPlanOpts{
opts := &PlanOpts{
Plan: plan,
Color: &colorstring.Colorize{
Colors: colorstring.DefaultColors,
@ -158,7 +158,7 @@ func TestFormatPlan_nestedDataSource(t *testing.T) {
ModuleDepth: 2,
}
actual := FormatPlan(opts)
actual := Plan(opts)
expected := strings.TrimSpace(`
<= module.nested.data.type.name

View File

@ -1,4 +1,4 @@
package command
package format
import (
"bytes"
@ -10,8 +10,8 @@ import (
"github.com/mitchellh/colorstring"
)
// FormatStateOpts are the options for formatting a state.
type FormatStateOpts struct {
// StateOpts are the options for formatting a state.
type StateOpts struct {
// State is the state to format. This is required.
State *terraform.State
@ -23,8 +23,8 @@ type FormatStateOpts struct {
ModuleDepth int
}
// FormatState takes a state and returns a string
func FormatState(opts *FormatStateOpts) string {
// State takes a state and returns a string
func State(opts *StateOpts) string {
if opts.Color == nil {
panic("colorize not given")
}
@ -79,7 +79,7 @@ func FormatState(opts *FormatStateOpts) string {
}
func formatStateModuleExpand(
buf *bytes.Buffer, m *terraform.ModuleState, opts *FormatStateOpts) {
buf *bytes.Buffer, m *terraform.ModuleState, opts *StateOpts) {
var moduleName string
if !m.IsRoot() {
moduleName = fmt.Sprintf("module.%s", strings.Join(m.Path[1:], "."))
@ -143,10 +143,123 @@ func formatStateModuleExpand(
}
func formatStateModuleSingle(
buf *bytes.Buffer, m *terraform.ModuleState, opts *FormatStateOpts) {
buf *bytes.Buffer, m *terraform.ModuleState, opts *StateOpts) {
// Header with the module name
buf.WriteString(fmt.Sprintf("module.%s\n", strings.Join(m.Path[1:], ".")))
// Now just write how many resources are in here.
buf.WriteString(fmt.Sprintf(" %d resource(s)\n", len(m.Resources)))
}
func formatNestedList(indent string, outputList []interface{}) string {
outputBuf := new(bytes.Buffer)
outputBuf.WriteString(fmt.Sprintf("%s[", indent))
lastIdx := len(outputList) - 1
for i, value := range outputList {
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s", indent, " ", value))
if i != lastIdx {
outputBuf.WriteString(",")
}
}
outputBuf.WriteString(fmt.Sprintf("\n%s]", indent))
return strings.TrimPrefix(outputBuf.String(), "\n")
}
func formatListOutput(indent, outputName string, outputList []interface{}) string {
keyIndent := ""
outputBuf := new(bytes.Buffer)
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("%s%s = [", indent, outputName))
keyIndent = " "
}
lastIdx := len(outputList) - 1
for i, value := range outputList {
switch typedValue := value.(type) {
case string:
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s", indent, keyIndent, value))
case []interface{}:
outputBuf.WriteString(fmt.Sprintf("\n%s%s", indent,
formatNestedList(indent+keyIndent, typedValue)))
case map[string]interface{}:
outputBuf.WriteString(fmt.Sprintf("\n%s%s", indent,
formatNestedMap(indent+keyIndent, typedValue)))
}
if lastIdx != i {
outputBuf.WriteString(",")
}
}
if outputName != "" {
if len(outputList) > 0 {
outputBuf.WriteString(fmt.Sprintf("\n%s]", indent))
} else {
outputBuf.WriteString("]")
}
}
return strings.TrimPrefix(outputBuf.String(), "\n")
}
func formatNestedMap(indent string, outputMap map[string]interface{}) string {
ks := make([]string, 0, len(outputMap))
for k, _ := range outputMap {
ks = append(ks, k)
}
sort.Strings(ks)
outputBuf := new(bytes.Buffer)
outputBuf.WriteString(fmt.Sprintf("%s{", indent))
lastIdx := len(outputMap) - 1
for i, k := range ks {
v := outputMap[k]
outputBuf.WriteString(fmt.Sprintf("\n%s%s = %v", indent+" ", k, v))
if lastIdx != i {
outputBuf.WriteString(",")
}
}
outputBuf.WriteString(fmt.Sprintf("\n%s}", indent))
return strings.TrimPrefix(outputBuf.String(), "\n")
}
func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string {
ks := make([]string, 0, len(outputMap))
for k, _ := range outputMap {
ks = append(ks, k)
}
sort.Strings(ks)
keyIndent := ""
outputBuf := new(bytes.Buffer)
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("%s%s = {", indent, outputName))
keyIndent = " "
}
for _, k := range ks {
v := outputMap[k]
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s = %v", indent, keyIndent, k, v))
}
if outputName != "" {
if len(outputMap) > 0 {
outputBuf.WriteString(fmt.Sprintf("\n%s}", indent))
} else {
outputBuf.WriteString("}")
}
}
return strings.TrimPrefix(outputBuf.String(), "\n")
}

View File

@ -3,7 +3,6 @@ package command
import (
"flag"
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform/config/module"
@ -28,19 +27,10 @@ func (c *GetCommand) Run(args []string) int {
}
var path string
args = cmdFlags.Args()
if len(args) > 1 {
c.Ui.Error("The get command expects one argument.\n")
cmdFlags.Usage()
path, err := ModulePath(cmdFlags.Args())
if err != nil {
c.Ui.Error(err.Error())
return 1
} else if len(args) == 1 {
path = args[0]
} else {
var err error
path, err = os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
}
}
mode := module.GetModeGet
@ -48,12 +38,8 @@ func (c *GetCommand) Run(args []string) int {
mode = module.GetModeUpdate
}
_, _, err := c.Context(contextOpts{
Path: path,
GetMode: mode,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err))
if err := getModules(&c.Meta, path, mode); err != nil {
c.Ui.Error(err.Error())
return 1
}
@ -86,3 +72,17 @@ Options:
func (c *GetCommand) Synopsis() string {
return "Download and install modules for the configuration"
}
func getModules(m *Meta, path string, mode module.GetMode) error {
mod, err := module.NewTreeModule("", path)
if err != nil {
return fmt.Errorf("Error loading configuration: %s", err)
}
err = mod.Load(m.moduleStorage(m.DataDir()), mode)
if err != nil {
return fmt.Errorf("Error loading modules: %s", err)
}
return nil
}

View File

@ -3,9 +3,10 @@ package command
import (
"flag"
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/terraform"
)
@ -34,34 +35,65 @@ func (c *GraphCommand) Run(args []string) int {
return 1
}
var path string
args = cmdFlags.Args()
if len(args) > 1 {
c.Ui.Error("The graph command expects one argument.\n")
cmdFlags.Usage()
configPath, err := ModulePath(cmdFlags.Args())
if err != nil {
c.Ui.Error(err.Error())
return 1
} else if len(args) == 1 {
path = args[0]
} else {
var err error
path, err = os.Getwd()
}
// Check if the path is a plan
plan, err := c.Plan(configPath)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if plan != nil {
// Reset for backend loading
configPath = ""
}
// Load the module
var mod *module.Tree
if plan == nil {
mod, err = c.Module(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
}
ctx, planFile, err := c.Context(contextOpts{
Path: path,
StatePath: "",
// Load the backend
b, err := c.Backend(&BackendOpts{
ConfigPath: configPath,
Plan: plan,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err))
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// We require a local backend
local, ok := b.(backend.Local)
if !ok {
c.Ui.Error(ErrUnsupportedLocalOp)
return 1
}
// Build the operation
opReq := c.Operation()
opReq.Module = mod
opReq.Plan = plan
// Get the context
ctx, _, err := local.Context(opReq)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
// Determine the graph type
graphType := terraform.GraphTypePlan
if planFile {
if plan != nil {
graphType = terraform.GraphTypeApply
}

View File

@ -6,6 +6,8 @@ import (
"os"
"strings"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
)
@ -45,13 +47,37 @@ func (c *ImportCommand) Run(args []string) int {
return 1
}
// Build the context based on the arguments given
ctx, _, err := c.Context(contextOpts{
Path: configPath,
PathEmptyOk: true,
StatePath: c.Meta.statePath,
Parallelism: c.Meta.parallelism,
})
// Load the module
var mod *module.Tree
if configPath != "" {
var err error
mod, err = c.Module(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
}
// Load the backend
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// We require a local backend
local, ok := b.(backend.Local)
if !ok {
c.Ui.Error(ErrUnsupportedLocalOp)
return 1
}
// Build the operation
opReq := c.Operation()
opReq.Module = mod
// Get the context
ctx, state, err := local.Context(opReq)
if err != nil {
c.Ui.Error(err.Error())
return 1
@ -76,7 +102,11 @@ func (c *ImportCommand) Run(args []string) int {
// Persist the final state
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
if err := c.Meta.PersistState(newState); err != nil {
if err := state.WriteState(newState); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
return 1
}
if err := state.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
return 1
}

View File

@ -1,7 +1,6 @@
package command
import (
"flag"
"fmt"
"os"
"path/filepath"
@ -10,7 +9,6 @@ import (
"github.com/hashicorp/go-getter"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
)
// InitCommand is a Command implementation that takes a Terraform
@ -20,39 +18,48 @@ type InitCommand struct {
}
func (c *InitCommand) Run(args []string) int {
var remoteBackend string
var flagBackend, flagGet bool
var flagConfigFile string
args = c.Meta.process(args, false)
remoteConfig := make(map[string]string)
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
cmdFlags.StringVar(&remoteBackend, "backend", "", "")
cmdFlags.Var((*FlagStringKV)(&remoteConfig), "backend-config", "config")
cmdFlags := c.flagSet("init")
cmdFlags.BoolVar(&flagBackend, "backend", true, "")
cmdFlags.StringVar(&flagConfigFile, "backend-config", "", "")
cmdFlags.BoolVar(&flagGet, "get", true, "")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
remoteBackend = strings.ToLower(remoteBackend)
var path string
// Validate the arg count
args = cmdFlags.Args()
if len(args) > 2 {
c.Ui.Error("The init command expects at most two arguments.\n")
cmdFlags.Usage()
return 1
} else if len(args) < 1 {
c.Ui.Error("The init command expects at least one arguments.\n")
cmdFlags.Usage()
}
// Get our pwd. We don't always need it but always getting it is easier
// than the logic to determine if it is or isn't needed.
pwd, err := os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
return 1
}
if len(args) == 2 {
// Get the path and source module to copy
var path string
var source string
switch len(args) {
case 0:
path = pwd
case 1:
path = pwd
source = args[0]
case 2:
source = args[0]
path = args[1]
} else {
var err error
path, err = os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
}
default:
panic("assertion failed on arg count")
}
// Set the state out path to be the path requested for the module
@ -60,110 +67,186 @@ func (c *InitCommand) Run(args []string) int {
// proper directory.
c.Meta.dataDir = filepath.Join(path, DefaultDataDir)
source := args[0]
// This will track whether we outputted anything so that we know whether
// to output a newline before the success message
var header bool
// Get our pwd since we need it
pwd, err := os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading working directory: %s", err))
return 1
// If we have a source, copy it
if source != "" {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold]"+
"Initializing configuration from: %q...", source)))
if err := c.copySource(path, source, pwd); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error copying source: %s", err))
return 1
}
header = true
}
// Verify the directory is empty
// If our directory is empty, then we're done. We can't get or setup
// the backend with an empty directory.
if empty, err := config.IsEmptyDir(path); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error checking on destination path: %s", err))
"Error checking configuration: %s", err))
return 1
} else if empty {
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitEmpty)))
return 0
}
// If we're performing a get or loading the backend, then we perform
// some extra tasks.
if flagGet || flagBackend {
// Load the configuration in this directory so that we can know
// if we have anything to get or any backend to configure. We do
// this to improve the UX. Practically, we could call the functions
// below without checking this to the same effect.
conf, err := config.LoadDir(path)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error loading configuration: %s", err))
return 1
}
// If we requested downloading modules and have modules in the config
if flagGet && len(conf.Modules) > 0 {
header = true
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold]" +
"Downloading modules (if any)...")))
if err := getModules(&c.Meta, path, module.GetModeGet); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error downloading modules: %s", err))
return 1
}
}
// If we're requesting backend configuration and configure it
hasBackend := conf.Terraform != nil && conf.Terraform.Backend != nil
if flagBackend && hasBackend {
header = true
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold]" +
"Initializing the backend...")))
opts := &BackendOpts{
ConfigPath: path,
ConfigFile: flagConfigFile,
Init: true,
}
if _, err := c.Backend(opts); err != nil {
c.Ui.Error(err.Error())
return 1
}
}
}
// If we outputted information, then we need to output a newline
// so that our success message is nicely spaced out from prior text.
if header {
c.Ui.Output("")
}
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccess)))
return 0
}
func (c *InitCommand) copySource(dst, src, pwd string) error {
// Verify the directory is empty
if empty, err := config.IsEmptyDir(dst); err != nil {
return fmt.Errorf("Error checking on destination path: %s", err)
} else if !empty {
c.Ui.Error(
"The destination path has Terraform configuration files. The\n" +
"init command can only be used on a directory without existing Terraform\n" +
"files.")
return 1
return fmt.Errorf(strings.TrimSpace(errInitCopyNotEmpty))
}
// Detect
source, err = getter.Detect(source, pwd, getter.Detectors)
source, err := getter.Detect(src, pwd, getter.Detectors)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error with module source: %s", err))
return 1
return fmt.Errorf("Error with module source: %s", err)
}
// Get it!
if err := module.GetCopy(path, source); err != nil {
c.Ui.Error(err.Error())
return 1
}
// Handle remote state if configured
if remoteBackend != "" {
var remoteConf terraform.RemoteState
remoteConf.Type = remoteBackend
remoteConf.Config = remoteConfig
state, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error checking for state: %s", err))
return 1
}
if state != nil {
s := state.State()
if !s.Empty() {
c.Ui.Error(fmt.Sprintf(
"State file already exists and is not empty! Please remove this\n" +
"state file before initializing. Note that removing the state file\n" +
"may result in a loss of information since Terraform uses this\n" +
"to track your infrastructure."))
return 1
}
if s.IsRemote() {
c.Ui.Error(fmt.Sprintf(
"State file already exists with remote state enabled! Please remove this\n" +
"state file before initializing. Note that removing the state file\n" +
"may result in a loss of information since Terraform uses this\n" +
"to track your infrastructure."))
return 1
}
}
// Initialize a blank state file with remote enabled
remoteCmd := &RemoteConfigCommand{
Meta: c.Meta,
remoteConf: &remoteConf,
}
return remoteCmd.initBlankState()
}
return 0
return module.GetCopy(dst, source)
}
func (c *InitCommand) Help() string {
helpText := `
Usage: terraform init [options] SOURCE [PATH]
Usage: terraform init [options] [SOURCE] [PATH]
Downloads the module given by SOURCE into the PATH. The PATH defaults
to the working directory. PATH must be empty of any Terraform files.
Any conflicting non-Terraform files will be overwritten.
Initialize a new or existing Terraform environment by creating
initial files, loading any remote state, downloading modules, etc.
The module downloaded is a copy. If you're downloading a module from
Git, it will not preserve the Git history, it will only copy the
latest files.
This is the first command that should be run for any new or existing
Terraform configuration per machine. This sets up all the local data
necessary to run Terraform that is typically not comitted to version
control.
This command is always safe to run multiple times. Though subsequent runs
may give errors, this command will never blow away your environment or state.
Even so, if you have important information, please back it up prior to
running this command just in case.
If no arguments are given, the configuration in this working directory
is initialized.
If one or two arguments are given, the first is a SOURCE of a module to
download to the second argument PATH. After downloading the module to PATH,
the configuration will be initialized as if this command were called pointing
only to that PATH. PATH must be empty of any Terraform files. Any
conflicting non-Terraform files will be overwritten. The module download
is a copy. If you're downloading a module from Git, it will not preserve
Git history.
Options:
-backend=atlas Specifies the type of remote backend. If not
specified, local storage will be used.
-backend=true Configure the backend for this environment.
-backend-config="k=v" Specifies configuration for the remote storage
backend. This can be specified multiple times.
-backend-config=path A path to load additional configuration for the backend.
This is merged with what is in the configuration file.
-no-color If specified, output won't contain any color.
-get=true Download any modules for this configuration.
-input=true Ask for input if necessary. If false, will error if
input was required.
-no-color If specified, output won't contain any color.
`
return strings.TrimSpace(helpText)
}
func (c *InitCommand) Synopsis() string {
return "Initializes Terraform configuration from a module"
return "Initialize a new or existing Terraform configuration"
}
const errInitCopyNotEmpty = `
The destination path contains Terraform configuration files. The init command
with a SOURCE parameter can only be used on a directory without existing
Terraform files.
Please resolve this issue and try again.
`
const outputInitEmpty = `
[reset][bold]Terraform initialized in an empty directory![reset]
The directory has no Terraform configuration files. You may begin working
with Terraform immediately by creating Terraform configuration files.
`
const outputInitSuccess = `
[reset][bold][green]Terraform has been successfully initialized![reset][green]
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your environment. If you forget, other
commands will detect it and remind you to do so if necessary.
`

View File

@ -3,9 +3,10 @@ package command
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/helper/copy"
"github.com/mitchellh/cli"
)
@ -69,6 +70,27 @@ func TestInit_cwd(t *testing.T) {
}
}
func TestInit_empty(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
os.MkdirAll(td, 0755)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestInit_multipleArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
@ -87,21 +109,6 @@ func TestInit_multipleArgs(t *testing.T) {
}
}
func TestInit_noArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
}
}
// https://github.com/hashicorp/terraform/issues/518
func TestInit_dstInSrc(t *testing.T) {
dir := tempDir(t)
@ -144,6 +151,148 @@ func TestInit_dstInSrc(t *testing.T) {
}
}
func TestInit_get(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-get"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// Check output
output := ui.OutputWriter.String()
if !strings.Contains(output, "Get: file://") {
t.Fatalf("doesn't look like get: %s", output)
}
}
func TestInit_copyGet(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
os.MkdirAll(td, 0755)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
testFixturePath("init-get"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// Check copy
if _, err := os.Stat("main.tf"); err != nil {
t.Fatalf("err: %s", err)
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "Get: file://") {
t.Fatalf("doesn't look like get: %s", output)
}
}
func TestInit_backend(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-backend"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestInit_backendConfigFile(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-backend-config-file"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{"-backend-config", "input.config"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// Read our saved backend config and verify we have our settings
state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
if v := state.Backend.Config["path"]; v != "hello" {
t.Fatalf("bad: %#v", v)
}
}
func TestInit_copyBackendDst(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
os.MkdirAll(td, 0755)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
testFixturePath("init-backend"),
"dst",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
if _, err := os.Stat(filepath.Join(
"dst", DefaultDataDir, DefaultStateFilename)); err != nil {
t.Fatalf("err: %s", err)
}
}
/*
func TestInit_remoteState(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
@ -287,3 +436,4 @@ func TestInit_remoteStateWithRemote(t *testing.T) {
t.Fatalf("should have failed: \n%s", ui.OutputWriter.String())
}
}
*/

View File

@ -10,16 +10,13 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/helper/variables"
"github.com/hashicorp/terraform/helper/wrappedstreams"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
@ -27,21 +24,31 @@ import (
// Meta are the meta-options that are available on all or most commands.
type Meta struct {
Color bool
ContextOpts *terraform.ContextOpts
Ui cli.Ui
// The exported fields below should be set by anyone using a
// command with a Meta field. These are expected to be set externally
// (not from within the command itself).
// State read when calling `Context`. This is available after calling
// `Context`.
state state.State
stateResult *StateResult
Color bool // True if output should be colored
ContextOpts *terraform.ContextOpts // Opts copied to initialize
Ui cli.Ui // Ui for output
// This can be set by the command itself to provide extra hooks.
extraHooks []terraform.Hook
// ExtraHooks are extra hooks to add to the context.
ExtraHooks []terraform.Hook
// This can be set by tests to change some directories
//----------------------------------------------------------
// Protected: commands can set these
//----------------------------------------------------------
// Modify the data directory location. Defaults to DefaultDataDir
dataDir string
//----------------------------------------------------------
// Private: do not set these
//----------------------------------------------------------
// backendState is the currently active backend state
backendState *terraform.BackendState
// Variables for the context (private)
autoKey string
autoVariables map[string]interface{}
@ -51,6 +58,7 @@ type Meta struct {
// Targets for this context (private)
targets []string
// Internal fields
color bool
oldUi cli.Ui
@ -111,103 +119,6 @@ func (m *Meta) Colorize() *colorstring.Colorize {
}
}
// Context returns a Terraform Context taking into account the context
// options used to initialize this meta configuration.
func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
opts := m.contextOpts()
// First try to just read the plan directly from the path given.
f, err := os.Open(copts.Path)
if err == nil {
plan, err := terraform.ReadPlan(f)
f.Close()
if err == nil {
// Setup our state, force it to use our plan's state
stateOpts := m.StateOpts()
if plan != nil {
stateOpts.ForceState = plan.State
}
// Get the state
result, err := State(stateOpts)
if err != nil {
return nil, false, fmt.Errorf("Error loading plan: %s", err)
}
// Set our state
m.state = result.State
// this is used for printing the saved location later
if m.stateOutPath == "" {
m.stateOutPath = result.StatePath
}
if len(m.variables) > 0 {
return nil, false, fmt.Errorf(
"You can't set variables with the '-var' or '-var-file' flag\n" +
"when you're applying a plan file. The variables used when\n" +
"the plan was created will be used. If you wish to use different\n" +
"variable values, create a new plan file.")
}
ctx, err := plan.Context(opts)
return ctx, true, err
}
}
// Load the statePath if not given
if copts.StatePath != "" {
m.statePath = copts.StatePath
}
// Tell the context if we're in a destroy plan / apply
opts.Destroy = copts.Destroy
// Store the loaded state
state, err := m.State()
if err != nil {
return nil, false, err
}
// Load the root module
var mod *module.Tree
if copts.Path != "" {
mod, err = module.NewTreeModule("", copts.Path)
// Check for the error where we have no config files but
// allow that. If that happens, clear the error.
if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) &&
copts.PathEmptyOk {
log.Printf(
"[WARN] Empty configuration dir, ignoring: %s", copts.Path)
err = nil
mod = module.NewEmptyTree()
}
if err != nil {
return nil, false, fmt.Errorf("Error loading config: %s", err)
}
} else {
mod = module.NewEmptyTree()
}
err = mod.Load(m.moduleStorage(m.DataDir()), copts.GetMode)
if err != nil {
return nil, false, fmt.Errorf("Error downloading modules: %s", err)
}
// Validate the module right away
if err := mod.Validate(); err != nil {
return nil, false, err
}
opts.Module = mod
opts.Parallelism = copts.Parallelism
opts.State = state.State()
ctx, err := terraform.NewContext(opts)
return ctx, false, err
}
// DataDir returns the directory where local data will be stored.
func (m *Meta) DataDir() string {
dataDir := DefaultDataDir
@ -248,53 +159,6 @@ func (m *Meta) InputMode() terraform.InputMode {
return mode
}
// State returns the state for this meta.
func (m *Meta) State() (state.State, error) {
if m.state != nil {
return m.state, nil
}
result, err := State(m.StateOpts())
if err != nil {
return nil, err
}
m.state = result.State
m.stateOutPath = result.StatePath
m.stateResult = result
return m.state, nil
}
// StateRaw is used to setup the state manually.
func (m *Meta) StateRaw(opts *StateOpts) (*StateResult, error) {
result, err := State(opts)
if err != nil {
return nil, err
}
m.state = result.State
m.stateOutPath = result.StatePath
m.stateResult = result
return result, nil
}
// StateOpts returns the default state options
func (m *Meta) StateOpts() *StateOpts {
localPath := m.statePath
if localPath == "" {
localPath = DefaultStateFilename
}
remotePath := filepath.Join(m.DataDir(), DefaultStateFilename)
return &StateOpts{
LocalPath: localPath,
LocalPathOut: m.stateOutPath,
RemotePath: remotePath,
RemoteRefresh: true,
BackupPath: m.backupPath,
}
}
// UIInput returns a UIInput object to be used for asking for input.
func (m *Meta) UIInput() terraform.UIInput {
return &UIInput{
@ -302,21 +166,6 @@ func (m *Meta) UIInput() terraform.UIInput {
}
}
// PersistState is used to write out the state, handling backup of
// the existing state file and respecting path configurations.
func (m *Meta) PersistState(s *terraform.State) error {
if err := m.state.WriteState(s); err != nil {
return err
}
return m.state.PersistState()
}
// Input returns true if we should ask for input for context.
func (m *Meta) Input() bool {
return !test && m.input && len(m.variables) == 0
}
// StdinPiped returns true if the input is piped.
func (m *Meta) StdinPiped() bool {
fi, err := wrappedstreams.Stdin().Stat()
@ -331,11 +180,16 @@ func (m *Meta) StdinPiped() bool {
// contextOpts returns the options to use to initialize a Terraform
// context with the settings from this Meta.
func (m *Meta) contextOpts() *terraform.ContextOpts {
var opts terraform.ContextOpts = *m.ContextOpts
var opts terraform.ContextOpts
if v := m.ContextOpts; v != nil {
opts = *v
}
opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}}
opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...)
opts.Hooks = append(opts.Hooks, m.extraHooks...)
if m.ContextOpts != nil {
opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...)
}
opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
vs := make(map[string]interface{})
for k, v := range opts.Variables {
@ -350,6 +204,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
opts.Variables = vs
opts.Targets = m.targets
opts.UIInput = m.UIInput()
opts.Parallelism = m.parallelism
opts.Shadow = m.shadow
return &opts
@ -469,6 +324,24 @@ func (m *Meta) uiHook() *UiHook {
}
}
// confirm asks a yes/no confirmation.
func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) {
for {
v, err := m.UIInput().Input(opts)
if err != nil {
return false, fmt.Errorf(
"Error asking for confirmation: %s", err)
}
switch strings.ToLower(v) {
case "no":
return false, nil
case "yes":
return true, nil
}
}
}
const (
// ModuleDepthDefault is the default value for
// module depth, which can be overridden by flag
@ -530,28 +403,3 @@ func (m *Meta) outputShadowError(err error, output bool) bool {
return true
}
// contextOpts are the options used to load a context from a command.
type contextOpts struct {
// Path to the directory where the root module is.
//
// PathEmptyOk, when set, will allow paths that have no Terraform
// configurations. The result in that case will be an empty module.
Path string
PathEmptyOk bool
// StatePath is the path to the state file. If this is empty, then
// no state will be loaded. It is also okay for this to be a path to
// a file that doesn't exist; it is assumed that this means that there
// is simply no state.
StatePath string
// GetMode is the module.GetMode to use when loading the module tree.
GetMode module.GetMode
// Set to true when running a destroy plan/apply.
Destroy bool
// Number of concurrent operations allowed
Parallelism int
}

1668
command/meta_backend.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,194 @@
package command
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
)
// backendMigrateState handles migrating (copying) state from one backend
// to another. This function handles asking the user for confirmation
// as well as the copy itself.
//
// This function can handle all scenarios of state migration regardless
// of the existence of state in either backend.
//
// After migrating the state, the existing state in the first backend
// remains untouched.
func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
one := opts.One.State()
two := opts.Two.State()
var confirmFunc func(opts *backendMigrateOpts) (bool, error)
switch {
// No migration necessary
case one.Empty() && two.Empty():
return nil
// No migration necessary if we're inheriting state.
case one.Empty() && !two.Empty():
return nil
// We have existing state moving into no state. Ask the user if
// they'd like to do this.
case !one.Empty() && two.Empty():
confirmFunc = m.backendMigrateEmptyConfirm
// Both states are non-empty, meaning we need to determine which
// state should be used and update accordingly.
case !one.Empty() && !two.Empty():
confirmFunc = m.backendMigrateNonEmptyConfirm
}
if confirmFunc == nil {
panic("confirmFunc must not be nil")
}
// Confirm with the user whether we want to copy state over
confirm, err := confirmFunc(opts)
if err != nil {
return err
}
if !confirm {
return nil
}
// Confirmed! Write.
if err := opts.Two.WriteState(one); err != nil {
return fmt.Errorf(strings.TrimSpace(errBackendStateCopy),
opts.OneType, opts.TwoType, err)
}
if err := opts.Two.PersistState(); err != nil {
return fmt.Errorf(strings.TrimSpace(errBackendStateCopy),
opts.OneType, opts.TwoType, err)
}
// And we're done.
return nil
}
func (m *Meta) backendMigrateEmptyConfirm(opts *backendMigrateOpts) (bool, error) {
inputOpts := &terraform.InputOpts{
Id: "backend-migrate-to-backend",
Query: fmt.Sprintf(
"Do you want to copy state from %q to %q?",
opts.OneType, opts.TwoType),
Description: fmt.Sprintf(
strings.TrimSpace(inputBackendMigrateEmpty),
opts.OneType, opts.TwoType),
}
// Confirm with the user that the copy should occur
for {
v, err := m.UIInput().Input(inputOpts)
if err != nil {
return false, fmt.Errorf(
"Error asking for state copy action: %s", err)
}
switch strings.ToLower(v) {
case "no":
return false, nil
case "yes":
return true, nil
}
}
}
func (m *Meta) backendMigrateNonEmptyConfirm(opts *backendMigrateOpts) (bool, error) {
// We need to grab both states so we can write them to a file
one := opts.One.State()
two := opts.Two.State()
// Save both to a temporary
td, err := ioutil.TempDir("", "terraform")
if err != nil {
return false, fmt.Errorf("Error creating temporary directory: %s", err)
}
defer os.RemoveAll(td)
// Helper to write the state
saveHelper := func(n, path string, s *terraform.State) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return terraform.WriteState(s, f)
}
// Write the states
onePath := filepath.Join(td, fmt.Sprintf("1-%s.tfstate", opts.OneType))
twoPath := filepath.Join(td, fmt.Sprintf("2-%s.tfstate", opts.TwoType))
if err := saveHelper(opts.OneType, onePath, one); err != nil {
return false, fmt.Errorf("Error saving temporary state: %s", err)
}
if err := saveHelper(opts.TwoType, twoPath, two); err != nil {
return false, fmt.Errorf("Error saving temporary state: %s", err)
}
// Ask for confirmation
inputOpts := &terraform.InputOpts{
Id: "backend-migrate-to-backend",
Query: fmt.Sprintf(
"Do you want to copy state from %q to %q?",
opts.OneType, opts.TwoType),
Description: fmt.Sprintf(
strings.TrimSpace(inputBackendMigrateNonEmpty),
opts.OneType, opts.TwoType, onePath, twoPath),
}
// Confirm with the user that the copy should occur
for {
v, err := m.UIInput().Input(inputOpts)
if err != nil {
return false, fmt.Errorf(
"Error asking for state copy action: %s", err)
}
switch strings.ToLower(v) {
case "no":
return false, nil
case "yes":
return true, nil
}
}
}
type backendMigrateOpts struct {
OneType, TwoType string
One, Two state.State
}
const errBackendStateCopy = `
Error copying state from %q to %q: %s
The state in %[1]q remains intact and unmodified. Please resolve the
error above and try again.
`
const inputBackendMigrateEmpty = `
Pre-existing state was found in %q while migrating to %q. No existing
state was found in %[2]q. Do you want to copy the state from %[1]q to
%[2]q? Enter "yes" to copy and "no" to start with an empty state.
`
const inputBackendMigrateNonEmpty = `
Pre-existing state was found in %q while migrating to %q. An existing
non-empty state exists in %[2]q. The two states have been saved to temporary
files that will be removed after responding to this query.
One (%[1]q): %[3]s
Two (%[2]q): %[4]s
Do you want to copy the state from %[1]q to %[2]q? Enter "yes" to copy
and "no" to start with the existing state in %[2]q.
`

2816
command/meta_backend_test.go Normal file

File diff suppressed because it is too large Load Diff

101
command/meta_new.go Normal file
View File

@ -0,0 +1,101 @@
package command
import (
"fmt"
"os"
"strconv"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
)
// NOTE: Temporary file until this branch is cleaned up.
// Input returns whether or not input asking is enabled.
func (m *Meta) Input() bool {
if test || !m.input {
return false
}
if envVar := os.Getenv(InputModeEnvVar); envVar != "" {
if v, err := strconv.ParseBool(envVar); err == nil && !v {
return false
}
}
return true
}
// Module loads the module tree for the given root path.
//
// It expects the modules to already be downloaded. This will never
// download any modules.
func (m *Meta) Module(path string) (*module.Tree, error) {
mod, err := module.NewTreeModule("", path)
if err != nil {
// Check for the error where we have no config files
if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) {
return nil, nil
}
return nil, err
}
err = mod.Load(m.moduleStorage(m.DataDir()), module.GetModeNone)
if err != nil {
return nil, fmt.Errorf("Error loading modules: %s", err)
}
return mod, nil
}
// Plan returns the plan for the given path.
//
// This only has an effect if the path itself looks like a plan.
// If error is nil and the plan is nil, then the path didn't look like
// a plan.
//
// Error will be non-nil if path looks like a plan and loading the plan
// failed.
func (m *Meta) Plan(path string) (*terraform.Plan, error) {
// Open the path no matter if its a directory or file
f, err := os.Open(path)
defer f.Close()
if err != nil {
return nil, fmt.Errorf(
"Failed to load Terraform configuration or plan: %s", err)
}
// Stat it so we can check if its a directory
fi, err := f.Stat()
if err != nil {
return nil, fmt.Errorf(
"Failed to load Terraform configuration or plan: %s", err)
}
// If this path is a directory, then it can't be a plan. Not an error.
if fi.IsDir() {
return nil, nil
}
// Read the plan
p, err := terraform.ReadPlan(f)
if err != nil {
return nil, err
}
// We do a validation here that seems odd but if any plan is given,
// we must not have set any extra variables. The plan itself contains
// the variables and those aren't overwritten.
if len(m.variables) > 0 {
return nil, fmt.Errorf(
"You can't set variables with the '-var' or '-var-file' flag\n" +
"when you're applying a plan file. The variables used when\n" +
"the plan was created will be used. If you wish to use different\n" +
"variable values, create a new plan file.")
}
return p, nil
}

View File

@ -20,13 +20,11 @@ func (c *OutputCommand) Run(args []string) int {
var module string
var jsonOutput bool
cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError)
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&module, "module", "", "module")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
@ -45,9 +43,17 @@ func (c *OutputCommand) Run(args []string) int {
name = args[0]
}
stateStore, err := c.Meta.State()
// Load the backend
b, err := c.Backend(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// Get the state
stateStore, err := b.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
@ -62,7 +68,6 @@ func (c *OutputCommand) Run(args []string) int {
state := stateStore.State()
mod := state.ModuleByPath(modPath)
if mod == nil {
c.Ui.Error(fmt.Sprintf(
"The module %s could not be found. There is nothing to output.",
@ -211,6 +216,7 @@ func formatNestedMap(indent string, outputMap map[string]interface{}) string {
return strings.TrimPrefix(outputBuf.String(), "\n")
}
func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string {
ks := make([]string, 0, len(outputMap))
for k, _ := range outputMap {

View File

@ -1,13 +1,12 @@
package command
import (
"context"
"fmt"
"log"
"os"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
)
// PlanCommand is a Command implementation that compares a Terraform
@ -30,153 +29,88 @@ func (c *PlanCommand) Run(args []string) int {
cmdFlags.StringVar(&outPath, "out", "", "path")
cmdFlags.IntVar(
&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
var path string
args = cmdFlags.Args()
if len(args) > 1 {
c.Ui.Error(
"The plan command expects at most one argument with the path\n" +
"to a Terraform configuration.\n")
cmdFlags.Usage()
return 1
} else if len(args) == 1 {
path = args[0]
} else {
var err error
path, err = os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
}
}
countHook := new(CountHook)
c.Meta.extraHooks = []terraform.Hook{countHook}
// This is going to keep track of shadow errors
var shadowErr error
ctx, planned, err := c.Context(contextOpts{
Destroy: destroy,
Path: path,
StatePath: c.Meta.statePath,
Parallelism: c.Meta.parallelism,
})
configPath, err := ModulePath(cmdFlags.Args())
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if planned {
c.Ui.Output(c.Colorize().Color(
"[reset][bold][yellow]" +
"The plan command received a saved plan file as input. This command\n" +
"will output the saved plan. This will not modify the already-existing\n" +
"plan. If you wish to generate a new plan, please pass in a configuration\n" +
"directory as an argument.\n\n"))
// Check if the path is a plan
plan, err := c.Plan(configPath)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if plan != nil {
// Disable refreshing no matter what since we only want to show the plan
refresh = false
// Set the config path to empty for backend loading
configPath = ""
}
err = terraform.SetDebugInfo(DefaultDataDir)
// Load the module if we don't have one yet (not running from plan)
var mod *module.Tree
if plan == nil {
mod, err = c.Module(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
}
// Load the backend
b, err := c.Backend(&BackendOpts{
ConfigPath: configPath,
Plan: plan,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// Build the operation
opReq := c.Operation()
opReq.Destroy = destroy
opReq.Module = mod
opReq.Plan = plan
opReq.PlanRefresh = refresh
opReq.PlanOutPath = outPath
opReq.Type = backend.OperationTypePlan
// Perform the operation
op, err := b.Operation(context.Background(), opReq)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err))
return 1
}
// Wait for the operation to complete
<-op.Done()
if err := op.Err; err != nil {
c.Ui.Error(err.Error())
return 1
}
if err := ctx.Input(c.InputMode()); err != nil {
c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
return 1
}
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "input operation:"))
}
if !validateContext(ctx, c.Ui) {
return 1
}
if refresh {
c.Ui.Output("Refreshing Terraform state in-memory prior to plan...")
c.Ui.Output("The refreshed state will be used to calculate this plan, but")
c.Ui.Output("will not be persisted to local or remote state storage.\n")
_, err := ctx.Refresh()
/*
err = terraform.SetDebugInfo(DefaultDataDir)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
c.Ui.Error(err.Error())
return 1
}
c.Ui.Output("")
}
*/
plan, err := ctx.Plan()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
return 1
}
if outPath != "" {
log.Printf("[INFO] Writing plan output to: %s", outPath)
f, err := os.Create(outPath)
if err == nil {
defer f.Close()
err = terraform.WritePlan(plan, f)
}
if err != nil {
c.Ui.Error(fmt.Sprintf("Error writing plan file: %s", err))
return 1
}
}
if plan.Diff.Empty() {
c.Ui.Output(
"No changes. Infrastructure is up-to-date. This means that Terraform\n" +
"could not detect any differences between your configuration and\n" +
"the real physical resources that exist. As a result, Terraform\n" +
"doesn't need to do anything.")
return 0
}
if outPath == "" {
c.Ui.Output(strings.TrimSpace(planHeaderNoOutput) + "\n")
} else {
c.Ui.Output(fmt.Sprintf(
strings.TrimSpace(planHeaderYesOutput)+"\n",
outPath))
}
c.Ui.Output(FormatPlan(&FormatPlanOpts{
Plan: plan,
Color: c.Colorize(),
ModuleDepth: moduleDepth,
}))
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold]Plan:[reset] "+
"%d to add, %d to change, %d to destroy.",
countHook.ToAdd+countHook.ToRemoveAndAdd,
countHook.ToChange,
countHook.ToRemove+countHook.ToRemoveAndAdd)))
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "plan operation:"))
}
// If we have an error in the shadow graph, let the user know.
c.outputShadowError(shadowErr, true)
if detailed {
if detailed && !op.PlanEmpty {
return 2
}
return 0
}
@ -241,28 +175,3 @@ Options:
func (c *PlanCommand) Synopsis() string {
return "Generate and show an execution plan"
}
const planHeaderNoOutput = `
The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed. Cyan entries are data sources to be read.
Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.
`
const planHeaderYesOutput = `
The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed. Cyan entries are data sources to be read.
Your plan was also saved to the path below. Call the "apply" subcommand
with this plan file and Terraform will exactly execute this execution
plan.
Path: %s
`

View File

@ -5,9 +5,11 @@ import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/hashicorp/terraform/helper/copy"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
@ -239,6 +241,128 @@ func TestPlan_outPathNoChange(t *testing.T) {
}
}
// When using "-out" with a backend, the plan should encode the backend config
func TestPlan_outBackend(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("plan-out-backend"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// Our state
originalState := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
},
},
},
},
},
}
originalState.Init()
// Setup our backend state
dataState, srv := testBackendState(t, originalState, 200)
defer srv.Close()
testStateFileRemote(t, dataState)
outPath := "foo"
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-out", outPath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
plan := testReadPlan(t, outPath)
if !plan.Diff.Empty() {
t.Fatalf("Expected empty plan to be written to plan file, got: %s", plan)
}
if plan.Backend.Empty() {
t.Fatal("should have backend info")
}
if !reflect.DeepEqual(plan.Backend, dataState.Backend) {
t.Fatalf("bad: %#v", plan.Backend)
}
}
// When using "-out" with a legacy remote state, the plan should encode
// the backend config
func TestPlan_outBackendLegacy(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("plan-out-backend-legacy"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// Our state
originalState := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
},
},
},
},
},
}
originalState.Init()
// Setup our legacy state
remoteState, srv := testRemoteState(t, originalState, 200)
defer srv.Close()
dataState := terraform.NewState()
dataState.Remote = remoteState
testStateFileRemote(t, dataState)
outPath := "foo"
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-out", outPath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
plan := testReadPlan(t, outPath)
if !plan.Diff.Empty() {
t.Fatalf("Expected empty plan to be written to plan file, got: %s", plan)
}
if plan.State.Remote.Empty() {
t.Fatal("should have remote info")
}
}
func TestPlan_refresh(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)

View File

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/atlas-go/archive"
"github.com/hashicorp/atlas-go/v1"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/terraform"
)
@ -63,59 +64,86 @@ func (c *PushCommand) Run(args []string) int {
}
}
// The pwd is used for the configuration path if one is not given
pwd, err := os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
return 1
}
// Get the path to the configuration depending on the args.
var configPath string
args = cmdFlags.Args()
if len(args) > 1 {
c.Ui.Error("The apply command expects at most one argument.")
cmdFlags.Usage()
return 1
} else if len(args) == 1 {
configPath = args[0]
} else {
configPath = pwd
}
// Verify the state is remote, we can't push without a remote state
s, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
return 1
}
if !s.State().IsRemote() {
c.Ui.Error(
"Remote state is not enabled. For Atlas to run Terraform\n" +
"for you, remote state must be used and configured. Remote\n" +
"state via any backend is accepted, not just Atlas. To\n" +
"configure remote state, use the `terraform remote config`\n" +
"command.")
return 1
}
// Build the context based on the arguments given
ctx, planned, err := c.Context(contextOpts{
Path: configPath,
StatePath: c.Meta.statePath,
})
configPath, err := ModulePath(cmdFlags.Args())
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if planned {
/*
// Verify the state is remote, we can't push without a remote state
s, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
return 1
}
if !s.State().IsRemote() {
c.Ui.Error(
"Remote state is not enabled. For Atlas to run Terraform\n" +
"for you, remote state must be used and configured. Remote\n" +
"state via any backend is accepted, not just Atlas. To\n" +
"configure remote state, use the `terraform remote config`\n" +
"command.")
return 1
}
*/
// Check if the path is a plan
plan, err := c.Plan(configPath)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if plan != nil {
c.Ui.Error(
"A plan file cannot be given as the path to the configuration.\n" +
"A path to a module (directory with configuration) must be given.")
return 1
}
// Load the module
mod, err := c.Module(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
if mod == nil {
c.Ui.Error(fmt.Sprintf(
"No configuration files found in the directory: %s\n\n"+
"This command requires configuration to run.",
configPath))
return 1
}
// Load the backend
b, err := c.Backend(&BackendOpts{
ConfigPath: configPath,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// We require a local backend
local, ok := b.(backend.Local)
if !ok {
c.Ui.Error(ErrUnsupportedLocalOp)
return 1
}
// Build the operation
opReq := c.Operation()
opReq.Module = mod
opReq.Plan = plan
// Get the context
ctx, _, err := local.Context(opReq)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
// Get the configuration
config := ctx.Module().Config()
if name == "" {

View File

@ -1,12 +1,11 @@
package command
import (
"context"
"fmt"
"log"
"os"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/terraform"
)
@ -29,111 +28,50 @@ func (c *RefreshCommand) Run(args []string) int {
return 1
}
var configPath string
args = cmdFlags.Args()
if len(args) > 1 {
c.Ui.Error("The refresh command expects at most one argument.")
cmdFlags.Usage()
return 1
} else if len(args) == 1 {
configPath = args[0]
} else {
var err error
configPath, err = os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
}
}
// Check if remote state is enabled
state, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
// Verify that the state path exists. The "ContextArg" function below
// will actually do this, but we want to provide a richer error message
// if possible.
if !state.State().IsRemote() {
if _, err := os.Stat(c.Meta.statePath); err != nil {
if os.IsNotExist(err) {
c.Ui.Error(fmt.Sprintf(
"The Terraform state file for your infrastructure does not\n"+
"exist. The 'refresh' command only works and only makes sense\n"+
"when there is existing state that Terraform is managing. Please\n"+
"double-check the value given below and try again. If you\n"+
"haven't created infrastructure with Terraform yet, use the\n"+
"'terraform apply' command.\n\n"+
"Path: %s",
c.Meta.statePath))
return 1
}
c.Ui.Error(fmt.Sprintf(
"There was an error reading the Terraform state that is needed\n"+
"for refreshing. The path and error are shown below.\n\n"+
"Path: %s\n\nError: %s",
c.Meta.statePath,
err))
return 1
}
}
// This is going to keep track of shadow errors
var shadowErr error
// Build the context based on the arguments given
ctx, _, err := c.Context(contextOpts{
Path: configPath,
StatePath: c.Meta.statePath,
Parallelism: c.Meta.parallelism,
})
configPath, err := ModulePath(cmdFlags.Args())
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if err := ctx.Input(c.InputMode()); err != nil {
c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
return 1
}
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "input operation:"))
}
if !validateContext(ctx, c.Ui) {
return 1
}
newState, err := ctx.Refresh()
// Load the module
mod, err := c.Module(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
if err := c.Meta.PersistState(newState); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
// Load the backend
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
if outputs := outputsAsString(newState, terraform.RootModulePath, ctx.Module().Config().Outputs, true); outputs != "" {
// Build the operation
opReq := c.Operation()
opReq.Type = backend.OperationTypeRefresh
opReq.Module = mod
// Perform the operation
op, err := b.Operation(context.Background(), opReq)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err))
return 1
}
// Wait for the operation to complete
<-op.Done()
if err := op.Err; err != nil {
c.Ui.Error(err.Error())
return 1
}
// Output the outputs
if outputs := outputsAsString(op.State, terraform.RootModulePath, nil, true); outputs != "" {
c.Ui.Output(c.Colorize().Color(outputs))
}
// Record any shadow errors for later
if err := ctx.ShadowError(); err != nil {
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
err, "refresh operation:"))
}
// If we have an error in the shadow graph, let the user know.
c.outputShadowError(shadowErr, true)
return 0
}

View File

@ -712,6 +712,10 @@ func TestRefresh_disableBackup(t *testing.T) {
if err == nil || !os.IsNotExist(err) {
t.Fatalf("backup should not exist")
}
_, err = os.Stat("-")
if err == nil || !os.IsNotExist(err) {
t.Fatalf("backup should not exist")
}
}
func TestRefresh_displaysOutputs(t *testing.T) {

View File

@ -1,61 +0,0 @@
package command
import (
"strings"
)
type RemoteCommand struct {
Meta
}
func (c *RemoteCommand) Run(argsRaw []string) int {
// Duplicate the args so we can munge them without affecting
// future subcommand invocations which will do the same.
args := make([]string, len(argsRaw))
copy(args, argsRaw)
args = c.Meta.process(args, false)
if len(args) == 0 {
c.Ui.Error(c.Help())
return 1
}
switch args[0] {
case "config":
cmd := &RemoteConfigCommand{Meta: c.Meta}
return cmd.Run(args[1:])
case "pull":
cmd := &RemotePullCommand{Meta: c.Meta}
return cmd.Run(args[1:])
case "push":
cmd := &RemotePushCommand{Meta: c.Meta}
return cmd.Run(args[1:])
default:
c.Ui.Error(c.Help())
return 1
}
}
func (c *RemoteCommand) Help() string {
helpText := `
Usage: terraform remote <subcommand> [options]
Configure remote state storage with Terraform.
Options:
-no-color If specified, output won't contain any color.
Available subcommands:
config Configure the remote storage settings.
pull Sync the remote storage by downloading to local storage.
push Sync the remote storage by uploading the local storage.
`
return strings.TrimSpace(helpText)
}
func (c *RemoteCommand) Synopsis() string {
return "Configure remote state storage"
}

View File

@ -1,385 +0,0 @@
package command
import (
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
// remoteCommandConfig is used to encapsulate our configuration
type remoteCommandConfig struct {
disableRemote bool
pullOnDisable bool
statePath string
backupPath string
}
// RemoteConfigCommand is a Command implementation that is used to
// enable and disable remote state management
type RemoteConfigCommand struct {
Meta
conf remoteCommandConfig
remoteConf *terraform.RemoteState
}
func (c *RemoteConfigCommand) Run(args []string) int {
// we expect a zero struct value here, but it's not explicitly set in tests
if c.remoteConf == nil {
c.remoteConf = &terraform.RemoteState{}
}
args = c.Meta.process(args, false)
config := make(map[string]string)
cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError)
cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "")
cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "")
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
cmdFlags.Var((*FlagStringKV)(&config), "backend-config", "config")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("\nError parsing CLI flags: %s", err))
return 1
}
// Lowercase the type
c.remoteConf.Type = strings.ToLower(c.remoteConf.Type)
// Set the local state path
c.statePath = c.conf.statePath
// Populate the various configurations
c.remoteConf.Config = config
// Get the state information. We specifically request the cache only
// for the remote state here because it is possible the remote state
// is invalid and we don't want to error.
stateOpts := c.StateOpts()
stateOpts.RemoteCacheOnly = true
if _, err := c.StateRaw(stateOpts); err != nil {
c.Ui.Error(fmt.Sprintf("Error loading local state: %s", err))
return 1
}
// Get the local and remote [cached] state
localState := c.stateResult.Local.State()
var remoteState *terraform.State
if remote := c.stateResult.Remote; remote != nil {
remoteState = remote.State()
}
// Check if remote state is being disabled
if c.conf.disableRemote {
if !remoteState.IsRemote() {
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
return 1
}
if !localState.Empty() {
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
c.conf.statePath))
return 1
}
return c.disableRemoteState()
}
// Ensure there is no conflict, and then do the correct operation
var result int
haveCache := !remoteState.Empty()
haveLocal := !localState.Empty()
switch {
case haveCache && haveLocal:
c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
c.conf.statePath))
result = 1
case !haveCache && !haveLocal:
// If we don't have either state file, initialize a blank state file
result = c.initBlankState()
case haveCache && !haveLocal:
// Update the remote state target potentially
result = c.updateRemoteConfig()
case !haveCache && haveLocal:
// Enable remote state management
result = c.enableRemoteState()
}
// If there was an error, return right away
if result != 0 {
return result
}
// If we're not pulling, then do nothing
if !c.conf.pullOnDisable {
return result
}
// Otherwise, refresh the state
stateResult, err := c.StateRaw(c.StateOpts())
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error while performing the initial pull. The error message is shown\n"+
"below. Note that remote state was properly configured, so you don't\n"+
"need to reconfigure. You can now use `push` and `pull` directly.\n"+
"\n%s", err))
return 1
}
state := stateResult.State
if err := state.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error while performing the initial pull. The error message is shown\n"+
"below. Note that remote state was properly configured, so you don't\n"+
"need to reconfigure. You can now use `push` and `pull` directly.\n"+
"\n%s", err))
return 1
}
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]Remote state configured and pulled.")))
return 0
}
// disableRemoteState is used to disable remote state management,
// and move the state file into place.
func (c *RemoteConfigCommand) disableRemoteState() int {
if c.stateResult == nil {
c.Ui.Error(fmt.Sprintf(
"Internal error. State() must be called internally before remote\n" +
"state can be disabled. Please report this as a bug."))
return 1
}
if !c.stateResult.State.State().IsRemote() {
c.Ui.Error(fmt.Sprintf(
"Remote state is not enabled. Can't disable remote state."))
return 1
}
local := c.stateResult.Local
remote := c.stateResult.Remote
// Ensure we have the latest state before disabling
if c.conf.pullOnDisable {
log.Printf("[INFO] Refreshing local state from remote server")
if err := remote.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %s", err))
return 1
}
// Exit if we were unable to update
if change := remote.RefreshResult(); !change.SuccessfulPull() {
c.Ui.Error(fmt.Sprintf("%s", change))
return 1
} else {
log.Printf("[INFO] %s", change)
}
}
// Clear the remote management, and copy into place
newState := remote.State()
newState.Remote = nil
if err := local.WriteState(newState); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
c.conf.statePath, err))
return 1
}
if err := local.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
c.conf.statePath, err))
return 1
}
// Remove the old state file
if err := os.Remove(c.stateResult.RemotePath); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
return 1
}
return 0
}
// validateRemoteConfig is used to verify that the remote configuration
// we have is valid
func (c *RemoteConfigCommand) validateRemoteConfig() error {
conf := c.remoteConf
_, err := remote.NewClient(conf.Type, conf.Config)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"%s\n\n"+
"If the error message above mentions requiring or modifying configuration\n"+
"options, these are set using the `-backend-config` flag. Example:\n"+
"-backend-config=\"name=foo\" to set the `name` configuration",
err))
}
return err
}
// initBlank state is used to initialize a blank state that is
// remote enabled
func (c *RemoteConfigCommand) initBlankState() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Make a blank state, attach the remote configuration
blank := terraform.NewState()
blank.Remote = c.remoteConf
// Persist the state
remote := &state.LocalState{Path: c.stateResult.RemotePath}
if err := remote.WriteState(blank); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1
}
// Success!
c.Ui.Output("Initialized blank state with remote state enabled!")
return 0
}
// updateRemoteConfig is used to update the configuration of the
// remote state store
func (c *RemoteConfigCommand) updateRemoteConfig() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Read in the local state, which is just the cache of the remote state
remote := c.stateResult.Remote.Cache
// Update the configuration
state := remote.State()
state.Remote = c.remoteConf
if err := remote.WriteState(state); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Success!
c.Ui.Output("Remote configuration updated")
return 0
}
// enableRemoteState is used to enable remote state management
// and to move a state file into place
func (c *RemoteConfigCommand) enableRemoteState() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Read the local state
local := c.stateResult.Local
if err := local.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %s", err))
return 1
}
// Backup the state file before we modify it
backupPath := c.conf.backupPath
if backupPath != "-" {
// Provide default backup path if none provided
if backupPath == "" {
backupPath = c.conf.statePath + DefaultBackupExtension
}
log.Printf("[INFO] Writing backup state to: %s", backupPath)
backup := &state.LocalState{Path: backupPath}
if err := backup.WriteState(local.State()); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1
}
if err := backup.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1
}
}
// Update the local configuration, move into place
state := local.State()
state.Remote = c.remoteConf
remote := c.stateResult.Remote
if err := remote.WriteState(state); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Remove the original, local state file
log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
if err := os.Remove(c.conf.statePath); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",
c.conf.statePath, err))
return 1
}
// Success!
c.Ui.Output("Remote state management enabled")
return 0
}
func (c *RemoteConfigCommand) Help() string {
helpText := `
Usage: terraform remote config [options]
Configures Terraform to use a remote state server. This allows state
to be pulled down when necessary and then pushed to the server when
updated. In this mode, the state file does not need to be stored durably
since the remote server provides the durability.
Options:
-backend=Atlas Specifies the type of remote backend. Must be one
of Atlas, Consul, Etcd, GCS, HTTP, MAS, S3, or Swift.
Defaults to Atlas.
-backend-config="k=v" Specifies configuration for the remote storage
backend. This can be specified multiple times.
-backup=path Path to backup the existing state file before
modifying. Defaults to the "-state" path with
".backup" extension. Set to "-" to disable backup.
-disable Disables remote state management and migrates the state
to the -state path.
-pull=true If disabling, this controls if the remote state is
pulled before disabling. If enabling, this controls
if the remote state is pulled after enabling. This
defaults to true.
-state=path Path to read state. Defaults to "terraform.tfstate"
unless remote state is enabled.
-no-color If specified, output won't contain any color.
`
return strings.TrimSpace(helpText)
}
func (c *RemoteConfigCommand) Synopsis() string {
return "Configures remote state management"
}

View File

@ -1,449 +0,0 @@
package command
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
// Test disabling remote management
func TestRemoteConfig_disable(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
s := terraform.NewState()
s.Serial = 10
conf, srv := testRemoteState(t, s, 200)
defer srv.Close()
// Persist local remote state
s = terraform.NewState()
s.Serial = 5
s.Remote = conf
// Write the state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
state := &state.LocalState{Path: statePath}
if err := state.WriteState(s); err != nil {
t.Fatalf("err: %s", err)
}
if err := state.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{"-disable"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// Local state file should be removed and the local cache should exist
testRemoteLocal(t, true)
testRemoteLocalCache(t, false)
// Check that the state file was updated
raw, _ := ioutil.ReadFile(DefaultStateFilename)
newState, err := terraform.ReadState(bytes.NewReader(raw))
if err != nil {
t.Fatalf("err: %v", err)
}
// Ensure we updated
if newState.Remote != nil {
t.Fatalf("remote configuration not removed")
}
}
// Test disabling remote management without pulling
func TestRemoteConfig_disable_noPull(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
s := terraform.NewState()
s.Serial = 10
conf, srv := testRemoteState(t, s, 200)
defer srv.Close()
// Persist local remote state
s = terraform.NewState()
s.Serial = 5
s.Remote = conf
// Write the state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
state := &state.LocalState{Path: statePath}
if err := state.WriteState(s); err != nil {
t.Fatalf("err: %s", err)
}
if err := state.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{"-disable", "-pull=false"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// Local state file should be removed and the local cache should exist
testRemoteLocal(t, true)
testRemoteLocalCache(t, false)
// Check that the state file was updated
raw, _ := ioutil.ReadFile(DefaultStateFilename)
newState, err := terraform.ReadState(bytes.NewReader(raw))
if err != nil {
t.Fatalf("err: %v", err)
}
if newState.Remote != nil {
t.Fatalf("remote configuration not removed")
}
}
// Test disabling remote management when not enabled
func TestRemoteConfig_disable_notEnabled(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi)
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{"-disable"}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
// Test disabling remote management with a state file in the way
func TestRemoteConfig_disable_otherState(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
// Write the state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
state := &state.LocalState{Path: statePath}
if err := state.WriteState(s); err != nil {
t.Fatalf("err: %s", err)
}
if err := state.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
// Also put a file at the default path
fh, err := os.Create(DefaultStateFilename)
if err != nil {
t.Fatalf("err: %v", err)
}
err = terraform.WriteState(s, fh)
fh.Close()
if err != nil {
t.Fatalf("err: %v", err)
}
ui := new(cli.MockUi)
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{"-disable"}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
// Test the case where both managed and non managed state present
func TestRemoteConfig_managedAndNonManaged(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
// Write the state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
state := &state.LocalState{Path: statePath}
if err := state.WriteState(s); err != nil {
t.Fatalf("err: %s", err)
}
if err := state.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
// Also put a file at the default path
fh, err := os.Create(DefaultStateFilename)
if err != nil {
t.Fatalf("err: %v", err)
}
err = terraform.WriteState(s, fh)
fh.Close()
if err != nil {
t.Fatalf("err: %v", err)
}
ui := new(cli.MockUi)
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
// Test initializing blank state
func TestRemoteConfig_initBlank(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi)
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"-backend=http",
"-backend-config", "address=http://example.com",
"-backend-config", "access_token=test",
"-pull=false",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
ls := &state.LocalState{Path: remotePath}
if err := ls.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
local := ls.State()
if local.Remote.Type != "http" {
t.Fatalf("Bad: %#v", local.Remote)
}
if local.Remote.Config["address"] != "http://example.com" {
t.Fatalf("Bad: %#v", local.Remote)
}
if local.Remote.Config["access_token"] != "test" {
t.Fatalf("Bad: %#v", local.Remote)
}
}
// Test initializing without remote settings
func TestRemoteConfig_initBlank_missingRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi)
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
// Test updating remote config
func TestRemoteConfig_updateRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = &terraform.RemoteState{
Type: "invalid",
}
// Write the state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
ls := &state.LocalState{Path: statePath}
if err := ls.WriteState(s); err != nil {
t.Fatalf("err: %s", err)
}
if err := ls.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"-backend=http",
"-backend-config", "address=http://example.com",
"-backend-config", "access_token=test",
"-pull=false",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
ls = &state.LocalState{Path: remotePath}
if err := ls.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
local := ls.State()
if local.Remote.Type != "http" {
t.Fatalf("Bad: %#v", local.Remote)
}
if local.Remote.Config["address"] != "http://example.com" {
t.Fatalf("Bad: %#v", local.Remote)
}
if local.Remote.Config["access_token"] != "test" {
t.Fatalf("Bad: %#v", local.Remote)
}
}
// Test enabling remote state
func TestRemoteConfig_enableRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create a non-remote enabled state
s := terraform.NewState()
s.Serial = 5
// Add the state at the default path
fh, err := os.Create(DefaultStateFilename)
if err != nil {
t.Fatalf("err: %v", err)
}
err = terraform.WriteState(s, fh)
fh.Close()
if err != nil {
t.Fatalf("err: %v", err)
}
ui := new(cli.MockUi)
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"-backend=http",
"-backend-config", "address=http://example.com",
"-backend-config", "access_token=test",
"-pull=false",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
ls := &state.LocalState{Path: remotePath}
if err := ls.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
local := ls.State()
if local.Remote.Type != "http" {
t.Fatalf("Bad: %#v", local.Remote)
}
if local.Remote.Config["address"] != "http://example.com" {
t.Fatalf("Bad: %#v", local.Remote)
}
if local.Remote.Config["access_token"] != "test" {
t.Fatalf("Bad: %#v", local.Remote)
}
// Backup file should exist, state file should not
testRemoteLocal(t, false)
testRemoteLocalBackup(t, true)
}
func testRemoteLocal(t *testing.T, exists bool) {
_, err := os.Stat(DefaultStateFilename)
if os.IsNotExist(err) && !exists {
return
}
if err == nil && exists {
return
}
t.Fatalf("bad: %#v", err)
}
func testRemoteLocalBackup(t *testing.T, exists bool) {
_, err := os.Stat(DefaultStateFilename + DefaultBackupExtension)
if os.IsNotExist(err) && !exists {
return
}
if err == nil && exists {
return
}
if err == nil && !exists {
t.Fatal("expected local backup to exist")
}
t.Fatalf("bad: %#v", err)
}
func testRemoteLocalCache(t *testing.T, exists bool) {
_, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename))
if os.IsNotExist(err) && !exists {
return
}
if err == nil && exists {
return
}
if err == nil && !exists {
t.Fatal("expected local cache to exist")
}
t.Fatalf("bad: %#v", err)
}

View File

@ -1,86 +0,0 @@
package command
import (
"flag"
"fmt"
"strings"
"github.com/hashicorp/terraform/state"
)
type RemotePullCommand struct {
Meta
}
func (c *RemotePullCommand) Run(args []string) int {
args = c.Meta.process(args, false)
cmdFlags := flag.NewFlagSet("pull", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Read out our state
s, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
return 1
}
localState := s.State()
// If remote state isn't enabled, it is a problem.
if !localState.IsRemote() {
c.Ui.Error("Remote state not enabled!")
return 1
}
// We need the CacheState structure in order to do anything
var cache *state.CacheState
if bs, ok := s.(*state.BackupState); ok {
if cs, ok := bs.Real.(*state.CacheState); ok {
cache = cs
}
}
if cache == nil {
c.Ui.Error(fmt.Sprintf(
"Failed to extract internal CacheState from remote state.\n" +
"This is an internal error, please report it as a bug."))
return 1
}
// Refresh the state
if err := cache.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %s", err))
return 1
}
// Use an error exit code if the update was not a success
change := cache.RefreshResult()
if !change.SuccessfulPull() {
c.Ui.Error(fmt.Sprintf("%s", change))
return 1
} else {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]%s", change)))
}
return 0
}
func (c *RemotePullCommand) Help() string {
helpText := `
Usage: terraform pull [options]
Refreshes the cached state file from the remote server.
Options:
-no-color If specified, output won't contain any color.
`
return strings.TrimSpace(helpText)
}
func (c *RemotePullCommand) Synopsis() string {
return "Refreshes the local state copy from the remote server"
}

View File

@ -1,116 +0,0 @@
package command
import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func TestRemotePull_noRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi)
c := &RemotePullCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestRemotePull_local(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
s := terraform.NewState()
s.Serial = 10
conf, srv := testRemoteState(t, s, 200)
s = terraform.NewState()
s.Serial = 5
s.Remote = conf
defer srv.Close()
// Store the local state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
t.Fatalf("err: %s", err)
}
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(s, f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
c := &RemotePullCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
// testRemoteState is used to make a test HTTP server to
// return a given state file
func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) {
var b64md5 string
buf := bytes.NewBuffer(nil)
cb := func(resp http.ResponseWriter, req *http.Request) {
if req.Method == "PUT" {
resp.WriteHeader(c)
return
}
if s == nil {
resp.WriteHeader(404)
return
}
resp.Header().Set("Content-MD5", b64md5)
resp.Write(buf.Bytes())
}
srv := httptest.NewServer(http.HandlerFunc(cb))
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{"address": srv.URL},
}
if s != nil {
// Set the remote data
s.Remote = remote
enc := json.NewEncoder(buf)
if err := enc.Encode(s); err != nil {
t.Fatalf("err: %v", err)
}
md5 := md5.Sum(buf.Bytes())
b64md5 = base64.StdEncoding.EncodeToString(md5[:16])
}
return remote, srv
}

View File

@ -1,96 +0,0 @@
package command
import (
"flag"
"fmt"
"strings"
"github.com/hashicorp/terraform/state"
)
type RemotePushCommand struct {
Meta
}
func (c *RemotePushCommand) Run(args []string) int {
var force bool
args = c.Meta.process(args, false)
cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError)
cmdFlags.BoolVar(&force, "force", false, "")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Read out our state
s, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
return 1
}
localState := s.State()
// If remote state isn't enabled, it is a problem.
if !localState.IsRemote() {
c.Ui.Error("Remote state not enabled!")
return 1
}
// We need the CacheState structure in order to do anything
var cache *state.CacheState
if bs, ok := s.(*state.BackupState); ok {
if cs, ok := bs.Real.(*state.CacheState); ok {
cache = cs
}
}
if cache == nil {
c.Ui.Error(fmt.Sprintf(
"Failed to extract internal CacheState from remote state.\n" +
"This is an internal error, please report it as a bug."))
return 1
}
// Refresh the cache state
if err := cache.Cache.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %s", err))
return 1
}
// Write it to the real storage
remote := cache.Durable
if err := remote.WriteState(cache.Cache.State()); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing state: %s", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error saving state: %s", err))
return 1
}
c.Ui.Output(c.Colorize().Color(
"[reset][bold][green]State successfully pushed!"))
return 0
}
func (c *RemotePushCommand) Help() string {
helpText := `
Usage: terraform push [options]
Uploads the latest state to the remote server.
Options:
-no-color If specified, output won't contain any color.
-force Forces the upload of the local state, ignoring any
conflicts. This should be used carefully, as force pushing
can cause remote state information to be lost.
`
return strings.TrimSpace(helpText)
}
func (c *RemotePushCommand) Synopsis() string {
return "Uploads the local state to the remote server"
}

View File

@ -1,69 +0,0 @@
package command
import (
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func TestRemotePush_noRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi)
c := &RemotePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestRemotePush_local(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
s := terraform.NewState()
s.Serial = 5
conf, srv := testRemoteState(t, s, 200)
defer srv.Close()
s = terraform.NewState()
s.Serial = 10
s.Remote = conf
// Store the local state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
t.Fatalf("err: %s", err)
}
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(s, f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
c := &RemotePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}

View File

@ -6,6 +6,7 @@ import (
"os"
"strings"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/terraform"
)
@ -66,14 +67,26 @@ func (c *ShowCommand) Run(args []string) int {
}
}
} else {
stateOpts := c.StateOpts()
stateOpts.RemoteCacheOnly = true
result, err := State(stateOpts)
// Load the backend
b, err := c.Backend(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
state = result.State.State()
// Get the state
stateStore, err := b.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
if err := stateStore.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
state = stateStore.State()
if state == nil {
c.Ui.Output("No state.")
return 0
@ -92,7 +105,7 @@ func (c *ShowCommand) Run(args []string) int {
}
if plan != nil {
c.Ui.Output(FormatPlan(&FormatPlanOpts{
c.Ui.Output(format.Plan(&format.PlanOpts{
Plan: plan,
Color: c.Colorize(),
ModuleDepth: moduleDepth,
@ -100,7 +113,7 @@ func (c *ShowCommand) Run(args []string) int {
return 0
}
c.Ui.Output(FormatState(&FormatStateOpts{
c.Ui.Output(format.State(&format.StateOpts{
State: state,
Color: c.Colorize(),
ModuleDepth: moduleDepth,

View File

@ -129,20 +129,11 @@ func TestShow_noArgsRemoteState(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Pretend like we have a local cache of remote state
remoteStatePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
if err := os.MkdirAll(filepath.Dir(remoteStatePath), 0755); err != nil {
t.Fatalf("err: %s", err)
}
f, err := os.Create(remoteStatePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(testState(), f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
// Create some legacy remote state
legacyState := testState()
_, srv := testRemoteState(t, legacyState, 200)
defer srv.Close()
testStateFileRemote(t, legacyState)
ui := new(cli.MockUi)
c := &ShowCommand{

View File

@ -1,284 +0,0 @@
package command
import (
"fmt"
"os"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
// StateOpts are options to get the state for a command.
type StateOpts struct {
// LocalPath is the path where the state is stored locally.
//
// LocalPathOut is the path where the local state will be saved. If this
// isn't set, it will be saved back to LocalPath.
LocalPath string
LocalPathOut string
// RemotePath is the path where the remote state cache would be.
//
// RemoteCache, if true, will set the result to only be the cache
// and not backed by any real durable storage.
RemotePath string
RemoteCacheOnly bool
RemoteRefresh bool
// BackupPath is the path where the backup will be placed. If not set,
// it is assumed to be the path where the state is stored locally
// plus the DefaultBackupExtension.
BackupPath string
// ForceState is a state structure to force the value to be. This
// is used by Terraform plans (which contain their state).
ForceState *terraform.State
}
// StateResult is the result of calling State and holds various different
// State implementations so they can be accessed directly.
type StateResult struct {
// State is the final outer state that should be used for all
// _real_ reads/writes.
//
// StatePath is the local path where the state will be stored or
// cached, no matter whether State is local or remote.
State state.State
StatePath string
// Local and Remote are the local/remote state implementations, raw
// and unwrapped by any backups. The paths here are the paths where
// these state files would be saved.
Local *state.LocalState
LocalPath string
Remote *state.CacheState
RemotePath string
}
// State returns the proper state.State implementation to represent the
// current environment.
//
// localPath is the path to where state would be if stored locally.
// dataDir is the path to the local data directory where the remote state
// cache would be stored.
func State(opts *StateOpts) (*StateResult, error) {
result := new(StateResult)
// Get the remote state cache path
if opts.RemotePath != "" {
result.RemotePath = opts.RemotePath
var remote *state.CacheState
if opts.RemoteCacheOnly {
// Setup the in-memory state
ls := &state.LocalState{Path: opts.RemotePath}
if err := ls.RefreshState(); err != nil {
return nil, err
}
// If we have a forced state, set it
if opts.ForceState != nil {
ls.SetState(opts.ForceState)
}
is := &state.InmemState{}
is.WriteState(ls.State())
// Setupt he remote state, cache-only, and refresh it so that
// we have access to the state right away.
remote = &state.CacheState{
Cache: ls,
Durable: is,
}
if err := remote.RefreshState(); err != nil {
return nil, err
}
} else {
// If we have a forced state that is remote, then we load that
if opts.ForceState != nil &&
opts.ForceState.Remote != nil &&
opts.ForceState.Remote.Type != "" {
var err error
remote, err = remoteState(
opts.ForceState,
opts.RemotePath,
false)
if err != nil {
return nil, err
}
} else {
// Only if we have no forced state, we check our normal
// remote path.
if _, err := os.Stat(opts.RemotePath); err == nil {
// We have a remote state, initialize that.
remote, err = remoteStateFromPath(
opts.RemotePath,
opts.RemoteRefresh)
if err != nil {
return nil, err
}
}
}
}
if remote != nil {
result.State = remote
result.StatePath = opts.RemotePath
result.Remote = remote
}
}
// If we have a forced state and we were able to initialize that
// into a remote state, we don't do any local state stuff. This is
// because normally we're able to test whether we should do local vs.
// remote by checking file existence. With ForceState, file existence
// doesn't work because neither may exist, so we use state attributes.
if opts.ForceState != nil && result.Remote != nil {
opts.LocalPath = ""
}
// Do we have a local state?
if opts.LocalPath != "" {
local := &state.LocalState{
Path: opts.LocalPath,
PathOut: opts.LocalPathOut,
}
// Always store it in the result even if we're not using it
result.Local = local
result.LocalPath = local.Path
if local.PathOut != "" {
result.LocalPath = local.PathOut
}
// If we're forcing, then set it
if opts.ForceState != nil {
local.SetState(opts.ForceState)
} else {
// If we're not forcing, then we load the state directly
// from disk.
err := local.RefreshState()
if err == nil {
if result.State != nil && !result.State.State().Empty() {
if !local.State().Empty() {
// We already have a remote state... that is an error.
return nil, fmt.Errorf(
"Remote state found, but state file '%s' also present.",
opts.LocalPath)
}
// Empty state
local = nil
}
}
if err != nil {
return nil, errwrap.Wrapf(
"Error reading local state: {{err}}", err)
}
}
if local != nil {
result.State = local
result.StatePath = opts.LocalPath
if opts.LocalPathOut != "" {
result.StatePath = opts.LocalPathOut
}
}
}
// If we have a result, make sure to back it up
if result.State != nil {
backupPath := result.StatePath + DefaultBackupExtension
if opts.BackupPath != "" {
backupPath = opts.BackupPath
}
if backupPath != "-" {
result.State = &state.BackupState{
Real: result.State,
Path: backupPath,
}
}
}
// Return whatever state we have
return result, nil
}
func remoteState(
local *terraform.State,
localPath string, refresh bool) (*state.CacheState, error) {
// If there is no remote settings, it is an error
if local.Remote == nil {
return nil, fmt.Errorf("Remote state cache has no remote info")
}
// Initialize the remote client based on the local state
client, err := remote.NewClient(strings.ToLower(local.Remote.Type), local.Remote.Config)
if err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf(
"Error initializing remote driver '%s': {{err}}",
local.Remote.Type), err)
}
// Create the remote client
durable := &remote.State{Client: client}
// Create the cached client
cache := &state.CacheState{
Cache: &state.LocalState{Path: localPath},
Durable: durable,
}
if refresh {
// Refresh the cache
if err := cache.RefreshState(); err != nil {
return nil, errwrap.Wrapf(
"Error reloading remote state: {{err}}", err)
}
switch cache.RefreshResult() {
// All the results below can be safely ignored since it means the
// pull was successful in some way. Noop = nothing happened.
// Init = both are empty. UpdateLocal = local state was older and
// updated.
//
// We don't have to do anything, the pull was successful.
case state.CacheRefreshNoop:
case state.CacheRefreshInit:
case state.CacheRefreshUpdateLocal:
// Our local state has a higher serial number than remote, so we
// want to explicitly sync the remote side with our local so that
// the remote gets the latest serial number.
case state.CacheRefreshLocalNewer:
// Write our local state out to the durable storage to start.
if err := cache.WriteState(local); err != nil {
return nil, errwrap.Wrapf(
"Error preparing remote state: {{err}}", err)
}
if err := cache.PersistState(); err != nil {
return nil, errwrap.Wrapf(
"Error preparing remote state: {{err}}", err)
}
default:
return nil, fmt.Errorf(
"Unknown refresh result: %s", cache.RefreshResult())
}
}
return cache, nil
}
func remoteStateFromPath(path string, refresh bool) (*state.CacheState, error) {
// First create the local state for the path
local := &state.LocalState{Path: path}
if err := local.RefreshState(); err != nil {
return nil, err
}
localState := local.State()
return remoteState(localState, path, refresh)
}

View File

@ -24,10 +24,18 @@ func (c *StateListCommand) Run(args []string) int {
}
args = cmdFlags.Args()
state, err := c.State()
// Load the backend
b, err := c.Backend(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
return cli.RunResultHelp
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// Get the state
state, err := b.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
stateReal := state.State()

View File

@ -5,6 +5,7 @@ import (
"fmt"
"time"
backendlocal "github.com/hashicorp/terraform/backend/local"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
)
@ -19,17 +20,31 @@ func (c *StateMeta) State(m *Meta) (state.State, error) {
// Disable backups since we wrap it manually below
m.backupPath = "-"
// Get the state (shouldn't be wrapped in a backup)
s, err := m.State()
// Load the backend
b, err := m.Backend(nil)
if err != nil {
return nil, err
}
// Get the state
s, err := b.State()
if err != nil {
return nil, err
}
// Get a local backend
localRaw, err := m.Backend(&BackendOpts{ForceLocal: true})
if err != nil {
// This should never fail
panic(err)
}
localB := localRaw.(*backendlocal.Local)
// Determine the backup path. stateOutPath is set to the resulting
// file where state is written (cached in the case of remote state)
backupPath := fmt.Sprintf(
"%s.%d%s",
m.stateOutPath,
localB.StateOutPath,
time.Now().UTC().Unix(),
DefaultBackupExtension)

71
command/state_pull.go Normal file
View File

@ -0,0 +1,71 @@
package command
import (
"bytes"
"fmt"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
// StatePullCommand is a Command implementation that shows a single resource.
type StatePullCommand struct {
Meta
StateMeta
}
func (c *StatePullCommand) Run(args []string) int {
args = c.Meta.process(args, true)
cmdFlags := c.Meta.flagSet("state pull")
if err := cmdFlags.Parse(args); err != nil {
return cli.RunResultHelp
}
args = cmdFlags.Args()
// Load the backend
b, err := c.Backend(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// Get the state
state, err := b.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
if err := state.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
var buf bytes.Buffer
if err := terraform.WriteState(state.State(), &buf); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
c.Ui.Output(buf.String())
return 0
}
func (c *StatePullCommand) Help() string {
helpText := `
Usage: terraform state pull [options]
Pull the state from its location and output it to stdout.
This command "pulls" the current state and outputs it to stdout.
The primary use of this is for state stored remotely. This command
will still work with local state but is less useful for this.
`
return strings.TrimSpace(helpText)
}
func (c *StatePullCommand) Synopsis() string {
return "Pull current state and output to stdout"
}

View File

@ -0,0 +1,39 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func TestStatePull(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create some legacy remote state
legacyState := testState()
_, srv := testRemoteState(t, legacyState, 200)
defer srv.Close()
testStateFileRemote(t, legacyState)
p := testProvider()
ui := new(cli.MockUi)
c := &StatePullCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
expected := "test_instance.foo"
actual := ui.OutputWriter.String()
if !strings.Contains(actual, expected) {
t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected)
}
}

144
command/state_push.go Normal file
View File

@ -0,0 +1,144 @@
package command
import (
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
// StatePushCommand is a Command implementation that shows a single resource.
type StatePushCommand struct {
Meta
StateMeta
}
func (c *StatePushCommand) Run(args []string) int {
args = c.Meta.process(args, true)
var flagForce bool
cmdFlags := c.Meta.flagSet("state push")
cmdFlags.BoolVar(&flagForce, "force", false, "")
if err := cmdFlags.Parse(args); err != nil {
return cli.RunResultHelp
}
args = cmdFlags.Args()
if len(args) != 1 {
c.Ui.Error("Exactly one argument expected: path to state to push")
return 1
}
// Read the state
f, err := os.Open(args[0])
if err != nil {
c.Ui.Error(err.Error())
return 1
}
sourceState, err := terraform.ReadState(f)
f.Close()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err))
return 1
}
// Load the backend
b, err := c.Backend(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// Get the state
state, err := b.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))
return 1
}
if err := state.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))
return 1
}
dstState := state.State()
// If we're not forcing, then perform safety checks
if !flagForce && !dstState.Empty() {
if !dstState.SameLineage(sourceState) {
c.Ui.Error(strings.TrimSpace(errStatePushLineage))
return 1
}
age, err := dstState.CompareAges(sourceState)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if age == terraform.StateAgeReceiverNewer {
c.Ui.Error(strings.TrimSpace(errStatePushSerialNewer))
return 1
}
}
// Overwrite it
if err := state.WriteState(sourceState); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err))
return 1
}
if err := state.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err))
return 1
}
return 0
}
func (c *StatePushCommand) Help() string {
helpText := `
Usage: terraform state push [options] PATH
Update remote state from a local state file at PATH.
This command "pushes" a local state and overwrites remote state
with a local state file. The command will protect you against writing
an older serial or a different state file lineage unless you specify the
"-force" flag.
This command works with local state (it will overwrite the local
state), but is less useful for this use case.
Options:
-force Write the state even if lineages don't match or the
remote serial is higher.
`
return strings.TrimSpace(helpText)
}
func (c *StatePushCommand) Synopsis() string {
return "Update remote state from a local state file"
}
const errStatePushLineage = `
The lineages do not match! The state will not be pushed.
The "lineage" is a unique identifier given to a state on creation. It helps
protect Terraform from overwriting a seemingly unrelated state file since it
represents potentially losing real state.
Please verify you're pushing the correct state. If you're sure you are, you
can force the behavior with the "-force" flag.
`
const errStatePushSerialNewer = `
The destination state has a higher serial number! The state will not be pushed.
A higher serial could indicate that there is data in the destination state
that was not present when the source state was created. As a protection measure,
Terraform will not automatically overwrite this state.
Please verify you're pushing the correct state. If you're sure you are, you
can force the behavior with the "-force" flag.
`

154
command/state_push_test.go Normal file
View File

@ -0,0 +1,154 @@
package command
import (
"os"
"testing"
"github.com/hashicorp/terraform/helper/copy"
"github.com/mitchellh/cli"
)
func TestStatePush_empty(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("state-push-good"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
expected := testStateRead(t, "replace.tfstate")
p := testProvider()
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{"replace.tfstate"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testStateRead(t, "local-state.tfstate")
if !actual.Equal(expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestStatePush_replaceMatch(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("state-push-replace-match"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
expected := testStateRead(t, "replace.tfstate")
p := testProvider()
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{"replace.tfstate"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testStateRead(t, "local-state.tfstate")
if !actual.Equal(expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestStatePush_lineageMismatch(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("state-push-bad-lineage"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
expected := testStateRead(t, "local-state.tfstate")
p := testProvider()
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{"replace.tfstate"}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testStateRead(t, "local-state.tfstate")
if !actual.Equal(expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestStatePush_serialNewer(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("state-push-serial-newer"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
expected := testStateRead(t, "local-state.tfstate")
p := testProvider()
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{"replace.tfstate"}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testStateRead(t, "local-state.tfstate")
if !actual.Equal(expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestStatePush_serialOlder(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("state-push-serial-older"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
expected := testStateRead(t, "replace.tfstate")
p := testProvider()
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{"replace.tfstate"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testStateRead(t, "local-state.tfstate")
if !actual.Equal(expected) {
t.Fatalf("bad: %#v", actual)
}
}

View File

@ -26,10 +26,18 @@ func (c *StateShowCommand) Run(args []string) int {
}
args = cmdFlags.Args()
state, err := c.Meta.State()
// Load the backend
b, err := c.Backend(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
return cli.RunResultHelp
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// Get the state
state, err := b.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
stateReal := state.State()

View File

@ -56,8 +56,15 @@ func (c *TaintCommand) Run(args []string) int {
return 1
}
// Get the state that we'll be modifying
state, err := c.State()
// Load the backend
b, err := c.Backend(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// Get the state
state, err := b.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
@ -122,7 +129,11 @@ func (c *TaintCommand) Run(args []string) int {
rs.Taint()
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
if err := c.Meta.PersistState(s); err != nil {
if err := state.WriteState(s); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
return 1
}
if err := state.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
return 1
}

View File

@ -0,0 +1,22 @@
{
"version": 3,
"serial": 0,
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
"backend": {
"type": "local",
"config": {
"path": "local-state.tfstate"
},
"hash": 9073424445967744180
},
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}

View File

@ -0,0 +1,6 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "backend-change"
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "local-state-2.tfstate"
}
}

View File

@ -0,0 +1,28 @@
{
"version": 3,
"serial": 0,
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
"backend": {
"type": "local",
"config": {
"path": "local-state.tfstate"
},
"hash": 9073424445967744180
},
"remote": {
"type": "local",
"config": {
"path": "local-state-old.tfstate"
}
},
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}

View File

@ -0,0 +1,6 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "legacy"
}

View File

@ -0,0 +1,6 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "configured"
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "local-state-2.tfstate"
}
}

View File

@ -0,0 +1,21 @@
{
"version": 3,
"serial": 0,
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
"remote": {
"type": "local",
"config": {
"path": "local-state-old.tfstate"
}
},
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}

View File

@ -0,0 +1,6 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "backend-new-legacy"
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "local-state.tfstate"
}
}

View File

@ -0,0 +1,16 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 8,
"lineage": "remote",
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "local-state.tfstate"
}
}

View File

@ -0,0 +1,16 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 8,
"lineage": "local",
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "local-state.tfstate"
}
}

View File

@ -0,0 +1,16 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 8,
"lineage": "backend-new-migrate",
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "local-state.tfstate"
}
}

Some files were not shown because too many files have changed in this diff Show More