Merge pull request #12710 from hashicorp/b-consul-lock-disable

backend/consul: support "lock" option to disable locking
This commit is contained in:
Mitchell Hashimoto 2017-03-15 06:47:09 -07:00 committed by GitHub
commit 9b1ae50bd4
7 changed files with 191 additions and 10 deletions

View File

@ -60,6 +60,13 @@ func New() backend.Backend {
Description: "Compress the state data using gzip", Description: "Compress the state data using gzip",
Default: false, Default: false,
}, },
"lock": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: "Lock state access",
Default: true,
},
}, },
} }
@ -71,13 +78,18 @@ func New() backend.Backend {
type Backend struct { type Backend struct {
*schema.Backend *schema.Backend
// The fields below are set from configure
configData *schema.ResourceData configData *schema.ResourceData
lock bool
} }
func (b *Backend) configure(ctx context.Context) error { func (b *Backend) configure(ctx context.Context) error {
// Grab the resource data // Grab the resource data
b.configData = schema.FromContextBackendConfig(ctx) b.configData = schema.FromContextBackendConfig(ctx)
// Store the lock information
b.lock = b.configData.Get("lock").(bool)
// Initialize a client to test config // Initialize a client to test config
_, err := b.clientRaw() _, err := b.clientRaw()
return err return err

View File

@ -89,7 +89,7 @@ func (b *Backend) State(name string) (state.State, error) {
gzip := b.configData.Get("gzip").(bool) gzip := b.configData.Get("gzip").(bool)
// Build the state client // Build the state client
stateMgr := &remote.State{ var stateMgr state.State = &remote.State{
Client: &RemoteClient{ Client: &RemoteClient{
Client: client, Client: client,
Path: path, Path: path,
@ -97,19 +97,27 @@ func (b *Backend) State(name string) (state.State, error) {
}, },
} }
// If we're not locking, disable it
if !b.lock {
stateMgr = &state.LockDisabled{Inner: stateMgr}
}
// Get the locker, which we know always exists
stateMgrLocker := stateMgr.(state.Locker)
// Grab a lock, we use this to write an empty state if one doesn't // Grab a lock, we use this to write an empty state if one doesn't
// exist already. We have to write an empty state as a sentinel value // exist already. We have to write an empty state as a sentinel value
// so States() knows it exists. // so States() knows it exists.
lockInfo := state.NewLockInfo() lockInfo := state.NewLockInfo()
lockInfo.Operation = "init" lockInfo.Operation = "init"
lockId, err := stateMgr.Lock(lockInfo) lockId, err := stateMgrLocker.Lock(lockInfo)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to lock state in Consul: %s", err) return nil, fmt.Errorf("failed to lock state in Consul: %s", err)
} }
// Local helper function so we can call it multiple places // Local helper function so we can call it multiple places
lockUnlock := func(parent error) error { lockUnlock := func(parent error) error {
if err := stateMgr.Unlock(lockId); err != nil { if err := stateMgrLocker.Unlock(lockId); err != nil {
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err) return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
} }

View File

@ -38,14 +38,44 @@ func TestBackend(t *testing.T) {
srv := newConsulTestServer(t) srv := newConsulTestServer(t)
defer srv.Stop() defer srv.Stop()
// Get the backend path := fmt.Sprintf("tf-unit/%s", time.Now().String())
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
// Get the backend. We need two to test locking.
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"address": srv.HTTPAddr, "address": srv.HTTPAddr,
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()), "path": path,
})
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"address": srv.HTTPAddr,
"path": path,
}) })
// Test // Test
backend.TestBackend(t, b) backend.TestBackend(t, b1, b2)
}
func TestBackend_lockDisabled(t *testing.T) {
srv := newConsulTestServer(t)
defer srv.Stop()
path := fmt.Sprintf("tf-unit/%s", time.Now().String())
// Get the backend. We need two to test locking.
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"address": srv.HTTPAddr,
"path": path,
"lock": false,
})
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"address": srv.HTTPAddr,
"path": path + "different", // Diff so locking test would fail if it was locking
"lock": false,
})
// Test
backend.TestBackend(t, b1, b2)
} }
func TestBackend_gzip(t *testing.T) { func TestBackend_gzip(t *testing.T) {
@ -60,5 +90,5 @@ func TestBackend_gzip(t *testing.T) {
}) })
// Test // Test
backend.TestBackend(t, b) backend.TestBackend(t, b, nil)
} }

View File

