terraform/state/cache.go

240 lines
6.7 KiB
Go

package state
import (
"fmt"
"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
}
// 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
}
}