mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
state: cache state test
This commit is contained in:
parent
1f7ddc30fe
commit
6ec1b2b455
122
state/cache.go
122
state/cache.go
@ -1,6 +1,8 @@
|
|||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -10,7 +12,8 @@ type CacheState struct {
|
|||||||
Cache CacheStateCache
|
Cache CacheStateCache
|
||||||
Durable CacheStateDurable
|
Durable CacheStateDurable
|
||||||
|
|
||||||
state *terraform.State
|
refreshResult CacheRefreshResult
|
||||||
|
state *terraform.State
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateReader impl.
|
// StateReader impl.
|
||||||
@ -26,6 +29,7 @@ func (s *CacheState) WriteState(state *terraform.State) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.state = state
|
||||||
return s.Cache.PersistState()
|
return s.Cache.PersistState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,9 +42,79 @@ func (s *CacheState) WriteState(state *terraform.State) error {
|
|||||||
//
|
//
|
||||||
// StateRefresher impl.
|
// StateRefresher impl.
|
||||||
func (s *CacheState) RefreshState() error {
|
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 == nil && durable != nil:
|
||||||
|
// Cache should be updated since the remote is set but cache isn't
|
||||||
|
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 reflect.DeepEqual(cached, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.state = cached
|
||||||
|
|
||||||
return nil
|
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
|
// 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
|
// 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
|
// assumption that the local state is the latest, call a RefreshState prior
|
||||||
@ -61,11 +135,57 @@ type CacheStateCache interface {
|
|||||||
StateReader
|
StateReader
|
||||||
StateWriter
|
StateWriter
|
||||||
StatePersister
|
StatePersister
|
||||||
|
StateRefresher
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheStateDurable is the meta-interface that must be implemented for
|
// CacheStateDurable is the meta-interface that must be implemented for
|
||||||
// the durable storage for CacheState.
|
// the durable storage for CacheState.
|
||||||
type CacheStateDurable interface {
|
type CacheStateDurable interface {
|
||||||
|
StateReader
|
||||||
StateWriter
|
StateWriter
|
||||||
StatePersister
|
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
|
||||||
|
)
|
||||||
|
58
state/cache_test.go
Normal file
58
state/cache_test.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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_impl(t *testing.T) {
|
||||||
|
var _ StateReader = new(CacheState)
|
||||||
|
var _ StateWriter = new(CacheState)
|
||||||
|
var _ StatePersister = new(CacheState)
|
||||||
|
var _ StateRefresher = new(CacheState)
|
||||||
|
}
|
@ -9,21 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestLocalState(t *testing.T) {
|
func TestLocalState(t *testing.T) {
|
||||||
f, err := ioutil.TempFile("", "tf")
|
ls := testLocalState(t)
|
||||||
if err != nil {
|
defer os.Remove(ls.Path)
|
||||||
t.Fatalf("err: %s", err)
|
TestState(t, ls)
|
||||||
}
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
|
|
||||||
err = terraform.WriteState(TestStateInitial, f)
|
|
||||||
f.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
TestState(t, &LocalState{
|
|
||||||
Path: f.Name(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocalState_impl(t *testing.T) {
|
func TestLocalState_impl(t *testing.T) {
|
||||||
@ -32,3 +20,23 @@ func TestLocalState_impl(t *testing.T) {
|
|||||||
var _ StatePersister = new(LocalState)
|
var _ StatePersister = new(LocalState)
|
||||||
var _ StateRefresher = new(LocalState)
|
var _ StateRefresher = new(LocalState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testLocalState(t *testing.T) *LocalState {
|
||||||
|
f, err := ioutil.TempFile("", "tf")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = terraform.WriteState(TestStateInitial(), f)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ls := &LocalState{Path: f.Name()}
|
||||||
|
if err := ls.RefreshState(); err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ls
|
||||||
|
}
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
func TestState(t *testing.T) {
|
func TestState(t *testing.T) {
|
||||||
s := &State{Client: new(InmemClient)}
|
s := &State{Client: new(InmemClient)}
|
||||||
s.WriteState(state.TestStateInitial)
|
s.WriteState(state.TestStateInitial())
|
||||||
if err := s.PersistState(); err != nil {
|
if err := s.PersistState(); err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,13 @@
|
|||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestStateInitial is the initial state that a State should have
|
|
||||||
// for TestState.
|
|
||||||
var TestStateInitial *terraform.State = &terraform.State{
|
|
||||||
Modules: []*terraform.ModuleState{
|
|
||||||
&terraform.ModuleState{
|
|
||||||
Path: []string{"root", "child"},
|
|
||||||
Outputs: map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestState is a helper for testing state implementations. It is expected
|
// TestState is a helper for testing state implementations. It is expected
|
||||||
// that the given implementation is pre-loaded with the TestStateInitial
|
// that the given implementation is pre-loaded with the TestStateInitial
|
||||||
// state.
|
// state.
|
||||||
@ -37,11 +25,12 @@ func TestState(t *testing.T, s interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// current will track our current state
|
// current will track our current state
|
||||||
current := TestStateInitial
|
current := TestStateInitial()
|
||||||
|
current.Serial++
|
||||||
|
|
||||||
// Check that the initial state is correct
|
// Check that the initial state is correct
|
||||||
if !reflect.DeepEqual(reader.State(), current) {
|
if !reflect.DeepEqual(reader.State(), current) {
|
||||||
t.Fatalf("not initial: %#v", reader.State())
|
t.Fatalf("not initial: %#v\n\n%#v", reader.State(), current)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write a new state and verify that we have it
|
// Write a new state and verify that we have it
|
||||||
@ -58,7 +47,7 @@ func TestState(t *testing.T, s interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if actual := reader.State(); !reflect.DeepEqual(actual, current) {
|
if actual := reader.State(); !reflect.DeepEqual(actual, current) {
|
||||||
t.Fatalf("bad: %#v", actual)
|
t.Fatalf("bad: %#v\n\n%#v", actual, current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,8 +64,30 @@ func TestState(t *testing.T, s interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if actual := reader.State(); !reflect.DeepEqual(actual, current) {
|
// Just set the serials the same... Then compare.
|
||||||
t.Fatalf("bad: %#v", actual)
|
actual := reader.State()
|
||||||
|
actual.Serial = current.Serial
|
||||||
|
if !reflect.DeepEqual(actual, current) {
|
||||||
|
t.Fatalf("bad: %#v\n\n%#v", actual, current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStateInitial is the initial state that a State should have
|
||||||
|
// for TestState.
|
||||||
|
func TestStateInitial() *terraform.State {
|
||||||
|
initial := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root", "child"},
|
||||||
|
Outputs: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var scratch bytes.Buffer
|
||||||
|
terraform.WriteState(initial, &scratch)
|
||||||
|
return initial
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user