Merge pull request #11724 from hashicorp/jbardin/state-locking

add force-unlock command
This commit is contained in:
James Bardin 2017-02-08 10:19:15 -05:00 committed by GitHub
commit 5ca5a3c78a
10 changed files with 195 additions and 8 deletions

View File

@ -111,7 +111,7 @@ func TestApply_destroyLockedState(t *testing.T) {
statePath := testStateFile(t, originalState)
unlock, err := testLockState(statePath)
unlock, err := testLockState("./testdata", statePath)
if err != nil {
t.Fatal(err)
}

View File

@ -63,7 +63,7 @@ func TestApply(t *testing.T) {
func TestApply_lockedState(t *testing.T) {
statePath := testTempFile(t)
unlock, err := testLockState(statePath)
unlock, err := testLockState("./testdata", statePath)
if err != nil {
t.Fatal(err)
}

View File

@ -535,7 +535,9 @@ func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.Remote
// 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) {
// Since many tests change the working durectory, the sourcedir argument must be
// supplied to locate the statelocker.go source.
func testLockState(sourceDir, 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 {
@ -545,8 +547,10 @@ func testLockState(path string) (func(), error) {
os.RemoveAll(buildDir)
}
source := filepath.Join(sourceDir, "statelocker.go")
lockBin := filepath.Join(buildDir, "statelocker")
out, err := exec.Command("go", "build", "-o", lockBin, "testdata/statelocker.go").CombinedOutput()
out, err := exec.Command("go", "build", "-o", lockBin, source).CombinedOutput()
if err != nil {
cleanFunc()
return nil, fmt.Errorf("%s %s", err, out)

View File

@ -46,7 +46,7 @@ func TestPlan_lockedState(t *testing.T) {
}
testPath := testFixturePath("plan")
unlock, err := testLockState(filepath.Join(testPath, DefaultStateFilename))
unlock, err := testLockState("./testdata", filepath.Join(testPath, DefaultStateFilename))
if err != nil {
t.Fatal(err)
}

View File

@ -63,7 +63,7 @@ func TestRefresh_lockedState(t *testing.T) {
state := testState()
statePath := testStateFile(t, state)
unlock, err := testLockState(statePath)
unlock, err := testLockState("./testdata", statePath)
if err != nil {
t.Fatal(err)
}

View File

@ -63,7 +63,7 @@ func TestTaint_lockedState(t *testing.T) {
}
statePath := testStateFile(t, state)
unlock, err := testLockState(statePath)
unlock, err := testLockState("./testdata", statePath)
if err != nil {
t.Fatal(err)
}

131
command/unlock.go Normal file
View File

@ -0,0 +1,131 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
)
// UnlockCommand is a cli.Command implementation that manually unlocks
// the state.
type UnlockCommand struct {
Meta
}
func (c *UnlockCommand) Run(args []string) int {
args = c.Meta.process(args, false)
force := false
cmdFlags := c.Meta.flagSet("force-unlock")
cmdFlags.BoolVar(&force, "force", false, "force")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// assume everything is initialized. The user can manually init if this is
// required.
configPath, err := ModulePath(cmdFlags.Args())
if err != nil {
c.Ui.Error(err.Error())
return 1
}
// Load the backend
b, err := c.Backend(&BackendOpts{
ConfigPath: configPath,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
st, err := b.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
s, ok := st.(state.Locker)
if !ok {
c.Ui.Error("The remote state backend in use does not support locking, and therefor\n" +
"cannot be unlocked.")
return 1
}
isLocal := false
switch s := st.(type) {
case *state.BackupState:
if _, ok := s.Real.(*state.LocalState); ok {
isLocal = true
}
case *state.LocalState:
isLocal = true
}
if !force {
// Forcing this doesn't do anything, but doesn't break anything either,
// and allows us to run the basic command test too.
if isLocal {
c.Ui.Error("Local state cannot be unlocked by another process")
return 1
}
desc := "Terraform will remove the lock on the remote state.\n" +
"This will allow local Terraform commands to modify this state, even though it\n" +
"may be still be in use. Only 'yes' will be accepted to confirm."
v, err := c.UIInput().Input(&terraform.InputOpts{
Id: "force-unlock",
Query: "Do you really want to force-unlock?",
Description: desc,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
return 1
}
if v != "yes" {
c.Ui.Output("force-unlock cancelled.")
return 1
}
}
if err := s.Unlock(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to unlock state: %s", err))
return 1
}
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputUnlockSuccess)))
return 0
}
func (c *UnlockCommand) Help() string {
helpText := `
Usage: terraform force-unlock [DIR]
Manually unlock the state for the defined configuration.
This will not modify your infrastructure. This command removes the lock on the
state for the current configuration. The behavior of this lock is dependent
on the backend being used. Local state files cannot be unlocked by another
process.
Options:
-force Don't ask for input for unlock confirmation.
`
return strings.TrimSpace(helpText)
}
func (c *UnlockCommand) Synopsis() string {
return "Manually unlock the terraform state"
}
const outputUnlockSuccess = `
[reset][bold][red]Terraform state has been successfully unlocked![reset][red]
The state has been unlocked, and Terraform commands should now be able to
obtain a new lock on the remote state.
`

46
command/unlock_test.go Normal file
View File

@ -0,0 +1,46 @@
package command
import (
"os"
"testing"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
// Since we can't unlock a local state file, just test that calling unlock
// doesn't fail.
// TODO: mock remote state for UI testing
func TestUnlock(t *testing.T) {
td := tempDir(t)
os.MkdirAll(td, 0755)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// Write the legacy state
statePath := DefaultStateFilename
{
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(testState(), f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
}
p := testProvider()
ui := new(cli.MockUi)
c := &UnlockCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
if code := c.Run([]string{"-force"}); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}

View File

@ -68,7 +68,7 @@ func TestUntaint_lockedState(t *testing.T) {
},
}
statePath := testStateFile(t, state)
unlock, err := testLockState(statePath)
unlock, err := testLockState("./testdata", statePath)
if err != nil {
t.Fatal(err)
}

View File

@ -75,6 +75,12 @@ func init() {
}, nil
},
"force-unlock": func() (cli.Command, error) {
return &command.UnlockCommand{
Meta: meta,
}, nil
},
"get": func() (cli.Command, error) {
return &command.GetCommand{
Meta: meta,