From 761c63d14a850c8c2cf55553769291919cfdcc24 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 21 Feb 2017 10:48:00 -0500 Subject: [PATCH 01/22] Update Backend to incorporate environments Add the missing methods/arguments to handle Terraform environments in Backends. Extra functionality simply returns defaults for now. --- backend/backend.go | 9 +++++++++ backend/legacy/backend.go | 9 +++++++++ backend/local/backend.go | 8 ++++++++ backend/nil.go | 9 +++++++++ backend/remote-state/backend.go | 9 +++++++++ 5 files changed, 44 insertions(+) diff --git a/backend/backend.go b/backend/backend.go index 86f885207c..da2e6daf17 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/terraform/terraform" ) +const DefaultStateName = "default" + // Backend is the minimal interface that must be implemented to enable Terraform. type Backend interface { // Ask for input and configure the backend. Similar to @@ -25,6 +27,13 @@ type Backend interface { // 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) + + // States returns a list of configured named states and the current state. + States() ([]string, string, error) + + // ChangeState changes to the named state. If this doesn't exist it'll be + // created. + ChangeState(name string) error } // Enhanced implements additional behavior on top of a normal backend. diff --git a/backend/legacy/backend.go b/backend/legacy/backend.go index 21ed7b1fb8..3fafb7e67b 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" @@ -60,3 +61,11 @@ func (b *Backend) State() (state.State, error) { return &remote.State{Client: b.client}, nil } + +func (b *Backend) States() ([]string, string, error) { + return []string{backend.DefaultStateName}, backend.DefaultStateName, nil +} + +func (b *Backend) ChangeState(name string) error { + return nil +} diff --git a/backend/local/backend.go b/backend/local/backend.go index 00704940c2..1609c924d4 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -96,6 +96,14 @@ func (b *Local) Configure(c *terraform.ResourceConfig) error { return f(c) } +func (b *Local) States() ([]string, string, error) { + return []string{backend.DefaultStateName}, backend.DefaultStateName, nil +} + +func (b *Local) ChangeState(name string) error { + return nil +} + func (b *Local) State() (state.State, error) { // If we have a backend handling state, defer to that. if b.Backend != nil { diff --git a/backend/nil.go b/backend/nil.go index 120ee41238..5482ca8a07 100644 --- a/backend/nil.go +++ b/backend/nil.go @@ -29,3 +29,12 @@ func (Nil) State() (state.State, error) { // We have to return a non-nil state to adhere to the interface return &state.InmemState{}, nil } + +func (Nil) States() ([]string, string, error) { + // The default state always exists + return []string{DefaultStateName}, DefaultStateName, nil +} + +func (Nil) ChangeState(string) error { + return nil +} diff --git a/backend/remote-state/backend.go b/backend/remote-state/backend.go index bcb6912bc9..8dd50e6f48 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,6 +47,14 @@ func (b *Backend) Configure(rc *terraform.ResourceConfig) error { return b.Backend.Configure(rc) } +func (b *Backend) States() ([]string, string, error) { + return []string{backend.DefaultStateName}, backend.DefaultStateName, nil +} + +func (b *Backend) ChangeState(name string) error { + return nil +} + func (b *Backend) State() (state.State, error) { // This shouldn't happen if b.client == nil { From dbc45b907c2fa2c1b25d734b8bdeb8f283720088 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 21 Feb 2017 19:07:27 -0500 Subject: [PATCH 02/22] Make the Local backend handle its own named states Add the functionality required for terraform environments --- backend/local/backend.go | 219 +++++++++++++++++++++++++++++++++- backend/local/backend_test.go | 107 +++++++++++++++++ 2 files changed, 320 insertions(+), 6 deletions(-) diff --git a/backend/local/backend.go b/backend/local/backend.go index 1609c924d4..541596983d 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,19 +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 + // the name of the current state + currentState string // ContextOpts are the base context options to set when initializing a // Terraform context. Many of these will be overridden or merged by @@ -97,10 +117,110 @@ func (b *Local) Configure(c *terraform.ResourceConfig) error { } func (b *Local) States() ([]string, string, error) { - return []string{backend.DefaultStateName}, backend.DefaultStateName, nil + // the listing always start with "default" + envs := []string{backend.DefaultStateName} + + current := b.currentState + if current == "" { + name, err := b.currentStateName() + if err != nil { + return nil, "", err + } + current = name + } + + entries, err := ioutil.ReadDir(DefaultEnvDir) + // no error if there's no envs configured + if os.IsNotExist(err) { + return envs, current, nil + } + if err != nil { + return nil, "", err + } + + var listed []string + for _, entry := range entries { + if entry.IsDir() { + listed = append(listed, filepath.Base(entry.Name())) + } + } + + sort.Strings(listed) + envs = append(envs, listed...) + + return envs, current, nil } +// DeleteState removes a named state. +// The "default" state cannot be removed. +func (b *Local) DeleteState(name string) error { + if name == "" { + return errors.New("empty state name") + } + + if name == backend.DefaultStateName { + return errors.New("cannot delete default state") + } + + // if we're deleting the current state, we change back to the default + if name == b.currentState { + if err := b.ChangeState(backend.DefaultStateName); err != nil { + return err + } + } + + return os.RemoveAll(filepath.Join(DefaultEnvDir, name)) +} + +// Change to the named state, creating it if it doesn't exist. func (b *Local) ChangeState(name string) error { + name = strings.TrimSpace(name) + if name == "" { + return errors.New("state name cannot be empty") + } + + envs, current, err := b.States() + if err != nil { + return err + } + + if name == current { + return nil + } + + exists := false + for _, env := range envs { + if env == name { + exists = true + break + } + } + + if !exists { + if err := b.createState(name); err != nil { + return err + } + } + + err = os.MkdirAll(DefaultDataDir, 0755) + if err != nil { + return err + } + + err = ioutil.WriteFile( + filepath.Join(DefaultDataDir, DefaultEnvFile), + []byte(name), + 0644, + ) + if err != nil { + return err + } + + b.currentState = name + + // remove the current state so it's reloaded on the next call to State + b.state = nil + return nil } @@ -114,17 +234,22 @@ func (b *Local) State() (state.State, error) { return b.state, nil } + statePath, stateOutPath, backupPath, err := b.StatePaths() + 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, + Path: statePath, + PathOut: stateOutPath, } // If we are backing up the state, wrap it - if path := b.StateBackupPath; path != "" { + if backupPath != "" { s = &state.BackupState{ Real: s, - Path: path, + Path: backupPath, } } @@ -220,3 +345,85 @@ func (b *Local) schemaConfigure(ctx context.Context) error { return nil } + +// StatePaths returns the StatePath, StateOutPath, and StateBackupPath as +// configured by the current environment. If backups are disabled, +// StateBackupPath will be an empty string. +func (b *Local) StatePaths() (string, string, string, error) { + statePath := b.StatePath + stateOutPath := b.StateOutPath + backupPath := b.StateBackupPath + + if statePath == "" { + path, err := b.statePath() + if err != nil { + return "", "", "", err + } + statePath = path + } + if stateOutPath == "" { + stateOutPath = statePath + } + + switch backupPath { + case "-": + backupPath = "" + case "": + backupPath = stateOutPath + DefaultBackupExtension + } + + return statePath, stateOutPath, backupPath, nil +} + +func (b *Local) statePath() (string, error) { + _, current, err := b.States() + if err != nil { + return "", err + } + path := DefaultStateFilename + + if current != backend.DefaultStateName && current != "" { + path = filepath.Join(DefaultEnvDir, b.currentState, DefaultStateFilename) + } + return path, nil +} + +func (b *Local) createState(name string) error { + stateNames, _, err := b.States() + if err != nil { + return err + } + + for _, n := range stateNames { + if name == n { + // state exists, nothing to do + return nil + } + } + + err = os.MkdirAll(filepath.Join(DefaultEnvDir, name), 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_test.go b/backend/local/backend_test.go index 5f86125f83..c97a8e7b95 100644 --- a/backend/local/backend_test.go +++ b/backend/local/backend_test.go @@ -1,7 +1,9 @@ package local import ( + "io/ioutil" "os" + "reflect" "strings" "testing" @@ -34,3 +36,108 @@ 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_addAndRemoveStates(t *testing.T) { + defer testTmpDir(t)() + dflt := backend.DefaultStateName + expectedStates := []string{dflt} + + b := &Local{} + states, current, err := b.States() + if err != nil { + t.Fatal(err) + } + + if current != dflt { + t.Fatalf("expected %q, got %q", dflt, current) + } + + if !reflect.DeepEqual(states, expectedStates) { + t.Fatal("expected []string{%q}, got %q", dflt, states) + } + + expectedA := "test_A" + if err := b.ChangeState(expectedA); err != nil { + t.Fatal(err) + } + + states, current, err = b.States() + if current != expectedA { + t.Fatalf("expected %q, got %q", expectedA, current) + } + + expectedStates = append(expectedStates, expectedA) + if !reflect.DeepEqual(states, expectedStates) { + t.Fatalf("expected %q, got %q", expectedStates, states) + } + + expectedB := "test_B" + if err := b.ChangeState(expectedB); err != nil { + t.Fatal(err) + } + + states, current, err = b.States() + if current != expectedB { + t.Fatalf("expected %q, got %q", expectedB, current) + } + + 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, current, err = b.States() + if current != expectedB { + t.Fatalf("expected %q, got %q", dflt, current) + } + + 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, current, err = b.States() + if current != dflt { + t.Fatalf("expected %q, got %q", dflt, current) + } + + 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") + } +} + +// 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) + } +} From 1ea9413c07209b98ec8c5011ce7a2907c4771c87 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 22 Feb 2017 13:11:26 -0500 Subject: [PATCH 03/22] Remove state path handling from commands The Local backend is now responsible for handling the paths to the local state files, since they are dependent on the current environment. --- command/meta_backend.go | 24 +++--------------------- command/state_meta.go | 6 +++++- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/command/meta_backend.go b/command/meta_backend.go index 86d7aaf7b0..629fc96421 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, diff --git a/command/state_meta.go b/command/state_meta.go index 9155d0f95c..eccfba447c 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -36,12 +36,16 @@ func (c *StateMeta) State(m *Meta) (state.State, error) { panic(err) } localB := localRaw.(*backendlocal.Local) + _, stateOutPath, _, err := localB.StatePaths() + 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) From 0933541a8c95ec5cb03df39cdd69bc81dc5adbae Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 22 Feb 2017 13:40:04 -0500 Subject: [PATCH 04/22] Split out the backend environment interface Split the interface to change environments out from the minimal Backend interface, to make it optional for backend implementations. If backend.MultiState isn't implemented, return a "not implemented" from environment related methods. Have the Local backend delegate the MultiState methods to the proper backend. --- backend/backend.go | 13 +++++++++++-- backend/local/backend.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index da2e6daf17..527c293859 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -27,13 +27,22 @@ type Backend interface { // 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) +} +// MultiState is an interface that a backend can implement to allow changing +// between named states depending on the configured environment. +type MultiState interface { // States returns a list of configured named states and the current state. States() ([]string, string, error) - // ChangeState changes to the named state. If this doesn't exist it'll be - // created. + // ChangeState changes to the named state. If the named state doesn't exist + // it will be created. ChangeState(name string) error + + // DeleteState removes the named state if it exists. If the current state is + // deleted, the backend should change to the default state. It is an error + // to delete the default state. + DeleteState(name string) error } // Enhanced implements additional behavior on top of a normal backend. diff --git a/backend/local/backend.go b/backend/local/backend.go index 541596983d..966f5e82d8 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -27,6 +27,8 @@ const ( DefaultBackupExtension = ".backup" ) +var ErrEnvNotSupported = errors.New("environments not supported") + // 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. @@ -117,6 +119,15 @@ func (b *Local) Configure(c *terraform.ResourceConfig) error { } func (b *Local) States() ([]string, string, error) { + // If we have a backend handling state, defer to that. + if b.Backend != nil { + if b, ok := b.Backend.(backend.MultiState); ok { + return b.States() + } else { + return nil, "", ErrEnvNotSupported + } + } + // the listing always start with "default" envs := []string{backend.DefaultStateName} @@ -154,6 +165,15 @@ func (b *Local) States() ([]string, string, error) { // 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 { + if b, ok := b.Backend.(backend.MultiState); ok { + return b.DeleteState(name) + } else { + return ErrEnvNotSupported + } + } + if name == "" { return errors.New("empty state name") } @@ -174,6 +194,15 @@ func (b *Local) DeleteState(name string) error { // Change to the named state, creating it if it doesn't exist. func (b *Local) ChangeState(name string) error { + // If we have a backend handling state, defer to that. + if b.Backend != nil { + if b, ok := b.Backend.(backend.MultiState); ok { + return b.ChangeState(name) + } else { + return ErrEnvNotSupported + } + } + name = strings.TrimSpace(name) if name == "" { return errors.New("state name cannot be empty") From e6eb71dde593629e874267d66f3fbb625b9e201e Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 22 Feb 2017 14:53:43 -0500 Subject: [PATCH 05/22] Add tests to check Backend delegation Ensure that when MultiState methods are properly delegated when there is a defined Local.Backend. --- backend/local/backend.go | 25 +++++--- backend/local/backend_test.go | 112 +++++++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/backend/local/backend.go b/backend/local/backend.go index 966f5e82d8..afb1af9eb9 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -82,6 +82,10 @@ type Local struct { schema *schema.Backend opLock sync.Mutex once sync.Once + + // workingDir is where the State* paths should be relative to. + // This is currently only used for tests. + workingDir string } func (b *Local) Input( @@ -140,7 +144,7 @@ func (b *Local) States() ([]string, string, error) { current = name } - entries, err := ioutil.ReadDir(DefaultEnvDir) + entries, err := ioutil.ReadDir(filepath.Join(b.workingDir, DefaultEnvDir)) // no error if there's no envs configured if os.IsNotExist(err) { return envs, current, nil @@ -182,14 +186,19 @@ func (b *Local) DeleteState(name string) error { return errors.New("cannot delete default state") } + _, current, err := b.States() + if err != nil { + return err + } + // if we're deleting the current state, we change back to the default - if name == b.currentState { + if name == current { if err := b.ChangeState(backend.DefaultStateName); err != nil { return err } } - return os.RemoveAll(filepath.Join(DefaultEnvDir, name)) + return os.RemoveAll(filepath.Join(b.workingDir, DefaultEnvDir, name)) } // Change to the named state, creating it if it doesn't exist. @@ -231,13 +240,13 @@ func (b *Local) ChangeState(name string) error { } } - err = os.MkdirAll(DefaultDataDir, 0755) + err = os.MkdirAll(filepath.Join(b.workingDir, DefaultDataDir), 0755) if err != nil { return err } err = ioutil.WriteFile( - filepath.Join(DefaultDataDir, DefaultEnvFile), + filepath.Join(b.workingDir, DefaultDataDir, DefaultEnvFile), []byte(name), 0644, ) @@ -412,7 +421,7 @@ func (b *Local) statePath() (string, error) { path := DefaultStateFilename if current != backend.DefaultStateName && current != "" { - path = filepath.Join(DefaultEnvDir, b.currentState, DefaultStateFilename) + path = filepath.Join(b.workingDir, DefaultEnvDir, b.currentState, DefaultStateFilename) } return path, nil } @@ -430,7 +439,7 @@ func (b *Local) createState(name string) error { } } - err = os.MkdirAll(filepath.Join(DefaultEnvDir, name), 0755) + err = os.MkdirAll(filepath.Join(b.workingDir, DefaultEnvDir, name), 0755) if err != nil { return err } @@ -442,7 +451,7 @@ func (b *Local) createState(name string) error { // 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)) + contents, err := ioutil.ReadFile(filepath.Join(b.workingDir, DefaultDataDir, DefaultEnvFile)) if os.IsNotExist(err) { return backend.DefaultStateName, nil } diff --git a/backend/local/backend_test.go b/backend/local/backend_test.go index c97a8e7b95..a72a0fcc0f 100644 --- a/backend/local/backend_test.go +++ b/backend/local/backend_test.go @@ -1,8 +1,10 @@ package local import ( + "fmt" "io/ioutil" "os" + "path/filepath" "reflect" "strings" "testing" @@ -15,6 +17,7 @@ func TestLocal_impl(t *testing.T) { var _ backend.Enhanced = new(Local) var _ backend.Local = new(Local) var _ backend.CLI = new(Local) + var _ backend.MultiState = new(Local) } func checkState(t *testing.T, path, expected string) { @@ -53,7 +56,7 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } if !reflect.DeepEqual(states, expectedStates) { - t.Fatal("expected []string{%q}, got %q", dflt, states) + t.Fatalf("expected []string{%q}, got %q", dflt, states) } expectedA := "test_A" @@ -62,6 +65,9 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } states, current, err = b.States() + if err != nil { + t.Fatal(err) + } if current != expectedA { t.Fatalf("expected %q, got %q", expectedA, current) } @@ -77,6 +83,9 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } states, current, err = b.States() + if err != nil { + t.Fatal(err) + } if current != expectedB { t.Fatalf("expected %q, got %q", expectedB, current) } @@ -91,6 +100,9 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } states, current, err = b.States() + if err != nil { + t.Fatal(err) + } if current != expectedB { t.Fatalf("expected %q, got %q", dflt, current) } @@ -105,6 +117,9 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } states, current, err = b.States() + if err != nil { + t.Fatal(err) + } if current != dflt { t.Fatalf("expected %q, got %q", dflt, current) } @@ -119,6 +134,101 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } } +// verify the behavior with a backend that doesn't support multiple states +func TestLocal_noMultiStateBackend(t *testing.T) { + type noMultiState struct { + backend.Backend + } + + b := &Local{ + Backend: &noMultiState{}, + } + + _, _, err := b.States() + if err != ErrEnvNotSupported { + t.Fatal("backend does not support environments.", err) + } + + err = b.ChangeState("test") + if err != ErrEnvNotSupported { + t.Fatal("backend does not support environments.", err) + } + + err = b.ChangeState("test") + if err != ErrEnvNotSupported { + t.Fatal("backend does not support environments.", err) + } +} + +// verify that the MultiState methods are dispatched to the correct Backend. +func TestLocal_multiStateBackend(t *testing.T) { + defer testTmpDir(t)() + + dflt := backend.DefaultStateName + expectedStates := []string{dflt} + + // make a second tmp dir for the sub-Backend. + // we verify the corret backend was called by checking the paths. + tmp, err := ioutil.TempDir("", "tf") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + fmt.Println("second tmp:", tmp) + + b := &Local{ + Backend: &Local{ + workingDir: tmp, + }, + } + + testA := "test_A" + if err := b.ChangeState(testA); err != nil { + t.Fatal(err) + } + + states, current, err := b.States() + if err != nil { + t.Fatal(err) + } + if current != testA { + t.Fatalf("expected %q, got %q", testA, current) + } + + expectedStates = append(expectedStates, testA) + if !reflect.DeepEqual(states, expectedStates) { + t.Fatalf("expected %q, got %q", expectedStates, states) + } + + // verify that no environment paths were created for the top-level Backend + if _, err := os.Stat(DefaultDataDir); !os.IsNotExist(err) { + t.Fatal("remote state operations should not have written local files") + } + + if _, err := os.Stat(filepath.Join(DefaultEnvDir, testA)); !os.IsNotExist(err) { + t.Fatal("remote state operations should not have written local files") + } + + // remove the new state + if err := b.DeleteState(testA); err != nil { + t.Fatal(err) + } + + states, current, err = b.States() + if err != nil { + t.Fatal(err) + } + if current != dflt { + t.Fatalf("expected %q, got %q", dflt, current) + } + + if !reflect.DeepEqual(states, expectedStates[:1]) { + t.Fatalf("expected %q, got %q", expectedStates, states) + } + +} + // 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") From 31f033827f7d81e5fefcce96aa5af0adb4c30ce8 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 16 Feb 2017 18:29:19 -0500 Subject: [PATCH 06/22] Add basic env commands Used a single command with flags for now. We may refactor this out to subcommands. --- command/env_command.go | 322 ++++++++++++++++++++++++++++++++++++ command/env_command_test.go | 312 ++++++++++++++++++++++++++++++++++ commands.go | 6 + 3 files changed, 640 insertions(+) create mode 100644 command/env_command.go create mode 100644 command/env_command_test.go diff --git a/command/env_command.go b/command/env_command.go new file mode 100644 index 0000000000..9ad03b5eb8 --- /dev/null +++ b/command/env_command.go @@ -0,0 +1,322 @@ +package command + +import ( + "bytes" + "fmt" + "os" + "strings" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +// EnvCommand is a Command Implementation that manipulates local state +// environments. +type EnvCommand struct { + Meta + + newEnv string + delEnv string + statePath string + force bool + + // backend returns by Meta.Backend + b backend.Backend + // MultiState Backend + multi backend.MultiState +} + +func (c *EnvCommand) Run(args []string) int { + args = c.Meta.process(args, true) + + cmdFlags := c.Meta.flagSet("env") + cmdFlags.StringVar(&c.newEnv, "new", "", "create a new environment") + cmdFlags.StringVar(&c.delEnv, "delete", "", "delete an existing environment") + cmdFlags.StringVar(&c.statePath, "state", "", "terraform state file") + cmdFlags.BoolVar(&c.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) > 1 { + c.Ui.Error("0 or 1 arguments expected.\n") + return cli.RunResultHelp + } + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + c.b = b + + multi, ok := b.(backend.MultiState) + if !ok { + c.Ui.Error(envNotSupported) + return 1 + } + c.multi = multi + + if c.newEnv != "" { + return c.createEnv() + } + + if c.delEnv != "" { + return c.deleteEnv() + } + + if len(args) == 1 { + return c.changeEnv(args[0]) + } + + return c.listEnvs() +} + +func (c *EnvCommand) createEnv() int { + states, _, err := c.multi.States() + for _, s := range states { + if c.newEnv == s { + c.Ui.Error(fmt.Sprintf(envExists, c.newEnv)) + return 1 + } + } + + err = c.multi.ChangeState(c.newEnv) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + c.Ui.Output( + c.Colorize().Color( + fmt.Sprintf(envCreated, c.newEnv), + ), + ) + + if c.statePath == "" { + // if we're not loading a state, then we're done + return 0 + } + + // load the new state + sMgr, err := c.b.State() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // load the existing state + stateFile, err := os.Open(c.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 + } + + err = sMgr.WriteState(s) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + return 0 +} + +func (c *EnvCommand) deleteEnv() int { + states, current, err := c.multi.States() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + exists := false + for _, s := range states { + if c.delEnv == s { + exists = true + break + } + } + + if !exists { + c.Ui.Error(fmt.Sprintf(envDoesNotExist, c.delEnv)) + return 1 + } + + // In order to check if the state being deleted is empty, we need to change + // to that state and load it. + if current != c.delEnv { + if err := c.multi.ChangeState(c.delEnv); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // always try to change back after + defer func() { + if err := c.multi.ChangeState(current); err != nil { + c.Ui.Error(err.Error()) + } + }() + } + + // we need the actual state to see if it's empty + sMgr, err := c.b.State() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if err := sMgr.RefreshState(); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + empty := sMgr.State().Empty() + + if !empty && !c.force { + c.Ui.Error(fmt.Sprintf(envNotEmpty, c.delEnv)) + return 1 + } + + err = c.multi.DeleteState(c.delEnv) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + c.Ui.Output( + c.Colorize().Color( + fmt.Sprintf(envDeleted, c.delEnv), + ), + ) + + if !empty { + c.Ui.Output( + c.Colorize().Color( + fmt.Sprintf(envWarnNotEmpty, c.delEnv), + ), + ) + } + + return 0 +} + +func (c *EnvCommand) changeEnv(name string) int { + states, current, err := c.multi.States() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if current == name { + 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.multi.ChangeState(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 *EnvCommand) listEnvs() int { + states, current, err := c.multi.States() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + var out bytes.Buffer + for _, s := range states { + if s == current { + out.WriteString("* ") + } else { + out.WriteString(" ") + } + out.WriteString(s + "\n") + } + + c.Ui.Output(out.String()) + return 0 +} + +func (c *EnvCommand) Help() string { + helpText := ` +Usage: terraform env [options] [NAME] + + Create, change and delete Terraform environments. + + By default env will list all configured environments. If NAME is provided, + env will change into to that named environment. + + +Options: + + -new=name Create a new environment. + -delete=name Delete an existing environment, + + -state=path Used with -new to copy a state file into the new environment. + -force Used with -delete to remove a non-empty 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. +` +) diff --git a/command/env_command_test.go b/command/env_command_test.go new file mode 100644 index 0000000000..8257932ad0 --- /dev/null +++ b/command/env_command_test.go @@ -0,0 +1,312 @@ +package command + +import ( + "io/ioutil" + "os" + "path/filepath" + "sort" + "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)() + + c := &EnvCommand{} + + current, err := currentEnv() + if err != nil { + t.Fatal(err) + } + if current != backend.DefaultStateName { + t.Fatal("current env should be 'default'") + } + + args := []string{"-new", "test"} + ui := new(cli.MockUi) + c.Meta = Meta{Ui: ui} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) + } + + current, err = currentEnv() + if err != nil { + t.Fatal(err) + } + if current != "test" { + t.Fatal("current env should be 'test'") + } + + args = []string{backend.DefaultStateName} + ui = new(cli.MockUi) + c.Meta = Meta{Ui: ui} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) + } + + current, err = currentEnv() + if err != nil { + t.Fatal(err) + } + + 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)() + + c := &EnvCommand{} + + envs := []string{"test_a", "test_b", "test_c"} + + // create multiple envs + for _, env := range envs { + args := []string{"-new", env} + ui := new(cli.MockUi) + c.Meta = Meta{Ui: ui} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) + } + } + + // now check the listing + expected := "default\n test_a\n test_b\n* test_c" + + ui := new(cli.MockUi) + c.Meta = Meta{Ui: ui} + + if code := c.Run(nil); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) + } + + actual := strings.TrimSpace(ui.OutputWriter.String()) + 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{"-new", "test", "-state", "test.tfstate"} + ui := new(cli.MockUi) + c := &EnvCommand{ + Meta: Meta{Ui: ui}, + } + if code := c.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) + } + + current, err := currentEnv() + if err != nil { + t.Fatal(err) + } + + if current != "test" { + t.Fatal("wrong env:", current) + } + + ui := new(cli.MockUi) + c := &EnvCommand{ + Meta: Meta{Ui: ui}, + } + args := []string{"-delete", "test"} + if code := c.Run(args); code != 0 { + t.Fatalf("failure: %s", ui.ErrorWriter) + } + + current, err = currentEnv() + if err != nil { + t.Fatal(err) + } + + 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) + c := &EnvCommand{ + Meta: Meta{Ui: ui}, + } + args := []string{"-delete", "test"} + if code := c.Run(args); code == 0 { + t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter) + } + + ui = new(cli.MockUi) + c.Meta.Ui = ui + + args = []string{"-delete", "test", "-force"} + if code := c.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!") + } +} + +func currentEnv() (string, error) { + contents, err := ioutil.ReadFile(filepath.Join(DefaultDataDir, local.DefaultEnvFile)) + if os.IsNotExist(err) { + return backend.DefaultStateName, nil + } + if err != nil { + return "", err + } + + current := strings.TrimSpace(string(contents)) + if current == "" { + current = backend.DefaultStateName + } + + return current, nil +} + +func envStatePath() (string, error) { + currentEnv, err := currentEnv() + if err != nil { + return "", err + } + + if currentEnv == backend.DefaultStateName { + return DefaultStateFilename, nil + } + + return filepath.Join(local.DefaultEnvDir, currentEnv, DefaultStateFilename), nil +} + +func listEnvs() ([]string, error) { + entries, err := ioutil.ReadDir(local.DefaultEnvDir) + // no error if there's no envs configured + if os.IsNotExist(err) { + return []string{backend.DefaultStateName}, nil + } + if err != nil { + return nil, err + } + + var envs []string + for _, entry := range entries { + if entry.IsDir() { + envs = append(envs, filepath.Base(entry.Name())) + } + } + + sort.Strings(envs) + + // always start with "default" + envs = append([]string{backend.DefaultStateName}, envs...) + + return envs, nil +} diff --git a/commands.go b/commands.go index 20e2ff8921..18ccd6d526 100644 --- a/commands.go +++ b/commands.go @@ -69,6 +69,12 @@ func init() { }, nil }, + "env": func() (cli.Command, error) { + return &command.EnvCommand{ + Meta: meta, + }, nil + }, + "fmt": func() (cli.Command, error) { return &command.FmtCommand{ Meta: meta, From c8526484b30b47694491664e331b8d5b5445d1b0 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 23 Feb 2017 13:13:28 -0500 Subject: [PATCH 07/22] split the env command into subcommands --- command/env_command.go | 243 ++---------------------------------- command/env_command_test.go | 54 ++++---- command/env_delete.go | 151 ++++++++++++++++++++++ command/env_list.go | 68 ++++++++++ command/env_new.go | 133 ++++++++++++++++++++ command/env_select.go | 93 ++++++++++++++ commands.go | 24 ++++ 7 files changed, 507 insertions(+), 259 deletions(-) create mode 100644 command/env_delete.go create mode 100644 command/env_list.go create mode 100644 command/env_new.go create mode 100644 command/env_select.go diff --git a/command/env_command.go b/command/env_command.go index 9ad03b5eb8..d2f16e8e16 100644 --- a/command/env_command.go +++ b/command/env_command.go @@ -1,13 +1,10 @@ package command import ( - "bytes" "fmt" - "os" "strings" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -15,33 +12,19 @@ import ( // environments. type EnvCommand struct { Meta - - newEnv string - delEnv string - statePath string - force bool - - // backend returns by Meta.Backend - b backend.Backend - // MultiState Backend - multi backend.MultiState } func (c *EnvCommand) Run(args []string) int { args = c.Meta.process(args, true) cmdFlags := c.Meta.flagSet("env") - cmdFlags.StringVar(&c.newEnv, "new", "", "create a new environment") - cmdFlags.StringVar(&c.delEnv, "delete", "", "delete an existing environment") - cmdFlags.StringVar(&c.statePath, "state", "", "terraform state file") - cmdFlags.BoolVar(&c.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) > 1 { - c.Ui.Error("0 or 1 arguments expected.\n") + if len(args) > 0 { + c.Ui.Error("0 arguments expected.\n") return cli.RunResultHelp } @@ -51,240 +34,36 @@ func (c *EnvCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) return 1 } - c.b = b multi, ok := b.(backend.MultiState) if !ok { c.Ui.Error(envNotSupported) return 1 } - c.multi = multi - - if c.newEnv != "" { - return c.createEnv() - } - - if c.delEnv != "" { - return c.deleteEnv() - } - - if len(args) == 1 { - return c.changeEnv(args[0]) - } - - return c.listEnvs() -} - -func (c *EnvCommand) createEnv() int { - states, _, err := c.multi.States() - for _, s := range states { - if c.newEnv == s { - c.Ui.Error(fmt.Sprintf(envExists, c.newEnv)) - return 1 - } - } - - err = c.multi.ChangeState(c.newEnv) + _, current, err := multi.States() if err != nil { c.Ui.Error(err.Error()) return 1 } - c.Ui.Output( - c.Colorize().Color( - fmt.Sprintf(envCreated, c.newEnv), - ), - ) - - if c.statePath == "" { - // if we're not loading a state, then we're done - return 0 - } - - // load the new state - sMgr, err := c.b.State() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - // load the existing state - stateFile, err := os.Open(c.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 - } - - err = sMgr.WriteState(s) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - return 0 -} - -func (c *EnvCommand) deleteEnv() int { - states, current, err := c.multi.States() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - exists := false - for _, s := range states { - if c.delEnv == s { - exists = true - break - } - } - - if !exists { - c.Ui.Error(fmt.Sprintf(envDoesNotExist, c.delEnv)) - return 1 - } - - // In order to check if the state being deleted is empty, we need to change - // to that state and load it. - if current != c.delEnv { - if err := c.multi.ChangeState(c.delEnv); err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - // always try to change back after - defer func() { - if err := c.multi.ChangeState(current); err != nil { - c.Ui.Error(err.Error()) - } - }() - } - - // we need the actual state to see if it's empty - sMgr, err := c.b.State() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - if err := sMgr.RefreshState(); err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - empty := sMgr.State().Empty() - - if !empty && !c.force { - c.Ui.Error(fmt.Sprintf(envNotEmpty, c.delEnv)) - return 1 - } - - err = c.multi.DeleteState(c.delEnv) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - c.Ui.Output( - c.Colorize().Color( - fmt.Sprintf(envDeleted, c.delEnv), - ), - ) - - if !empty { - c.Ui.Output( - c.Colorize().Color( - fmt.Sprintf(envWarnNotEmpty, c.delEnv), - ), - ) - } - - return 0 -} - -func (c *EnvCommand) changeEnv(name string) int { - states, current, err := c.multi.States() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - if current == name { - 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.multi.ChangeState(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 *EnvCommand) listEnvs() int { - states, current, err := c.multi.States() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - var out bytes.Buffer - for _, s := range states { - if s == current { - out.WriteString("* ") - } else { - out.WriteString(" ") - } - out.WriteString(s + "\n") - } - - c.Ui.Output(out.String()) + c.Ui.Output(fmt.Sprintf("Current environment is %q\n", current)) + c.Ui.Output(c.Help()) return 0 } func (c *EnvCommand) Help() string { helpText := ` -Usage: terraform env [options] [NAME] +Usage: terraform env Create, change and delete Terraform environments. - By default env will list all configured environments. If NAME is provided, - env will change into to that named environment. +Subcommands: -Options: - - -new=name Create a new environment. - -delete=name Delete an existing environment, - - -state=path Used with -new to copy a state file into the new environment. - -force Used with -delete to remove a non-empty environment. + list List environments. + select Select an environment. + new Create a new environment. + delete Delete an existing environment. ` return strings.TrimSpace(helpText) } diff --git a/command/env_command_test.go b/command/env_command_test.go index 8257932ad0..7caa119327 100644 --- a/command/env_command_test.go +++ b/command/env_command_test.go @@ -22,7 +22,7 @@ func TestEnv_createAndChange(t *testing.T) { defer os.RemoveAll(td) defer testChdir(t, td)() - c := &EnvCommand{} + newCmd := &EnvNewCommand{} current, err := currentEnv() if err != nil { @@ -32,10 +32,10 @@ func TestEnv_createAndChange(t *testing.T) { t.Fatal("current env should be 'default'") } - args := []string{"-new", "test"} + args := []string{"test"} ui := new(cli.MockUi) - c.Meta = Meta{Ui: ui} - if code := c.Run(args); code != 0 { + newCmd.Meta = Meta{Ui: ui} + if code := newCmd.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } @@ -47,10 +47,11 @@ func TestEnv_createAndChange(t *testing.T) { t.Fatal("current env should be 'test'") } + selCmd := &EnvSelectCommand{} args = []string{backend.DefaultStateName} ui = new(cli.MockUi) - c.Meta = Meta{Ui: ui} - if code := c.Run(args); code != 0 { + selCmd.Meta = Meta{Ui: ui} + if code := selCmd.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } @@ -74,31 +75,30 @@ func TestEnv_createAndList(t *testing.T) { defer os.RemoveAll(td) defer testChdir(t, td)() - c := &EnvCommand{} + newCmd := &EnvNewCommand{} envs := []string{"test_a", "test_b", "test_c"} // create multiple envs for _, env := range envs { - args := []string{"-new", env} ui := new(cli.MockUi) - c.Meta = Meta{Ui: ui} - if code := c.Run(args); code != 0 { + newCmd.Meta = Meta{Ui: ui} + if code := newCmd.Run([]string{env}); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } } - // now check the listing - expected := "default\n test_a\n test_b\n* test_c" - + listCmd := &EnvListCommand{} ui := new(cli.MockUi) - c.Meta = Meta{Ui: ui} + listCmd.Meta = Meta{Ui: ui} - if code := c.Run(nil); code != 0 { + 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) } @@ -132,12 +132,12 @@ func TestEnv_createWithState(t *testing.T) { t.Fatal(err) } - args := []string{"-new", "test", "-state", "test.tfstate"} + args := []string{"-state", "test.tfstate", "test"} ui := new(cli.MockUi) - c := &EnvCommand{ + newCmd := &EnvNewCommand{ Meta: Meta{Ui: ui}, } - if code := c.Run(args); code != 0 { + if code := newCmd.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } @@ -183,11 +183,11 @@ func TestEnv_delete(t *testing.T) { } ui := new(cli.MockUi) - c := &EnvCommand{ + delCmd := &EnvDeleteCommand{ Meta: Meta{Ui: ui}, } - args := []string{"-delete", "test"} - if code := c.Run(args); code != 0 { + args := []string{"test"} + if code := delCmd.Run(args); code != 0 { t.Fatalf("failure: %s", ui.ErrorWriter) } @@ -235,19 +235,19 @@ func TestEnv_deleteWithState(t *testing.T) { } ui := new(cli.MockUi) - c := &EnvCommand{ + delCmd := &EnvDeleteCommand{ Meta: Meta{Ui: ui}, } - args := []string{"-delete", "test"} - if code := c.Run(args); code == 0 { + args := []string{"test"} + if code := delCmd.Run(args); code == 0 { t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter) } ui = new(cli.MockUi) - c.Meta.Ui = ui + delCmd.Meta.Ui = ui - args = []string{"-delete", "test", "-force"} - if code := c.Run(args); code != 0 { + args = []string{"-force", "test"} + if code := delCmd.Run(args); code != 0 { t.Fatalf("failure: %s", ui.ErrorWriter) } diff --git a/command/env_delete.go b/command/env_delete.go new file mode 100644 index 0000000000..5295e4db42 --- /dev/null +++ b/command/env_delete.go @@ -0,0 +1,151 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/backend" + "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) != 1 { + c.Ui.Error("expected NAME.\n") + return cli.RunResultHelp + } + + delEnv := args[0] + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + multi, ok := b.(backend.MultiState) + if !ok { + c.Ui.Error(envNotSupported) + return 1 + } + + states, current, err := multi.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 + } + + // In order to check if the state being deleted is empty, we need to change + // to that state and load it. + if current != delEnv { + if err := multi.ChangeState(delEnv); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // always try to change back after + defer func() { + if err := multi.ChangeState(current); err != nil { + c.Ui.Error(err.Error()) + } + }() + } + + // we need the actual state to see if it's empty + sMgr, err := b.State() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if err := sMgr.RefreshState(); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + empty := sMgr.State().Empty() + + if !empty && !force { + c.Ui.Error(fmt.Sprintf(envNotEmpty, delEnv)) + 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()) + + err = multi.DeleteState(delEnv) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + c.Ui.Output( + c.Colorize().Color( + fmt.Sprintf(envDeleted, delEnv), + ), + ) + + if !empty { + c.Ui.Output( + c.Colorize().Color( + fmt.Sprintf(envWarnNotEmpty, delEnv), + ), + ) + } + + return 0 +} +func (c *EnvDeleteCommand) Help() string { + helpText := ` +Usage: terraform env delete [OPTIONS] NAME + + 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 0000000000..23761b99e4 --- /dev/null +++ b/command/env_list.go @@ -0,0 +1,68 @@ +package command + +import ( + "bytes" + "fmt" + "strings" + + "github.com/hashicorp/terraform/backend" +) + +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 + } + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + multi, ok := b.(backend.MultiState) + if !ok { + c.Ui.Error(envNotSupported) + return 1 + } + + states, current, err := multi.States() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + var out bytes.Buffer + for _, s := range states { + if s == current { + 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 + + 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 0000000000..026199eeb6 --- /dev/null +++ b/command/env_new.go @@ -0,0 +1,133 @@ +package command + +import ( + "fmt" + "os" + "strings" + + "github.com/hashicorp/terraform/backend" + "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) != 1 { + c.Ui.Error("expected NAME.\n") + return cli.RunResultHelp + } + + newEnv := args[0] + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + multi, ok := b.(backend.MultiState) + if !ok { + c.Ui.Error(envNotSupported) + return 1 + } + + states, _, err := multi.States() + for _, s := range states { + if newEnv == s { + c.Ui.Error(fmt.Sprintf(envExists, newEnv)) + return 1 + } + } + + err = multi.ChangeState(newEnv) + if err != nil { + c.Ui.Error(err.Error()) + 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() + 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 + + 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 0000000000..7598b42041 --- /dev/null +++ b/command/env_select.go @@ -0,0 +1,93 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/backend" + "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) != 1 { + c.Ui.Error("expected NAME.\n") + return cli.RunResultHelp + } + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + name := args[0] + + multi, ok := b.(backend.MultiState) + if !ok { + c.Ui.Error(envNotSupported) + return 1 + } + + states, current, err := multi.States() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if current == name { + 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 = multi.ChangeState(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 + + Change Terraform environment. +` + return strings.TrimSpace(helpText) +} + +func (c *EnvSelectCommand) Synopsis() string { + return "Change environments" +} diff --git a/commands.go b/commands.go index 18ccd6d526..f2f7b4eda3 100644 --- a/commands.go +++ b/commands.go @@ -75,6 +75,30 @@ func init() { }, 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, From 06663991d12c539a0fe05f28acdc7b4b131d7d95 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 23 Feb 2017 14:14:51 -0500 Subject: [PATCH 08/22] Add config path argument to env commands In order to operate in parity with other commands, the env command should take a path argument to locate the configuration. This however introduces the issue of a possible name conflict between a path and subcommand, or printing an incorrect current environment for the bare `env` command. In favor of simplicity this removes the current env output and only prints usage when no subcommand is provided. --- command/env_command.go | 35 +---------------------------------- command/env_delete.go | 12 +++++++++--- command/env_list.go | 10 ++++++++-- command/env_new.go | 12 +++++++++--- command/env_select.go | 12 +++++++++--- 5 files changed, 36 insertions(+), 45 deletions(-) diff --git a/command/env_command.go b/command/env_command.go index d2f16e8e16..7701a2b78e 100644 --- a/command/env_command.go +++ b/command/env_command.go @@ -1,12 +1,6 @@ package command -import ( - "fmt" - "strings" - - "github.com/hashicorp/terraform/backend" - "github.com/mitchellh/cli" -) +import "strings" // EnvCommand is a Command Implementation that manipulates local state // environments. @@ -19,34 +13,7 @@ func (c *EnvCommand) Run(args []string) int { cmdFlags := c.Meta.flagSet("env") 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("0 arguments expected.\n") - return cli.RunResultHelp - } - // Load the backend - b, err := c.Backend(nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) - return 1 - } - - multi, ok := b.(backend.MultiState) - if !ok { - c.Ui.Error(envNotSupported) - return 1 - } - _, current, err := multi.States() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - c.Ui.Output(fmt.Sprintf("Current environment is %q\n", current)) c.Ui.Output(c.Help()) return 0 } diff --git a/command/env_delete.go b/command/env_delete.go index 5295e4db42..95b734d44e 100644 --- a/command/env_delete.go +++ b/command/env_delete.go @@ -26,15 +26,21 @@ func (c *EnvDeleteCommand) Run(args []string) int { return 1 } args = cmdFlags.Args() - if len(args) != 1 { + 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(nil) + b, err := c.Backend(&BackendOpts{ConfigPath: configPath}) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) return 1 @@ -134,7 +140,7 @@ func (c *EnvDeleteCommand) Run(args []string) int { } func (c *EnvDeleteCommand) Help() string { helpText := ` -Usage: terraform env delete [OPTIONS] NAME +Usage: terraform env delete [OPTIONS] NAME [DIR] Delete a Terraform environment diff --git a/command/env_list.go b/command/env_list.go index 23761b99e4..cffeb38f3e 100644 --- a/command/env_list.go +++ b/command/env_list.go @@ -21,8 +21,14 @@ func (c *EnvListCommand) Run(args []string) int { return 1 } + configPath, err := ModulePath(args) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + // Load the backend - b, err := c.Backend(nil) + b, err := c.Backend(&BackendOpts{ConfigPath: configPath}) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) return 1 @@ -56,7 +62,7 @@ func (c *EnvListCommand) Run(args []string) int { func (c *EnvListCommand) Help() string { helpText := ` -Usage: terraform env list +Usage: terraform env list [DIR] List Terraform environments. ` diff --git a/command/env_new.go b/command/env_new.go index 026199eeb6..eb5c990769 100644 --- a/command/env_new.go +++ b/command/env_new.go @@ -29,15 +29,21 @@ func (c *EnvNewCommand) Run(args []string) int { return 1 } args = cmdFlags.Args() - if len(args) != 1 { + 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(nil) + b, err := c.Backend(&BackendOpts{ConfigPath: configPath}) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) return 1 @@ -116,7 +122,7 @@ func (c *EnvNewCommand) Run(args []string) int { func (c *EnvNewCommand) Help() string { helpText := ` -Usage: terraform env new [OPTIONS] NAME +Usage: terraform env new [OPTIONS] NAME [DIR] Create a new Terraform environment. diff --git a/command/env_select.go b/command/env_select.go index 7598b42041..adfd2f056f 100644 --- a/command/env_select.go +++ b/command/env_select.go @@ -21,13 +21,19 @@ func (c *EnvSelectCommand) Run(args []string) int { return 1 } args = cmdFlags.Args() - if len(args) != 1 { + 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(nil) + b, err := c.Backend(&BackendOpts{ConfigPath: configPath}) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) return 1 @@ -81,7 +87,7 @@ func (c *EnvSelectCommand) Run(args []string) int { func (c *EnvSelectCommand) Help() string { helpText := ` -Usage: terraform env select NAME +Usage: terraform env select NAME [DIR] Change Terraform environment. ` From fbc11c79618ff28c5ee29b6f71f576a9300dca5f Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 24 Feb 2017 08:53:37 -0500 Subject: [PATCH 09/22] fix incorrect current state in local backend Forgot to remove the currentState field, which was not always set. The current state should always just be read from the environment file. Always return the default state name when we can't determine the state. --- backend/local/backend.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/backend/local/backend.go b/backend/local/backend.go index afb1af9eb9..6cd405950f 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -55,10 +55,6 @@ type Local struct { // we only want to create a single instance of the local state state state.State - // the name of the current state - currentState string - - // ContextOpts are the base context options to set when initializing a // Terraform context. Many of these will be overridden or merged by // Operation. See Operation for more details. ContextOpts *terraform.ContextOpts @@ -135,31 +131,37 @@ func (b *Local) States() ([]string, string, error) { // the listing always start with "default" envs := []string{backend.DefaultStateName} - current := b.currentState - if current == "" { - name, err := b.currentStateName() - if err != nil { - return nil, "", err - } - current = name + current, err := b.currentStateName() + if err != nil { + return nil, "", err } entries, err := ioutil.ReadDir(filepath.Join(b.workingDir, DefaultEnvDir)) // no error if there's no envs configured if os.IsNotExist(err) { - return envs, current, nil + return envs, backend.DefaultStateName, nil } if err != nil { return nil, "", err } + currentExists := false var listed []string for _, entry := range entries { if entry.IsDir() { - listed = append(listed, filepath.Base(entry.Name())) + name := filepath.Base(entry.Name()) + if name == current { + currentExists = true + } + listed = append(listed, name) } } + // current was out of sync for some reason, so return defualt + if !currentExists { + current = backend.DefaultStateName + } + sort.Strings(listed) envs = append(envs, listed...) @@ -254,8 +256,6 @@ func (b *Local) ChangeState(name string) error { return err } - b.currentState = name - // remove the current state so it's reloaded on the next call to State b.state = nil @@ -421,7 +421,7 @@ func (b *Local) statePath() (string, error) { path := DefaultStateFilename if current != backend.DefaultStateName && current != "" { - path = filepath.Join(b.workingDir, DefaultEnvDir, b.currentState, DefaultStateFilename) + path = filepath.Join(b.workingDir, DefaultEnvDir, current, DefaultStateFilename) } return path, nil } From 2a73331c6247f8e42d16901fa14fab1408fbe13e Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 24 Feb 2017 09:26:14 -0500 Subject: [PATCH 10/22] use State.HasResources rather than State.Empty Destroying a terraform state can't always create an empty state, as outputs and the root module may remain. Use HasResources to warn about deleting an environment with resources. --- command/env_delete.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/command/env_delete.go b/command/env_delete.go index 95b734d44e..b573e57d52 100644 --- a/command/env_delete.go +++ b/command/env_delete.go @@ -99,9 +99,9 @@ func (c *EnvDeleteCommand) Run(args []string) int { return 1 } - empty := sMgr.State().Empty() + hasResources := sMgr.State().HasResources() - if !empty && !force { + if hasResources && !force { c.Ui.Error(fmt.Sprintf(envNotEmpty, delEnv)) return 1 } @@ -128,7 +128,7 @@ func (c *EnvDeleteCommand) Run(args []string) int { ), ) - if !empty { + if hasResources { c.Ui.Output( c.Colorize().Color( fmt.Sprintf(envWarnNotEmpty, delEnv), From 7f453f334166b606df8996c9588371a2febb5525 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 24 Feb 2017 13:26:17 -0500 Subject: [PATCH 11/22] remove some leftover methods in the legacy backend These were left from the initial implementation, but are not used. --- backend/legacy/backend.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/legacy/backend.go b/backend/legacy/backend.go index 3fafb7e67b..21ed7b1fb8 100644 --- a/backend/legacy/backend.go +++ b/backend/legacy/backend.go @@ -3,7 +3,6 @@ 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" @@ -61,11 +60,3 @@ func (b *Backend) State() (state.State, error) { return &remote.State{Client: b.client}, nil } - -func (b *Backend) States() ([]string, string, error) { - return []string{backend.DefaultStateName}, backend.DefaultStateName, nil -} - -func (b *Backend) ChangeState(name string) error { - return nil -} From 96194fbc0d554dea6c8731c936439bad3780f83d Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 27 Feb 2017 14:00:18 -0500 Subject: [PATCH 12/22] Update Backend interface to latest iteration What will hopfully be the final version of the Backend interface. This combines the MultiState interface into Backend since it will be required to implement, and simplifies the interface because the Backend is no longer responsible for tracking the current state. --- backend/backend.go | 26 ++++++++++++-------------- backend/nil.go | 13 ++++++------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index 527c293859..28c59f1901 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -26,23 +26,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) -// MultiState is an interface that a backend can implement to allow changing -// between named states depending on the configured environment. -type MultiState interface { - // States returns a list of configured named states and the current state. - States() ([]string, string, error) - - // ChangeState changes to the named state. If the named state doesn't exist - // it will be created. - ChangeState(name string) error - - // DeleteState removes the named state if it exists. If the current state is - // deleted, the backend should change to the default state. It is an 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. diff --git a/backend/nil.go b/backend/nil.go index 5482ca8a07..e2f11e91d1 100644 --- a/backend/nil.go +++ b/backend/nil.go @@ -25,16 +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) States() ([]string, string, error) { - // The default state always exists - return []string{DefaultStateName}, DefaultStateName, nil -} - -func (Nil) ChangeState(string) error { +func (Nil) DeleteState(string) error { return nil } + +func (Nil) States() ([]string, error) { + return []string{DefaultStateName}, nil +} From 65527f35a4581fa7fc139199812f06a967db5c8c Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 27 Feb 2017 16:43:31 -0500 Subject: [PATCH 13/22] update local.Local to match the latest Backend Update the methods, remove the handling of "current", and make tests pass. --- backend/backend.go | 3 + backend/local/backend.go | 194 +++++++++------------------------ backend/local/backend_local.go | 2 +- backend/local/backend_test.go | 135 ++++++----------------- 4 files changed, 86 insertions(+), 248 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index 28c59f1901..0139b5ad36 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -123,6 +123,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/local/backend.go b/backend/local/backend.go index 6cd405950f..2413d9ee3f 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -53,8 +53,10 @@ type Local struct { 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 + // Terraform context. Many of these will be overridden or merged by // Operation. See Operation for more details. ContextOpts *terraform.ContextOpts @@ -78,10 +80,6 @@ type Local struct { schema *schema.Backend opLock sync.Mutex once sync.Once - - // workingDir is where the State* paths should be relative to. - // This is currently only used for tests. - workingDir string } func (b *Local) Input( @@ -118,54 +116,35 @@ func (b *Local) Configure(c *terraform.ResourceConfig) error { return f(c) } -func (b *Local) States() ([]string, string, error) { +func (b *Local) States() ([]string, error) { // If we have a backend handling state, defer to that. if b.Backend != nil { - if b, ok := b.Backend.(backend.MultiState); ok { - return b.States() - } else { - return nil, "", ErrEnvNotSupported - } + return b.Backend.States() } // the listing always start with "default" envs := []string{backend.DefaultStateName} - current, err := b.currentStateName() - if err != nil { - return nil, "", err - } - - entries, err := ioutil.ReadDir(filepath.Join(b.workingDir, DefaultEnvDir)) + entries, err := ioutil.ReadDir(DefaultEnvDir) // no error if there's no envs configured if os.IsNotExist(err) { - return envs, backend.DefaultStateName, nil + return envs, nil } if err != nil { - return nil, "", err + return nil, err } - currentExists := false var listed []string for _, entry := range entries { if entry.IsDir() { - name := filepath.Base(entry.Name()) - if name == current { - currentExists = true - } - listed = append(listed, name) + listed = append(listed, filepath.Base(entry.Name())) } } - // current was out of sync for some reason, so return defualt - if !currentExists { - current = backend.DefaultStateName - } - sort.Strings(listed) envs = append(envs, listed...) - return envs, current, nil + return envs, nil } // DeleteState removes a named state. @@ -173,11 +152,7 @@ func (b *Local) States() ([]string, string, error) { func (b *Local) DeleteState(name string) error { // If we have a backend handling state, defer to that. if b.Backend != nil { - if b, ok := b.Backend.(backend.MultiState); ok { - return b.DeleteState(name) - } else { - return ErrEnvNotSupported - } + return b.Backend.DeleteState(name) } if name == "" { @@ -188,91 +163,25 @@ func (b *Local) DeleteState(name string) error { return errors.New("cannot delete default state") } - _, current, err := b.States() - if err != nil { - return err - } - - // if we're deleting the current state, we change back to the default - if name == current { - if err := b.ChangeState(backend.DefaultStateName); err != nil { - return err - } - } - - return os.RemoveAll(filepath.Join(b.workingDir, DefaultEnvDir, name)) + delete(b.states, name) + return os.RemoveAll(filepath.Join(DefaultEnvDir, name)) } -// Change to the named state, creating it if it doesn't exist. -func (b *Local) ChangeState(name string) error { +func (b *Local) State(name string) (state.State, error) { // If we have a backend handling state, defer to that. if b.Backend != nil { - if b, ok := b.Backend.(backend.MultiState); ok { - return b.ChangeState(name) - } else { - return ErrEnvNotSupported - } + return b.Backend.State(name) } - name = strings.TrimSpace(name) - if name == "" { - return errors.New("state name cannot be empty") + if s, ok := b.states[name]; ok { + return s, nil } - envs, current, err := b.States() - if err != nil { - return err + if err := b.createState(name); err != nil { + return nil, err } - if name == current { - return nil - } - - exists := false - for _, env := range envs { - if env == name { - exists = true - break - } - } - - if !exists { - if err := b.createState(name); err != nil { - return err - } - } - - err = os.MkdirAll(filepath.Join(b.workingDir, DefaultDataDir), 0755) - if err != nil { - return err - } - - err = ioutil.WriteFile( - filepath.Join(b.workingDir, DefaultDataDir, DefaultEnvFile), - []byte(name), - 0644, - ) - if err != nil { - return err - } - - // remove the current state so it's reloaded on the next call to State - b.state = nil - - return nil -} - -func (b *Local) State() (state.State, error) { - // If we have a backend handling state, defer to that. - if b.Backend != nil { - return b.Backend.State() - } - - if b.state != nil { - return b.state, nil - } - - statePath, stateOutPath, backupPath, err := b.StatePaths() + statePath, stateOutPath, backupPath, err := b.StatePaths(name) if err != nil { return nil, err } @@ -291,7 +200,10 @@ func (b *Local) State() (state.State, error) { } } - b.state = s + if b.states == nil { + b.states = map[string]state.State{} + } + b.states[name] = s return s, nil } @@ -385,20 +297,24 @@ func (b *Local) schemaConfigure(ctx context.Context) error { } // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as -// configured by the current environment. If backups are disabled, -// StateBackupPath will be an empty string. -func (b *Local) StatePaths() (string, string, string, error) { +// configured from the CLI. +func (b *Local) StatePaths(name string) (string, string, string, error) { statePath := b.StatePath stateOutPath := b.StateOutPath backupPath := b.StateBackupPath - if statePath == "" { - path, err := b.statePath() - if err != nil { - return "", "", "", err - } - statePath = path + if name == "" { + name = backend.DefaultStateName } + + if name == backend.DefaultStateName { + if statePath == "" { + statePath = name + } + } else { + statePath = filepath.Join(DefaultEnvDir, name, DefaultStateFilename) + } + if stateOutPath == "" { stateOutPath = statePath } @@ -413,33 +329,21 @@ func (b *Local) StatePaths() (string, string, string, error) { return statePath, stateOutPath, backupPath, nil } -func (b *Local) statePath() (string, error) { - _, current, err := b.States() - if err != nil { - return "", err - } - path := DefaultStateFilename - - if current != backend.DefaultStateName && current != "" { - path = filepath.Join(b.workingDir, DefaultEnvDir, current, DefaultStateFilename) - } - return path, nil -} - +// this only ensures that the named directory exists func (b *Local) createState(name string) error { - stateNames, _, err := b.States() - if err != nil { - return err + if name == backend.DefaultStateName { + return nil } - for _, n := range stateNames { - if name == n { - // state exists, nothing to do - 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(filepath.Join(b.workingDir, DefaultEnvDir, name), 0755) + err = os.MkdirAll(stateDir, 0755) if err != nil { return err } @@ -451,7 +355,7 @@ func (b *Local) createState(name string) error { // configuration files. // If there are no configured environments, currentStateName returns "default" func (b *Local) currentStateName() (string, error) { - contents, err := ioutil.ReadFile(filepath.Join(b.workingDir, DefaultDataDir, DefaultEnvFile)) + contents, err := ioutil.ReadFile(filepath.Join(DefaultDataDir, DefaultEnvFile)) if os.IsNotExist(err) { return backend.DefaultStateName, nil } diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index 0ecba44ce0..8336323212 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 a72a0fcc0f..1d9459e64a 100644 --- a/backend/local/backend_test.go +++ b/backend/local/backend_test.go @@ -1,15 +1,15 @@ package local import ( - "fmt" + "errors" "io/ioutil" "os" - "path/filepath" "reflect" "strings" "testing" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" ) @@ -17,7 +17,6 @@ func TestLocal_impl(t *testing.T) { var _ backend.Enhanced = new(Local) var _ backend.Local = new(Local) var _ backend.CLI = new(Local) - var _ backend.MultiState = new(Local) } func checkState(t *testing.T, path, expected string) { @@ -46,31 +45,24 @@ func TestLocal_addAndRemoveStates(t *testing.T) { expectedStates := []string{dflt} b := &Local{} - states, current, err := b.States() + states, err := b.States() if err != nil { t.Fatal(err) } - if current != dflt { - t.Fatalf("expected %q, got %q", dflt, current) - } - if !reflect.DeepEqual(states, expectedStates) { t.Fatalf("expected []string{%q}, got %q", dflt, states) } expectedA := "test_A" - if err := b.ChangeState(expectedA); err != nil { + if _, err := b.State(expectedA); err != nil { t.Fatal(err) } - states, current, err = b.States() + states, err = b.States() if err != nil { t.Fatal(err) } - if current != expectedA { - t.Fatalf("expected %q, got %q", expectedA, current) - } expectedStates = append(expectedStates, expectedA) if !reflect.DeepEqual(states, expectedStates) { @@ -78,17 +70,14 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } expectedB := "test_B" - if err := b.ChangeState(expectedB); err != nil { + if _, err := b.State(expectedB); err != nil { t.Fatal(err) } - states, current, err = b.States() + states, err = b.States() if err != nil { t.Fatal(err) } - if current != expectedB { - t.Fatalf("expected %q, got %q", expectedB, current) - } expectedStates = append(expectedStates, expectedB) if !reflect.DeepEqual(states, expectedStates) { @@ -99,13 +88,10 @@ func TestLocal_addAndRemoveStates(t *testing.T) { t.Fatal(err) } - states, current, err = b.States() + states, err = b.States() if err != nil { t.Fatal(err) } - if current != expectedB { - t.Fatalf("expected %q, got %q", dflt, current) - } expectedStates = []string{dflt, expectedB} if !reflect.DeepEqual(states, expectedStates) { @@ -116,13 +102,10 @@ func TestLocal_addAndRemoveStates(t *testing.T) { t.Fatal(err) } - states, current, err = b.States() + states, err = b.States() if err != nil { t.Fatal(err) } - if current != dflt { - t.Fatalf("expected %q, got %q", dflt, current) - } expectedStates = []string{dflt} if !reflect.DeepEqual(states, expectedStates) { @@ -134,97 +117,45 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } } -// verify the behavior with a backend that doesn't support multiple states -func TestLocal_noMultiStateBackend(t *testing.T) { - type noMultiState struct { - backend.Backend - } +// a local backend which return sentinel errors for NamedState methods to +// verify it's being called. +type testDelegateBackend struct { + *Local +} - b := &Local{ - Backend: &noMultiState{}, - } +var errTestDelegateState = errors.New("State called") +var errTestDelegateStates = errors.New("States called") +var errTestDelegateDeleteState = errors.New("Delete called") - _, _, err := b.States() - if err != ErrEnvNotSupported { - t.Fatal("backend does not support environments.", err) - } +func (b *testDelegateBackend) State(name string) (state.State, error) { + return nil, errTestDelegateState +} - err = b.ChangeState("test") - if err != ErrEnvNotSupported { - t.Fatal("backend does not support environments.", err) - } +func (b *testDelegateBackend) States() ([]string, error) { + return nil, errTestDelegateStates +} - err = b.ChangeState("test") - if err != ErrEnvNotSupported { - t.Fatal("backend does not support environments.", err) - } +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) { - defer testTmpDir(t)() - - dflt := backend.DefaultStateName - expectedStates := []string{dflt} - - // make a second tmp dir for the sub-Backend. - // we verify the corret backend was called by checking the paths. - tmp, err := ioutil.TempDir("", "tf") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmp) - - fmt.Println("second tmp:", tmp) - + // assign a separate backend where we can read the state b := &Local{ - Backend: &Local{ - workingDir: tmp, - }, + Backend: &testDelegateBackend{}, } - testA := "test_A" - if err := b.ChangeState(testA); err != nil { - t.Fatal(err) + if _, err := b.State("test"); err != errTestDelegateState { + t.Fatal("expected errTestDelegateState, got:", err) } - states, current, err := b.States() - if err != nil { - t.Fatal(err) - } - if current != testA { - t.Fatalf("expected %q, got %q", testA, current) + if _, err := b.States(); err != errTestDelegateStates { + t.Fatal("expected errTestDelegateStates, got:", err) } - expectedStates = append(expectedStates, testA) - if !reflect.DeepEqual(states, expectedStates) { - t.Fatalf("expected %q, got %q", expectedStates, states) - } - - // verify that no environment paths were created for the top-level Backend - if _, err := os.Stat(DefaultDataDir); !os.IsNotExist(err) { - t.Fatal("remote state operations should not have written local files") - } - - if _, err := os.Stat(filepath.Join(DefaultEnvDir, testA)); !os.IsNotExist(err) { - t.Fatal("remote state operations should not have written local files") - } - - // remove the new state - if err := b.DeleteState(testA); err != nil { - t.Fatal(err) - } - - states, current, err = b.States() - if err != nil { - t.Fatal(err) - } - if current != dflt { - t.Fatalf("expected %q, got %q", dflt, current) - } - - if !reflect.DeepEqual(states, expectedStates[:1]) { - t.Fatalf("expected %q, got %q", expectedStates, states) + if err := b.DeleteState("test"); err != errTestDelegateDeleteState { + t.Fatal("expected errTestDelegateDeleteState, got:", err) } } From 5762878eba91a5e7f002edd2d125317478f13668 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 27 Feb 2017 16:51:03 -0500 Subject: [PATCH 14/22] Make backcend/legacy match new Backend iface move the unsupported error value to backend.ErrNamedStatesNotSupported to be used by any backend implementation. --- backend/backend.go | 4 ++++ backend/legacy/backend.go | 15 ++++++++++++++- backend/legacy/backend_test.go | 2 +- backend/local/backend.go | 2 -- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index 0139b5ad36..50cf144658 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -6,6 +6,7 @@ package backend import ( "context" + "errors" "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/state" @@ -14,6 +15,9 @@ import ( 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 diff --git a/backend/legacy/backend.go b/backend/legacy/backend.go index 21ed7b1fb8..a8b0cad9fb 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 eceb48510c..b46ed9252d 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 2413d9ee3f..a6329404c0 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -27,8 +27,6 @@ const ( DefaultBackupExtension = ".backup" ) -var ErrEnvNotSupported = errors.New("environments not supported") - // 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. From 8fdf3a42b800e08f56cbd479b8d96c1867e77371 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 27 Feb 2017 16:56:55 -0500 Subject: [PATCH 15/22] update remote-state.Backend --- backend/remote-state/backend.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/backend/remote-state/backend.go b/backend/remote-state/backend.go index 8dd50e6f48..b0f546a776 100644 --- a/backend/remote-state/backend.go +++ b/backend/remote-state/backend.go @@ -47,20 +47,24 @@ func (b *Backend) Configure(rc *terraform.ResourceConfig) error { return b.Backend.Configure(rc) } -func (b *Backend) States() ([]string, string, error) { - return []string{backend.DefaultStateName}, backend.DefaultStateName, nil +func (b *Backend) States() ([]string, error) { + return nil, backend.ErrNamedStatesNotSupported } -func (b *Backend) ChangeState(name string) error { - return nil +func (b *Backend) DeleteState(name string) error { + return backend.ErrNamedStatesNotSupported } -func (b *Backend) State() (state.State, error) { +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 } From 597eb6c91819de3a7e7984b0ded7791356bd9a87 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 27 Feb 2017 17:02:08 -0500 Subject: [PATCH 16/22] update remote-state/inmem client --- backend/remote-state/inmem/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/remote-state/inmem/client_test.go b/backend/remote-state/inmem/client_test.go index 549cbd80bd..f3de567157 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) } From f866bb545c1cb36c74693eaab1185f7ad4a0355f Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 27 Feb 2017 17:09:26 -0500 Subject: [PATCH 17/22] update remote-state/consul --- backend/remote-state/consul/client_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/remote-state/consul/client_test.go b/backend/remote-state/consul/client_test.go index 52246e48ba..2cb11f0d91 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) } From b53704ed8747b4740dd0a876d2284c2f5fd0c9e5 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 28 Feb 2017 13:13:03 -0500 Subject: [PATCH 18/22] Thread the environment through all commands Add Env and SetEnv methods to command.Meta to retrieve the current environment name inside any command. Make sure all calls to Backend.State contain an environment name, and make the package compile against the update backend package. --- backend/local/backend.go | 2 +- command/env_command.go | 5 ++ command/env_command_test.go | 108 ++++++++--------------------------- command/env_delete.go | 30 ++-------- command/env_list.go | 14 ++--- command/env_new.go | 19 +++--- command/env_select.go | 14 ++--- command/meta.go | 44 ++++++++++++++ command/meta_backend.go | 46 ++++++++++----- command/meta_backend_test.go | 65 ++++++++++----------- command/meta_test.go | 35 ++++++++++++ command/output.go | 4 +- command/show.go | 4 +- command/state_command.go | 2 +- command/state_list.go | 5 +- command/state_list_test.go | 16 ++++-- command/state_meta.go | 9 ++- command/state_mv.go | 1 - command/state_mv_test.go | 48 ++++++++++------ command/state_pull.go | 4 +- command/state_pull_test.go | 8 ++- command/state_push.go | 4 +- command/state_push_test.go | 40 ++++++++----- command/state_rm.go | 1 - command/state_rm_test.go | 16 ++++-- command/state_show.go | 4 +- command/state_show_test.go | 40 ++++++++----- command/taint.go | 3 +- command/unlock.go | 4 +- command/untaint.go | 3 +- commands.go | 19 +++--- 31 files changed, 343 insertions(+), 274 deletions(-) diff --git a/backend/local/backend.go b/backend/local/backend.go index a6329404c0..84965850b5 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -307,7 +307,7 @@ func (b *Local) StatePaths(name string) (string, string, string, error) { if name == backend.DefaultStateName { if statePath == "" { - statePath = name + statePath = DefaultStateFilename } } else { statePath = filepath.Join(DefaultEnvDir, name, DefaultStateFilename) diff --git a/command/env_command.go b/command/env_command.go index 7701a2b78e..425013401f 100644 --- a/command/env_command.go +++ b/command/env_command.go @@ -64,5 +64,10 @@ anyways and risk dangling resources, use the '-force' flag. 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 index 7caa119327..356c8d66aa 100644 --- a/command/env_command_test.go +++ b/command/env_command_test.go @@ -4,7 +4,6 @@ import ( "io/ioutil" "os" "path/filepath" - "sort" "strings" "testing" @@ -24,10 +23,7 @@ func TestEnv_createAndChange(t *testing.T) { newCmd := &EnvNewCommand{} - current, err := currentEnv() - if err != nil { - t.Fatal(err) - } + current := newCmd.Env() if current != backend.DefaultStateName { t.Fatal("current env should be 'default'") } @@ -39,12 +35,9 @@ func TestEnv_createAndChange(t *testing.T) { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } - current, err = currentEnv() - if err != nil { - t.Fatal(err) - } + current = newCmd.Env() if current != "test" { - t.Fatal("current env should be 'test'") + t.Fatalf("current env should be 'test', got %q", current) } selCmd := &EnvSelectCommand{} @@ -55,11 +48,7 @@ func TestEnv_createAndChange(t *testing.T) { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } - current, err = currentEnv() - if err != nil { - t.Fatal(err) - } - + current = newCmd.Env() if current != backend.DefaultStateName { t.Fatal("current env should be 'default'") } @@ -173,29 +162,35 @@ func TestEnv_delete(t *testing.T) { t.Fatal(err) } - current, err := currentEnv() - if err != nil { - t.Fatal(err) - } - - if current != "test" { - t.Fatal("wrong env:", current) - } - ui := new(cli.MockUi) delCmd := &EnvDeleteCommand{ Meta: Meta{Ui: ui}, } - args := []string{"test"} - if code := delCmd.Run(args); code != 0 { - t.Fatalf("failure: %s", ui.ErrorWriter) + + current := delCmd.Env() + if current != "test" { + t.Fatal("wrong env:", current) } - current, err = currentEnv() - if err != nil { + // 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) } @@ -255,58 +250,3 @@ func TestEnv_deleteWithState(t *testing.T) { t.Fatal("env 'test' still exists!") } } - -func currentEnv() (string, error) { - contents, err := ioutil.ReadFile(filepath.Join(DefaultDataDir, local.DefaultEnvFile)) - if os.IsNotExist(err) { - return backend.DefaultStateName, nil - } - if err != nil { - return "", err - } - - current := strings.TrimSpace(string(contents)) - if current == "" { - current = backend.DefaultStateName - } - - return current, nil -} - -func envStatePath() (string, error) { - currentEnv, err := currentEnv() - if err != nil { - return "", err - } - - if currentEnv == backend.DefaultStateName { - return DefaultStateFilename, nil - } - - return filepath.Join(local.DefaultEnvDir, currentEnv, DefaultStateFilename), nil -} - -func listEnvs() ([]string, error) { - entries, err := ioutil.ReadDir(local.DefaultEnvDir) - // no error if there's no envs configured - if os.IsNotExist(err) { - return []string{backend.DefaultStateName}, nil - } - if err != nil { - return nil, err - } - - var envs []string - for _, entry := range entries { - if entry.IsDir() { - envs = append(envs, filepath.Base(entry.Name())) - } - } - - sort.Strings(envs) - - // always start with "default" - envs = append([]string{backend.DefaultStateName}, envs...) - - return envs, nil -} diff --git a/command/env_delete.go b/command/env_delete.go index b573e57d52..1a773c0cca 100644 --- a/command/env_delete.go +++ b/command/env_delete.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state" "github.com/mitchellh/cli" @@ -46,13 +45,7 @@ func (c *EnvDeleteCommand) Run(args []string) int { return 1 } - multi, ok := b.(backend.MultiState) - if !ok { - c.Ui.Error(envNotSupported) - return 1 - } - - states, current, err := multi.States() + states, err := b.States() if err != nil { c.Ui.Error(err.Error()) return 1 @@ -71,24 +64,13 @@ func (c *EnvDeleteCommand) Run(args []string) int { return 1 } - // In order to check if the state being deleted is empty, we need to change - // to that state and load it. - if current != delEnv { - if err := multi.ChangeState(delEnv); err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - // always try to change back after - defer func() { - if err := multi.ChangeState(current); err != nil { - c.Ui.Error(err.Error()) - } - }() + 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() + sMgr, err := b.State(delEnv) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -116,7 +98,7 @@ func (c *EnvDeleteCommand) Run(args []string) int { } defer clistate.Unlock(sMgr, lockID, c.Ui, c.Colorize()) - err = multi.DeleteState(delEnv) + err = b.DeleteState(delEnv) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/env_list.go b/command/env_list.go index cffeb38f3e..219b32bd05 100644 --- a/command/env_list.go +++ b/command/env_list.go @@ -4,8 +4,6 @@ import ( "bytes" "fmt" "strings" - - "github.com/hashicorp/terraform/backend" ) type EnvListCommand struct { @@ -34,21 +32,17 @@ func (c *EnvListCommand) Run(args []string) int { return 1 } - multi, ok := b.(backend.MultiState) - if !ok { - c.Ui.Error(envNotSupported) - return 1 - } - - states, current, err := multi.States() + 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 == current { + if s == env { out.WriteString("* ") } else { out.WriteString(" ") diff --git a/command/env_new.go b/command/env_new.go index eb5c990769..47457a0c1a 100644 --- a/command/env_new.go +++ b/command/env_new.go @@ -5,7 +5,6 @@ import ( "os" "strings" - "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" @@ -49,13 +48,7 @@ func (c *EnvNewCommand) Run(args []string) int { return 1 } - multi, ok := b.(backend.MultiState) - if !ok { - c.Ui.Error(envNotSupported) - return 1 - } - - states, _, err := multi.States() + states, err := b.States() for _, s := range states { if newEnv == s { c.Ui.Error(fmt.Sprintf(envExists, newEnv)) @@ -63,12 +56,18 @@ func (c *EnvNewCommand) Run(args []string) int { } } - err = multi.ChangeState(newEnv) + _, 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), @@ -81,7 +80,7 @@ func (c *EnvNewCommand) Run(args []string) int { } // load the new Backend state - sMgr, err := b.State() + sMgr, err := b.State(newEnv) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/env_select.go b/command/env_select.go index adfd2f056f..d13bdd5562 100644 --- a/command/env_select.go +++ b/command/env_select.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/terraform/backend" "github.com/mitchellh/cli" ) @@ -41,19 +40,14 @@ func (c *EnvSelectCommand) Run(args []string) int { name := args[0] - multi, ok := b.(backend.MultiState) - if !ok { - c.Ui.Error(envNotSupported) - return 1 - } - - states, current, err := multi.States() + states, err := b.States() if err != nil { c.Ui.Error(err.Error()) return 1 } - if current == name { + if name == c.Env() { + // already using this env return 0 } @@ -70,7 +64,7 @@ func (c *EnvSelectCommand) Run(args []string) int { return 1 } - err = multi.ChangeState(name) + err = c.SetEnv(name) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/meta.go b/command/meta.go index cc29b2aafd..12f767d2a4 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 + } + + // return default if the file simply doesn't exist + if err != nil && !os.IsNotExist(err) { + 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 629fc96421..d47d6c513f 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -148,6 +148,7 @@ func (m *Meta) Operation() *backend.Operation { PlanOutBackend: m.backendState, Targets: m.targets, UIIn: m.UIInput(), + Environment: m.Env(), } } @@ -526,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) } @@ -642,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) } @@ -656,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) @@ -751,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) } @@ -782,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) @@ -884,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) @@ -895,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) } @@ -949,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) } @@ -960,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) } @@ -1065,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) @@ -1076,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) } @@ -1226,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) @@ -1237,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 fc9cfb5b90..4ce7c1b808 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 5dde0f1ffc..1a74484fba 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 2b12dee629..2f5f6468bd 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 07afae0535..9be4828115 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 ce4e0a2ec8..7e3a6af1b8 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 afc5c98890..0e7436397c 100644 --- a/command/state_list.go +++ b/command/state_list.go @@ -11,7 +11,7 @@ import ( // StateListCommand is a Command implementation that lists the resources // within a state file. type StateListCommand struct { - Meta + StateMeta } func (c *StateListCommand) Run(args []string) int { @@ -31,8 +31,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_list_test.go b/command/state_list_test.go index 86c4f7194b..f2c85e26f3 100644 --- a/command/state_list_test.go +++ b/command/state_list_test.go @@ -16,9 +16,11 @@ func TestStateList(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateListCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -47,9 +49,11 @@ func TestStateList_backendState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateListCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } diff --git a/command/state_meta.go b/command/state_meta.go index eccfba447c..0e31b874b2 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -11,7 +11,9 @@ import ( ) // StateMeta is the meta struct that should be embedded in state subcommands. -type StateMeta struct{} +type StateMeta struct { + Meta +} // 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 @@ -23,8 +25,9 @@ func (c *StateMeta) State(m *Meta) (state.State, error) { return nil, err } + env := c.Env() // Get the state - s, err := b.State() + s, err := b.State(env) if err != nil { return nil, err } @@ -36,7 +39,7 @@ func (c *StateMeta) State(m *Meta) (state.State, error) { panic(err) } localB := localRaw.(*backendlocal.Local) - _, stateOutPath, _, err := localB.StatePaths() + _, stateOutPath, _, err := localB.StatePaths(env) if err != nil { return nil, err } diff --git a/command/state_mv.go b/command/state_mv.go index 7982d7b926..ad6b5c048d 100644 --- a/command/state_mv.go +++ b/command/state_mv.go @@ -10,7 +10,6 @@ import ( // StateMvCommand is a Command implementation that shows a single resource. type StateMvCommand struct { - Meta StateMeta } diff --git a/command/state_mv_test.go b/command/state_mv_test.go index d479b4ccb9..9fff6ed126 100644 --- a/command/state_mv_test.go +++ b/command/state_mv_test.go @@ -46,9 +46,11 @@ func TestStateMv(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -113,9 +115,11 @@ func TestStateMv_backupExplicit(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -168,9 +172,11 @@ func TestStateMv_stateOutNew(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -240,9 +246,11 @@ func TestStateMv_stateOutExisting(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -281,9 +289,11 @@ func TestStateMv_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -342,9 +352,11 @@ func TestStateMv_stateOutNew_count(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } diff --git a/command/state_pull.go b/command/state_pull.go index 73059c6cc0..a3679a26aa 100644 --- a/command/state_pull.go +++ b/command/state_pull.go @@ -11,7 +11,6 @@ import ( // StatePullCommand is a Command implementation that shows a single resource. type StatePullCommand struct { - Meta StateMeta } @@ -32,7 +31,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_pull_test.go b/command/state_pull_test.go index 3176a4c542..d468dbae4c 100644 --- a/command/state_pull_test.go +++ b/command/state_pull_test.go @@ -20,9 +20,11 @@ func TestStatePull(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePullCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } diff --git a/command/state_push.go b/command/state_push.go index a2b130f308..b6e7a90d63 100644 --- a/command/state_push.go +++ b/command/state_push.go @@ -11,7 +11,6 @@ import ( // StatePushCommand is a Command implementation that shows a single resource. type StatePushCommand struct { - Meta StateMeta } @@ -52,7 +51,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_push_test.go b/command/state_push_test.go index d63b193f6f..43e0e14c67 100644 --- a/command/state_push_test.go +++ b/command/state_push_test.go @@ -20,9 +20,11 @@ func TestStatePush_empty(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePushCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -49,9 +51,11 @@ func TestStatePush_replaceMatch(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePushCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -78,9 +82,11 @@ func TestStatePush_lineageMismatch(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePushCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -107,9 +113,11 @@ func TestStatePush_serialNewer(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePushCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -136,9 +144,11 @@ func TestStatePush_serialOlder(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePushCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } diff --git a/command/state_rm.go b/command/state_rm.go index b2491c4e8e..a80a0d80d6 100644 --- a/command/state_rm.go +++ b/command/state_rm.go @@ -9,7 +9,6 @@ import ( // StateRmCommand is a Command implementation that shows a single resource. type StateRmCommand struct { - Meta StateMeta } diff --git a/command/state_rm_test.go b/command/state_rm_test.go index bb37345029..0c3eacb3ad 100644 --- a/command/state_rm_test.go +++ b/command/state_rm_test.go @@ -46,9 +46,11 @@ func TestStateRm(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateRmCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -112,9 +114,11 @@ func TestStateRm_backupExplicit(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateRmCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } diff --git a/command/state_show.go b/command/state_show.go index be6df8bf9a..a7a939bfff 100644 --- a/command/state_show.go +++ b/command/state_show.go @@ -12,7 +12,6 @@ import ( // StateShowCommand is a Command implementation that shows a single resource. type StateShowCommand struct { - Meta StateMeta } @@ -34,7 +33,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/state_show_test.go b/command/state_show_test.go index 9e0ede3966..f8b56c25f9 100644 --- a/command/state_show_test.go +++ b/command/state_show_test.go @@ -34,9 +34,11 @@ func TestStateShow(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateShowCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -92,9 +94,11 @@ func TestStateShow_multi(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateShowCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -114,9 +118,11 @@ func TestStateShow_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateShowCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -134,9 +140,11 @@ func TestStateShow_emptyState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateShowCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -163,9 +171,11 @@ func TestStateShow_emptyStateWithModule(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateShowCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } diff --git a/command/taint.go b/command/taint.go index 0fd2b4de96..4870991871 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 b50713aaa6..010fd9332f 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 c3b413252a..ab697b8235 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 f2f7b4eda3..e1618003eb 100644 --- a/commands.go +++ b/commands.go @@ -46,6 +46,11 @@ func init() { "debug": struct{}{}, // includes all subcommands } + // meta struct used in state commands + stateMeta := command.StateMeta{ + Meta: meta, + } + Commands = map[string]cli.CommandFactory{ "apply": func() (cli.Command, error) { return &command.ApplyCommand{ @@ -217,43 +222,43 @@ func init() { "state": func() (cli.Command, error) { return &command.StateCommand{ - Meta: meta, + StateMeta: stateMeta, }, nil }, "state list": func() (cli.Command, error) { return &command.StateListCommand{ - Meta: meta, + StateMeta: stateMeta, }, nil }, "state rm": func() (cli.Command, error) { return &command.StateRmCommand{ - Meta: meta, + StateMeta: stateMeta, }, nil }, "state mv": func() (cli.Command, error) { return &command.StateMvCommand{ - Meta: meta, + StateMeta: stateMeta, }, nil }, "state pull": func() (cli.Command, error) { return &command.StatePullCommand{ - Meta: meta, + StateMeta: stateMeta, }, nil }, "state push": func() (cli.Command, error) { return &command.StatePushCommand{ - Meta: meta, + StateMeta: stateMeta, }, nil }, "state show": func() (cli.Command, error) { return &command.StateShowCommand{ - Meta: meta, + StateMeta: stateMeta, }, nil }, } From 6c3800d17f246a9e125356fd1d0a81f9da3058da Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 28 Feb 2017 16:25:42 -0500 Subject: [PATCH 19/22] update the remote state datasource Add an `environment` field to the terraform remote state data source. --- builtin/providers/terraform/data_source_state.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/builtin/providers/terraform/data_source_state.go b/builtin/providers/terraform/data_source_state.go index ee9a636304..730158550d 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) } From dc675540de6adf1eb53ab3e9bd001da5a1c5e45d Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 28 Feb 2017 16:30:55 -0500 Subject: [PATCH 20/22] fix rebased tests --- command/state_list_test.go | 8 +++++--- command/state_mv_test.go | 16 ++++++++++------ command/state_rm_test.go | 8 +++++--- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/command/state_list_test.go b/command/state_list_test.go index f2c85e26f3..b439592e9b 100644 --- a/command/state_list_test.go +++ b/command/state_list_test.go @@ -77,9 +77,11 @@ func TestStateList_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateListCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } diff --git a/command/state_mv_test.go b/command/state_mv_test.go index 9fff6ed126..e804b35290 100644 --- a/command/state_mv_test.go +++ b/command/state_mv_test.go @@ -532,9 +532,11 @@ func TestStateMv_stateOutNew_largeCount(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } @@ -613,9 +615,11 @@ func TestStateMv_stateOutNew_nestedModule(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } diff --git a/command/state_rm_test.go b/command/state_rm_test.go index 0c3eacb3ad..178aa8c5d6 100644 --- a/command/state_rm_test.go +++ b/command/state_rm_test.go @@ -150,9 +150,11 @@ func TestStateRm_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateRmCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, + StateMeta: StateMeta{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, }, } From 4dac986a9188fa26887ef57d23ed7a11886922e3 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 28 Feb 2017 19:18:16 -0500 Subject: [PATCH 21/22] Local.StatePaths doesn't need to reutrn an error add a test to ensure we have consistent output --- backend/local/backend.go | 9 +++---- backend/local/backend_test.go | 44 ++++++++++++++++++++++++++++++++++- command/meta.go | 2 +- command/state_meta.go | 2 +- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/backend/local/backend.go b/backend/local/backend.go index 84965850b5..0b592e33d7 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -179,10 +179,7 @@ func (b *Local) State(name string) (state.State, error) { return nil, err } - statePath, stateOutPath, backupPath, err := b.StatePaths(name) - if err != nil { - return nil, err - } + statePath, stateOutPath, backupPath := b.StatePaths(name) // Otherwise, we need to load the state. var s state.State = &state.LocalState{ @@ -296,7 +293,7 @@ func (b *Local) schemaConfigure(ctx context.Context) error { // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as // configured from the CLI. -func (b *Local) StatePaths(name string) (string, string, string, error) { +func (b *Local) StatePaths(name string) (string, string, string) { statePath := b.StatePath stateOutPath := b.StateOutPath backupPath := b.StateBackupPath @@ -324,7 +321,7 @@ func (b *Local) StatePaths(name string) (string, string, string, error) { backupPath = stateOutPath + DefaultBackupExtension } - return statePath, stateOutPath, backupPath, nil + return statePath, stateOutPath, backupPath } // this only ensures that the named directory exists diff --git a/backend/local/backend_test.go b/backend/local/backend_test.go index 1d9459e64a..f929e74413 100644 --- a/backend/local/backend_test.go +++ b/backend/local/backend_test.go @@ -4,6 +4,7 @@ import ( "errors" "io/ioutil" "os" + "path/filepath" "reflect" "strings" "testing" @@ -39,6 +40,47 @@ func checkState(t *testing.T, path, expected string) { } } +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 @@ -117,7 +159,7 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } } -// a local backend which return sentinel errors for NamedState methods to +// a local backend which returns sentinel errors for NamedState methods to // verify it's being called. type testDelegateBackend struct { *Local diff --git a/command/meta.go b/command/meta.go index 12f767d2a4..c18c77fc74 100644 --- a/command/meta.go +++ b/command/meta.go @@ -424,8 +424,8 @@ func (m *Meta) Env() string { current = backend.DefaultStateName } - // return default if the file simply doesn't exist 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) } diff --git a/command/state_meta.go b/command/state_meta.go index 0e31b874b2..9381c6a455 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -39,7 +39,7 @@ func (c *StateMeta) State(m *Meta) (state.State, error) { panic(err) } localB := localRaw.(*backendlocal.Local) - _, stateOutPath, _, err := localB.StatePaths(env) + _, stateOutPath, _ := localB.StatePaths(env) if err != nil { return nil, err } From 39a5ddd381fc5f80a798d822b3f11f2896136c90 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 1 Mar 2017 10:10:47 -0500 Subject: [PATCH 22/22] Split Meta back out of StateMeta Removing the call to StateMeta.Env, so that it doesn't need an embedded Meta field. Embed Meta and StateMeta separately in all State commands. --- command/env_delete.go | 2 +- command/state_list.go | 1 + command/state_list_test.go | 24 ++++++-------- command/state_meta.go | 13 ++++---- command/state_mv.go | 1 + command/state_mv_test.go | 64 ++++++++++++++------------------------ command/state_pull.go | 1 + command/state_pull_test.go | 8 ++--- command/state_push.go | 1 + command/state_push_test.go | 40 +++++++++--------------- command/state_rm.go | 1 + command/state_rm_test.go | 24 ++++++-------- command/state_show.go | 1 + command/state_show_test.go | 40 +++++++++--------------- commands.go | 21 +++++-------- 15 files changed, 95 insertions(+), 147 deletions(-) diff --git a/command/env_delete.go b/command/env_delete.go index 1a773c0cca..ad484a3f9f 100644 --- a/command/env_delete.go +++ b/command/env_delete.go @@ -90,7 +90,7 @@ func (c *EnvDeleteCommand) Run(args []string) int { // Lock the state if we can lockInfo := state.NewLockInfo() - lockInfo.Operation = "env new" + 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)) diff --git a/command/state_list.go b/command/state_list.go index 0e7436397c..d7087d1b57 100644 --- a/command/state_list.go +++ b/command/state_list.go @@ -11,6 +11,7 @@ import ( // StateListCommand is a Command implementation that lists the resources // within a state file. type StateListCommand struct { + Meta StateMeta } diff --git a/command/state_list_test.go b/command/state_list_test.go index b439592e9b..86c4f7194b 100644 --- a/command/state_list_test.go +++ b/command/state_list_test.go @@ -16,11 +16,9 @@ func TestStateList(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateListCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -49,11 +47,9 @@ func TestStateList_backendState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateListCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -77,11 +73,9 @@ func TestStateList_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateListCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } diff --git a/command/state_meta.go b/command/state_meta.go index 9381c6a455..879f8f6413 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -11,13 +11,12 @@ import ( ) // StateMeta is the meta struct that should be embedded in state subcommands. -type StateMeta struct { - Meta -} +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) @@ -25,7 +24,7 @@ func (c *StateMeta) State(m *Meta) (state.State, error) { return nil, err } - env := c.Env() + env := m.Env() // Get the state s, err := b.State(env) if err != nil { diff --git a/command/state_mv.go b/command/state_mv.go index ad6b5c048d..7982d7b926 100644 --- a/command/state_mv.go +++ b/command/state_mv.go @@ -10,6 +10,7 @@ import ( // StateMvCommand is a Command implementation that shows a single resource. type StateMvCommand struct { + Meta StateMeta } diff --git a/command/state_mv_test.go b/command/state_mv_test.go index e804b35290..d479b4ccb9 100644 --- a/command/state_mv_test.go +++ b/command/state_mv_test.go @@ -46,11 +46,9 @@ func TestStateMv(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -115,11 +113,9 @@ func TestStateMv_backupExplicit(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -172,11 +168,9 @@ func TestStateMv_stateOutNew(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -246,11 +240,9 @@ func TestStateMv_stateOutExisting(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -289,11 +281,9 @@ func TestStateMv_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -352,11 +342,9 @@ func TestStateMv_stateOutNew_count(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -532,11 +520,9 @@ func TestStateMv_stateOutNew_largeCount(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -615,11 +601,9 @@ func TestStateMv_stateOutNew_nestedModule(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateMvCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } diff --git a/command/state_pull.go b/command/state_pull.go index a3679a26aa..a51cf5c6ae 100644 --- a/command/state_pull.go +++ b/command/state_pull.go @@ -11,6 +11,7 @@ import ( // StatePullCommand is a Command implementation that shows a single resource. type StatePullCommand struct { + Meta StateMeta } diff --git a/command/state_pull_test.go b/command/state_pull_test.go index d468dbae4c..3176a4c542 100644 --- a/command/state_pull_test.go +++ b/command/state_pull_test.go @@ -20,11 +20,9 @@ func TestStatePull(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePullCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } diff --git a/command/state_push.go b/command/state_push.go index b6e7a90d63..d23f8b7e3d 100644 --- a/command/state_push.go +++ b/command/state_push.go @@ -11,6 +11,7 @@ import ( // StatePushCommand is a Command implementation that shows a single resource. type StatePushCommand struct { + Meta StateMeta } diff --git a/command/state_push_test.go b/command/state_push_test.go index 43e0e14c67..d63b193f6f 100644 --- a/command/state_push_test.go +++ b/command/state_push_test.go @@ -20,11 +20,9 @@ func TestStatePush_empty(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePushCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -51,11 +49,9 @@ func TestStatePush_replaceMatch(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePushCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -82,11 +78,9 @@ func TestStatePush_lineageMismatch(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePushCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -113,11 +107,9 @@ func TestStatePush_serialNewer(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePushCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -144,11 +136,9 @@ func TestStatePush_serialOlder(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StatePushCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } diff --git a/command/state_rm.go b/command/state_rm.go index a80a0d80d6..b2491c4e8e 100644 --- a/command/state_rm.go +++ b/command/state_rm.go @@ -9,6 +9,7 @@ import ( // StateRmCommand is a Command implementation that shows a single resource. type StateRmCommand struct { + Meta StateMeta } diff --git a/command/state_rm_test.go b/command/state_rm_test.go index 178aa8c5d6..bb37345029 100644 --- a/command/state_rm_test.go +++ b/command/state_rm_test.go @@ -46,11 +46,9 @@ func TestStateRm(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateRmCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -114,11 +112,9 @@ func TestStateRm_backupExplicit(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateRmCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -150,11 +146,9 @@ func TestStateRm_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateRmCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } diff --git a/command/state_show.go b/command/state_show.go index a7a939bfff..235481f2c7 100644 --- a/command/state_show.go +++ b/command/state_show.go @@ -12,6 +12,7 @@ import ( // StateShowCommand is a Command implementation that shows a single resource. type StateShowCommand struct { + Meta StateMeta } diff --git a/command/state_show_test.go b/command/state_show_test.go index f8b56c25f9..9e0ede3966 100644 --- a/command/state_show_test.go +++ b/command/state_show_test.go @@ -34,11 +34,9 @@ func TestStateShow(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateShowCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -94,11 +92,9 @@ func TestStateShow_multi(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateShowCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -118,11 +114,9 @@ func TestStateShow_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateShowCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -140,11 +134,9 @@ func TestStateShow_emptyState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateShowCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } @@ -171,11 +163,9 @@ func TestStateShow_emptyStateWithModule(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &StateShowCommand{ - StateMeta: StateMeta{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, }, } diff --git a/commands.go b/commands.go index e1618003eb..b9343301c7 100644 --- a/commands.go +++ b/commands.go @@ -46,11 +46,6 @@ func init() { "debug": struct{}{}, // includes all subcommands } - // meta struct used in state commands - stateMeta := command.StateMeta{ - Meta: meta, - } - Commands = map[string]cli.CommandFactory{ "apply": func() (cli.Command, error) { return &command.ApplyCommand{ @@ -221,44 +216,42 @@ func init() { }, "state": func() (cli.Command, error) { - return &command.StateCommand{ - StateMeta: stateMeta, - }, nil + return &command.StateCommand{}, nil }, "state list": func() (cli.Command, error) { return &command.StateListCommand{ - StateMeta: stateMeta, + Meta: meta, }, nil }, "state rm": func() (cli.Command, error) { return &command.StateRmCommand{ - StateMeta: stateMeta, + Meta: meta, }, nil }, "state mv": func() (cli.Command, error) { return &command.StateMvCommand{ - StateMeta: stateMeta, + Meta: meta, }, nil }, "state pull": func() (cli.Command, error) { return &command.StatePullCommand{ - StateMeta: stateMeta, + Meta: meta, }, nil }, "state push": func() (cli.Command, error) { return &command.StatePushCommand{ - StateMeta: stateMeta, + Meta: meta, }, nil }, "state show": func() (cli.Command, error) { return &command.StateShowCommand{ - StateMeta: stateMeta, + Meta: meta, }, nil }, }