Merge pull request #18826 from hashicorp/f-state-locker

backend/remote: add support for state locking
This commit is contained in:
Sander van Harmelen 2018-09-10 21:16:45 +02:00 committed by GitHub
commit c64d8bdf35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 112 additions and 14 deletions

View File

@ -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) {

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {