diff --git a/command/state_mv.go b/command/state_mv.go new file mode 100644 index 000000000..5b2a67b79 --- /dev/null +++ b/command/state_mv.go @@ -0,0 +1,126 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +// StateMvCommand is a Command implementation that shows a single resource. +type StateMvCommand struct { + Meta + StateMeta +} + +func (c *StateMvCommand) Run(args []string) int { + args = c.Meta.process(args, true) + + var backupPath string + cmdFlags := c.Meta.flagSet("state show") + cmdFlags.StringVar(&backupPath, "backup", "", "backup") + cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") + if err := cmdFlags.Parse(args); err != nil { + return cli.RunResultHelp + } + args = cmdFlags.Args() + if len(args) != 2 { + c.Ui.Error("Exactly two arguments expected.\n") + return cli.RunResultHelp + } + + state, err := c.StateMeta.State(&c.Meta) + if err != nil { + c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) + return cli.RunResultHelp + } + + stateReal := state.State() + if stateReal == nil { + c.Ui.Error(fmt.Sprintf(errStateNotFound)) + return 1 + } + + filter := &terraform.StateFilter{State: stateReal} + results, err := filter.Filter(args[0]) + if err != nil { + c.Ui.Error(fmt.Sprintf(errStateMv, err)) + return cli.RunResultHelp + } + + if err := stateReal.Remove(args[0]); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMv, err)) + return 1 + } + + if err := stateReal.Add(args[0], args[1], results[0].Value); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMv, err)) + return 1 + } + + if err := state.WriteState(stateReal); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) + return 1 + } + + if err := state.PersistState(); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) + return 1 + } + + c.Ui.Output(fmt.Sprintf( + "Moved %s to %s", args[0], args[1])) + return 0 +} + +func (c *StateMvCommand) Help() string { + helpText := ` +Usage: terraform state mv [options] ADDRESS ADDRESS + + Move an item in the state to another location within the same state. + + This command is useful for module refactors (moving items into a module) + or generally renaming of resources. + + This command creates a timestamped backup of the state on every invocation. + This can't be disabled. Due to the destructive nature of this command, + the backup is ensured by Terraform for safety reasons. + + This command can't currently move an item from one state file to a + completely new state file, but this functionality will come in an update. + +Options: + + -backup=PATH Path where Terraform should write the backup + state. This can't be disabled. If not set, Terraform + will write it to the same path as the statefile with + a backup extension. + + -state=PATH Path to a Terraform state file to use to look + up Terraform-managed resources. By default it will + use the state "terraform.tfstate" if it exists. + + -state-out=PATH Path to the destination state file to move the item + to. This defaults to the same statefile. This will + overwrite the destination state file. + +` + return strings.TrimSpace(helpText) +} + +func (c *StateMvCommand) Synopsis() string { + return "Move an item in the state" +} + +const errStateMv = `Error moving state: %[1]s + +Please ensure your addresses and state paths are valid. No +state was persisted. Your existing states are untouched.` + +const errStateMvPersist = `Error saving the state: %s + +The state wasn't saved properly. If the error happening after a partial +write occurred, a backup file will have been created. Otherwise, the state +is in the same state it was when the operation started.` diff --git a/command/state_mv_test.go b/command/state_mv_test.go new file mode 100644 index 000000000..06de1a6ca --- /dev/null +++ b/command/state_mv_test.go @@ -0,0 +1,113 @@ +package command + +import ( + "path/filepath" + "testing" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func TestStateMv(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", + Attributes: map[string]string{ + "foo": "value", + "bar": "value", + }, + }, + }, + + "test_instance.baz": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "foo", + Attributes: map[string]string{ + "foo": "value", + "bar": "value", + }, + }, + }, + }, + }, + }, + } + + statePath := testStateFile(t, state) + + p := testProvider() + ui := new(cli.MockUi) + c := &StateMvCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "test_instance.foo", + "test_instance.bar", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test it is correct + testStateOutput(t, statePath, testStateMvOutput) + + // Test we have backups + backups := testStateBackups(t, filepath.Dir(statePath)) + if len(backups) != 1 { + t.Fatalf("bad: %#v", backups) + } + testStateOutput(t, backups[0], testStateMvOutputOriginal) +} + +func TestStateMv_noState(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + p := testProvider() + ui := new(cli.MockUi) + c := &StateMvCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{"from", "to"} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + +const testStateMvOutputOriginal = ` +test_instance.baz: + ID = foo + bar = value + foo = value +test_instance.foo: + ID = bar + bar = value + foo = value +` + +const testStateMvOutput = ` +test_instance.bar: + ID = bar + bar = value + foo = value +test_instance.baz: + ID = foo + bar = value + foo = value +`