diff --git a/backend/backend.go b/backend/backend.go index 86f885207..50cf14465 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -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. diff --git a/backend/legacy/backend.go b/backend/legacy/backend.go index 21ed7b1fb..a8b0cad9f 100644 --- a/backend/legacy/backend.go +++ b/backend/legacy/backend.go @@ -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 +} diff --git a/backend/legacy/backend_test.go b/backend/legacy/backend_test.go index eceb48510..b46ed9252 100644 --- a/backend/legacy/backend_test.go +++ b/backend/legacy/backend_test.go @@ -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) } diff --git a/backend/local/backend.go b/backend/local/backend.go index 00704940c..0b592e33d 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -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 +} diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index 0ecba44ce..833632321 100644 --- a/backend/local/backend_local.go +++ b/backend/local/backend_local.go @@ -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) } diff --git a/backend/local/backend_test.go b/backend/local/backend_test.go index 5f86125f8..f929e7441 100644 --- a/backend/local/backend_test.go +++ b/backend/local/backend_test.go @@ -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) + } +} diff --git a/backend/nil.go b/backend/nil.go index 120ee4123..e2f11e91d 100644 --- a/backend/nil.go +++ b/backend/nil.go @@ -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 +} diff --git a/backend/remote-state/backend.go b/backend/remote-state/backend.go index bcb6912bc..b0f546a77 100644 --- a/backend/remote-state/backend.go +++ b/backend/remote-state/backend.go @@ -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 } diff --git a/backend/remote-state/consul/client_test.go b/backend/remote-state/consul/client_test.go index 52246e48b..2cb11f0d9 100644 --- a/backend/remote-state/consul/client_test.go +++ b/backend/remote-state/consul/client_test.go @@ -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) } diff --git a/backend/remote-state/inmem/client_test.go b/backend/remote-state/inmem/client_test.go index 549cbd80b..f3de56715 100644 --- a/backend/remote-state/inmem/client_test.go +++ b/backend/remote-state/inmem/client_test.go @@ -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) } diff --git a/builtin/providers/terraform/data_source_state.go b/builtin/providers/terraform/data_source_state.go index ee9a63630..730158550 100644 --- a/builtin/providers/terraform/data_source_state.go +++ b/builtin/providers/terraform/data_source_state.go @@ -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) } diff --git a/command/env_command.go b/command/env_command.go new file mode 100644 index 000000000..425013401 --- /dev/null +++ b/command/env_command.go @@ -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. +` +) diff --git a/command/env_command_test.go b/command/env_command_test.go new file mode 100644 index 000000000..356c8d66a --- /dev/null +++ b/command/env_command_test.go @@ -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!") + } +} diff --git a/command/env_delete.go b/command/env_delete.go new file mode 100644 index 000000000..ad484a3f9 --- /dev/null +++ b/command/env_delete.go @@ -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" +} diff --git a/command/env_list.go b/command/env_list.go new file mode 100644 index 000000000..219b32bd0 --- /dev/null +++ b/command/env_list.go @@ -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" +} diff --git a/command/env_new.go b/command/env_new.go new file mode 100644 index 000000000..47457a0c1 --- /dev/null +++ b/command/env_new.go @@ -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" +} diff --git a/command/env_select.go b/command/env_select.go new file mode 100644 index 000000000..d13bdd556 --- /dev/null +++ b/command/env_select.go @@ -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" +} diff --git a/command/meta.go b/command/meta.go index cc29b2aaf..c18c77fc7 100644 --- a/command/meta.go +++ b/command/meta.go @@ -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 +} diff --git a/command/meta_backend.go b/command/meta_backend.go index 86d7aaf7b..d47d6c513 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -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) } diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index fc9cfb5b9..4ce7c1b80 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -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) } diff --git a/command/meta_test.go b/command/meta_test.go index 5dde0f1ff..1a74484fb 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -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) + } +} diff --git a/command/output.go b/command/output.go index 2b12dee62..2f5f6468b 100644 --- a/command/output.go +++ b/command/output.go @@ -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 diff --git a/command/show.go b/command/show.go index 07afae053..9be482811 100644 --- a/command/show.go +++ b/command/show.go @@ -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 diff --git a/command/state_command.go b/command/state_command.go index ce4e0a2ec..7e3a6af1b 100644 --- a/command/state_command.go +++ b/command/state_command.go @@ -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 { diff --git a/command/state_list.go b/command/state_list.go index afc5c9889..d7087d1b5 100644 --- a/command/state_list.go +++ b/command/state_list.go @@ -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 diff --git a/command/state_meta.go b/command/state_meta.go index 9155d0f95..879f8f641 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -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) diff --git a/command/state_pull.go b/command/state_pull.go index 73059c6cc..a51cf5c6a 100644 --- a/command/state_pull.go +++ b/command/state_pull.go @@ -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 diff --git a/command/state_push.go b/command/state_push.go index a2b130f30..d23f8b7e3 100644 --- a/command/state_push.go +++ b/command/state_push.go @@ -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 diff --git a/command/state_show.go b/command/state_show.go index be6df8bf9..235481f2c 100644 --- a/command/state_show.go +++ b/command/state_show.go @@ -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 diff --git a/command/taint.go b/command/taint.go index 0fd2b4de9..487099187 100644 --- a/command/taint.go +++ b/command/taint.go @@ -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 diff --git a/command/unlock.go b/command/unlock.go index b50713aaa..010fd9332 100644 --- a/command/unlock.go +++ b/command/unlock.go @@ -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 diff --git a/command/untaint.go b/command/untaint.go index c3b413252..ab697b823 100644 --- a/command/untaint.go +++ b/command/untaint.go @@ -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 diff --git a/commands.go b/commands.go index 20e2ff892..b9343301c 100644 --- a/commands.go +++ b/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) {