Merge pull request #12182 from hashicorp/jbardin/environments

Environments
This commit is contained in:
James Bardin 2017-03-01 10:30:03 -05:00 committed by GitHub
commit f9aa3d3a0b
33 changed files with 1412 additions and 115 deletions

View File

@ -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.

View File

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

View File

@ -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)
}

View File

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

View File

@ -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)
}

View File

@ -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)
}
}

View File

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

View File

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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

73
command/env_command.go Normal file
View File

@ -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.
`
)

252
command/env_command_test.go Normal file
View File

@ -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!")
}
}

139
command/env_delete.go Normal file
View File

@ -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"
}

68
command/env_list.go Normal file
View File

@ -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"
}

138
command/env_new.go Normal file
View File

@ -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"
}

93
command/env_select.go Normal file
View File

@ -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"
}

View File

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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) {