Merge pull request #18826 from hashicorp/f-state-locker
backend/remote: add support for state locking
This commit is contained in:
commit
c64d8bdf35
|
@ -504,11 +504,21 @@ func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace str
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
|
func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
|
||||||
panic("not implemented")
|
w, ok := m.workspaceIDs[workspaceID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
w.Locked = true
|
||||||
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
|
func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
|
||||||
panic("not implemented")
|
w, ok := m.workspaceIDs[workspaceID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
w.Locked = false
|
||||||
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) {
|
func (m *mockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) {
|
||||||
|
|
|
@ -8,12 +8,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
tfe "github.com/hashicorp/go-tfe"
|
tfe "github.com/hashicorp/go-tfe"
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/state/remote"
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
type remoteClient struct {
|
type remoteClient struct {
|
||||||
client *tfe.Client
|
client *tfe.Client
|
||||||
|
lockInfo *state.LockInfo
|
||||||
organization string
|
organization string
|
||||||
runID string
|
runID string
|
||||||
workspace string
|
workspace string
|
||||||
|
@ -108,3 +110,72 @@ func (r *remoteClient) Delete() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lock the remote state.
|
||||||
|
func (r *remoteClient) Lock(info *state.LockInfo) (string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
lockErr := &state.LockError{Info: r.lockInfo}
|
||||||
|
|
||||||
|
// Retrieve the workspace to lock.
|
||||||
|
w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
|
||||||
|
if err != nil {
|
||||||
|
lockErr.Err = err
|
||||||
|
return "", lockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the workspace is already locked.
|
||||||
|
if w.Locked {
|
||||||
|
lockErr.Err = fmt.Errorf(
|
||||||
|
"remote state already\nlocked (lock ID: \"%s/%s\")", r.organization, r.workspace)
|
||||||
|
return "", lockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock the workspace.
|
||||||
|
w, err = r.client.Workspaces.Lock(ctx, w.ID, tfe.WorkspaceLockOptions{
|
||||||
|
Reason: tfe.String("Locked by Terraform"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
lockErr.Err = err
|
||||||
|
return "", lockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
r.lockInfo = info
|
||||||
|
|
||||||
|
return r.lockInfo.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock the remote state.
|
||||||
|
func (r *remoteClient) Unlock(id string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
lockErr := &state.LockError{Info: r.lockInfo}
|
||||||
|
|
||||||
|
// Verify the expected lock ID.
|
||||||
|
if r.lockInfo != nil && r.lockInfo.ID != id {
|
||||||
|
lockErr.Err = fmt.Errorf("lock ID does not match existing lock")
|
||||||
|
return lockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the optional force-unlock lock ID.
|
||||||
|
if r.lockInfo == nil && r.organization+"/"+r.workspace != id {
|
||||||
|
lockErr.Err = fmt.Errorf("lock ID does not match existing lock")
|
||||||
|
return lockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the workspace to lock.
|
||||||
|
w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
|
||||||
|
if err != nil {
|
||||||
|
lockErr.Err = err
|
||||||
|
return lockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock the workspace.
|
||||||
|
w, err = r.client.Workspaces.Unlock(ctx, w.ID)
|
||||||
|
if err != nil {
|
||||||
|
lockErr.Err = err
|
||||||
|
return lockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/state/remote"
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
@ -18,6 +19,22 @@ func TestRemoteClient(t *testing.T) {
|
||||||
remote.TestClient(t, client)
|
remote.TestClient(t, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemoteClient_stateLock(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
s1, err := b.State(backend.DefaultStateName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s2, err := b.State(backend.DefaultStateName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRemoteClient_withRunID(t *testing.T) {
|
func TestRemoteClient_withRunID(t *testing.T) {
|
||||||
// Set the TFE_RUN_ID environment variable before creating the client!
|
// Set the TFE_RUN_ID environment variable before creating the client!
|
||||||
if err := os.Setenv("TFE_RUN_ID", generateID("run-")); err != nil {
|
if err := os.Setenv("TFE_RUN_ID", generateID("run-")); err != nil {
|
||||||
|
|
|
@ -16,6 +16,18 @@ func TestRemote(t *testing.T) {
|
||||||
var _ backend.CLI = New(nil)
|
var _ backend.CLI = New(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemote_backendDefault(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
backend.TestBackendStates(t, b)
|
||||||
|
backend.TestBackendStateLocks(t, b, b)
|
||||||
|
backend.TestBackendStateForceUnlock(t, b, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_backendNoDefault(t *testing.T) {
|
||||||
|
b := testBackendNoDefault(t)
|
||||||
|
backend.TestBackendStates(t, b)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRemote_config(t *testing.T) {
|
func TestRemote_config(t *testing.T) {
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
config map[string]interface{}
|
config map[string]interface{}
|
||||||
|
@ -125,18 +137,6 @@ func TestRemote_nonexistingOrganization(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemote_backendDefault(t *testing.T) {
|
|
||||||
b := testBackendDefault(t)
|
|
||||||
backend.TestBackendStates(t, b)
|
|
||||||
backend.TestBackendStateLocks(t, b, b)
|
|
||||||
backend.TestBackendStateForceUnlock(t, b, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemote_backendNoDefault(t *testing.T) {
|
|
||||||
b := testBackendNoDefault(t)
|
|
||||||
backend.TestBackendStates(t, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemote_addAndRemoveStatesDefault(t *testing.T) {
|
func TestRemote_addAndRemoveStatesDefault(t *testing.T) {
|
||||||
b := testBackendDefault(t)
|
b := testBackendDefault(t)
|
||||||
if _, err := b.States(); err != backend.ErrNamedStatesNotSupported {
|
if _, err := b.States(); err != backend.ErrNamedStatesNotSupported {
|
||||||
|
|
Loading…
Reference in New Issue