From ac60ddcd40714684209e0495a05d29df4060dbdf Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 1 Aug 2017 12:11:04 -0400 Subject: [PATCH] Support named states in inmeme backend Used to expand test coverage --- backend/remote-state/inmem/backend.go | 162 +++++++++++++++++++-- backend/remote-state/inmem/backend_test.go | 63 ++++++++ backend/remote-state/inmem/client.go | 38 +---- backend/remote-state/inmem/client_test.go | 11 +- 4 files changed, 222 insertions(+), 52 deletions(-) create mode 100644 backend/remote-state/inmem/backend_test.go diff --git a/backend/remote-state/inmem/backend.go b/backend/remote-state/inmem/backend.go index effa1381c2..8d434b5fb6 100644 --- a/backend/remote-state/inmem/backend.go +++ b/backend/remote-state/inmem/backend.go @@ -2,40 +2,172 @@ package inmem import ( "context" + "errors" + "fmt" + "sort" + "sync" + "time" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/backend/remote-state" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" ) +// we keep the states and locks in package-level variables, so that they can be +// accessed from multiple instances of the backend. This better emulates +// backend instances accessing a single remote data store. +var states = stateMap{ + m: map[string]*remote.State{}, +} + +var locks = lockMap{ + m: map[string]*state.LockInfo{}, +} + // New creates a new backend for Inmem remote state. func New() backend.Backend { - return &remotestate.Backend{ - ConfigureFunc: configure, - - // Set the schema - Backend: &schema.Backend{ - Schema: map[string]*schema.Schema{ - "lock_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "initializes the state in a locked configuration", - }, + // Set the schema + s := &schema.Backend{ + Schema: map[string]*schema.Schema{ + "lock_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "initializes the state in a locked configuration", }, }, } + backend := &Backend{Backend: s} + backend.Backend.ConfigureFunc = backend.configure + return backend } -func configure(ctx context.Context) (remote.Client, error) { +type Backend struct { + *schema.Backend +} + +func (b *Backend) configure(ctx context.Context) error { + states.Lock() + defer states.Unlock() + + defaultClient := &RemoteClient{ + Name: backend.DefaultStateName, + } + + states.m[backend.DefaultStateName] = &remote.State{ + Client: defaultClient, + } + + // set the default client lock info per the test config data := schema.FromContextBackendConfig(ctx) if v, ok := data.GetOk("lock_id"); ok && v.(string) != "" { info := state.NewLockInfo() info.ID = v.(string) info.Operation = "test" info.Info = "test config" - return &RemoteClient{LockInfo: info}, nil + + locks.lock(backend.DefaultStateName, info) } - return &RemoteClient{}, nil + + return nil +} + +func (b *Backend) States() ([]string, error) { + states.Lock() + defer states.Unlock() + + var workspaces []string + + for s := range states.m { + workspaces = append(workspaces, s) + } + + sort.Strings(workspaces) + return workspaces, nil +} + +func (b *Backend) DeleteState(name string) error { + states.Lock() + defer states.Unlock() + + if name == backend.DefaultStateName || name == "" { + return fmt.Errorf("can't delete default state") + } + + delete(states.m, name) + return nil +} + +func (b *Backend) State(name string) (state.State, error) { + states.Lock() + defer states.Unlock() + + s := states.m[name] + if s == nil { + s = &remote.State{ + Client: &RemoteClient{ + Name: name, + }, + } + states.m[name] = s + } + + return s, nil +} + +type stateMap struct { + sync.Mutex + m map[string]*remote.State +} + +// Global level locks for inmem backends. +type lockMap struct { + sync.Mutex + m map[string]*state.LockInfo +} + +func (l *lockMap) lock(name string, info *state.LockInfo) (string, error) { + l.Lock() + defer l.Unlock() + + lockInfo := l.m[name] + if lockInfo != nil { + lockErr := &state.LockError{ + Info: lockInfo, + } + + lockErr.Err = errors.New("state locked") + // make a copy of the lock info to avoid any testing shenanigans + *lockErr.Info = *lockInfo + return "", lockErr + } + + info.Created = time.Now().UTC() + l.m[name] = info + + return info.ID, nil +} + +func (l *lockMap) unlock(name, id string) error { + l.Lock() + defer l.Unlock() + + lockInfo := l.m[name] + + if lockInfo == nil { + return errors.New("state not locked") + } + + lockErr := &state.LockError{ + Info: &state.LockInfo{}, + } + + if id != lockInfo.ID { + lockErr.Err = errors.New("invalid lock id") + *lockErr.Info = *lockInfo + return lockErr + } + + delete(l.m, name) + return nil } diff --git a/backend/remote-state/inmem/backend_test.go b/backend/remote-state/inmem/backend_test.go new file mode 100644 index 0000000000..4398591bdf --- /dev/null +++ b/backend/remote-state/inmem/backend_test.go @@ -0,0 +1,63 @@ +package inmem + +import ( + "testing" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/state/remote" +) + +func TestBackend_impl(t *testing.T) { + var _ backend.Backend = new(Backend) +} + +// reset the states and locks between tests +func reset() { + states = stateMap{ + m: map[string]*remote.State{}, + } + + locks = lockMap{ + m: map[string]*state.LockInfo{}, + } +} + +func TestBackendConfig(t *testing.T) { + defer reset() + testID := "test_lock_id" + + config := map[string]interface{}{ + "lock_id": testID, + } + + b := backend.TestBackendConfig(t, New(), config).(*Backend) + + s, err := b.State(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + + c := s.(*remote.State).Client.(*RemoteClient) + if c.Name != backend.DefaultStateName { + t.Fatal("client name is not configured") + } + + if err := locks.unlock(backend.DefaultStateName, testID); err != nil { + t.Fatalf("default state should have been locked: %s", err) + } +} + +func TestBackend(t *testing.T) { + defer reset() + b := backend.TestBackendConfig(t, New(), nil).(*Backend) + backend.TestBackend(t, b, nil) +} + +func TestBackendLocked(t *testing.T) { + defer reset() + b1 := backend.TestBackendConfig(t, New(), nil).(*Backend) + b2 := backend.TestBackendConfig(t, New(), nil).(*Backend) + + backend.TestBackend(t, b1, b2) +} diff --git a/backend/remote-state/inmem/client.go b/backend/remote-state/inmem/client.go index 703d4a2671..51c8d7251b 100644 --- a/backend/remote-state/inmem/client.go +++ b/backend/remote-state/inmem/client.go @@ -2,8 +2,6 @@ package inmem import ( "crypto/md5" - "errors" - "time" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" @@ -13,8 +11,7 @@ import ( type RemoteClient struct { Data []byte MD5 []byte - - LockInfo *state.LockInfo + Name string } func (c *RemoteClient) Get() (*remote.Payload, error) { @@ -43,37 +40,8 @@ func (c *RemoteClient) Delete() error { } func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) { - lockErr := &state.LockError{ - Info: &state.LockInfo{}, - } - - if c.LockInfo != nil { - lockErr.Err = errors.New("state locked") - // make a copy of the lock info to avoid any testing shenanigans - *lockErr.Info = *c.LockInfo - return "", lockErr - } - - info.Created = time.Now().UTC() - c.LockInfo = info - - return c.LockInfo.ID, nil + return locks.lock(c.Name, info) } - func (c *RemoteClient) Unlock(id string) error { - if c.LockInfo == nil { - return errors.New("state not locked") - } - - lockErr := &state.LockError{ - Info: &state.LockInfo{}, - } - if id != c.LockInfo.ID { - lockErr.Err = errors.New("invalid lock id") - *lockErr.Info = *c.LockInfo - return lockErr - } - - c.LockInfo = nil - return nil + return locks.unlock(c.Name, id) } diff --git a/backend/remote-state/inmem/client_test.go b/backend/remote-state/inmem/client_test.go index f3de567157..c040a6e72f 100644 --- a/backend/remote-state/inmem/client_test.go +++ b/backend/remote-state/inmem/client_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/hashicorp/terraform/backend" - remotestate "github.com/hashicorp/terraform/backend/remote-state" "github.com/hashicorp/terraform/state/remote" ) @@ -14,11 +13,19 @@ func TestRemoteClient_impl(t *testing.T) { } func TestRemoteClient(t *testing.T) { + defer reset() b := backend.TestBackendConfig(t, New(), nil) - remotestate.TestClient(t, b) + + s, err := b.State(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + + remote.TestClient(t, s.(*remote.State).Client) } func TestInmemLocks(t *testing.T) { + defer reset() s, err := backend.TestBackendConfig(t, New(), nil).State(backend.DefaultStateName) if err != nil { t.Fatal(err)