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:
parent
59baa13953
commit
1daff7a826
|
@ -60,6 +60,13 @@ func New() backend.Backend {
|
|||
Description: "Compress the state data using gzip",
|
||||
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 {
|
||||
*schema.Backend
|
||||
|
||||
// The fields below are set from configure
|
||||
configData *schema.ResourceData
|
||||
lock bool
|
||||
}
|
||||
|
||||
func (b *Backend) configure(ctx context.Context) error {
|
||||
// Grab the resource data
|
||||
b.configData = schema.FromContextBackendConfig(ctx)
|
||||
|
||||
// Store the lock information
|
||||
b.lock = b.configData.Get("lock").(bool)
|
||||
|
||||
// Initialize a client to test config
|
||||
_, err := b.clientRaw()
|
||||
return err
|
||||
|
|
|
@ -89,7 +89,7 @@ func (b *Backend) State(name string) (state.State, error) {
|
|||
gzip := b.configData.Get("gzip").(bool)
|
||||
|
||||
// Build the state client
|
||||
stateMgr := &remote.State{
|
||||
var stateMgr state.State = &remote.State{
|
||||
Client: &RemoteClient{
|
||||
Client: client,
|
||||
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
|
||||
// exist already. We have to write an empty state as a sentinel value
|
||||
// so States() knows it exists.
|
||||
lockInfo := state.NewLockInfo()
|
||||
lockInfo.Operation = "init"
|
||||
lockId, err := stateMgr.Lock(lockInfo)
|
||||
lockId, err := stateMgrLocker.Lock(lockInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to lock state in Consul: %s", err)
|
||||
}
|
||||
|
||||
// Local helper function so we can call it multiple places
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -38,14 +38,44 @@ func TestBackend(t *testing.T) {
|
|||
srv := newConsulTestServer(t)
|
||||
defer srv.Stop()
|
||||
|
||||
// Get the backend
|
||||
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
||||
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": fmt.Sprintf("tf-unit/%s", time.Now().String()),
|
||||
"path": path,
|
||||
})
|
||||
|
||||
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
||||
"address": srv.HTTPAddr,
|
||||
"path": path,
|
||||
})
|
||||
|
||||
// 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) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"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.
|
||||
// If the backend reports it doesn't support multi-state by returning the
|
||||
// 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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLockDisabled_impl(t *testing.T) {
|
||||
var _ State = new(LockDisabled)
|
||||
var _ Locker = new(LockDisabled)
|
||||
}
|
|
@ -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
|
||||
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.
|
||||
* `lock` - (Optional) `false` to disable locking. This defaults to true, but will require session permissions with Consul to perform locking.
|
||||
|
|
Loading…
Reference in New Issue