Merge pull request #12069 from hashicorp/jbardin/state-locking

Add inmem remote backend
This commit is contained in:
James Bardin 2017-02-21 19:00:21 -05:00 committed by GitHub
commit c080334c3f
9 changed files with 208 additions and 50 deletions

View File

@ -0,0 +1,41 @@
package inmem
import (
"context"
"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"
)
// 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",
},
},
},
}
}
func configure(ctx context.Context) (remote.Client, error) {
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
}
return &RemoteClient{}, nil
}

View File

@ -0,0 +1,79 @@
package inmem
import (
"crypto/md5"
"errors"
"time"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
)
// RemoteClient is a remote client that stores data in memory for testing.
type RemoteClient struct {
Data []byte
MD5 []byte
LockInfo *state.LockInfo
}
func (c *RemoteClient) Get() (*remote.Payload, error) {
if c.Data == nil {
return nil, nil
}
return &remote.Payload{
Data: c.Data,
MD5: c.MD5,
}, nil
}
func (c *RemoteClient) Put(data []byte) error {
md5 := md5.Sum(data)
c.Data = data
c.MD5 = md5[:]
return nil
}
func (c *RemoteClient) Delete() error {
c.Data = nil
c.MD5 = nil
return nil
}
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
}
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
}

View File

@ -0,0 +1,28 @@
package inmem
import (
"testing"
"github.com/hashicorp/terraform/backend"
remotestate "github.com/hashicorp/terraform/backend/remote-state"
"github.com/hashicorp/terraform/state/remote"
)
func TestRemoteClient_impl(t *testing.T) {
var _ remote.Client = new(RemoteClient)
var _ remote.ClientLocker = new(RemoteClient)
}
func TestRemoteClient(t *testing.T) {
b := backend.TestBackendConfig(t, New(), nil)
remotestate.TestClient(t, b)
}
func TestInmemLocks(t *testing.T) {
s, err := backend.TestBackendConfig(t, New(), nil).State()
if err != nil {
t.Fatal(err)
}
remote.TestRemoteLocks(t, s.(*remote.State).Client, s.(*remote.State).Client)
}

View File

@ -25,6 +25,7 @@ import (
backendlegacy "github.com/hashicorp/terraform/backend/legacy" backendlegacy "github.com/hashicorp/terraform/backend/legacy"
backendlocal "github.com/hashicorp/terraform/backend/local" backendlocal "github.com/hashicorp/terraform/backend/local"
backendconsul "github.com/hashicorp/terraform/backend/remote-state/consul" backendconsul "github.com/hashicorp/terraform/backend/remote-state/consul"
backendinmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
) )
// BackendOpts are the options used to initialize a backend.Backend. // BackendOpts are the options used to initialize a backend.Backend.
@ -1409,6 +1410,7 @@ func init() {
Backends = map[string]func() backend.Backend{ Backends = map[string]func() backend.Backend{
"local": func() backend.Backend { return &backendlocal.Local{} }, "local": func() backend.Backend { return &backendlocal.Local{} },
"consul": func() backend.Backend { return backendconsul.New() }, "consul": func() backend.Backend { return backendconsul.New() },
"inmem": func() backend.Backend { return backendinmem.New() },
} }
// Add the legacy remote backends that haven't yet been convertd to // Add the legacy remote backends that haven't yet been convertd to

View File

@ -0,0 +1,5 @@
terraform {
backend "inmem" {
lock_id = "2b6a6738-5dd5-50d6-c0ae-f6352977666b"
}
}

View File

@ -33,10 +33,7 @@ func (c *UnlockCommand) Run(args []string) int {
} }
lockID := args[0] lockID := args[0]
if len(args) > 1 {
args = args[1:] args = args[1:]
}
// assume everything is initialized. The user can manually init if this is // assume everything is initialized. The user can manually init if this is
// required. // required.

View File

@ -4,13 +4,13 @@ import (
"os" "os"
"testing" "testing"
"github.com/hashicorp/terraform/helper/copy"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
// Since we can't unlock a local state file, just test that calling unlock // Since we can't unlock a local state file, just test that calling unlock
// doesn't fail. // doesn't fail.
// TODO: mock remote state for UI testing
func TestUnlock(t *testing.T) { func TestUnlock(t *testing.T) {
td := tempDir(t) td := tempDir(t)
os.MkdirAll(td, 0755) os.MkdirAll(td, 0755)
@ -49,3 +49,54 @@ func TestUnlock(t *testing.T) {
t.Fatalf("bad: %d\n%s\n%s", code, ui.OutputWriter.String(), ui.ErrorWriter.String()) t.Fatalf("bad: %d\n%s\n%s", code, ui.OutputWriter.String(), ui.ErrorWriter.String())
} }
} }
// Newly configured backend
func TestUnlock_inmemBackend(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("backend-inmem-locked"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// init backend
ui := new(cli.MockUi)
ci := &InitCommand{
Meta: Meta{
Ui: ui,
},
}
if code := ci.Run(nil); code != 0 {
t.Fatalf("bad: %d\n%s", code, ui.ErrorWriter)
}
ui = new(cli.MockUi)
c := &UnlockCommand{
Meta: Meta{
Ui: ui,
},
}
// run with the incorrect lock ID
args := []string{
"-force",
"LOCK_ID",
}
if code := c.Run(args); code == 0 {
t.Fatalf("bad: %d\n%s\n%s", code, ui.OutputWriter.String(), ui.ErrorWriter.String())
}
ui = new(cli.MockUi)
c = &UnlockCommand{
Meta: Meta{
Ui: ui,
},
}
// lockID set in the test fixture
args[1] = "2b6a6738-5dd5-50d6-c0ae-f6352977666b"
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n%s\n%s", code, ui.OutputWriter.String(), ui.ErrorWriter.String())
}
}

View File

@ -1,32 +0,0 @@
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
}

View File

@ -6,19 +6,6 @@ import (
"github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state"
) )
func TestState(t *testing.T) {
s := &State{
Client: new(InmemClient),
state: state.TestStateInitial(),
readState: state.TestStateInitial(),
}
if err := s.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
state.TestState(t, s)
}
func TestState_impl(t *testing.T) { func TestState_impl(t *testing.T) {
var _ state.StateReader = new(State) var _ state.StateReader = new(State)
var _ state.StateWriter = new(State) var _ state.StateWriter = new(State)