Merge pull request #11686 from hashicorp/jbardin/state-locking
Enable state locking for plan/apply/destroy/refresh/taint/untaint
This commit is contained in:
commit
9fbc5b1ff6
|
@ -22,7 +22,8 @@ type Backend interface {
|
||||||
|
|
||||||
// State returns the current state for this environment. This state may
|
// State returns the current state for this environment. This state may
|
||||||
// not be loaded locally: the proper APIs should be called on state.State
|
// 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)
|
State() (state.State, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +39,9 @@ type Enhanced interface {
|
||||||
// It is up to the implementation to determine what "performing" means.
|
// It is up to the implementation to determine what "performing" means.
|
||||||
// This DOES NOT BLOCK. The context returned as part of RunningOperation
|
// This DOES NOT BLOCK. The context returned as part of RunningOperation
|
||||||
// should be used to block for completion.
|
// 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)
|
Operation(context.Context, *Operation) (*RunningOperation, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,6 +103,10 @@ type Operation struct {
|
||||||
// Input/output/control options.
|
// Input/output/control options.
|
||||||
UIIn terraform.UIInput
|
UIIn terraform.UIInput
|
||||||
UIOut terraform.UIOutput
|
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.
|
// RunningOperation is the result of starting an operation.
|
||||||
|
|
|
@ -34,6 +34,9 @@ type Local struct {
|
||||||
StateOutPath string
|
StateOutPath string
|
||||||
StateBackupPath 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
|
// ContextOpts are the base context options to set when initializing a
|
||||||
// Terraform context. Many of these will be overridden or merged by
|
// Terraform context. Many of these will be overridden or merged by
|
||||||
// Operation. See Operation for more details.
|
// Operation. See Operation for more details.
|
||||||
|
@ -100,6 +103,10 @@ func (b *Local) State() (state.State, error) {
|
||||||
return b.Backend.State()
|
return b.Backend.State()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.state != nil {
|
||||||
|
return b.state, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, we need to load the state.
|
// Otherwise, we need to load the state.
|
||||||
var s state.State = &state.LocalState{
|
var s state.State = &state.LocalState{
|
||||||
Path: b.StatePath,
|
Path: b.StatePath,
|
||||||
|
@ -119,6 +126,7 @@ func (b *Local) State() (state.State, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.state = s
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/hashicorp/errwrap"
|
"github.com/hashicorp/errwrap"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,12 +29,29 @@ func (b *Local) opApply(
|
||||||
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook)
|
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook)
|
||||||
|
|
||||||
// Get our context
|
// Get our context
|
||||||
tfCtx, state, err := b.context(op)
|
tfCtx, opState, err := b.context(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
runningOp.Err = err
|
runningOp.Err = err
|
||||||
return
|
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 {
|
||||||
|
runningOp.Err = multierror.Append(runningOp.Err,
|
||||||
|
errwrap.Wrapf("Error unlocking state:\n\n"+
|
||||||
|
"{{err}}\n\n"+
|
||||||
|
"The Terraform operation completed but there was an error unlocking the state.\n"+
|
||||||
|
"This may require unlocking the state manually with the `terraform unlock` command\n",
|
||||||
|
err,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Setup the state
|
// Setup the state
|
||||||
runningOp.State = tfCtx.State()
|
runningOp.State = tfCtx.State()
|
||||||
|
|
||||||
|
@ -58,7 +76,7 @@ func (b *Local) opApply(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup our hook for continuous state updates
|
// 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.
|
// Start the apply in a goroutine so that we can be interrupted.
|
||||||
var applyState *terraform.State
|
var applyState *terraform.State
|
||||||
|
@ -98,11 +116,11 @@ func (b *Local) opApply(
|
||||||
runningOp.State = applyState
|
runningOp.State = applyState
|
||||||
|
|
||||||
// Persist the state
|
// 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)
|
runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := state.PersistState(); err != nil {
|
if err := opState.PersistState(); err != nil {
|
||||||
runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
|
runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,13 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
|
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 {
|
if err := s.RefreshState(); err != nil {
|
||||||
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
|
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/command/format"
|
"github.com/hashicorp/terraform/command/format"
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,12 +52,22 @@ func (b *Local) opPlan(
|
||||||
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook)
|
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook)
|
||||||
|
|
||||||
// Get our context
|
// Get our context
|
||||||
tfCtx, _, err := b.context(op)
|
tfCtx, opState, err := b.context(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
runningOp.Err = err
|
runningOp.Err = err
|
||||||
return
|
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
|
// Setup the state
|
||||||
runningOp.State = tfCtx.State()
|
runningOp.State = tfCtx.State()
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,12 @@ package local
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/hashicorp/errwrap"
|
"github.com/hashicorp/errwrap"
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *Local) opRefresh(
|
func (b *Local) opRefresh(
|
||||||
|
@ -40,14 +42,24 @@ func (b *Local) opRefresh(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get our context
|
// Get our context
|
||||||
tfCtx, state, err := b.context(op)
|
tfCtx, opState, err := b.context(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
runningOp.Err = err
|
runningOp.Err = err
|
||||||
return
|
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
|
// Set our state
|
||||||
runningOp.State = state.State()
|
runningOp.State = opState.State()
|
||||||
|
|
||||||
// Perform operation and write the resulting state to the running op
|
// Perform operation and write the resulting state to the running op
|
||||||
newState, err := tfCtx.Refresh()
|
newState, err := tfCtx.Refresh()
|
||||||
|
@ -58,11 +70,11 @@ func (b *Local) opRefresh(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write and persist the state
|
// 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)
|
runningOp.Err = errwrap.Wrapf("Error writing state: {{err}}", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := state.PersistState(); err != nil {
|
if err := opState.PersistState(); err != nil {
|
||||||
runningOp.Err = errwrap.Wrapf("Error saving state: {{err}}", err)
|
runningOp.Err = errwrap.Wrapf("Error saving state: {{err}}", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
|
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
|
||||||
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
||||||
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
||||||
|
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
@ -182,6 +183,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
opReq.Plan = plan
|
opReq.Plan = plan
|
||||||
opReq.PlanRefresh = refresh
|
opReq.PlanRefresh = refresh
|
||||||
opReq.Type = backend.OperationTypeApply
|
opReq.Type = backend.OperationTypeApply
|
||||||
|
opReq.LockState = c.Meta.stateLock
|
||||||
|
|
||||||
// Perform the operation
|
// Perform the operation
|
||||||
ctx, ctxCancel := context.WithCancel(context.Background())
|
ctx, ctxCancel := context.WithCancel(context.Background())
|
||||||
|
@ -272,6 +274,8 @@ Options:
|
||||||
modifying. Defaults to the "-state-out" path with
|
modifying. Defaults to the "-state-out" path with
|
||||||
".backup" extension. Set to "-" to disable backup.
|
".backup" extension. Set to "-" to disable backup.
|
||||||
|
|
||||||
|
-lock=true Lock the state file when locking is supported.
|
||||||
|
|
||||||
-input=true Ask for input for variables if not directly set.
|
-input=true Ask for input for variables if not directly set.
|
||||||
|
|
||||||
-no-color If specified, output won't contain any color.
|
-no-color If specified, output won't contain any color.
|
||||||
|
@ -319,6 +323,8 @@ Options:
|
||||||
|
|
||||||
-force Don't ask for input for destroy confirmation.
|
-force Don't ask for input for destroy confirmation.
|
||||||
|
|
||||||
|
-lock=true Lock the state file when locking is supported.
|
||||||
|
|
||||||
-no-color If specified, output won't contain any color.
|
-no-color If specified, output won't contain any color.
|
||||||
|
|
||||||
-parallelism=n Limit the number of concurrent operations.
|
-parallelism=n Limit the number of concurrent operations.
|
||||||
|
|
|
@ -92,6 +92,58 @@ func TestApply_destroy(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApply_destroyLockedState(t *testing.T) {
|
||||||
|
originalState := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
statePath := testStateFile(t, originalState)
|
||||||
|
|
||||||
|
unlock, err := testLockState(statePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &ApplyCommand{
|
||||||
|
Destroy: true,
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the apply command pointing to our existing state
|
||||||
|
args := []string{
|
||||||
|
"-force",
|
||||||
|
"-state", statePath,
|
||||||
|
testFixturePath("apply"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if code := c.Run(args); code == 0 {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if !strings.Contains(output, "locked") {
|
||||||
|
t.Fatal("command output does not look like a lock error:", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestApply_destroyPlan(t *testing.T) {
|
func TestApply_destroyPlan(t *testing.T) {
|
||||||
planPath := testPlanFile(t, &terraform.Plan{
|
planPath := testPlanFile(t, &terraform.Plan{
|
||||||
Module: testModule(t, "apply"),
|
Module: testModule(t, "apply"),
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -58,6 +59,39 @@ func TestApply(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test apply with locked state
|
||||||
|
func TestApply_lockedState(t *testing.T) {
|
||||||
|
statePath := testTempFile(t)
|
||||||
|
|
||||||
|
unlock, err := testLockState(statePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &ApplyCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state", statePath,
|
||||||
|
testFixturePath("apply"),
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code == 0 {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if !strings.Contains(output, "locked") {
|
||||||
|
t.Fatal("command output does not look like a lock error:", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// high water mark counter
|
// high water mark counter
|
||||||
type hwm struct {
|
type hwm struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
@ -550,7 +584,8 @@ func TestApply_plan(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApply_plan_backup(t *testing.T) {
|
func TestApply_plan_backup(t *testing.T) {
|
||||||
planPath := testPlanFile(t, testPlan(t))
|
plan := testPlan(t)
|
||||||
|
planPath := testPlanFile(t, plan)
|
||||||
statePath := testTempFile(t)
|
statePath := testTempFile(t)
|
||||||
backupPath := testTempFile(t)
|
backupPath := testTempFile(t)
|
||||||
|
|
||||||
|
@ -563,6 +598,12 @@ func TestApply_plan_backup(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a state file that needs to be backed up
|
||||||
|
err := (&state.LocalState{Path: statePath}).WriteState(plan.State)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-state-out", statePath,
|
"-state-out", statePath,
|
||||||
"-backup", backupPath,
|
"-backup", backupPath,
|
||||||
|
|
|
@ -6,14 +6,17 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/go-getter"
|
"github.com/hashicorp/go-getter"
|
||||||
|
@ -529,3 +532,55 @@ func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.Remote
|
||||||
|
|
||||||
return remote, srv
|
return remote, srv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testlockState calls a separate process to the lock the state file at path.
|
||||||
|
// deferFunc should be called in the caller to properly unlock the file.
|
||||||
|
func testLockState(path string) (func(), error) {
|
||||||
|
// build and run the binary ourselves so we can quickly terminate it for cleanup
|
||||||
|
buildDir, err := ioutil.TempDir("", "locker")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cleanFunc := func() {
|
||||||
|
os.RemoveAll(buildDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
lockBin := filepath.Join(buildDir, "statelocker")
|
||||||
|
out, err := exec.Command("go", "build", "-o", lockBin, "testdata/statelocker.go").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
cleanFunc()
|
||||||
|
return nil, fmt.Errorf("%s %s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
locker := exec.Command(lockBin, path)
|
||||||
|
pr, pw, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
cleanFunc()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer pr.Close()
|
||||||
|
defer pw.Close()
|
||||||
|
locker.Stderr = pw
|
||||||
|
locker.Stdout = pw
|
||||||
|
|
||||||
|
if err := locker.Start(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
deferFunc := func() {
|
||||||
|
cleanFunc()
|
||||||
|
locker.Process.Signal(syscall.SIGTERM)
|
||||||
|
locker.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for the process to lock
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := pr.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return deferFunc, fmt.Errorf("read from statelocker returned: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(buf[:n]) != "LOCKED" {
|
||||||
|
return deferFunc, fmt.Errorf("statelocker wrote: %s", string(buf[:n]))
|
||||||
|
}
|
||||||
|
return deferFunc, nil
|
||||||
|
}
|
||||||
|
|
|
@ -83,12 +83,15 @@ type Meta struct {
|
||||||
// shadow is used to enable/disable the shadow graph
|
// shadow is used to enable/disable the shadow graph
|
||||||
//
|
//
|
||||||
// provider is to specify specific resource providers
|
// provider is to specify specific resource providers
|
||||||
|
//
|
||||||
|
// lockState is set to false to disable state locking
|
||||||
statePath string
|
statePath string
|
||||||
stateOutPath string
|
stateOutPath string
|
||||||
backupPath string
|
backupPath string
|
||||||
parallelism int
|
parallelism int
|
||||||
shadow bool
|
shadow bool
|
||||||
provider string
|
provider string
|
||||||
|
stateLock bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// initStatePaths is used to initialize the default values for
|
// initStatePaths is used to initialize the default values for
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/copy"
|
"github.com/hashicorp/terraform/helper/copy"
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -2208,6 +2209,12 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) {
|
||||||
// Create an alternate output path
|
// Create an alternate output path
|
||||||
statePath := "foo.tfstate"
|
statePath := "foo.tfstate"
|
||||||
|
|
||||||
|
// put a initial state there that needs to be backed up
|
||||||
|
err := (&state.LocalState{Path: statePath}).WriteState(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Setup the meta
|
// Setup the meta
|
||||||
m := testMetaBackend(t, nil)
|
m := testMetaBackend(t, nil)
|
||||||
m.stateOutPath = statePath
|
m.stateOutPath = statePath
|
||||||
|
|
|
@ -31,6 +31,7 @@ func (c *PlanCommand) Run(args []string) int {
|
||||||
&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
|
&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
|
||||||
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
|
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
|
||||||
cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode")
|
cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode")
|
||||||
|
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
@ -84,6 +85,7 @@ func (c *PlanCommand) Run(args []string) int {
|
||||||
opReq.PlanRefresh = refresh
|
opReq.PlanRefresh = refresh
|
||||||
opReq.PlanOutPath = outPath
|
opReq.PlanOutPath = outPath
|
||||||
opReq.Type = backend.OperationTypePlan
|
opReq.Type = backend.OperationTypePlan
|
||||||
|
opReq.LockState = c.Meta.stateLock
|
||||||
|
|
||||||
// Perform the operation
|
// Perform the operation
|
||||||
op, err := b.Operation(context.Background(), opReq)
|
op, err := b.Operation(context.Background(), opReq)
|
||||||
|
@ -141,6 +143,8 @@ Options:
|
||||||
|
|
||||||
-input=true Ask for input for variables if not directly set.
|
-input=true Ask for input for variables if not directly set.
|
||||||
|
|
||||||
|
-lock=true Lock the state file when locking is supported.
|
||||||
|
|
||||||
-module-depth=n Specifies the depth of modules to show in the output.
|
-module-depth=n Specifies the depth of modules to show in the output.
|
||||||
This does not affect the plan itself, only the output
|
This does not affect the plan itself, only the output
|
||||||
shown. By default, this is -1, which will expand all.
|
shown. By default, this is -1, which will expand all.
|
||||||
|
|
|
@ -39,6 +39,44 @@ func TestPlan(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlan_lockedState(t *testing.T) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testPath := testFixturePath("plan")
|
||||||
|
unlock, err := testLockState(filepath.Join(testPath, DefaultStateFilename))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
if err := os.Chdir(testPath); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Chdir(cwd)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &PlanCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
if code := c.Run(args); code == 0 {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if !strings.Contains(output, "locked") {
|
||||||
|
t.Fatal("command output does not look like a lock error:", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPlan_plan(t *testing.T) {
|
func TestPlan_plan(t *testing.T) {
|
||||||
tmp, cwd := testCwd(t)
|
tmp, cwd := testCwd(t)
|
||||||
defer testFixCwd(t, tmp, cwd)
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
|
@ -23,6 +23,7 @@ func (c *RefreshCommand) Run(args []string) int {
|
||||||
cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", 0, "parallelism")
|
cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", 0, "parallelism")
|
||||||
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
||||||
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
||||||
|
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
@ -52,6 +53,7 @@ func (c *RefreshCommand) Run(args []string) int {
|
||||||
opReq := c.Operation()
|
opReq := c.Operation()
|
||||||
opReq.Type = backend.OperationTypeRefresh
|
opReq.Type = backend.OperationTypeRefresh
|
||||||
opReq.Module = mod
|
opReq.Module = mod
|
||||||
|
opReq.LockState = c.Meta.stateLock
|
||||||
|
|
||||||
// Perform the operation
|
// Perform the operation
|
||||||
op, err := b.Operation(context.Background(), opReq)
|
op, err := b.Operation(context.Background(), opReq)
|
||||||
|
@ -94,6 +96,8 @@ Options:
|
||||||
|
|
||||||
-input=true Ask for input for variables if not directly set.
|
-input=true Ask for input for variables if not directly set.
|
||||||
|
|
||||||
|
-lock=true Lock the state file when locking is supported.
|
||||||
|
|
||||||
-no-color If specified, output won't contain any color.
|
-no-color If specified, output won't contain any color.
|
||||||
|
|
||||||
-state=path Path to read and save state (unless state-out
|
-state=path Path to read and save state (unless state-out
|
||||||
|
|
|
@ -59,6 +59,43 @@ func TestRefresh(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRefresh_lockedState(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
statePath := testStateFile(t, state)
|
||||||
|
|
||||||
|
unlock, err := testLockState(statePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &RefreshCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
p.RefreshFn = nil
|
||||||
|
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state", statePath,
|
||||||
|
testFixturePath("refresh"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if code := c.Run(args); code == 0 {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if !strings.Contains(output, "locked") {
|
||||||
|
t.Fatal("command output does not look like a lock error:", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRefresh_badState(t *testing.T) {
|
func TestRefresh_badState(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ func (c *TaintCommand) Run(args []string) int {
|
||||||
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
||||||
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
||||||
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
||||||
|
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
@ -64,14 +66,23 @@ func (c *TaintCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the state
|
// Get the state
|
||||||
state, err := b.State()
|
st, err := b.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s, ok := st.(state.Locker); c.Meta.stateLock && ok {
|
||||||
|
if err := s.Lock("taint"); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to lock state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
defer s.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Get the actual state structure
|
// Get the actual state structure
|
||||||
s := state.State()
|
s := st.State()
|
||||||
if s.Empty() {
|
if s.Empty() {
|
||||||
if allowMissing {
|
if allowMissing {
|
||||||
return c.allowMissingExit(name, module)
|
return c.allowMissingExit(name, module)
|
||||||
|
@ -129,11 +140,11 @@ func (c *TaintCommand) Run(args []string) int {
|
||||||
rs.Taint()
|
rs.Taint()
|
||||||
|
|
||||||
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
||||||
if err := state.WriteState(s); err != nil {
|
if err := st.WriteState(s); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if err := state.PersistState(); err != nil {
|
if err := st.PersistState(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
@ -166,6 +177,8 @@ Options:
|
||||||
modifying. Defaults to the "-state-out" path with
|
modifying. Defaults to the "-state-out" path with
|
||||||
".backup" extension. Set to "-" to disable backup.
|
".backup" extension. Set to "-" to disable backup.
|
||||||
|
|
||||||
|
-lock=true Lock the state file when locking is supported.
|
||||||
|
|
||||||
-module=path The module path where the resource lives. By
|
-module=path The module path where the resource lives. By
|
||||||
default this will be root. Child modules can be specified
|
default this will be root. Child modules can be specified
|
||||||
by names. Ex. "consul" or "consul.vpc" (nested modules).
|
by names. Ex. "consul" or "consul.vpc" (nested modules).
|
||||||
|
|
|
@ -2,6 +2,7 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
@ -44,6 +45,50 @@ func TestTaint(t *testing.T) {
|
||||||
testStateOutput(t, statePath, testTaintStr)
|
testStateOutput(t, statePath, testTaintStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTaint_lockedState(t *testing.T) {
|
||||||
|
state := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
statePath := testStateFile(t, state)
|
||||||
|
|
||||||
|
unlock, err := testLockState(statePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &TaintCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state", statePath,
|
||||||
|
"test_instance.foo",
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code == 0 {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if !strings.Contains(output, "locked") {
|
||||||
|
t.Fatal("command output does not look like a lock error:", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTaint_backup(t *testing.T) {
|
func TestTaint_backup(t *testing.T) {
|
||||||
// Get a temp cwd
|
// Get a temp cwd
|
||||||
tmp, cwd := testCwd(t)
|
tmp, cwd := testCwd(t)
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
// statelocker use used for testing command with a locked state.
|
||||||
|
// This will lock the state file at a given path, then wait for a sigal. On
|
||||||
|
// SIGINT and SIGTERM the state will be Unlocked before exit.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
log.Fatal(os.Args[0], "statefile")
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &state.LocalState{
|
||||||
|
Path: os.Args[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Lock("command test")
|
||||||
|
if err != nil {
|
||||||
|
io.WriteString(os.Stderr, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// signal to the caller that we're locked
|
||||||
|
io.WriteString(os.Stdout, "LOCKED")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := s.Unlock(); err != nil {
|
||||||
|
io.WriteString(os.Stderr, err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
|
||||||
|
// timeout after 10 second in case we don't get cleaned up by the test
|
||||||
|
select {
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
case <-c:
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UntaintCommand is a cli.Command implementation that manually untaints
|
// UntaintCommand is a cli.Command implementation that manually untaints
|
||||||
|
@ -23,6 +25,7 @@ func (c *UntaintCommand) Run(args []string) int {
|
||||||
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
||||||
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
||||||
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
||||||
|
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
@ -51,14 +54,23 @@ func (c *UntaintCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the state
|
// Get the state
|
||||||
state, err := b.State()
|
st, err := b.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s, ok := st.(state.Locker); c.Meta.stateLock && ok {
|
||||||
|
if err := s.Lock("untaint"); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to lock state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
defer s.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Get the actual state structure
|
// Get the actual state structure
|
||||||
s := state.State()
|
s := st.State()
|
||||||
if s.Empty() {
|
if s.Empty() {
|
||||||
if allowMissing {
|
if allowMissing {
|
||||||
return c.allowMissingExit(name, module)
|
return c.allowMissingExit(name, module)
|
||||||
|
@ -116,11 +128,11 @@ func (c *UntaintCommand) Run(args []string) int {
|
||||||
rs.Untaint()
|
rs.Untaint()
|
||||||
|
|
||||||
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
||||||
if err := state.WriteState(s); err != nil {
|
if err := st.WriteState(s); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if err := state.PersistState(); err != nil {
|
if err := st.PersistState(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
@ -153,6 +165,8 @@ Options:
|
||||||
modifying. Defaults to the "-state-out" path with
|
modifying. Defaults to the "-state-out" path with
|
||||||
".backup" extension. Set to "-" to disable backup.
|
".backup" extension. Set to "-" to disable backup.
|
||||||
|
|
||||||
|
-lock=true Lock the state file when locking is supported.
|
||||||
|
|
||||||
-module=path The module path where the resource lives. By
|
-module=path The module path where the resource lives. By
|
||||||
default this will be root. Child modules can be specified
|
default this will be root. Child modules can be specified
|
||||||
by names. Ex. "consul" or "consul.vpc" (nested modules).
|
by names. Ex. "consul" or "consul.vpc" (nested modules).
|
||||||
|
|
|
@ -50,6 +50,51 @@ test_instance.foo:
|
||||||
testStateOutput(t, statePath, expected)
|
testStateOutput(t, statePath, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUntaint_lockedState(t *testing.T) {
|
||||||
|
state := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
Tainted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
statePath := testStateFile(t, state)
|
||||||
|
unlock, err := testLockState(statePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &UntaintCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state", statePath,
|
||||||
|
"test_instance.foo",
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code == 0 {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if !strings.Contains(output, "locked") {
|
||||||
|
t.Fatal("command output does not look like a lock error:", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUntaint_backup(t *testing.T) {
|
func TestUntaint_backup(t *testing.T) {
|
||||||
// Get a temp cwd
|
// Get a temp cwd
|
||||||
tmp, cwd := testCwd(t)
|
tmp, cwd := testCwd(t)
|
||||||
|
|
|
@ -19,16 +19,14 @@ type lockInfo struct {
|
||||||
Path string
|
Path string
|
||||||
// The time the lock was taken
|
// The time the lock was taken
|
||||||
Created time.Time
|
Created time.Time
|
||||||
// The time this lock expires
|
// Extra info passed to State.Lock
|
||||||
Expires time.Time
|
|
||||||
// The lock reason passed to State.Lock
|
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the lock info formatted in an error
|
// return the lock info formatted in an error
|
||||||
func (l *lockInfo) Err() error {
|
func (l *lockInfo) Err() error {
|
||||||
return fmt.Errorf("state file %q locked. created:%s, expires:%s, reason:%s",
|
return fmt.Errorf("state file %q locked. created:%s, reason:%s",
|
||||||
l.Path, l.Created, l.Expires, l.Reason)
|
l.Path, l.Created, l.Reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalState manages a state storage that is local to the filesystem.
|
// LocalState manages a state storage that is local to the filesystem.
|
||||||
|
@ -41,6 +39,10 @@ type LocalState struct {
|
||||||
|
|
||||||
// the file handle corresponding to PathOut
|
// the file handle corresponding to PathOut
|
||||||
stateFileOut *os.File
|
stateFileOut *os.File
|
||||||
|
// created is set to tru if stateFileOut didn't exist before we created it.
|
||||||
|
// This is mostly so we can clean up emtpy files during tests, but doesn't
|
||||||
|
// hurt to remove file we never wrote to.
|
||||||
|
created bool
|
||||||
|
|
||||||
state *terraform.State
|
state *terraform.State
|
||||||
readState *terraform.State
|
readState *terraform.State
|
||||||
|
@ -77,8 +79,26 @@ func (s *LocalState) Lock(reason string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LocalState) Unlock() error {
|
func (s *LocalState) Unlock() error {
|
||||||
|
// we can't be locked if we don't have a file
|
||||||
|
if s.stateFileOut == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
os.Remove(s.lockInfoPath())
|
os.Remove(s.lockInfoPath())
|
||||||
return s.unlock()
|
|
||||||
|
fileName := s.stateFileOut.Name()
|
||||||
|
|
||||||
|
unlockErr := s.unlock()
|
||||||
|
s.stateFileOut.Close()
|
||||||
|
s.stateFileOut = nil
|
||||||
|
|
||||||
|
// clean up the state file if we created it an never wrote to it
|
||||||
|
stat, err := os.Stat(fileName)
|
||||||
|
if err == nil && stat.Size() == 0 && s.created {
|
||||||
|
os.Remove(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlockErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the state file, creating the directories and file as needed.
|
// Open the state file, creating the directories and file as needed.
|
||||||
|
@ -87,28 +107,25 @@ func (s *LocalState) createStateFiles() error {
|
||||||
s.PathOut = s.Path
|
s.PathOut = s.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := createFileAndDirs(s.PathOut)
|
// yes this could race, but we only use it to clean up empty files
|
||||||
|
if _, err := os.Stat(s.PathOut); os.IsNotExist(err) {
|
||||||
|
s.created = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create all the directories
|
||||||
|
if err := os.MkdirAll(filepath.Dir(s.PathOut), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(s.PathOut, os.O_RDWR|os.O_CREATE, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.stateFileOut = f
|
s.stateFileOut = f
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFileAndDirs(path string) (*os.File, error) {
|
|
||||||
// Create all the directories
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteState for LocalState always persists the state as well.
|
// WriteState for LocalState always persists the state as well.
|
||||||
// TODO: this should use a more robust method of writing state, by first
|
// TODO: this should use a more robust method of writing state, by first
|
||||||
// writing to a temp file on the same filesystem, and renaming the file over
|
// writing to a temp file on the same filesystem, and renaming the file over
|
||||||
|
@ -229,7 +246,6 @@ func (s *LocalState) writeLockInfo(reason string) error {
|
||||||
lockInfo := &lockInfo{
|
lockInfo := &lockInfo{
|
||||||
Path: s.Path,
|
Path: s.Path,
|
||||||
Created: time.Now().UTC(),
|
Created: time.Now().UTC(),
|
||||||
Expires: time.Now().Add(time.Hour).UTC(),
|
|
||||||
Reason: reason,
|
Reason: reason,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -207,7 +207,6 @@ func (c *S3Client) Lock(reason string) error {
|
||||||
Item: map[string]*dynamodb.AttributeValue{
|
Item: map[string]*dynamodb.AttributeValue{
|
||||||
"LockID": {S: aws.String(stateName)},
|
"LockID": {S: aws.String(stateName)},
|
||||||
"Created": {S: aws.String(time.Now().UTC().Format(time.RFC3339))},
|
"Created": {S: aws.String(time.Now().UTC().Format(time.RFC3339))},
|
||||||
"Expires": {S: aws.String(time.Now().Add(time.Hour).UTC().Format(time.RFC3339))},
|
|
||||||
"Info": {S: aws.String(reason)},
|
"Info": {S: aws.String(reason)},
|
||||||
},
|
},
|
||||||
TableName: aws.String(c.lockTable),
|
TableName: aws.String(c.lockTable),
|
||||||
|
@ -220,7 +219,7 @@ func (c *S3Client) Lock(reason string) error {
|
||||||
Key: map[string]*dynamodb.AttributeValue{
|
Key: map[string]*dynamodb.AttributeValue{
|
||||||
"LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.keyName))},
|
"LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.keyName))},
|
||||||
},
|
},
|
||||||
ProjectionExpression: aws.String("LockID, Created, Expires, Info"),
|
ProjectionExpression: aws.String("LockID, Created, Info"),
|
||||||
TableName: aws.String(c.lockTable),
|
TableName: aws.String(c.lockTable),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,19 +228,16 @@ func (c *S3Client) Lock(reason string) error {
|
||||||
return fmt.Errorf("s3 state file %q locked, cfailed to retrive info: %s", stateName, err)
|
return fmt.Errorf("s3 state file %q locked, cfailed to retrive info: %s", stateName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var created, expires, info string
|
var created, info string
|
||||||
if v, ok := resp.Item["Created"]; ok && v.S != nil {
|
if v, ok := resp.Item["Created"]; ok && v.S != nil {
|
||||||
created = *v.S
|
created = *v.S
|
||||||
}
|
}
|
||||||
if v, ok := resp.Item["Expires"]; ok && v.S != nil {
|
|
||||||
expires = *v.S
|
|
||||||
}
|
|
||||||
if v, ok := resp.Item["Info"]; ok && v.S != nil {
|
if v, ok := resp.Item["Info"]; ok && v.S != nil {
|
||||||
info = *v.S
|
info = *v.S
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("state file %q locked. created:%s, expires:%s, reason:%s",
|
return fmt.Errorf("state file %q locked. created:%s, reason:%s",
|
||||||
stateName, created, expires, info)
|
stateName, created, info)
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -42,7 +42,9 @@ type StatePersister interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Locker is implemented to lock state during command execution.
|
// Locker is implemented to lock state during command execution.
|
||||||
|
// The optional info parameter can be recorded with the lock, but the
|
||||||
|
// implementation should not depend in its value.
|
||||||
type Locker interface {
|
type Locker interface {
|
||||||
Lock(reason string) error
|
Lock(info string) error
|
||||||
Unlock() error
|
Unlock() error
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue