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