backend/consul: support "lock" option to disable locking

This adds a "lock" config (default true) to allow users to optionally
disable state locking with Consul. This is necessary if the token given
doesn't have session permission and is necessary for backwards
compatibility.
This commit is contained in:
Mitchell Hashimoto 2017-03-14 17:28:42 -07:00
parent 59baa13953
commit 1daff7a826
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
7 changed files with 190 additions and 9 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) {

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.