update local.Local to match the latest Backend

Update the methods, remove the handling of "current", and make tests
pass.
This commit is contained in:
James Bardin 2017-02-27 16:43:31 -05:00
parent 96194fbc0d
commit 65527f35a4
4 changed files with 86 additions and 248 deletions

View File

@ -123,6 +123,9 @@ type Operation struct {
// If LockState is true, the Operation must Lock any // If LockState is true, the Operation must Lock any
// state.Lockers for its duration, and Unlock when complete. // state.Lockers for its duration, and Unlock when complete.
LockState bool LockState bool
// Environment is the named state that should be loaded from the Backend.
Environment string
} }
// RunningOperation is the result of starting an operation. // RunningOperation is the result of starting an operation.

View File

@ -53,8 +53,10 @@ type Local struct {
StateOutPath string StateOutPath string
StateBackupPath string StateBackupPath string
// we only want to create a single instance of the local state // We only want to create a single instance of a local state, so store them
state state.State // here as they're loaded.
states map[string]state.State
// Terraform context. Many of these will be overridden or merged by // Terraform context. Many of these will be overridden or merged by
// Operation. See Operation for more details. // Operation. See Operation for more details.
ContextOpts *terraform.ContextOpts ContextOpts *terraform.ContextOpts
@ -78,10 +80,6 @@ type Local struct {
schema *schema.Backend schema *schema.Backend
opLock sync.Mutex opLock sync.Mutex
once sync.Once 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( func (b *Local) Input(
@ -118,54 +116,35 @@ func (b *Local) Configure(c *terraform.ResourceConfig) error {
return f(c) 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 we have a backend handling state, defer to that.
if b.Backend != nil { if b.Backend != nil {
if b, ok := b.Backend.(backend.MultiState); ok { return b.Backend.States()
return b.States()
} else {
return nil, "", ErrEnvNotSupported
}
} }
// the listing always start with "default" // the listing always start with "default"
envs := []string{backend.DefaultStateName} envs := []string{backend.DefaultStateName}
current, err := b.currentStateName() entries, err := ioutil.ReadDir(DefaultEnvDir)
if err != nil {
return nil, "", err
}
entries, err := ioutil.ReadDir(filepath.Join(b.workingDir, DefaultEnvDir))
// no error if there's no envs configured // no error if there's no envs configured
if os.IsNotExist(err) { if os.IsNotExist(err) {
return envs, backend.DefaultStateName, nil return envs, nil
} }
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
currentExists := false
var listed []string var listed []string
for _, entry := range entries { for _, entry := range entries {
if entry.IsDir() { if entry.IsDir() {
name := filepath.Base(entry.Name()) listed = append(listed, 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) sort.Strings(listed)
envs = append(envs, listed...) envs = append(envs, listed...)
return envs, current, nil return envs, nil
} }
// DeleteState removes a named state. // DeleteState removes a named state.
@ -173,11 +152,7 @@ func (b *Local) States() ([]string, string, error) {
func (b *Local) DeleteState(name string) error { func (b *Local) DeleteState(name string) error {
// If we have a backend handling state, defer to that. // If we have a backend handling state, defer to that.
if b.Backend != nil { if b.Backend != nil {
if b, ok := b.Backend.(backend.MultiState); ok { return b.Backend.DeleteState(name)
return b.DeleteState(name)
} else {
return ErrEnvNotSupported
}
} }
if name == "" { if name == "" {
@ -188,91 +163,25 @@ func (b *Local) DeleteState(name string) error {
return errors.New("cannot delete default state") return errors.New("cannot delete default state")
} }
_, current, err := b.States() delete(b.states, name)
if err != nil { return os.RemoveAll(filepath.Join(DefaultEnvDir, name))
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))
} }
// Change to the named state, creating it if it doesn't exist. func (b *Local) State(name string) (state.State, error) {
func (b *Local) ChangeState(name string) error {
// If we have a backend handling state, defer to that. // If we have a backend handling state, defer to that.
if b.Backend != nil { if b.Backend != nil {
if b, ok := b.Backend.(backend.MultiState); ok { return b.Backend.State(name)
return b.ChangeState(name)
} else {
return ErrEnvNotSupported
}
} }
name = strings.TrimSpace(name) if s, ok := b.states[name]; ok {
if name == "" { return s, nil
return errors.New("state name cannot be empty")
} }
envs, current, err := b.States() if err := b.createState(name); err != nil {
if err != nil { return nil, err
return err
} }
if name == current { statePath, stateOutPath, backupPath, err := b.StatePaths(name)
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()
if err != nil { if err != nil {
return nil, err 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 return s, nil
} }
@ -385,20 +297,24 @@ func (b *Local) schemaConfigure(ctx context.Context) error {
} }
// StatePaths returns the StatePath, StateOutPath, and StateBackupPath as // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
// configured by the current environment. If backups are disabled, // configured from the CLI.
// StateBackupPath will be an empty string. func (b *Local) StatePaths(name string) (string, string, string, error) {
func (b *Local) StatePaths() (string, string, string, error) {
statePath := b.StatePath statePath := b.StatePath
stateOutPath := b.StateOutPath stateOutPath := b.StateOutPath
backupPath := b.StateBackupPath backupPath := b.StateBackupPath
if statePath == "" { if name == "" {
path, err := b.statePath() name = backend.DefaultStateName
if err != nil {
return "", "", "", err
}
statePath = path
} }
if name == backend.DefaultStateName {
if statePath == "" {
statePath = name
}
} else {
statePath = filepath.Join(DefaultEnvDir, name, DefaultStateFilename)
}
if stateOutPath == "" { if stateOutPath == "" {
stateOutPath = statePath stateOutPath = statePath
} }
@ -413,33 +329,21 @@ func (b *Local) StatePaths() (string, string, string, error) {
return statePath, stateOutPath, backupPath, nil return statePath, stateOutPath, backupPath, nil
} }
func (b *Local) statePath() (string, error) { // this only ensures that the named directory exists
_, 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
}
func (b *Local) createState(name string) error { func (b *Local) createState(name string) error {
stateNames, _, err := b.States() if name == backend.DefaultStateName {
if err != nil { return nil
return err
} }
for _, n := range stateNames { stateDir := filepath.Join(DefaultEnvDir, name)
if name == n { s, err := os.Stat(stateDir)
// state exists, nothing to do if err == nil && s.IsDir() {
return nil // 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 { if err != nil {
return err return err
} }
@ -451,7 +355,7 @@ func (b *Local) createState(name string) error {
// configuration files. // configuration files.
// If there are no configured environments, currentStateName returns "default" // If there are no configured environments, currentStateName returns "default"
func (b *Local) currentStateName() (string, error) { 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) { if os.IsNotExist(err) {
return backend.DefaultStateName, nil return backend.DefaultStateName, nil
} }

View File

@ -23,7 +23,7 @@ func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State,
func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, error) { func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, error) {
// Get the state. // Get the state.
s, err := b.State() s, err := b.State(op.Environment)
if err != nil { if err != nil {
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err) return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
} }

View File

@ -1,15 +1,15 @@
package local package local
import ( import (
"fmt" "errors"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -17,7 +17,6 @@ func TestLocal_impl(t *testing.T) {
var _ backend.Enhanced = new(Local) var _ backend.Enhanced = new(Local)
var _ backend.Local = new(Local) var _ backend.Local = new(Local)
var _ backend.CLI = new(Local) var _ backend.CLI = new(Local)
var _ backend.MultiState = new(Local)
} }
func checkState(t *testing.T, path, expected string) { func checkState(t *testing.T, path, expected string) {
@ -46,31 +45,24 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
expectedStates := []string{dflt} expectedStates := []string{dflt}
b := &Local{} b := &Local{}
states, current, err := b.States() states, err := b.States()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if current != dflt {
t.Fatalf("expected %q, got %q", dflt, current)
}
if !reflect.DeepEqual(states, expectedStates) { if !reflect.DeepEqual(states, expectedStates) {
t.Fatalf("expected []string{%q}, got %q", dflt, states) t.Fatalf("expected []string{%q}, got %q", dflt, states)
} }
expectedA := "test_A" expectedA := "test_A"
if err := b.ChangeState(expectedA); err != nil { if _, err := b.State(expectedA); err != nil {
t.Fatal(err) t.Fatal(err)
} }
states, current, err = b.States() states, err = b.States()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if current != expectedA {
t.Fatalf("expected %q, got %q", expectedA, current)
}
expectedStates = append(expectedStates, expectedA) expectedStates = append(expectedStates, expectedA)
if !reflect.DeepEqual(states, expectedStates) { if !reflect.DeepEqual(states, expectedStates) {
@ -78,17 +70,14 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
} }
expectedB := "test_B" expectedB := "test_B"
if err := b.ChangeState(expectedB); err != nil { if _, err := b.State(expectedB); err != nil {
t.Fatal(err) t.Fatal(err)
} }
states, current, err = b.States() states, err = b.States()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if current != expectedB {
t.Fatalf("expected %q, got %q", expectedB, current)
}
expectedStates = append(expectedStates, expectedB) expectedStates = append(expectedStates, expectedB)
if !reflect.DeepEqual(states, expectedStates) { if !reflect.DeepEqual(states, expectedStates) {
@ -99,13 +88,10 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
states, current, err = b.States() states, err = b.States()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if current != expectedB {
t.Fatalf("expected %q, got %q", dflt, current)
}
expectedStates = []string{dflt, expectedB} expectedStates = []string{dflt, expectedB}
if !reflect.DeepEqual(states, expectedStates) { if !reflect.DeepEqual(states, expectedStates) {
@ -116,13 +102,10 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
states, current, err = b.States() states, err = b.States()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if current != dflt {
t.Fatalf("expected %q, got %q", dflt, current)
}
expectedStates = []string{dflt} expectedStates = []string{dflt}
if !reflect.DeepEqual(states, expectedStates) { 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 // a local backend which return sentinel errors for NamedState methods to
func TestLocal_noMultiStateBackend(t *testing.T) { // verify it's being called.
type noMultiState struct { type testDelegateBackend struct {
backend.Backend *Local
} }
b := &Local{ var errTestDelegateState = errors.New("State called")
Backend: &noMultiState{}, var errTestDelegateStates = errors.New("States called")
} var errTestDelegateDeleteState = errors.New("Delete called")
_, _, err := b.States() func (b *testDelegateBackend) State(name string) (state.State, error) {
if err != ErrEnvNotSupported { return nil, errTestDelegateState
t.Fatal("backend does not support environments.", err) }
}
err = b.ChangeState("test") func (b *testDelegateBackend) States() ([]string, error) {
if err != ErrEnvNotSupported { return nil, errTestDelegateStates
t.Fatal("backend does not support environments.", err) }
}
err = b.ChangeState("test") func (b *testDelegateBackend) DeleteState(name string) error {
if err != ErrEnvNotSupported { return errTestDelegateDeleteState
t.Fatal("backend does not support environments.", err)
}
} }
// verify that the MultiState methods are dispatched to the correct Backend. // verify that the MultiState methods are dispatched to the correct Backend.
func TestLocal_multiStateBackend(t *testing.T) { func TestLocal_multiStateBackend(t *testing.T) {
defer testTmpDir(t)() // assign a separate backend where we can read the state
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{ b := &Local{
Backend: &Local{ Backend: &testDelegateBackend{},
workingDir: tmp,
},
} }
testA := "test_A" if _, err := b.State("test"); err != errTestDelegateState {
if err := b.ChangeState(testA); err != nil { t.Fatal("expected errTestDelegateState, got:", err)
t.Fatal(err)
} }
states, current, err := b.States() if _, err := b.States(); err != errTestDelegateStates {
if err != nil { t.Fatal("expected errTestDelegateStates, got:", err)
t.Fatal(err)
}
if current != testA {
t.Fatalf("expected %q, got %q", testA, current)
} }
expectedStates = append(expectedStates, testA) if err := b.DeleteState("test"); err != errTestDelegateDeleteState {
if !reflect.DeepEqual(states, expectedStates) { t.Fatal("expected errTestDelegateDeleteState, got:", err)
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)
} }
} }