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
// state.Lockers for its duration, and Unlock when complete.
LockState bool
// Environment is the named state that should be loaded from the Backend.
Environment string
}
// RunningOperation is the result of starting an operation.

View File

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

View File

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

View File

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