Merge pull request #12182 from hashicorp/jbardin/environments
Environments
This commit is contained in:
commit
f9aa3d3a0b
|
@ -6,12 +6,18 @@ package backend
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
const DefaultStateName = "default"
|
||||
|
||||
// Error value to return when a named state operation isn't supported
|
||||
var ErrNamedStatesNotSupported = errors.New("named states not supported")
|
||||
|
||||
// Backend is the minimal interface that must be implemented to enable Terraform.
|
||||
type Backend interface {
|
||||
// Ask for input and configure the backend. Similar to
|
||||
|
@ -24,7 +30,21 @@ type Backend interface {
|
|||
// not be loaded locally: the proper APIs should be called on state.State
|
||||
// to load the state. If the state.State is a state.Locker, it's up to the
|
||||
// caller to call Lock and Unlock as needed.
|
||||
State() (state.State, error)
|
||||
//
|
||||
// If the named state doesn't exist it will be created. The "default" state
|
||||
// is always assumed to exist.
|
||||
State(name string) (state.State, error)
|
||||
|
||||
// DeleteState removes the named state if it exists. It is an error
|
||||
// to delete the default state.
|
||||
//
|
||||
// DeleteState does not prevent deleting a state that is in use. It is the
|
||||
// responsibility of the caller to hold a Lock on the state when calling
|
||||
// this method.
|
||||
DeleteState(name string) error
|
||||
|
||||
// States returns a list of configured named states.
|
||||
States() ([]string, error)
|
||||
}
|
||||
|
||||
// Enhanced implements additional behavior on top of a normal backend.
|
||||
|
@ -107,6 +127,9 @@ type Operation struct {
|
|||
// If LockState is true, the Operation must Lock any
|
||||
// state.Lockers for its duration, and Unlock when complete.
|
||||
LockState bool
|
||||
|
||||
// Environment is the named state that should be loaded from the Backend.
|
||||
Environment string
|
||||
}
|
||||
|
||||
// RunningOperation is the result of starting an operation.
|
||||
|
|
|
@ -3,6 +3,7 @@ package legacy
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/state/remote"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
|
@ -53,10 +54,22 @@ func (b *Backend) Configure(c *terraform.ResourceConfig) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (b *Backend) State() (state.State, error) {
|
||||
func (b *Backend) State(name string) (state.State, error) {
|
||||
if name != backend.DefaultStateName {
|
||||
return nil, backend.ErrNamedStatesNotSupported
|
||||
}
|
||||
|
||||
if b.client == nil {
|
||||
panic("State called with nil remote state client")
|
||||
}
|
||||
|
||||
return &remote.State{Client: b.client}, nil
|
||||
}
|
||||
|
||||
func (b *Backend) States() ([]string, error) {
|
||||
return nil, backend.ErrNamedStatesNotSupported
|
||||
}
|
||||
|
||||
func (b *Backend) DeleteState(string) error {
|
||||
return backend.ErrNamedStatesNotSupported
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestBackend(t *testing.T) {
|
|||
}
|
||||
|
||||
// Grab state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,13 @@ package local
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
|
@ -13,6 +19,14 @@ import (
|
|||
"github.com/mitchellh/colorstring"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultEnvDir = "terraform.tfstate.d"
|
||||
DefaultEnvFile = "environment"
|
||||
DefaultStateFilename = "terraform.tfstate"
|
||||
DefaultDataDir = ".terraform"
|
||||
DefaultBackupExtension = ".backup"
|
||||
)
|
||||
|
||||
// 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.
|
||||
|
@ -22,21 +36,25 @@ type Local struct {
|
|||
CLI cli.Ui
|
||||
CLIColor *colorstring.Colorize
|
||||
|
||||
// The State* paths are set from the CLI options, and may be left blank to
|
||||
// use the defaults. If the actual paths for the local backend state are
|
||||
// needed, use the StatePaths method.
|
||||
//
|
||||
// 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.
|
||||
// Set this to "-" to disable state backup.
|
||||
StatePath string
|
||||
StateOutPath string
|
||||
StateBackupPath string
|
||||
|
||||
// we only want to create a single instance of the local state
|
||||
state state.State
|
||||
// We only want to create a single instance of a local state, so store them
|
||||
// here as they're loaded.
|
||||
states map[string]state.State
|
||||
|
||||
// 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
|
||||
|
@ -96,31 +114,91 @@ func (b *Local) Configure(c *terraform.ResourceConfig) error {
|
|||
return f(c)
|
||||
}
|
||||
|
||||
func (b *Local) State() (state.State, error) {
|
||||
func (b *Local) States() ([]string, error) {
|
||||
// If we have a backend handling state, defer to that.
|
||||
if b.Backend != nil {
|
||||
return b.Backend.State()
|
||||
return b.Backend.States()
|
||||
}
|
||||
|
||||
if b.state != nil {
|
||||
return b.state, nil
|
||||
// the listing always start with "default"
|
||||
envs := []string{backend.DefaultStateName}
|
||||
|
||||
entries, err := ioutil.ReadDir(DefaultEnvDir)
|
||||
// no error if there's no envs configured
|
||||
if os.IsNotExist(err) {
|
||||
return envs, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Otherwise, we need to load the state.
|
||||
var s state.State = &state.LocalState{
|
||||
Path: b.StatePath,
|
||||
PathOut: b.StateOutPath,
|
||||
}
|
||||
|
||||
// If we are backing up the state, wrap it
|
||||
if path := b.StateBackupPath; path != "" {
|
||||
s = &state.BackupState{
|
||||
Real: s,
|
||||
Path: path,
|
||||
var listed []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
listed = append(listed, filepath.Base(entry.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
b.state = s
|
||||
sort.Strings(listed)
|
||||
envs = append(envs, listed...)
|
||||
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
// DeleteState removes a named state.
|
||||
// The "default" state cannot be removed.
|
||||
func (b *Local) DeleteState(name string) error {
|
||||
// If we have a backend handling state, defer to that.
|
||||
if b.Backend != nil {
|
||||
return b.Backend.DeleteState(name)
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return errors.New("empty state name")
|
||||
}
|
||||
|
||||
if name == backend.DefaultStateName {
|
||||
return errors.New("cannot delete default state")
|
||||
}
|
||||
|
||||
delete(b.states, name)
|
||||
return os.RemoveAll(filepath.Join(DefaultEnvDir, name))
|
||||
}
|
||||
|
||||
func (b *Local) State(name string) (state.State, error) {
|
||||
// If we have a backend handling state, defer to that.
|
||||
if b.Backend != nil {
|
||||
return b.Backend.State(name)
|
||||
}
|
||||
|
||||
if s, ok := b.states[name]; ok {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
if err := b.createState(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statePath, stateOutPath, backupPath := b.StatePaths(name)
|
||||
|
||||
// Otherwise, we need to load the state.
|
||||
var s state.State = &state.LocalState{
|
||||
Path: statePath,
|
||||
PathOut: stateOutPath,
|
||||
}
|
||||
|
||||
// If we are backing up the state, wrap it
|
||||
if backupPath != "" {
|
||||
s = &state.BackupState{
|
||||
Real: s,
|
||||
Path: backupPath,
|
||||
}
|
||||
}
|
||||
|
||||
if b.states == nil {
|
||||
b.states = map[string]state.State{}
|
||||
}
|
||||
b.states[name] = s
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
@ -212,3 +290,77 @@ func (b *Local) schemaConfigure(ctx context.Context) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
|
||||
// configured from the CLI.
|
||||
func (b *Local) StatePaths(name string) (string, string, string) {
|
||||
statePath := b.StatePath
|
||||
stateOutPath := b.StateOutPath
|
||||
backupPath := b.StateBackupPath
|
||||
|
||||
if name == "" {
|
||||
name = backend.DefaultStateName
|
||||
}
|
||||
|
||||
if name == backend.DefaultStateName {
|
||||
if statePath == "" {
|
||||
statePath = DefaultStateFilename
|
||||
}
|
||||
} else {
|
||||
statePath = filepath.Join(DefaultEnvDir, name, DefaultStateFilename)
|
||||
}
|
||||
|
||||
if stateOutPath == "" {
|
||||
stateOutPath = statePath
|
||||
}
|
||||
|
||||
switch backupPath {
|
||||
case "-":
|
||||
backupPath = ""
|
||||
case "":
|
||||
backupPath = stateOutPath + DefaultBackupExtension
|
||||
}
|
||||
|
||||
return statePath, stateOutPath, backupPath
|
||||
}
|
||||
|
||||
// this only ensures that the named directory exists
|
||||
func (b *Local) createState(name string) error {
|
||||
if name == backend.DefaultStateName {
|
||||
return nil
|
||||
}
|
||||
|
||||
stateDir := filepath.Join(DefaultEnvDir, name)
|
||||
s, err := os.Stat(stateDir)
|
||||
if err == nil && s.IsDir() {
|
||||
// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
|
||||
// which will catch the other possible errors as well.
|
||||
return nil
|
||||
}
|
||||
|
||||
err = os.MkdirAll(stateDir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// currentStateName returns the name of the current named state as set in the
|
||||
// configuration files.
|
||||
// If there are no configured environments, currentStateName returns "default"
|
||||
func (b *Local) currentStateName() (string, error) {
|
||||
contents, err := ioutil.ReadFile(filepath.Join(DefaultDataDir, DefaultEnvFile))
|
||||
if os.IsNotExist(err) {
|
||||
return backend.DefaultStateName, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fromFile := strings.TrimSpace(string(contents)); fromFile != "" {
|
||||
return fromFile, nil
|
||||
}
|
||||
|
||||
return backend.DefaultStateName, nil
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State,
|
|||
|
||||
func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, error) {
|
||||
// Get the state.
|
||||
s, err := b.State()
|
||||
s, err := b.State(op.Environment)
|
||||
if err != nil {
|
||||
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
|
@ -34,3 +39,188 @@ func checkState(t *testing.T, path, expected string) {
|
|||
t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocal_StatePaths(t *testing.T) {
|
||||
b := &Local{}
|
||||
|
||||
// Test the defaults
|
||||
path, out, back := b.StatePaths("")
|
||||
|
||||
if path != DefaultStateFilename {
|
||||
t.Fatalf("expected %q, got %q", DefaultStateFilename, path)
|
||||
}
|
||||
|
||||
if out != DefaultStateFilename {
|
||||
t.Fatalf("expected %q, got %q", DefaultStateFilename, out)
|
||||
}
|
||||
|
||||
dfltBackup := DefaultStateFilename + DefaultBackupExtension
|
||||
if back != dfltBackup {
|
||||
t.Fatalf("expected %q, got %q", dfltBackup, back)
|
||||
}
|
||||
|
||||
// check with env
|
||||
testEnv := "test_env"
|
||||
path, out, back = b.StatePaths(testEnv)
|
||||
|
||||
expectedPath := filepath.Join(DefaultEnvDir, testEnv, DefaultStateFilename)
|
||||
expectedOut := expectedPath
|
||||
expectedBackup := expectedPath + DefaultBackupExtension
|
||||
|
||||
if path != expectedPath {
|
||||
t.Fatalf("expected %q, got %q", expectedPath, path)
|
||||
}
|
||||
|
||||
if out != expectedOut {
|
||||
t.Fatalf("expected %q, got %q", expectedOut, out)
|
||||
}
|
||||
|
||||
if back != expectedBackup {
|
||||
t.Fatalf("expected %q, got %q", expectedBackup, back)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLocal_addAndRemoveStates(t *testing.T) {
|
||||
defer testTmpDir(t)()
|
||||
dflt := backend.DefaultStateName
|
||||
expectedStates := []string{dflt}
|
||||
|
||||
b := &Local{}
|
||||
states, err := b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
t.Fatalf("expected []string{%q}, got %q", dflt, states)
|
||||
}
|
||||
|
||||
expectedA := "test_A"
|
||||
if _, err := b.State(expectedA); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
states, err = b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedStates = append(expectedStates, expectedA)
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
t.Fatalf("expected %q, got %q", expectedStates, states)
|
||||
}
|
||||
|
||||
expectedB := "test_B"
|
||||
if _, err := b.State(expectedB); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
states, err = b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedStates = append(expectedStates, expectedB)
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
t.Fatalf("expected %q, got %q", expectedStates, states)
|
||||
}
|
||||
|
||||
if err := b.DeleteState(expectedA); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
states, err = b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedStates = []string{dflt, expectedB}
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
t.Fatalf("expected %q, got %q", expectedStates, states)
|
||||
}
|
||||
|
||||
if err := b.DeleteState(expectedB); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
states, err = b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedStates = []string{dflt}
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
t.Fatalf("expected %q, got %q", expectedStates, states)
|
||||
}
|
||||
|
||||
if err := b.DeleteState(dflt); err == nil {
|
||||
t.Fatal("expected error deleting default state")
|
||||
}
|
||||
}
|
||||
|
||||
// a local backend which returns sentinel errors for NamedState methods to
|
||||
// verify it's being called.
|
||||
type testDelegateBackend struct {
|
||||
*Local
|
||||
}
|
||||
|
||||
var errTestDelegateState = errors.New("State called")
|
||||
var errTestDelegateStates = errors.New("States called")
|
||||
var errTestDelegateDeleteState = errors.New("Delete called")
|
||||
|
||||
func (b *testDelegateBackend) State(name string) (state.State, error) {
|
||||
return nil, errTestDelegateState
|
||||
}
|
||||
|
||||
func (b *testDelegateBackend) States() ([]string, error) {
|
||||
return nil, errTestDelegateStates
|
||||
}
|
||||
|
||||
func (b *testDelegateBackend) DeleteState(name string) error {
|
||||
return errTestDelegateDeleteState
|
||||
}
|
||||
|
||||
// verify that the MultiState methods are dispatched to the correct Backend.
|
||||
func TestLocal_multiStateBackend(t *testing.T) {
|
||||
// assign a separate backend where we can read the state
|
||||
b := &Local{
|
||||
Backend: &testDelegateBackend{},
|
||||
}
|
||||
|
||||
if _, err := b.State("test"); err != errTestDelegateState {
|
||||
t.Fatal("expected errTestDelegateState, got:", err)
|
||||
}
|
||||
|
||||
if _, err := b.States(); err != errTestDelegateStates {
|
||||
t.Fatal("expected errTestDelegateStates, got:", err)
|
||||
}
|
||||
|
||||
if err := b.DeleteState("test"); err != errTestDelegateDeleteState {
|
||||
t.Fatal("expected errTestDelegateDeleteState, got:", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// change into a tmp dir and return a deferable func to change back and cleanup
|
||||
func testTmpDir(t *testing.T) func() {
|
||||
tmp, err := ioutil.TempDir("", "tf")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
old, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.Chdir(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return func() {
|
||||
// ignore errors and try to clean up
|
||||
os.Chdir(old)
|
||||
os.RemoveAll(tmp)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,15 @@ func (Nil) Configure(*terraform.ResourceConfig) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (Nil) State() (state.State, error) {
|
||||
func (Nil) State(string) (state.State, error) {
|
||||
// We have to return a non-nil state to adhere to the interface
|
||||
return &state.InmemState{}, nil
|
||||
}
|
||||
|
||||
func (Nil) DeleteState(string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Nil) States() ([]string, error) {
|
||||
return []string{DefaultStateName}, nil
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ package remotestate
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/state/remote"
|
||||
|
@ -46,12 +47,24 @@ func (b *Backend) Configure(rc *terraform.ResourceConfig) error {
|
|||
return b.Backend.Configure(rc)
|
||||
}
|
||||
|
||||
func (b *Backend) State() (state.State, error) {
|
||||
func (b *Backend) States() ([]string, error) {
|
||||
return nil, backend.ErrNamedStatesNotSupported
|
||||
}
|
||||
|
||||
func (b *Backend) DeleteState(name string) error {
|
||||
return backend.ErrNamedStatesNotSupported
|
||||
}
|
||||
|
||||
func (b *Backend) State(name string) (state.State, error) {
|
||||
// This shouldn't happen
|
||||
if b.client == nil {
|
||||
panic("nil remote client")
|
||||
}
|
||||
|
||||
if name != backend.DefaultStateName {
|
||||
return nil, backend.ErrNamedStatesNotSupported
|
||||
}
|
||||
|
||||
s := &remote.State{Client: b.client}
|
||||
return s, nil
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ func TestConsul_stateLock(t *testing.T) {
|
|||
sA, err := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
||||
"address": addr,
|
||||
"path": path,
|
||||
}).State()
|
||||
}).State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ func TestConsul_stateLock(t *testing.T) {
|
|||
sB, err := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
||||
"address": addr,
|
||||
"path": path,
|
||||
}).State()
|
||||
}).State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ func TestRemoteClient(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInmemLocks(t *testing.T) {
|
||||
s, err := backend.TestBackendConfig(t, New(), nil).State()
|
||||
s, err := backend.TestBackendConfig(t, New(), nil).State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
backendinit "github.com/hashicorp/terraform/backend/init"
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
|
@ -36,6 +37,12 @@ func dataSourceRemoteState() *schema.Resource {
|
|||
Optional: true,
|
||||
},
|
||||
|
||||
"environment": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Default: backend.DefaultStateName,
|
||||
},
|
||||
|
||||
"__has_dynamic_attributes": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
|
@ -73,7 +80,8 @@ func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error {
|
|||
}
|
||||
|
||||
// Get the state
|
||||
state, err := b.State()
|
||||
env := d.Get("environment").(string)
|
||||
state, err := b.State(env)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading the remote state: %s", err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package command
|
||||
|
||||
import "strings"
|
||||
|
||||
// EnvCommand is a Command Implementation that manipulates local state
|
||||
// environments.
|
||||
type EnvCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *EnvCommand) Run(args []string) int {
|
||||
args = c.Meta.process(args, true)
|
||||
|
||||
cmdFlags := c.Meta.flagSet("env")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
|
||||
c.Ui.Output(c.Help())
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *EnvCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: terraform env
|
||||
|
||||
Create, change and delete Terraform environments.
|
||||
|
||||
|
||||
Subcommands:
|
||||
|
||||
list List environments.
|
||||
select Select an environment.
|
||||
new Create a new environment.
|
||||
delete Delete an existing environment.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *EnvCommand) Synopsis() string {
|
||||
return "Environment management"
|
||||
}
|
||||
|
||||
const (
|
||||
envNotSupported = `Backend does not support environments`
|
||||
|
||||
envExists = `Environment %q already exists`
|
||||
|
||||
envDoesNotExist = `Environment %q doesn't exist!
|
||||
You can create this environment with the "-new" option.`
|
||||
|
||||
envChanged = `[reset][green]Switched to environment %q!`
|
||||
|
||||
envCreated = `[reset][green]Created environment %q!`
|
||||
|
||||
envDeleted = `[reset][green]Deleted environment %q!`
|
||||
|
||||
envNotEmpty = `Environment %[1]q is not empty!
|
||||
Deleting %[1]q can result in dangling resources: resources that
|
||||
exist but are no longer manageable by Terraform. Please destroy
|
||||
these resources first. If you want to delete this environment
|
||||
anyways and risk dangling resources, use the '-force' flag.
|
||||
`
|
||||
|
||||
envWarnNotEmpty = `[reset][yellow]WARNING: %q was non-empty.
|
||||
The resources managed by the deleted environment may still exist,
|
||||
but are no longer manageable by Terraform since the state has
|
||||
been deleted.
|
||||
`
|
||||
|
||||
envDelCurrent = `Environment %[1]q is your active environment!
|
||||
You cannot delete the currently active environment. Please switch
|
||||
to another environment and try again.
|
||||
`
|
||||
)
|
|
@ -0,0 +1,252 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/backend/local"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestEnv_createAndChange(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)()
|
||||
|
||||
newCmd := &EnvNewCommand{}
|
||||
|
||||
current := newCmd.Env()
|
||||
if current != backend.DefaultStateName {
|
||||
t.Fatal("current env should be 'default'")
|
||||
}
|
||||
|
||||
args := []string{"test"}
|
||||
ui := new(cli.MockUi)
|
||||
newCmd.Meta = Meta{Ui: ui}
|
||||
if code := newCmd.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
||||
}
|
||||
|
||||
current = newCmd.Env()
|
||||
if current != "test" {
|
||||
t.Fatalf("current env should be 'test', got %q", current)
|
||||
}
|
||||
|
||||
selCmd := &EnvSelectCommand{}
|
||||
args = []string{backend.DefaultStateName}
|
||||
ui = new(cli.MockUi)
|
||||
selCmd.Meta = Meta{Ui: ui}
|
||||
if code := selCmd.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
||||
}
|
||||
|
||||
current = newCmd.Env()
|
||||
if current != backend.DefaultStateName {
|
||||
t.Fatal("current env should be 'default'")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create some environments and test the list output.
|
||||
// This also ensures we switch to the correct env after each call
|
||||
func TestEnv_createAndList(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)()
|
||||
|
||||
newCmd := &EnvNewCommand{}
|
||||
|
||||
envs := []string{"test_a", "test_b", "test_c"}
|
||||
|
||||
// create multiple envs
|
||||
for _, env := range envs {
|
||||
ui := new(cli.MockUi)
|
||||
newCmd.Meta = Meta{Ui: ui}
|
||||
if code := newCmd.Run([]string{env}); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
||||
}
|
||||
}
|
||||
|
||||
listCmd := &EnvListCommand{}
|
||||
ui := new(cli.MockUi)
|
||||
listCmd.Meta = Meta{Ui: ui}
|
||||
|
||||
if code := listCmd.Run(nil); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(ui.OutputWriter.String())
|
||||
expected := "default\n test_a\n test_b\n* test_c"
|
||||
|
||||
if actual != expected {
|
||||
t.Fatalf("\nexpcted: %q\nactual: %q", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnv_createWithState(t *testing.T) {
|
||||
td := tempDir(t)
|
||||
os.MkdirAll(td, 0755)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
// create a non-empty 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := (&state.LocalState{Path: "test.tfstate"}).WriteState(originalState)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
args := []string{"-state", "test.tfstate", "test"}
|
||||
ui := new(cli.MockUi)
|
||||
newCmd := &EnvNewCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
if code := newCmd.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
||||
}
|
||||
|
||||
newPath := filepath.Join(local.DefaultEnvDir, "test", DefaultStateFilename)
|
||||
envState := state.LocalState{Path: newPath}
|
||||
err = envState.RefreshState()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newState := envState.State()
|
||||
if !originalState.Equal(newState) {
|
||||
t.Fatalf("states not equal\norig: %s\nnew: %s", originalState, newState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnv_delete(t *testing.T) {
|
||||
td := tempDir(t)
|
||||
os.MkdirAll(td, 0755)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
// create the env directories
|
||||
if err := os.MkdirAll(filepath.Join(local.DefaultEnvDir, "test"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create the environment file
|
||||
if err := os.MkdirAll(DefaultDataDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(filepath.Join(DefaultDataDir, local.DefaultEnvFile), []byte("test"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
delCmd := &EnvDeleteCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
|
||||
current := delCmd.Env()
|
||||
if current != "test" {
|
||||
t.Fatal("wrong env:", current)
|
||||
}
|
||||
|
||||
// we can't delete out current environment
|
||||
args := []string{"test"}
|
||||
if code := delCmd.Run(args); code == 0 {
|
||||
t.Fatal("expected error deleting current env")
|
||||
}
|
||||
|
||||
// change back to default
|
||||
if err := delCmd.SetEnv(backend.DefaultStateName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// try the delete again
|
||||
ui = new(cli.MockUi)
|
||||
delCmd.Meta.Ui = ui
|
||||
if code := delCmd.Run(args); code != 0 {
|
||||
t.Fatalf("error deleting env: %s", ui.ErrorWriter)
|
||||
}
|
||||
|
||||
current = delCmd.Env()
|
||||
if current != backend.DefaultStateName {
|
||||
t.Fatalf("wrong env: %q", current)
|
||||
}
|
||||
}
|
||||
func TestEnv_deleteWithState(t *testing.T) {
|
||||
td := tempDir(t)
|
||||
os.MkdirAll(td, 0755)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
// create the env directories
|
||||
if err := os.MkdirAll(filepath.Join(local.DefaultEnvDir, "test"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create a non-empty 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
envStatePath := filepath.Join(local.DefaultEnvDir, "test", DefaultStateFilename)
|
||||
err := (&state.LocalState{Path: envStatePath}).WriteState(originalState)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
delCmd := &EnvDeleteCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
args := []string{"test"}
|
||||
if code := delCmd.Run(args); code == 0 {
|
||||
t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter)
|
||||
}
|
||||
|
||||
ui = new(cli.MockUi)
|
||||
delCmd.Meta.Ui = ui
|
||||
|
||||
args = []string{"-force", "test"}
|
||||
if code := delCmd.Run(args); code != 0 {
|
||||
t.Fatalf("failure: %s", ui.ErrorWriter)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(local.DefaultEnvDir, "test")); !os.IsNotExist(err) {
|
||||
t.Fatal("env 'test' still exists!")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/mitchellh/cli"
|
||||
|
||||
clistate "github.com/hashicorp/terraform/command/state"
|
||||
)
|
||||
|
||||
type EnvDeleteCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *EnvDeleteCommand) Run(args []string) int {
|
||||
args = c.Meta.process(args, true)
|
||||
|
||||
force := false
|
||||
cmdFlags := c.Meta.flagSet("env")
|
||||
cmdFlags.BoolVar(&force, "force", false, "force removal of a non-empty environment")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
args = cmdFlags.Args()
|
||||
if len(args) == 0 {
|
||||
c.Ui.Error("expected NAME.\n")
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
delEnv := args[0]
|
||||
|
||||
configPath, err := ModulePath(args[1:])
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
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
|
||||
}
|
||||
|
||||
states, err := b.States()
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
exists := false
|
||||
for _, s := range states {
|
||||
if delEnv == s {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
c.Ui.Error(fmt.Sprintf(envDoesNotExist, delEnv))
|
||||
return 1
|
||||
}
|
||||
|
||||
if delEnv == c.Env() {
|
||||
c.Ui.Error(fmt.Sprintf(envDelCurrent, delEnv))
|
||||
return 1
|
||||
}
|
||||
|
||||
// we need the actual state to see if it's empty
|
||||
sMgr, err := b.State(delEnv)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := sMgr.RefreshState(); err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
hasResources := sMgr.State().HasResources()
|
||||
|
||||
if hasResources && !force {
|
||||
c.Ui.Error(fmt.Sprintf(envNotEmpty, delEnv))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Lock the state if we can
|
||||
lockInfo := state.NewLockInfo()
|
||||
lockInfo.Operation = "env delete"
|
||||
lockID, err := clistate.Lock(sMgr, lockInfo, c.Ui, c.Colorize())
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error locking state: %s", err))
|
||||
return 1
|
||||
}
|
||||
defer clistate.Unlock(sMgr, lockID, c.Ui, c.Colorize())
|
||||
|
||||
err = b.DeleteState(delEnv)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(
|
||||
c.Colorize().Color(
|
||||
fmt.Sprintf(envDeleted, delEnv),
|
||||
),
|
||||
)
|
||||
|
||||
if hasResources {
|
||||
c.Ui.Output(
|
||||
c.Colorize().Color(
|
||||
fmt.Sprintf(envWarnNotEmpty, delEnv),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
func (c *EnvDeleteCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: terraform env delete [OPTIONS] NAME [DIR]
|
||||
|
||||
Delete a Terraform environment
|
||||
|
||||
|
||||
Options:
|
||||
|
||||
-force remove a non-empty environment.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *EnvDeleteCommand) Synopsis() string {
|
||||
return "Delete an environment"
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EnvListCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *EnvListCommand) Run(args []string) int {
|
||||
args = c.Meta.process(args, true)
|
||||
|
||||
cmdFlags := c.Meta.flagSet("env list")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
configPath, err := ModulePath(args)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
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
|
||||
}
|
||||
|
||||
states, err := b.States()
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
env := c.Env()
|
||||
|
||||
var out bytes.Buffer
|
||||
for _, s := range states {
|
||||
if s == env {
|
||||
out.WriteString("* ")
|
||||
} else {
|
||||
out.WriteString(" ")
|
||||
}
|
||||
out.WriteString(s + "\n")
|
||||
}
|
||||
|
||||
c.Ui.Output(out.String())
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *EnvListCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: terraform env list [DIR]
|
||||
|
||||
List Terraform environments.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *EnvListCommand) Synopsis() string {
|
||||
return "List Environments"
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/cli"
|
||||
|
||||
clistate "github.com/hashicorp/terraform/command/state"
|
||||
)
|
||||
|
||||
type EnvNewCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *EnvNewCommand) Run(args []string) int {
|
||||
args = c.Meta.process(args, true)
|
||||
|
||||
statePath := ""
|
||||
|
||||
cmdFlags := c.Meta.flagSet("env new")
|
||||
cmdFlags.StringVar(&statePath, "state", "", "terraform state file")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
args = cmdFlags.Args()
|
||||
if len(args) == 0 {
|
||||
c.Ui.Error("expected NAME.\n")
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
newEnv := args[0]
|
||||
|
||||
configPath, err := ModulePath(args[1:])
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
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
|
||||
}
|
||||
|
||||
states, err := b.States()
|
||||
for _, s := range states {
|
||||
if newEnv == s {
|
||||
c.Ui.Error(fmt.Sprintf(envExists, newEnv))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
_, err = b.State(newEnv)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// now save the current env locally
|
||||
if err := c.SetEnv(newEnv); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("error saving new environment name: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(
|
||||
c.Colorize().Color(
|
||||
fmt.Sprintf(envCreated, newEnv),
|
||||
),
|
||||
)
|
||||
|
||||
if statePath == "" {
|
||||
// if we're not loading a state, then we're done
|
||||
return 0
|
||||
}
|
||||
|
||||
// load the new Backend state
|
||||
sMgr, err := b.State(newEnv)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Lock the state if we can
|
||||
lockInfo := state.NewLockInfo()
|
||||
lockInfo.Operation = "env new"
|
||||
lockID, err := clistate.Lock(sMgr, lockInfo, c.Ui, c.Colorize())
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error locking state: %s", err))
|
||||
return 1
|
||||
}
|
||||
defer clistate.Unlock(sMgr, lockID, c.Ui, c.Colorize())
|
||||
|
||||
// read the existing state file
|
||||
stateFile, err := os.Open(statePath)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
s, err := terraform.ReadState(stateFile)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// save the existing state in the new Backend.
|
||||
err = sMgr.WriteState(s)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *EnvNewCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: terraform env new [OPTIONS] NAME [DIR]
|
||||
|
||||
Create a new Terraform environment.
|
||||
|
||||
|
||||
Options:
|
||||
|
||||
-state=path Copy an existing state file into the new environment.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *EnvNewCommand) Synopsis() string {
|
||||
return "Create a new environment"
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
type EnvSelectCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *EnvSelectCommand) Run(args []string) int {
|
||||
args = c.Meta.process(args, true)
|
||||
|
||||
cmdFlags := c.Meta.flagSet("env select")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
args = cmdFlags.Args()
|
||||
if len(args) == 0 {
|
||||
c.Ui.Error("expected NAME.\n")
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
configPath, err := ModulePath(args[1:])
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
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
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
|
||||
states, err := b.States()
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if name == c.Env() {
|
||||
// already using this env
|
||||
return 0
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, s := range states {
|
||||
if name == s {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
c.Ui.Error(fmt.Sprintf(envDoesNotExist, name))
|
||||
return 1
|
||||
}
|
||||
|
||||
err = c.SetEnv(name)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(
|
||||
c.Colorize().Color(
|
||||
fmt.Sprintf(envChanged, name),
|
||||
),
|
||||
)
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *EnvSelectCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: terraform env select NAME [DIR]
|
||||
|
||||
Change Terraform environment.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *EnvSelectCommand) Synopsis() string {
|
||||
return "Change environments"
|
||||
}
|
|
@ -2,6 +2,7 @@ package command
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -14,6 +15,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/backend/local"
|
||||
"github.com/hashicorp/terraform/helper/experiment"
|
||||
"github.com/hashicorp/terraform/helper/variables"
|
||||
"github.com/hashicorp/terraform/helper/wrappedstreams"
|
||||
|
@ -406,3 +409,44 @@ func (m *Meta) outputShadowError(err error, output bool) bool {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
// Env returns the name of the currently configured environment, corresponding
|
||||
// to the desired named state.
|
||||
func (m *Meta) Env() string {
|
||||
dataDir := m.dataDir
|
||||
if m.dataDir == "" {
|
||||
dataDir = DefaultDataDir
|
||||
}
|
||||
|
||||
envData, err := ioutil.ReadFile(filepath.Join(dataDir, local.DefaultEnvFile))
|
||||
current := string(bytes.TrimSpace(envData))
|
||||
if current == "" {
|
||||
current = backend.DefaultStateName
|
||||
}
|
||||
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
// always return the default if we can't get an environment name
|
||||
log.Printf("[ERROR] failed to read current environment: %s", err)
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
// SetEnv saves the named environment to the local filesystem.
|
||||
func (m *Meta) SetEnv(name string) error {
|
||||
dataDir := m.dataDir
|
||||
if m.dataDir == "" {
|
||||
dataDir = DefaultDataDir
|
||||
}
|
||||
|
||||
err := os.MkdirAll(dataDir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(dataDir, local.DefaultEnvFile), []byte(name), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -72,24 +72,6 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) {
|
|||
opts = &BackendOpts{}
|
||||
}
|
||||
|
||||
// Setup the local state paths
|
||||
statePath := m.statePath
|
||||
stateOutPath := m.stateOutPath
|
||||
backupPath := m.backupPath
|
||||
if statePath == "" {
|
||||
statePath = DefaultStateFilename
|
||||
}
|
||||
if stateOutPath == "" {
|
||||
stateOutPath = statePath
|
||||
}
|
||||
if backupPath == "" {
|
||||
backupPath = stateOutPath + DefaultBackupExtension
|
||||
}
|
||||
if backupPath == "-" {
|
||||
// The local backend expects an empty string for not taking backups.
|
||||
backupPath = ""
|
||||
}
|
||||
|
||||
// Initialize a backend from the config unless we're forcing a purely
|
||||
// local operation.
|
||||
var b backend.Backend
|
||||
|
@ -114,9 +96,9 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) {
|
|||
cliOpts := &backend.CLIOpts{
|
||||
CLI: m.Ui,
|
||||
CLIColor: m.Colorize(),
|
||||
StatePath: statePath,
|
||||
StateOutPath: stateOutPath,
|
||||
StateBackupPath: backupPath,
|
||||
StatePath: m.statePath,
|
||||
StateOutPath: m.stateOutPath,
|
||||
StateBackupPath: m.backupPath,
|
||||
ContextOpts: m.contextOpts(),
|
||||
Input: m.Input(),
|
||||
Validation: true,
|
||||
|
@ -166,6 +148,7 @@ func (m *Meta) Operation() *backend.Operation {
|
|||
PlanOutBackend: m.backendState,
|
||||
Targets: m.targets,
|
||||
UIIn: m.UIInput(),
|
||||
Environment: m.Env(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -544,8 +527,10 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
env := m.Env()
|
||||
|
||||
// Get the state so we can determine the effect of using this plan
|
||||
realMgr, err := b.State()
|
||||
realMgr, err := b.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading state: %s", err)
|
||||
}
|
||||
|
@ -660,7 +645,10 @@ func (m *Meta) backend_c_r_S(
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendLocalRead), err)
|
||||
}
|
||||
localState, err := localB.State()
|
||||
|
||||
env := m.Env()
|
||||
|
||||
localState, err := localB.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendLocalRead), err)
|
||||
}
|
||||
|
@ -674,7 +662,7 @@ func (m *Meta) backend_c_r_S(
|
|||
return nil, fmt.Errorf(
|
||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
||||
}
|
||||
backendState, err := b.State()
|
||||
backendState, err := b.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
||||
|
@ -769,7 +757,10 @@ func (m *Meta) backend_c_R_S(
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf(errBackendLocalRead, err)
|
||||
}
|
||||
localState, err := localB.State()
|
||||
|
||||
env := m.Env()
|
||||
|
||||
localState, err := localB.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errBackendLocalRead, err)
|
||||
}
|
||||
|
@ -800,7 +791,7 @@ func (m *Meta) backend_c_R_S(
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldState, err := oldB.State()
|
||||
oldState, err := oldB.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
||||
|
@ -902,7 +893,10 @@ func (m *Meta) backend_C_R_s(
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldState, err := oldB.State()
|
||||
|
||||
env := m.Env()
|
||||
|
||||
oldState, err := oldB.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
||||
|
@ -913,7 +907,7 @@ func (m *Meta) backend_C_R_s(
|
|||
}
|
||||
|
||||
// Get the new state
|
||||
newState, err := b.State()
|
||||
newState, err := b.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewRead), err)
|
||||
}
|
||||
|
@ -967,7 +961,10 @@ func (m *Meta) backend_C_r_s(
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf(errBackendLocalRead, err)
|
||||
}
|
||||
localState, err := localB.State()
|
||||
|
||||
env := m.Env()
|
||||
|
||||
localState, err := localB.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errBackendLocalRead, err)
|
||||
}
|
||||
|
@ -978,7 +975,7 @@ func (m *Meta) backend_C_r_s(
|
|||
// If the local state is not empty, we need to potentially do a
|
||||
// state migration to the new backend (with user permission).
|
||||
if localS := localState.State(); !localS.Empty() {
|
||||
backendState, err := b.State()
|
||||
backendState, err := b.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errBackendRemoteRead, err)
|
||||
}
|
||||
|
@ -1083,7 +1080,9 @@ func (m *Meta) backend_C_r_S_changed(
|
|||
"Error loading previously configured backend: %s", err)
|
||||
}
|
||||
|
||||
oldState, err := oldB.State()
|
||||
env := m.Env()
|
||||
|
||||
oldState, err := oldB.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
||||
|
@ -1094,7 +1093,7 @@ func (m *Meta) backend_C_r_S_changed(
|
|||
}
|
||||
|
||||
// Get the new state
|
||||
newState, err := b.State()
|
||||
newState, err := b.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewRead), err)
|
||||
}
|
||||
|
@ -1244,7 +1243,10 @@ func (m *Meta) backend_C_R_S_unchanged(
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldState, err := oldB.State()
|
||||
|
||||
env := m.Env()
|
||||
|
||||
oldState, err := oldB.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Remote.Type, err)
|
||||
|
@ -1255,7 +1257,7 @@ func (m *Meta) backend_C_R_S_unchanged(
|
|||
}
|
||||
|
||||
// Get the new state
|
||||
newState, err := b.State()
|
||||
newState, err := b.State(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewRead), err)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/helper/copy"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
|
@ -29,7 +30,7 @@ func TestMetaBackend_emptyDir(t *testing.T) {
|
|||
}
|
||||
|
||||
// Write some state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -99,7 +100,7 @@ func TestMetaBackend_emptyWithDefaultState(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -172,7 +173,7 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -231,7 +232,7 @@ func TestMetaBackend_emptyLegacyRemote(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -280,7 +281,7 @@ func TestMetaBackend_configureNew(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -349,7 +350,7 @@ func TestMetaBackend_configureNewWithState(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -425,7 +426,7 @@ func TestMetaBackend_configureNewWithStateNoMigrate(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -470,7 +471,7 @@ func TestMetaBackend_configureNewWithStateExisting(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -544,7 +545,7 @@ func TestMetaBackend_configureNewWithStateExistingNoMigrate(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -618,7 +619,7 @@ func TestMetaBackend_configureNewLegacy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -712,7 +713,7 @@ func TestMetaBackend_configureNewLegacyCopy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -798,7 +799,7 @@ func TestMetaBackend_configuredUnchanged(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -845,7 +846,7 @@ func TestMetaBackend_configuredChange(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -924,7 +925,7 @@ func TestMetaBackend_configuredChangeCopy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -971,7 +972,7 @@ func TestMetaBackend_configuredUnset(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -1055,7 +1056,7 @@ func TestMetaBackend_configuredUnsetCopy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -1134,7 +1135,7 @@ func TestMetaBackend_configuredUnchangedLegacy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -1237,7 +1238,7 @@ func TestMetaBackend_configuredUnchangedLegacyCopy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -1340,7 +1341,7 @@ func TestMetaBackend_configuredChangedLegacy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -1440,7 +1441,7 @@ func TestMetaBackend_configuredChangedLegacyCopyBackend(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -1543,7 +1544,7 @@ func TestMetaBackend_configuredChangedLegacyCopyLegacy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -1646,7 +1647,7 @@ func TestMetaBackend_configuredChangedLegacyCopyBoth(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -1749,7 +1750,7 @@ func TestMetaBackend_configuredUnsetWithLegacyNoCopy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -1839,7 +1840,7 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBackend(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -1937,7 +1938,7 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyLegacy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -2035,7 +2036,7 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBoth(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -2136,7 +2137,7 @@ func TestMetaBackend_planLocal(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -2233,7 +2234,7 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -2319,7 +2320,7 @@ func TestMetaBackend_planLocalMatch(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -2519,7 +2520,7 @@ func TestMetaBackend_planBackendEmptyDir(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -2621,7 +2622,7 @@ func TestMetaBackend_planBackendMatch(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
@ -2784,7 +2785,7 @@ func TestMetaBackend_planLegacy(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(backend.DefaultStateName)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
|
@ -272,3 +273,37 @@ func TestMeta_addModuleDepthFlag(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeta_Env(t *testing.T) {
|
||||
td := tempDir(t)
|
||||
os.MkdirAll(td, 0755)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
m := new(Meta)
|
||||
|
||||
env := m.Env()
|
||||
|
||||
if env != backend.DefaultStateName {
|
||||
t.Fatalf("expected env %q, got env %q", backend.DefaultStateName, env)
|
||||
}
|
||||
|
||||
testEnv := "test_env"
|
||||
if err := m.SetEnv(testEnv); err != nil {
|
||||
t.Fatal("error setting env:", err)
|
||||
}
|
||||
|
||||
env = m.Env()
|
||||
if env != testEnv {
|
||||
t.Fatalf("expected env %q, got env %q", testEnv, env)
|
||||
}
|
||||
|
||||
if err := m.SetEnv(backend.DefaultStateName); err != nil {
|
||||
t.Fatal("error setting env:", err)
|
||||
}
|
||||
|
||||
env = m.Env()
|
||||
if env != backend.DefaultStateName {
|
||||
t.Fatalf("expected env %q, got env %q", backend.DefaultStateName, env)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,8 +50,10 @@ func (c *OutputCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
env := c.Env()
|
||||
|
||||
// Get the state
|
||||
stateStore, err := b.State()
|
||||
stateStore, err := b.State(env)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -74,8 +74,10 @@ func (c *ShowCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
env := c.Env()
|
||||
|
||||
// Get the state
|
||||
stateStore, err := b.State()
|
||||
stateStore, err := b.State(env)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
// StateCommand is a Command implementation that just shows help for
|
||||
// the subcommands nested below it.
|
||||
type StateCommand struct {
|
||||
Meta
|
||||
StateMeta
|
||||
}
|
||||
|
||||
func (c *StateCommand) Run(args []string) int {
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
// within a state file.
|
||||
type StateListCommand struct {
|
||||
Meta
|
||||
StateMeta
|
||||
}
|
||||
|
||||
func (c *StateListCommand) Run(args []string) int {
|
||||
|
@ -31,8 +32,9 @@ func (c *StateListCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
env := c.Env()
|
||||
// Get the state
|
||||
state, err := b.State()
|
||||
state, err := b.State(env)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -13,9 +13,10 @@ import (
|
|||
// StateMeta is the meta struct that should be embedded in state subcommands.
|
||||
type StateMeta struct{}
|
||||
|
||||
// State returns the state for this meta. This is different then Meta.State
|
||||
// in the way that backups are done. This configures backups to be timestamped
|
||||
// rather than just the original state path plus a backup path.
|
||||
// State returns the state for this meta. This gets the appropriate state from
|
||||
// the backend, but changes the way that backups are done. This configures
|
||||
// backups to be timestamped rather than just the original state path plus a
|
||||
// backup path.
|
||||
func (c *StateMeta) State(m *Meta) (state.State, error) {
|
||||
// Load the backend
|
||||
b, err := m.Backend(nil)
|
||||
|
@ -23,8 +24,9 @@ func (c *StateMeta) State(m *Meta) (state.State, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
env := m.Env()
|
||||
// Get the state
|
||||
s, err := b.State()
|
||||
s, err := b.State(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -36,12 +38,16 @@ func (c *StateMeta) State(m *Meta) (state.State, error) {
|
|||
panic(err)
|
||||
}
|
||||
localB := localRaw.(*backendlocal.Local)
|
||||
_, stateOutPath, _ := localB.StatePaths(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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",
|
||||
localB.StateOutPath,
|
||||
stateOutPath,
|
||||
time.Now().UTC().Unix(),
|
||||
DefaultBackupExtension)
|
||||
|
||||
|
|
|
@ -32,7 +32,8 @@ func (c *StatePullCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Get the state
|
||||
state, err := b.State()
|
||||
env := c.Env()
|
||||
state, err := b.State(env)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -52,7 +52,8 @@ func (c *StatePushCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Get the state
|
||||
state, err := b.State()
|
||||
env := c.Env()
|
||||
state, err := b.State(env)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -34,7 +34,8 @@ func (c *StateShowCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Get the state
|
||||
state, err := b.State()
|
||||
env := c.Env()
|
||||
state, err := b.State(env)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -67,7 +67,8 @@ func (c *TaintCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Get the state
|
||||
st, err := b.State()
|
||||
env := c.Env()
|
||||
st, err := b.State(env)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -52,7 +52,8 @@ func (c *UnlockCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
st, err := b.State()
|
||||
env := c.Env()
|
||||
st, err := b.State(env)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||
return 1
|
||||
|
@ -102,7 +103,6 @@ func (c *UnlockCommand) Run(args []string) int {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME: unlock should require the lock ID
|
||||
if err := s.Unlock(lockID); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to unlock state: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -55,7 +55,8 @@ func (c *UntaintCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Get the state
|
||||
st, err := b.State()
|
||||
env := c.Env()
|
||||
st, err := b.State(env)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||
return 1
|
||||
|
|
34
commands.go
34
commands.go
|
@ -69,6 +69,36 @@ func init() {
|
|||
}, nil
|
||||
},
|
||||
|
||||
"env": func() (cli.Command, error) {
|
||||
return &command.EnvCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env list": func() (cli.Command, error) {
|
||||
return &command.EnvListCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env select": func() (cli.Command, error) {
|
||||
return &command.EnvSelectCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env new": func() (cli.Command, error) {
|
||||
return &command.EnvNewCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env delete": func() (cli.Command, error) {
|
||||
return &command.EnvDeleteCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"fmt": func() (cli.Command, error) {
|
||||
return &command.FmtCommand{
|
||||
Meta: meta,
|
||||
|
@ -186,9 +216,7 @@ func init() {
|
|||
},
|
||||
|
||||
"state": func() (cli.Command, error) {
|
||||
return &command.StateCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
return &command.StateCommand{}, nil
|
||||
},
|
||||
|
||||
"state list": func() (cli.Command, error) {
|
||||
|
|
Loading…
Reference in New Issue