Update the state.Locker interface

Remove CacheState rather than update it, since it's no longer used.
This commit is contained in:
James Bardin 2017-02-14 17:43:42 -05:00
parent bbae22007d
commit 200c8de4e9
5 changed files with 61 additions and 438 deletions

View File

@ -42,16 +42,16 @@ func (s *BackupState) PersistState() error {
}
// all states get wrapped by BackupState, so it has to be a Locker
func (s *BackupState) Lock(reason string) error {
func (s *BackupState) Lock(info *LockInfo) (string, error) {
if s, ok := s.Real.(Locker); ok {
return s.Lock(reason)
return s.Lock(info)
}
return nil
return "", nil
}
func (s *BackupState) Unlock() error {
func (s *BackupState) Unlock(id string) error {
if s, ok := s.Real.(Locker); ok {
return s.Unlock()
return s.Unlock(id)
}
return nil
}

View File

@ -1,290 +0,0 @@
package state
import (
"fmt"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/terraform"
)
// CacheState is an implementation of the state interfaces that uses
// a StateReadWriter for a local cache.
type CacheState struct {
Cache CacheStateCache
Durable CacheStateDurable
refreshResult CacheRefreshResult
state *terraform.State
}
// Locker implementation.
// Since remote states are wrapped in a CacheState, we need to implement the
// Lock/Unlock methods here to delegate them to the remote client.
func (s *CacheState) Lock(reason string) error {
durable, durableIsLocker := s.Durable.(Locker)
cache, cacheIsLocker := s.Cache.(Locker)
if durableIsLocker {
if err := durable.Lock(reason); err != nil {
return err
}
}
// We try to lock the Cache too, which is usually a local file. This also
// protects against multiple local processes if the remote state doesn't
// support locking.
if cacheIsLocker {
if err := cache.Lock(reason); err != nil {
// try to unlock Durable if this failed
if unlockErr := durable.Unlock(); unlockErr != nil {
err = multierror.Append(err, unlockErr)
}
return err
}
}
return nil
}
// Unlock unlocks both the Durable and Cache states.
func (s *CacheState) Unlock() error {
durable, durableIsLocker := s.Durable.(Locker)
cache, cacheIsLocker := s.Cache.(Locker)
var err error
if durableIsLocker {
if unlockErr := durable.Unlock(); unlockErr != nil {
err = multierror.Append(err, unlockErr)
}
}
if cacheIsLocker {
if unlockErr := cache.Unlock(); unlockErr != nil {
err = multierror.Append(err, unlockErr)
}
}
return err
}
// StateReader impl.
func (s *CacheState) State() *terraform.State {
return s.state.DeepCopy()
}
// WriteState will write and persist the state to the cache.
//
// StateWriter impl.
func (s *CacheState) WriteState(state *terraform.State) error {
if err := s.Cache.WriteState(state); err != nil {
return err
}
s.state = state
return s.Cache.PersistState()
}
// RefreshState will refresh both the cache and the durable states. It
// can return a myriad of errors (defined at the top of this file) depending
// on potential conflicts that can occur while doing this.
//
// If the durable state is newer than the local cache, then the local cache
// will be replaced with the durable.
//
// StateRefresher impl.
func (s *CacheState) RefreshState() error {
// Refresh the durable state
if err := s.Durable.RefreshState(); err != nil {
return err
}
// Refresh the cached state
if err := s.Cache.RefreshState(); err != nil {
return err
}
// Handle the matrix of cases that can happen when comparing these
// two states.
cached := s.Cache.State()
durable := s.Durable.State()
switch {
case cached == nil && durable == nil:
// Initialized
s.refreshResult = CacheRefreshInit
case cached != nil && durable == nil:
// Cache is newer than remote. Not a big deal, user can just
// persist to get correct state.
s.refreshResult = CacheRefreshLocalNewer
case !cached.HasResources() && durable != nil:
// Cache should be updated since the remote is set but cache isn't
//
// If local is empty then we'll treat it as missing so that
// it can be overwritten by an already-existing remote. This
// allows the user to activate remote state for the first time
// against an already-existing remote state.
s.refreshResult = CacheRefreshUpdateLocal
case durable.Serial < cached.Serial:
// Cache is newer than remote. Not a big deal, user can just
// persist to get correct state.
s.refreshResult = CacheRefreshLocalNewer
case durable.Serial > cached.Serial:
// Cache should be updated since the remote is newer
s.refreshResult = CacheRefreshUpdateLocal
case durable.Serial == cached.Serial:
// They're supposedly equal, verify.
if cached.Equal(durable) {
// Hashes are the same, everything is great
s.refreshResult = CacheRefreshNoop
break
}
// This is very bad. This means we have two state files that
// have the same serial but have a different hash. We can't
// reconcile this. The most likely cause is parallel apply
// operations.
s.refreshResult = CacheRefreshConflict
// Return early so we don't updtae the state
return nil
default:
panic("unhandled cache refresh state")
}
if s.refreshResult == CacheRefreshUpdateLocal {
if err := s.Cache.WriteState(durable); err != nil {
s.refreshResult = CacheRefreshNoop
return err
}
if err := s.Cache.PersistState(); err != nil {
s.refreshResult = CacheRefreshNoop
return err
}
cached = durable
}
s.state = cached
return nil
}
// RefreshResult returns the result of the last refresh.
func (s *CacheState) RefreshResult() CacheRefreshResult {
return s.refreshResult
}
// PersistState takes the local cache, assuming it is newer than the remote
// state, and persists it to the durable storage. If you want to challenge the
// assumption that the local state is the latest, call a RefreshState prior
// to this.
//
// StatePersister impl.
func (s *CacheState) PersistState() error {
if err := s.Durable.WriteState(s.state); err != nil {
return err
}
return s.Durable.PersistState()
}
// CacheStateCache is the meta-interface that must be implemented for
// the cache for the CacheState.
type CacheStateCache interface {
StateReader
StateWriter
StatePersister
StateRefresher
}
// CacheStateDurable is the meta-interface that must be implemented for
// the durable storage for CacheState.
type CacheStateDurable interface {
StateReader
StateWriter
StatePersister
StateRefresher
}
// CacheRefreshResult is used to explain the result of the previous
// RefreshState for a CacheState.
type CacheRefreshResult int
const (
// CacheRefreshNoop indicates nothing has happened,
// but that does not indicate an error. Everything is
// just up to date. (Push/Pull)
CacheRefreshNoop CacheRefreshResult = iota
// CacheRefreshInit indicates that there is no local or
// remote state, and that the state was initialized
CacheRefreshInit
// CacheRefreshUpdateLocal indicates the local state
// was updated. (Pull)
CacheRefreshUpdateLocal
// CacheRefreshUpdateRemote indicates the remote state
// was updated. (Push)
CacheRefreshUpdateRemote
// CacheRefreshLocalNewer means the pull was a no-op
// because the local state is newer than that of the
// server. This means a Push should take place. (Pull)
CacheRefreshLocalNewer
// CacheRefreshRemoteNewer means the push was a no-op
// because the remote state is newer than that of the
// local state. This means a Pull should take place.
// (Push)
CacheRefreshRemoteNewer
// CacheRefreshConflict means that the push or pull
// was a no-op because there is a conflict. This means
// there are multiple state definitions at the same
// serial number with different contents. This requires
// an operator to intervene and resolve the conflict.
// Shame on the user for doing concurrent apply.
// (Push/Pull)
CacheRefreshConflict
)
func (sc CacheRefreshResult) String() string {
switch sc {
case CacheRefreshNoop:
return "Local and remote state in sync"
case CacheRefreshInit:
return "Local state initialized"
case CacheRefreshUpdateLocal:
return "Local state updated"
case CacheRefreshUpdateRemote:
return "Remote state updated"
case CacheRefreshLocalNewer:
return "Local state is newer than remote state, push required"
case CacheRefreshRemoteNewer:
return "Remote state is newer than local state, pull required"
case CacheRefreshConflict:
return "Local and remote state conflict, manual resolution required"
default:
return fmt.Sprintf("Unknown state change type: %d", sc)
}
}
// SuccessfulPull is used to clasify the CacheRefreshResult for
// a refresh operation. This is different by operation, but can be used
// to determine a proper exit code.
func (sc CacheRefreshResult) SuccessfulPull() bool {
switch sc {
case CacheRefreshNoop:
return true
case CacheRefreshInit:
return true
case CacheRefreshUpdateLocal:
return true
case CacheRefreshLocalNewer:
return false
case CacheRefreshConflict:
return false
default:
return false
}
}

View File

@ -1,118 +0,0 @@
package state
import (
"fmt"
"os"
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestCacheState(t *testing.T) {
cache := testLocalState(t)
durable := testLocalState(t)
defer os.Remove(cache.Path)
defer os.Remove(durable.Path)
TestState(t, &CacheState{
Cache: cache,
Durable: durable,
})
}
func TestCacheState_persistDurable(t *testing.T) {
cache := testLocalState(t)
durable := testLocalState(t)
defer os.Remove(cache.Path)
defer os.Remove(durable.Path)
cs := &CacheState{
Cache: cache,
Durable: durable,
}
state := cache.State()
state.Modules = nil
if err := cs.WriteState(state); err != nil {
t.Fatalf("err: %s", err)
}
if reflect.DeepEqual(cache.State(), durable.State()) {
t.Fatal("cache and durable should not be the same")
}
if err := cs.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(cache.State(), durable.State()) {
t.Fatalf(
"cache and durable should be the same\n\n%#v\n\n%#v",
cache.State(), durable.State())
}
}
func TestCacheState_RefreshState(t *testing.T) {
for i, test := range []struct {
cacheModules []*terraform.ModuleState
expected CacheRefreshResult
}{
{
cacheModules: nil,
expected: CacheRefreshUpdateLocal,
},
{
cacheModules: []*terraform.ModuleState{},
expected: CacheRefreshUpdateLocal,
},
{
cacheModules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: terraform.RootModulePath,
Resources: map[string]*terraform.ResourceState{
"foo.foo": &terraform.ResourceState{
Primary: &terraform.InstanceState{
ID: "ID",
},
},
},
},
},
expected: CacheRefreshLocalNewer,
},
} {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
cache := testLocalState(t)
durable := testLocalState(t)
defer os.Remove(cache.Path)
defer os.Remove(durable.Path)
cs := &CacheState{
Cache: cache,
Durable: durable,
}
state := cache.State()
state.Modules = test.cacheModules
if err := cs.WriteState(state); err != nil {
t.Fatalf("err: %s", err)
}
if err := cs.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
if cs.RefreshResult() != test.expected {
t.Fatalf("bad %d: %v", i, cs.RefreshResult())
}
})
}
}
func TestCacheState_impl(t *testing.T) {
var _ StateReader = new(CacheState)
var _ StateWriter = new(CacheState)
var _ StatePersister = new(CacheState)
var _ StateRefresher = new(CacheState)
}

