state: a bunch of state stuff

This commit is contained in:
Mitchell Hashimoto 2015-02-21 11:52:55 -08:00
parent 492c6ef377
commit 1f7ddc30fe
9 changed files with 408 additions and 0 deletions

71
state/cache.go Normal file
View File

@ -0,0 +1,71 @@
package state
import (
"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
state *terraform.State
}
// StateReader impl.
func (s *CacheState) State() *terraform.State {
return s.state
}
// 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
}
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 {
return nil
}
// 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
}
// CacheStateDurable is the meta-interface that must be implemented for
// the durable storage for CacheState.
type CacheStateDurable interface {
StateWriter
StatePersister
}

58
state/local.go Normal file
View File

@ -0,0 +1,58 @@
package state
import (
"os"
"github.com/hashicorp/terraform/terraform"
)
// LocalState manages a state storage that is local to the filesystem.
type LocalState struct {
Path string
state *terraform.State
}
// StateReader impl.
func (s *LocalState) State() *terraform.State {
return s.state
}
// WriteState for LocalState always persists the state as well.
//
// StateWriter impl.
func (s *LocalState) WriteState(state *terraform.State) error {
s.state = state
f, err := os.Create(s.Path)
if err != nil {
return err
}
defer f.Close()
return terraform.WriteState(s.state, f)
}
// PersistState for LocalState is a no-op since WriteState always persists.
//
// StatePersister impl.
func (s *LocalState) PersistState() error {
return nil
}
// StateRefresher impl.
func (s *LocalState) RefreshState() error {
f, err := os.Open(s.Path)
if err != nil {
return err
}
defer f.Close()
state, err := terraform.ReadState(f)
if err != nil {
return err
}
s.state = state
return nil
}

34
state/local_test.go Normal file
View File

@ -0,0 +1,34 @@
package state
import (
"io/ioutil"
"os"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestLocalState(t *testing.T) {
f, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
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) {
var _ StateReader = new(LocalState)
var _ StateWriter = new(LocalState)
var _ StatePersister = new(LocalState)
var _ StateRefresher = new(LocalState)
}

View File

@ -0,0 +1,32 @@
package remote
import (
"crypto/md5"
)
// InmemClient is a Client implementation that stores data in memory.
type InmemClient struct {
Data []byte
MD5 []byte
}
func (c *InmemClient) Get() (*Payload, error) {
return &Payload{
Data: c.Data,
MD5: c.MD5,
}, nil
}
func (c *InmemClient) Put(data []byte) error {
md5 := md5.Sum(data)
c.Data = data
c.MD5 = md5[:]
return nil
}
func (c *InmemClient) Delete() error {
c.Data = nil
c.MD5 = nil
return nil
}

19
state/remote/remote.go Normal file
View File

@ -0,0 +1,19 @@
package remote
// Client is the interface that must be implemented for a remote state
// driver. It supports dumb put/get/delete, and the higher level structs
// handle persisting the state properly here.
type Client interface {
Get() (*Payload, error)
Put([]byte) error
Delete() error
}
// Payload is the return value from the remote state storage.
type Payload struct {
MD5 []byte
Data []byte
}
// Factory is the factory function to create a remote client.
type Factory func(map[string]string) (Client, error)

54
state/remote/state.go Normal file
View File

@ -0,0 +1,54 @@
package remote
import (
"bytes"
"github.com/hashicorp/terraform/terraform"
)
// State implements the State interfaces in the state package to handle
// reading and writing the remote state. This State on its own does no
// local caching so every persist will go to the remote storage and local
// writes will go to memory.
type State struct {
Client Client
state *terraform.State
}
// StateReader impl.
func (s *State) State() *terraform.State {
return s.state
}
// StateWriter impl.
func (s *State) WriteState(state *terraform.State) error {
s.state = state
return nil
}
// StateRefresher impl.
func (s *State) RefreshState() error {
payload, err := s.Client.Get()
if err != nil {
return err
}
state, err := terraform.ReadState(bytes.NewReader(payload.Data))
if err != nil {
return err
}
s.state = state
return nil
}
// StatePersister impl.
func (s *State) PersistState() error {
var buf bytes.Buffer
if err := terraform.WriteState(s.state, &buf); err != nil {
return err
}
return s.Client.Put(buf.Bytes())
}

View File

@ -0,0 +1,24 @@
package remote
import (
"testing"
"github.com/hashicorp/terraform/state"
)
func TestState(t *testing.T) {
s := &State{Client: new(InmemClient)}
s.WriteState(state.TestStateInitial)
if err := s.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
state.TestState(t, s)
}
func TestState_impl(t *testing.T) {
var _ state.StateReader = new(State)
var _ state.StateWriter = new(State)
var _ state.StatePersister = new(State)
var _ state.StateRefresher = new(State)
}

34
state/state.go Normal file
View File

@ -0,0 +1,34 @@
package state
import (
"github.com/hashicorp/terraform/terraform"
)
// StateReader is the interface for things that can return a state. Retrieving
// the state here must not error. Loading the state fresh (an operation that
// can likely error) should be implemented by RefreshState. If a state hasn't
// been loaded yet, it is okay for State to return nil.
type StateReader interface {
State() *terraform.State
}
// StateWriter is the interface that must be implemented by something that
// can write a state. Writing the state can be cached or in-memory, as
// full persistence should be implemented by StatePersister.
type StateWriter interface {
WriteState(*terraform.State) error
}
// StateRefresher is the interface that is implemented by something that
// can load a state. This might be refreshing it from a remote location or
// it might simply be reloading it from disk.
type StateRefresher interface {
RefreshState() error
}
// StatePersister is implemented to truly persist a state. Whereas StateWriter
// is allowed to perhaps be caching in memory, PersistState must write the
// state to some durable storage.
type StatePersister interface {
PersistState() error
}

82
state/testing.go Normal file
View File

@ -0,0 +1,82 @@
package state
import (
"reflect"
"testing"
"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
// that the given implementation is pre-loaded with the TestStateInitial
// state.
func TestState(t *testing.T, s interface{}) {
reader, ok := s.(StateReader)
if !ok {
t.Fatalf("must at least be a StateReader")
}
// If it implements refresh, refresh
if rs, ok := s.(StateRefresher); ok {
if err := rs.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
}
// current will track our current state
current := TestStateInitial
// Check that the initial state is correct
if !reflect.DeepEqual(reader.State(), current) {
t.Fatalf("not initial: %#v", reader.State())
}
// Write a new state and verify that we have it
if ws, ok := s.(StateWriter); ok {
current.Modules = append(current.Modules, &terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]string{
"bar": "baz",
},
})
if err := ws.WriteState(current); err != nil {
t.Fatalf("err: %s", err)
}
if actual := reader.State(); !reflect.DeepEqual(actual, current) {
t.Fatalf("bad: %#v", actual)
}
}
// Test persistence
if ps, ok := s.(StatePersister); ok {
if err := ps.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
// Refresh if we got it
if rs, ok := s.(StateRefresher); ok {
if err := rs.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
}
if actual := reader.State(); !reflect.DeepEqual(actual, current) {
t.Fatalf("bad: %#v", actual)
}
}
}