always wrap remote state in a BackupState

Use a local backup for remote state operations. This allows for manual
recovery in the case of a put failure.
This commit is contained in:
James Bardin 2017-04-21 18:57:39 -04:00
parent 7a07c4e99c
commit 563cfd00df
2 changed files with 84 additions and 7 deletions

View File

@ -170,9 +170,30 @@ func (b *Local) DeleteState(name string) error {
} }
func (b *Local) State(name string) (state.State, error) { func (b *Local) State(name string) (state.State, error) {
statePath, stateOutPath, backupPath := b.StatePaths(name)
// 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 {
return b.Backend.State(name) s, err := b.Backend.State(name)
if err != nil {
return nil, err
}
// make sure we always have a backup state, unless it disabled
if backupPath == "" {
return s, nil
}
// see if the delegated backend returned a BackupState of its own
if s, ok := s.(*state.BackupState); ok {
return s, nil
}
s = &state.BackupState{
Real: s,
Path: backupPath,
}
return s, nil
} }
if s, ok := b.states[name]; ok { if s, ok := b.states[name]; ok {
@ -183,8 +204,6 @@ func (b *Local) State(name string) (state.State, error) {
return nil, err return nil, err
} }
statePath, stateOutPath, backupPath := b.StatePaths(name)
// Otherwise, we need to load the state. // Otherwise, we need to load the state.
var s state.State = &state.LocalState{ var s state.State = &state.LocalState{
Path: statePath, Path: statePath,

View File

@ -169,6 +169,11 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
// verify it's being called. // verify it's being called.
type testDelegateBackend struct { type testDelegateBackend struct {
*Local *Local
// return a sentinel error on these calls
stateErr bool
statesErr bool
deleteErr bool
} }
var errTestDelegateState = errors.New("State called") var errTestDelegateState = errors.New("State called")
@ -176,22 +181,39 @@ var errTestDelegateStates = errors.New("States called")
var errTestDelegateDeleteState = errors.New("Delete called") var errTestDelegateDeleteState = errors.New("Delete called")
func (b *testDelegateBackend) State(name string) (state.State, error) { func (b *testDelegateBackend) State(name string) (state.State, error) {
if b.stateErr {
return nil, errTestDelegateState return nil, errTestDelegateState
} }
s := &state.LocalState{
Path: "terraform.tfstate",
PathOut: "terraform.tfstate",
}
return s, nil
}
func (b *testDelegateBackend) States() ([]string, error) { func (b *testDelegateBackend) States() ([]string, error) {
if b.statesErr {
return nil, errTestDelegateStates return nil, errTestDelegateStates
} }
return []string{"default"}, nil
}
func (b *testDelegateBackend) DeleteState(name string) error { func (b *testDelegateBackend) DeleteState(name string) error {
if b.deleteErr {
return errTestDelegateDeleteState return errTestDelegateDeleteState
} }
return nil
}
// 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) {
// assign a separate backend where we can read the state // assign a separate backend where we can read the state
b := &Local{ b := &Local{
Backend: &testDelegateBackend{}, Backend: &testDelegateBackend{
stateErr: true,
statesErr: true,
deleteErr: true,
},
} }
if _, err := b.State("test"); err != errTestDelegateState { if _, err := b.State("test"); err != errTestDelegateState {
@ -205,7 +227,43 @@ func TestLocal_multiStateBackend(t *testing.T) {
if err := b.DeleteState("test"); err != errTestDelegateDeleteState { if err := b.DeleteState("test"); err != errTestDelegateDeleteState {
t.Fatal("expected errTestDelegateDeleteState, got:", err) t.Fatal("expected errTestDelegateDeleteState, got:", err)
} }
}
// verify that a remote state backend is always wrapped in a BackupState
func TestLocal_remoteStateBackup(t *testing.T) {
// assign a separate backend to mock a remote state backend
b := &Local{
Backend: &testDelegateBackend{},
}
s, err := b.State("default")
if err != nil {
t.Fatal(err)
}
bs, ok := s.(*state.BackupState)
if !ok {
t.Fatal("remote state is not backed up")
}
if bs.Path != DefaultStateFilename+DefaultBackupExtension {
t.Fatal("bad backup location:", bs.Path)
}
// do the same with a named state, which should use the local env directories
s, err = b.State("test")
if err != nil {
t.Fatal(err)
}
bs, ok = s.(*state.BackupState)
if !ok {
t.Fatal("remote state is not backed up")
}
if bs.Path != filepath.Join(DefaultEnvDir, "test", DefaultStateFilename+DefaultBackupExtension) {
t.Fatal("bad backup location:", bs.Path)
}
} }
// change into a tmp dir and return a deferable func to change back and cleanup // change into a tmp dir and return a deferable func to change back and cleanup