View File

@ -34,27 +34,6 @@ type LocalState struct {
written bool
}
// LockInfo stores metadata for locks taken.
type LockInfo struct {
Path string // Path to the state file
Created time.Time // The time the lock was taken
Info string // Extra info passed to State.Lock
}
// Err returns the lock info formatted in an error
func (l *LockInfo) Err() error {
return fmt.Errorf("state locked. path:%q, created:%s, info:%q",
l.Path, l.Created, l.Info)
}
func (l *LockInfo) String() string {
js, err := json.Marshal(l)
if err != nil {
panic(err)
}
return string(js)
}
// SetState will force a specific state in-memory for this local state.
func (s *LocalState) SetState(state *terraform.State) {
s.state = state

View File

@ -1,6 +1,11 @@
package state
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/hashicorp/terraform/terraform"
)
@ -42,9 +47,56 @@ type StatePersister interface {
}
// Locker is implemented to lock state during command execution.
// The optional info parameter can be recorded with the lock, but the
// implementation should not depend in its value.
// The info parameter can be recorded with the lock, but the
// implementation should not depend in its value. The string returned by Lock
// is an ID corresponding to the lock acquired, and must be passed to Unlock to
// ensure that the correct lock is being released.
//
// Lock and Unlock may return an error value of type LockError which in turn
// can contain the LockInfo of a conflicting lock.
type Locker interface {
Lock(info string) error
Unlock() error
Lock(info *LockInfo) (string, error)
Unlock(id string) error
}
// LockInfo stores metadata for locks taken.
type LockInfo struct {
ID string // unique ID
Path string // Path to the state file
Created time.Time // The time the lock was taken
Version string // Terraform version
Operation string // Terraform operation
Who string // user@hostname when available
Info string // Extra info field
}
// Err returns the lock info formatted in an error
func (l *LockInfo) Err() error {
return fmt.Errorf("state locked. path:%q, created:%s, info:%q",
l.Path, l.Created, l.Info)
}
func (l *LockInfo) String() string {
js, err := json.Marshal(l)
if err != nil {
panic(err)
}
return string(js)
}
type LockError struct {
Info *LockInfo
Err error
}
func (e *LockError) Error() string {
var out []string
if e.Err != nil {
out = append(out, e.Err.Error())
}
if e.Info != nil {
out = append(out, e.Info.Err().Error())
}
return strings.Join(out, "\n")
}