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