mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-01 11:47:07 -06:00
35307d5a60
In order to provide lockign for remote states, the Cache state, remote.State need to expose Lock and Unlock methods. The actual locking will be done by the remote.Client, which can implement the same state.Locker methods.
291 lines
8.0 KiB
Go
291 lines
8.0 KiB
Go
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
|
|
}
|
|
}
|