From 1f7ddc30fe069467477aa99a8f574ec9aa552f77 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 21 Feb 2015 11:52:55 -0800 Subject: [PATCH] state: a bunch of state stuff --- state/cache.go | 71 +++++++++++++++++++++++++++++++ state/local.go | 58 +++++++++++++++++++++++++ state/local_test.go | 34 +++++++++++++++ state/remote/client_inmem.go | 32 ++++++++++++++ state/remote/remote.go | 19 +++++++++ state/remote/state.go | 54 ++++++++++++++++++++++++ state/remote/state_test.go | 24 +++++++++++ state/state.go | 34 +++++++++++++++ state/testing.go | 82 ++++++++++++++++++++++++++++++++++++ 9 files changed, 408 insertions(+) create mode 100644 state/cache.go create mode 100644 state/local.go create mode 100644 state/local_test.go create mode 100644 state/remote/client_inmem.go create mode 100644 state/remote/remote.go create mode 100644 state/remote/state.go create mode 100644 state/remote/state_test.go create mode 100644 state/state.go create mode 100644 state/testing.go diff --git a/state/cache.go b/state/cache.go new file mode 100644 index 000000000..8d562dea2 --- /dev/null +++ b/state/cache.go @@ -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 +} diff --git a/state/local.go b/state/local.go new file mode 100644 index 000000000..ce0effe98 --- /dev/null +++ b/state/local.go @@ -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 +} diff --git a/state/local_test.go b/state/local_test.go new file mode 100644 index 000000000..b77fced92 --- /dev/null +++ b/state/local_test.go @@ -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) +} diff --git a/state/remote/client_inmem.go b/state/remote/client_inmem.go new file mode 100644 index 000000000..1358b938f --- /dev/null +++ b/state/remote/client_inmem.go @@ -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 +} diff --git a/state/remote/remote.go b/state/remote/remote.go new file mode 100644 index 000000000..28cba741b --- /dev/null +++ b/state/remote/remote.go @@ -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) diff --git a/state/remote/state.go b/state/remote/state.go new file mode 100644 index 000000000..92c0b847d --- /dev/null +++ b/state/remote/state.go @@ -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()) +} diff --git a/state/remote/state_test.go b/state/remote/state_test.go new file mode 100644 index 000000000..18c76d449 --- /dev/null +++ b/state/remote/state_test.go @@ -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) +} diff --git a/state/state.go b/state/state.go new file mode 100644 index 000000000..456059df8 --- /dev/null +++ b/state/state.go @@ -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 +} diff --git a/state/testing.go b/state/testing.go new file mode 100644 index 000000000..5233d06f0 --- /dev/null +++ b/state/testing.go @@ -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) + } + } +}