Merge pull request #12069 from hashicorp/jbardin/state-locking
Add inmem remote backend
This commit is contained in:
commit
c080334c3f
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "inmem" {
|
||||||
|
lock_id = "2b6a6738-5dd5-50d6-c0ae-f6352977666b"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue