Add state locking during backend init

During backend initialization, especially during a migration, there is a
chance that an existing state could be overwritten.

Attempt to get a locks when writing the new state. It would be nice to
always have a lock when reading the states, but the recursive structure
of the Meta.Backend config functions makes that quite complex.
This commit is contained in:
James Bardin 2017-02-09 15:35:49 -05:00
parent 5c2e945b3c
commit 0c1b138719
5 changed files with 73 additions and 4 deletions

View File

@ -530,6 +530,13 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) {
if err != nil {
return nil, fmt.Errorf("Error reading state: %s", err)
}
unlock, err := lockState(realMgr, "backend from plan")
if err != nil {
return nil, err
}
defer unlock()
if err := realMgr.RefreshState(); err != nil {
return nil, fmt.Errorf("Error reading state: %s", err)
}
@ -574,6 +581,8 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) {
newState.Remote = nil
newState.Backend = nil
}
// realMgr locked above
if err := realMgr.WriteState(newState); err != nil {
return nil, fmt.Errorf("Error writing state: %s", err)
}
@ -974,6 +983,12 @@ func (m *Meta) backend_C_r_s(
}
}
unlock, err := lockState(sMgr, "backend_C_r_s")
if err != nil {
return nil, err
}
defer unlock()
// Store the metadata in our saved state location
s := sMgr.State()
if s == nil {
@ -984,6 +999,7 @@ func (m *Meta) backend_C_r_s(
Config: c.RawConfig.Raw,
Hash: c.Hash,
}
if err := sMgr.WriteState(s); err != nil {
return nil, fmt.Errorf(errBackendWriteSaved, err)
}
@ -1009,6 +1025,9 @@ func (m *Meta) backend_C_r_S_changed(
strings.TrimSpace(outputBackendReconfigure))))
}
// Get the old state
s := sMgr.State()
// Get the backend
b, err := m.backendInitFromConfig(c)
if err != nil {
@ -1037,8 +1056,6 @@ func (m *Meta) backend_C_r_S_changed(
"Error loading previously configured backend: %s", err)
}
// Get the old state
s := sMgr.State()
oldState, err := oldB.State()
if err != nil {
return nil, fmt.Errorf(
@ -1070,8 +1087,14 @@ func (m *Meta) backend_C_r_S_changed(
}
}
unlock, err := lockState(sMgr, "backend_C_r_S_changed")
if err != nil {
return nil, err
}
defer unlock()
// Update the backend state
s := sMgr.State()
s = sMgr.State()
if s == nil {
s = terraform.NewState()
}
@ -1080,6 +1103,7 @@ func (m *Meta) backend_C_r_S_changed(
Config: c.RawConfig.Raw,
Hash: c.Hash,
}
if err := sMgr.WriteState(s); err != nil {
return nil, fmt.Errorf(errBackendWriteSaved, err)
}
@ -1220,12 +1244,19 @@ func (m *Meta) backend_C_R_S_unchanged(
}
}
unlock, err := lockState(sMgr, "backend_C_R_S_unchanged")
if err != nil {
return nil, err
}
defer unlock()
// Unset the remote state
s = sMgr.State()
if s == nil {
s = terraform.NewState()
}
s.Remote = nil
if err := sMgr.WriteState(s); err != nil {
return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err)
}
@ -1368,6 +1399,21 @@ func init() {
backendlegacy.Init(Backends)
}
// simple wrapper to check for a state.Locker and always provide an unlock
// function to defer.
func lockState(s state.State, info string) (func() error, error) {
l, ok := s.(state.Locker)
if !ok {
return func() error { return nil }, nil
}
if err := l.Lock(info); err != nil {
return nil, err
}
return l.Unlock, nil
}
// errBackendInitRequired is the final error message shown when reinit
// is required for some reason. The error message includes the reason.
var errBackendInitRequired = errors.New(

View File

@ -20,7 +20,21 @@ import (
//
// After migrating the state, the existing state in the first backend
// remains untouched.
//
// This will attempt to lock both states for the migration.
func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
unlockOne, err := lockState(opts.One, "migrate from")
if err != nil {
return err
}
defer unlockOne()
unlockTwo, err := lockState(opts.Two, "migrate to")
if err != nil {
return err
}
defer unlockTwo()
one := opts.One.State()
two := opts.Two.State()

View File

@ -354,6 +354,7 @@ func TestMetaBackend_configureNewWithState(t *testing.T) {
if state == nil {
t.Fatal("state is nil")
}
if state.Lineage != "backend-new-migrate" {
t.Fatalf("bad: %#v", state)
}

View File

@ -97,6 +97,7 @@ func (s *LocalState) Unlock() error {
fileName := s.stateFileOut.Name()
unlockErr := s.unlock()
s.stateFileOut.Close()
s.stateFileOut = nil
@ -201,6 +202,11 @@ func (s *LocalState) RefreshState() error {
reader = f
}
} else {
// no state to refresh
if s.stateFileOut == nil {
return nil
}
// we have a state file, make sure we're at the start
s.stateFileOut.Seek(0, os.SEEK_SET)
reader = s.stateFileOut

View File

@ -1808,7 +1808,9 @@ var ErrNoState = errors.New("no state")
// was written by WriteState.
func ReadState(src io.Reader) (*State, error) {
buf := bufio.NewReader(src)
if _, err := buf.Peek(1); err == io.EOF {
if _, err := buf.Peek(1); err != nil {
// the error is either io.EOF or "invalid argument", and both are from
// an empty state.
return nil, ErrNoState
}