diff --git a/backend/remote-state/inmem/backend.go b/backend/remote-state/inmem/backend.go new file mode 100644 index 000000000..effa1381c --- /dev/null +++ b/backend/remote-state/inmem/backend.go @@ -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 +} diff --git a/backend/remote-state/inmem/client.go b/backend/remote-state/inmem/client.go new file mode 100644 index 000000000..703d4a267 --- /dev/null +++ b/backend/remote-state/inmem/client.go @@ -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 +} diff --git a/backend/remote-state/inmem/client_test.go b/backend/remote-state/inmem/client_test.go new file mode 100644 index 000000000..549cbd80b --- /dev/null +++ b/backend/remote-state/inmem/client_test.go @@ -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) +} diff --git a/command/meta_backend.go b/command/meta_backend.go index 8cc95fd0c..4eb79ed5c 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -25,6 +25,7 @@ import ( backendlegacy "github.com/hashicorp/terraform/backend/legacy" backendlocal "github.com/hashicorp/terraform/backend/local" 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. @@ -1409,6 +1410,7 @@ func init() { Backends = map[string]func() backend.Backend{ "local": func() backend.Backend { return &backendlocal.Local{} }, "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 diff --git a/command/test-fixtures/backend-inmem-locked/main.tf b/command/test-fixtures/backend-inmem-locked/main.tf new file mode 100644 index 000000000..9fb065d7e --- /dev/null +++ b/command/test-fixtures/backend-inmem-locked/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "inmem" { + lock_id = "2b6a6738-5dd5-50d6-c0ae-f6352977666b" + } +} diff --git a/command/unlock.go b/command/unlock.go index 2e197e775..b50713aaa 100644 --- a/command/unlock.go +++ b/command/unlock.go @@ -33,10 +33,7 @@ func (c *UnlockCommand) Run(args []string) int { } 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 // required. diff --git a/command/unlock_test.go b/command/unlock_test.go index 761701b2f..892947afb 100644 --- a/command/unlock_test.go +++ b/command/unlock_test.go @@ -4,13 +4,13 @@ import ( "os" "testing" + "github.com/hashicorp/terraform/helper/copy" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) // Since we can't unlock a local state file, just test that calling unlock // doesn't fail. -// TODO: mock remote state for UI testing func TestUnlock(t *testing.T) { td := tempDir(t) 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()) } } + +// 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()) + } + +} diff --git a/state/remote/client_inmem.go b/state/remote/client_inmem.go deleted file mode 100644 index 1358b938f..000000000 --- a/state/remote/client_inmem.go +++ /dev/null @@ -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 -} diff --git a/state/remote/state_test.go b/state/remote/state_test.go index 01315b99e..90a60e9c4 100644 --- a/state/remote/state_test.go +++ b/state/remote/state_test.go @@ -6,19 +6,6 @@ import ( "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) { var _ state.StateReader = new(State) var _ state.StateWriter = new(State)