Update the state.Locker interface
Remove CacheState rather than update it, since it's no longer used.
This commit is contained in:
parent
bbae22007d
commit
200c8de4e9
|
@ -42,16 +42,16 @@ func (s *BackupState) PersistState() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// all states get wrapped by BackupState, so it has to be a Locker
|
// 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 {
|
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 {
|
if s, ok := s.Real.(Locker); ok {
|
||||||
return s.Unlock()
|
return s.Unlock(id)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
290
state/cache.go
290
state/cache.go
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -34,27 +34,6 @@ type LocalState struct {
|
||||||
written bool
|
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.
|
// SetState will force a specific state in-memory for this local state.
|
||||||
func (s *LocalState) SetState(state *terraform.State) {
|
func (s *LocalState) SetState(state *terraform.State) {
|
||||||
s.state = state
|
s.state = state
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,9 +47,56 @@ type StatePersister interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Locker is implemented to lock state during command execution.
|
// Locker is implemented to lock state during command execution.
|
||||||
// The optional info parameter can be recorded with the lock, but the
|
// The info parameter can be recorded with the lock, but the
|
||||||
// implementation should not depend in its value.
|
// 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 {
|
type Locker interface {
|
||||||
Lock(info string) error
|
Lock(info *LockInfo) (string, error)
|
||||||
Unlock() 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")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue