state: cache state test

This commit is contained in:
Mitchell Hashimoto 2015-02-21 12:25:10 -08:00
parent 1f7ddc30fe
commit 6ec1b2b455
5 changed files with 232 additions and 35 deletions

View File

@ -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
View 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)
}

View File

@ -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
}

View File

@ -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)
} }

View File

@ -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
}