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:
commit
873f86aee9
|
@ -25,3 +25,7 @@ website/node_modules
|
|||
*.iml
|
||||
|
||||
website/vendor
|
||||
|
||||
# Test exclusions
|
||||
!command/test-fixtures/**/*.tfstate
|
||||
!command/test-fixtures/**/.terraform/
|
||||
|
|
6
Makefile
6
Makefile
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
`
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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]]
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package local
|
||||
|
||||
//go:generate stringer -type=countHookAction hook_count_action.go
|
||||
|
||||
type countHookAction byte
|
||||
|
||||
const (
|
||||
countHookActionAdd countHookAction = iota
|
||||
countHookActionChange
|
||||
countHookActionRemove
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
resource "test_instance" "foo" {
|
||||
ami = "bar"
|
||||
}
|
||||
|
||||
resource "test_instance" "bar" {
|
||||
error = "true"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
resource "test_instance" "foo" {
|
||||
ami = "bar"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
variable "should_ask" {}
|
||||
|
||||
provider "test" {
|
||||
value = "${var.should_ask}"
|
||||
}
|
||||
|
||||
resource "test_instance" "foo" {
|
||||
foo = "bar"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
resource "test_instance" "foo" {
|
||||
ami = "bar"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNil_impl(t *testing.T) {
|
||||
var _ Backend = new(Nil)
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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]]
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package remotestate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
)
|
||||
|
||||
func TestBackend_impl(t *testing.T) {
|
||||
var _ backend.Backend = new(Backend)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
255
command/apply.go
255
command/apply.go
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
273
command/init.go
273
command/init.go
|
@ -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.
|
||||
`
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
250
command/meta.go
250
command/meta.go
|
@ -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
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
||||
`
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
209
command/plan.go
209
command/plan.go
|
@ -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
|
||||
`
|
||||
|
|
|
@ -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)
|
||||
|
|
112
command/push.go
112
command/push.go
|
@ -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 == "" {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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{
|
||||
|
|
284
command/state.go
284
command/state.go
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
`
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"version": 3,
|
||||
"terraform_version": "0.8.2",
|
||||
"serial": 7,
|
||||
"lineage": "backend-change"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
terraform {
|
||||
backend "local" {
|
||||
path = "local-state-2.tfstate"
|
||||
}
|
||||
}
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"version": 3,
|
||||
"terraform_version": "0.8.2",
|
||||
"serial": 7,
|
||||
"lineage": "legacy"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"version": 3,
|
||||
"terraform_version": "0.8.2",
|
||||
"serial": 7,
|
||||
"lineage": "configured"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
terraform {
|
||||
backend "local" {
|
||||
path = "local-state-2.tfstate"
|
||||
}
|
||||
}
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"version": 3,
|
||||
"terraform_version": "0.8.2",
|
||||
"serial": 7,
|
||||
"lineage": "backend-new-legacy"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
terraform {
|
||||
backend "local" {
|
||||
path = "local-state.tfstate"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"version": 3,
|
||||
"terraform_version": "0.8.2",
|
||||
"serial": 8,
|
||||
"lineage": "remote",
|
||||
"modules": [
|
||||
{
|
||||
"path": [
|
||||
"root"
|
||||
],
|
||||
"outputs": {},
|
||||
"resources": {},
|
||||
"depends_on": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
terraform {
|
||||
backend "local" {
|
||||
path = "local-state.tfstate"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"version": 3,
|
||||
"terraform_version": "0.8.2",
|
||||
"serial": 8,
|
||||
"lineage": "local",
|
||||
"modules": [
|
||||
{
|
||||
"path": [
|
||||
"root"
|
||||
],
|
||||
"outputs": {},
|
||||
"resources": {},
|
||||
"depends_on": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
terraform {
|
||||
backend "local" {
|
||||
path = "local-state.tfstate"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"version": 3,
|
||||
"terraform_version": "0.8.2",
|
||||
"serial": 8,
|
||||
"lineage": "backend-new-migrate",
|
||||
"modules": [
|
||||
{
|
||||
"path": [
|
||||
"root"
|
||||
],
|
||||
"outputs": {},
|
||||
"resources": {},
|
||||
"depends_on": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue