diff --git a/command/lock.go b/command/lock.go new file mode 100644 index 0000000000..e6b2d211bd --- /dev/null +++ b/command/lock.go @@ -0,0 +1,78 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/state" +) + +// LockCommand is a cli.Command implementation that manually locks +// the state. +type LockCommand struct { + Meta +} + +func (c *LockCommand) Run(args []string) int { + args = c.Meta.process(args, false) + + cmdFlags := c.Meta.flagSet("lock") + 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("Current state does not support locking") + return 1 + } + + if err := s.Lock("lock"); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to lock state: %s", err)) + return 1 + } + + return 0 +} + +func (c *LockCommand) Help() string { + helpText := ` +Usage: terraform lock [DIR] + + Manually lock the state for the defined configuration. + + This will not modify your infrastructure. This command obtains a lock on the + state for the current configuration. The behavior of this lock is dependent + on the backend being used. A lock on a local state file only lasts for the + duration of the calling process. +` + return strings.TrimSpace(helpText) +} + +func (c *LockCommand) Synopsis() string { + return "Manually lock the terraform state" +} diff --git a/command/lock_test.go b/command/lock_test.go new file mode 100644 index 0000000000..0e5331133e --- /dev/null +++ b/command/lock_test.go @@ -0,0 +1,97 @@ +package command + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func TestLock(t *testing.T) { + testData, _ := filepath.Abs("./testdata") + + 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 := &LockCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + if code := c.Run(nil); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + unlock, err := testLockState(testData, statePath) + if err == nil { + unlock() + t.Fatal("expected error locking state") + } else if !strings.Contains(err.Error(), "locked") { + t.Fatal("does not appear to be a lock error:", err) + } +} + +func TestLock_lockedState(t *testing.T) { + testData, _ := filepath.Abs("./testdata") + + 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 := &LockCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + unlock, err := testLockState(testData, statePath) + if err != nil { + t.Fatal(err) + } + defer unlock() + + if code := c.Run(nil); code == 0 { + t.Fatal("expected error when locking a locked state") + } +} diff --git a/command/unlock.go b/command/unlock.go new file mode 100644 index 0000000000..c31e82ad97 --- /dev/null +++ b/command/unlock.go @@ -0,0 +1,78 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/state" +) + +// 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) + + cmdFlags := c.Meta.flagSet("unlock") + 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("Current state does not support locking") + return 1 + } + + if err := s.Unlock(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to unlock state: %s", err)) + return 1 + } + + return 0 +} + +func (c *UnlockCommand) Help() string { + helpText := ` +Usage: terraform 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. +` + return strings.TrimSpace(helpText) +} + +func (c *UnlockCommand) Synopsis() string { + return "Manually unlock the terraform state" +} diff --git a/command/unlock_test.go b/command/unlock_test.go new file mode 100644 index 0000000000..b133dcb187 --- /dev/null +++ b/command/unlock_test.go @@ -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(nil); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +}