Merge pull request #12710 from hashicorp/b-consul-lock-disable
backend/consul: support "lock" option to disable locking
This commit is contained in:
commit
9b1ae50bd4
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
* `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.
|
||||||
|
|
Loading…
Reference in New Issue