state: a bunch of state stuff
This commit is contained in:
parent
492c6ef377
commit
1f7ddc30fe
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue