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) {
|
||||
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) {
|
||||
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) {
|
||||
|
|
|
@ -8,12 +8,14 @@ import (
|
|||
"fmt"
|
||||
|
||||
tfe "github.com/hashicorp/go-tfe"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/state/remote"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
type remoteClient struct {
|
||||
client *tfe.Client
|
||||
lockInfo *state.LockInfo
|
||||
organization string
|
||||
runID string
|
||||
workspace string
|
||||
|
@ -108,3 +110,72 @@ func (r *remoteClient) Delete() error {
|
|||
|
||||
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"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/state/remote"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
@ -18,6 +19,22 @@ func TestRemoteClient(t *testing.T) {
|
|||
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) {
|
||||
// Set the TFE_RUN_ID environment variable before creating the client!
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
cases := map[string]struct {
|
||||
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) {
|
||||
b := testBackendDefault(t)
|
||||
if _, err := b.States(); err != backend.ErrNamedStatesNotSupported {
|
||||
|
|
Loading…
Reference in New Issue