Support named states in inmeme backend

Used to expand test coverage
This commit is contained in:
James Bardin 2017-08-01 12:11:04 -04:00
parent 470448d8ce
commit ac60ddcd40
4 changed files with 222 additions and 52 deletions

View File

@ -2,21 +2,33 @@ 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{
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"lock_id": &schema.Schema{
Type: schema.TypeString,
@ -24,18 +36,138 @@ func New() backend.Backend {
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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)