@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -40,8 +41,15 @@ func TestBackendConfig(t *testing.T, b Backend, c map[string]interface{}) Backen
// assumed to already be configured. This will test state functionality. // assumed to already be configured. This will test state functionality.
// If the backend reports it doesn't support multi-state by returning the // If the backend reports it doesn't support multi-state by returning the
// error ErrNamedStatesNotSupported, then it will not test that. // error ErrNamedStatesNotSupported, then it will not test that.
func TestBackend(t *testing.T, b Backend) { //
testBackendStates(t, b) // If you want to test locking, two backends must be given. If b2 is nil,
// then state lockign won't be tested.
func TestBackend(t *testing.T, b1, b2 Backend) {
testBackendStates(t, b1)
if b2 != nil {
testBackendStateLock(t, b1, b2)
}
} }
func testBackendStates(t *testing.T, b Backend) { func testBackendStates(t *testing.T, b Backend) {
@ -138,3 +146,77 @@ func testBackendStates(t *testing.T, b Backend) {
} }
} }
} }
func testBackendStateLock(t *testing.T, b1, b2 Backend) {
// Get the default state for each
b1StateMgr, err := b1.State(DefaultStateName)
if err != nil {
t.Fatalf("error: %s", err)
}
if err := b1StateMgr.RefreshState(); err != nil {
t.Fatalf("bad: %s", err)
}
// Fast exit if this doesn't support locking at all
if _, ok := b1StateMgr.(state.Locker); !ok {
t.Logf("TestBackend: backend %T doesn't support state locking, not testing", b1)
return
}
t.Logf("TestBackend: testing state locking for %T", b1)
b2StateMgr, err := b2.State(DefaultStateName)
if err != nil {
t.Fatalf("error: %s", err)
}
if err := b2StateMgr.RefreshState(); err != nil {
t.Fatalf("bad: %s", err)
}
// Reassign so its obvious whats happening
lockerA := b1StateMgr.(state.Locker)
lockerB := b2StateMgr.(state.Locker)
infoA := state.NewLockInfo()
infoA.Operation = "test"
infoA.Who = "clientA"
infoB := state.NewLockInfo()
infoB.Operation = "test"
infoB.Who = "clientB"
lockIDA, err := lockerA.Lock(infoA)
if err != nil {
t.Fatal("unable to get initial lock:", err)
}
// If the lock ID is blank, assume locking is disabled
if lockIDA == "" {
t.Logf("TestBackend: %T: empty string returned for lock, assuming disabled", b1)
return
}
_, err = lockerB.Lock(infoB)
if err == nil {
lockerA.Unlock(lockIDA)
t.Fatal("client B obtained lock while held by client A")
}
if err := lockerA.Unlock(lockIDA); err != nil {
t.Fatal("error unlocking client A", err)
}
lockIDB, err := lockerB.Lock(infoB)
if err != nil {
t.Fatal("unable to obtain lock from client B")
}
if lockIDB == lockIDA {
t.Fatalf("duplicate lock IDs: %q", lockIDB)
}
if err = lockerB.Unlock(lockIDB); err != nil {
t.Fatal("error unlocking client B:", err)
}
}

38
state/lock.go Normal file
View File

@ -0,0 +1,38 @@
package state
import (
"github.com/hashicorp/terraform/terraform"
)
// LockDisabled implements State and Locker but disables state locking.
// If State doesn't support locking, this is a no-op. This is useful for
// easily disabling locking of an existing state or for tests.
type LockDisabled struct {
// We can't embed State directly since Go dislikes that a field is
// State and State interface has a method State
Inner State
}
func (s *LockDisabled) State() *terraform.State {
return s.Inner.State()
}
func (s *LockDisabled) WriteState(v *terraform.State) error {
return s.Inner.WriteState(v)
}
func (s *LockDisabled) RefreshState() error {
return s.Inner.RefreshState()
}
func (s *LockDisabled) PersistState() error {
return s.Inner.PersistState()
}
func (s *LockDisabled) Lock(info *LockInfo) (string, error) {
return "", nil
}
func (s *LockDisabled) Unlock(id string) error {
return nil
}

10
state/lock_test.go Normal file
View File

@ -0,0 +1,10 @@
package state
import (
"testing"
)
func TestLockDisabled_impl(t *testing.T) {
var _ State = new(LockDisabled)
var _ Locker = new(LockDisabled)
}

View File

@ -54,3 +54,4 @@ The following configuration options / environment variables are supported:
* `http_auth` / `CONSUL_HTTP_AUTH` - (Optional) HTTP Basic Authentication credentials to be used when * `http_auth` / `CONSUL_HTTP_AUTH` - (Optional) HTTP Basic Authentication credentials to be used when
communicating with Consul, in the format of either `user` or `user:pass`. communicating with Consul, in the format of either `user` or `user:pass`.
* `gzip` - (Optional) `true` to compress the state data using gzip, or `false` (the default) to leave it uncompressed. * `gzip` - (Optional) `true` to compress the state data using gzip, or `false` (the default) to leave it uncompressed.
* `lock` - (Optional) `false` to disable locking. This defaults to true, but will require session permissions with Consul to perform locking.