diff --git a/backend/backend.go b/backend/backend.go index 16b8a2ba4..86f885207 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -22,7 +22,8 @@ type Backend interface { // State returns the current state for this environment. This state may // not be loaded locally: the proper APIs should be called on state.State - // to load the state. + // to load the state. If the state.State is a state.Locker, it's up to the + // caller to call Lock and Unlock as needed. State() (state.State, error) } @@ -38,6 +39,9 @@ type Enhanced interface { // It is up to the implementation to determine what "performing" means. // This DOES NOT BLOCK. The context returned as part of RunningOperation // should be used to block for completion. + // If the state used in the operation can be locked, it is the + // responsibility of the Backend to lock the state for the duration of the + // running operation. Operation(context.Context, *Operation) (*RunningOperation, error) } @@ -99,6 +103,10 @@ type Operation struct { // Input/output/control options. UIIn terraform.UIInput UIOut terraform.UIOutput + + // If LockState is true, the Operation must Lock any + // state.Lockers for its duration, and Unlock when complete. + LockState bool } // RunningOperation is the result of starting an operation. diff --git a/backend/local/backend.go b/backend/local/backend.go index 7e6aa9c0a..e45eaec25 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -34,6 +34,9 @@ type Local struct { StateOutPath string StateBackupPath string + // we only want to create a single instance of the local state + state state.State + // ContextOpts are the base context options to set when initializing a // Terraform context. Many of these will be overridden or merged by // Operation. See Operation for more details. @@ -100,6 +103,10 @@ func (b *Local) State() (state.State, error) { return b.Backend.State() } + if b.state != nil { + return b.state, nil + } + // Otherwise, we need to load the state. var s state.State = &state.LocalState{ Path: b.StatePath, @@ -119,6 +126,7 @@ func (b *Local) State() (state.State, error) { } } + b.state = s return s, nil } diff --git a/backend/local/backend_apply.go b/backend/local/backend_apply.go index 4ba1d27be..381edfb56 100644 --- a/backend/local/backend_apply.go +++ b/backend/local/backend_apply.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" ) @@ -28,12 +29,22 @@ func (b *Local) opApply( b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook) // Get our context - tfCtx, state, err := b.context(op) + tfCtx, opState, err := b.context(op) if err != nil { runningOp.Err = err return } + // context acquired the state, and therefor the lock. + // Unlock it when the operation is complete + defer func() { + if s, ok := opState.(state.Locker); op.LockState && ok { + if err := s.Unlock(); err != nil { + log.Printf("[ERROR]: %s", err) + } + } + }() + // Setup the state runningOp.State = tfCtx.State() @@ -58,7 +69,7 @@ func (b *Local) opApply( } // Setup our hook for continuous state updates - stateHook.State = state + stateHook.State = opState // Start the apply in a goroutine so that we can be interrupted. var applyState *terraform.State @@ -98,11 +109,11 @@ func (b *Local) opApply( runningOp.State = applyState // Persist the state - if err := state.WriteState(applyState); err != nil { + if err := opState.WriteState(applyState); err != nil { runningOp.Err = fmt.Errorf("Failed to save state: %s", err) return } - if err := state.PersistState(); err != nil { + if err := opState.PersistState(); err != nil { runningOp.Err = fmt.Errorf("Failed to save state: %s", err) return } diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index 9f5d418d4..f816aeae8 100644 --- a/backend/local/backend_local.go +++ b/backend/local/backend_local.go @@ -23,6 +23,13 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, if err != nil { return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err) } + + if s, ok := s.(state.Locker); op.LockState && ok { + if err := s.Lock(op.Type.String()); err != nil { + return nil, nil, errwrap.Wrapf("Error locking state: {{err}}", err) + } + } + if err := s.RefreshState(); err != nil { return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err) } diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index 3598cceca..2c117aae2 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/format" "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" ) @@ -51,12 +52,22 @@ func (b *Local) opPlan( b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook) // Get our context - tfCtx, _, err := b.context(op) + tfCtx, opState, err := b.context(op) if err != nil { runningOp.Err = err return } + // context acquired the state, and therefor the lock. + // Unlock it when the operation is complete + defer func() { + if s, ok := opState.(state.Locker); op.LockState && ok { + if err := s.Unlock(); err != nil { + log.Printf("[ERROR]: %s", err) + } + } + }() + // Setup the state runningOp.State = tfCtx.State() diff --git a/backend/local/backend_refresh.go b/backend/local/backend_refresh.go index a79ffd6af..d3f41cc21 100644 --- a/backend/local/backend_refresh.go +++ b/backend/local/backend_refresh.go @@ -3,10 +3,12 @@ package local import ( "context" "fmt" + "log" "os" "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state" ) func (b *Local) opRefresh( @@ -40,14 +42,24 @@ func (b *Local) opRefresh( } // Get our context - tfCtx, state, err := b.context(op) + tfCtx, opState, err := b.context(op) if err != nil { runningOp.Err = err return } + // context acquired the state, and therefor the lock. + // Unlock it when the operation is complete + defer func() { + if s, ok := opState.(state.Locker); op.LockState && ok { + if err := s.Unlock(); err != nil { + log.Printf("[ERROR]: %s", err) + } + } + }() + // Set our state - runningOp.State = state.State() + runningOp.State = opState.State() // Perform operation and write the resulting state to the running op newState, err := tfCtx.Refresh() @@ -58,11 +70,11 @@ func (b *Local) opRefresh( } // Write and persist the state - if err := state.WriteState(newState); err != nil { + if err := opState.WriteState(newState); err != nil { runningOp.Err = errwrap.Wrapf("Error writing state: {{err}}", err) return } - if err := state.PersistState(); err != nil { + if err := opState.PersistState(); err != nil { runningOp.Err = errwrap.Wrapf("Error saving state: {{err}}", err) return } diff --git a/command/apply.go b/command/apply.go index 0dc51dfa4..05a7e5988 100644 --- a/command/apply.go +++ b/command/apply.go @@ -47,6 +47,7 @@ func (c *ApplyCommand) Run(args []string) int { cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") + cmdFlags.BoolVar(&c.Meta.lockState, "state-lock", true, "lock state") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 @@ -182,6 +183,7 @@ func (c *ApplyCommand) Run(args []string) int { opReq.Plan = plan opReq.PlanRefresh = refresh opReq.Type = backend.OperationTypeApply + opReq.LockState = c.Meta.lockState // Perform the operation ctx, ctxCancel := context.WithCancel(context.Background()) diff --git a/command/meta.go b/command/meta.go index 64361eeab..75e66355d 100644 --- a/command/meta.go +++ b/command/meta.go @@ -83,12 +83,15 @@ type Meta struct { // shadow is used to enable/disable the shadow graph // // provider is to specify specific resource providers + // + // lockState is set to false to disable state locking statePath string stateOutPath string backupPath string parallelism int shadow bool provider string + lockState bool } // initStatePaths is used to initialize the default values for