diff --git a/command/state_rm.go b/command/state_rm.go new file mode 100644 index 000000000..273fd46aa --- /dev/null +++ b/command/state_rm.go @@ -0,0 +1,102 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" +) + +// StateRmCommand is a Command implementation that shows a single resource. +type StateRmCommand struct { + Meta + StateMeta +} + +func (c *StateRmCommand) 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") + if err := cmdFlags.Parse(args); err != nil { + return cli.RunResultHelp + } + args = cmdFlags.Args() + + 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 + } + + if err := stateReal.Remove(args...); err != nil { + c.Ui.Error(fmt.Sprintf(errStateRm, err)) + return 1 + } + + if err := state.WriteState(stateReal); err != nil { + c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) + return 1 + } + + if err := state.PersistState(); err != nil { + c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) + return 1 + } + + c.Ui.Output("Item removal successful.") + return 0 +} + +func (c *StateRmCommand) Help() string { + helpText := ` +Usage: terraform state rm [options] ADDRESS... + + Remove one or more items from the Terraform state. + + This command removes one or more items from the Terraform state based + on the address given. You can view and list the available resources + with "terraform state list". + + 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. + +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=statefile 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. + +` + return strings.TrimSpace(helpText) +} + +func (c *StateRmCommand) Synopsis() string { + return "Remove an item from the state" +} + +const errStateRm = `Error removing items from the state: %s + +The state was not saved. No items were removed from the persisted +state. No backup was created since no modification occurred. Please +resolve the issue above and try again.` + +const errStateRmPersist = `Error saving the state: %s + +The state was not saved. No items were removed from the persisted +state. No backup was created since no modification occurred. Please +resolve the issue above and try again.` diff --git a/command/state_rm_test.go b/command/state_rm_test.go new file mode 100644 index 000000000..036fed159 --- /dev/null +++ b/command/state_rm_test.go @@ -0,0 +1,108 @@ +package command + +import ( + "path/filepath" + "testing" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func TestStateRm(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.bar": &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 := &StateRmCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "test_instance.foo", + } + 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, testStateRmOutput) + + // Test we have backups + backups := testStateBackups(t, filepath.Dir(statePath)) + if len(backups) != 1 { + t.Fatalf("bad: %#v", backups) + } + testStateOutput(t, backups[0], testStateRmOutputOriginal) +} + +func TestStateRm_noState(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + p := testProvider() + ui := new(cli.MockUi) + c := &StateRmCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + +const testStateRmOutputOriginal = ` +test_instance.bar: + ID = foo + bar = value + foo = value +test_instance.foo: + ID = bar + bar = value + foo = value +` + +const testStateRmOutput = ` +test_instance.bar: + ID = foo + bar = value + foo = value +` diff --git a/commands.go b/commands.go index e33e55540..13e05ab6e 100644 --- a/commands.go +++ b/commands.go @@ -171,6 +171,12 @@ func init() { }, nil }, + "state rm": func() (cli.Command, error) { + return &command.StateRmCommand{ + Meta: meta, + }, nil + }, + "state mv": func() (cli.Command, error) { return &command.StateMvCommand{ Meta: meta, diff --git a/terraform/state_filter.go b/terraform/state_filter.go index 89cf0d898..1b41a3b7e 100644 --- a/terraform/state_filter.go +++ b/terraform/state_filter.go @@ -94,6 +94,11 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult { continue } + if a.Name != "" && a.Name != key.Name { + // Name doesn't match + continue + } + if a.Index >= 0 && key.Index != a.Index { // Index doesn't match continue diff --git a/terraform/state_filter_test.go b/terraform/state_filter_test.go index f9187b4e9..5e66d9176 100644 --- a/terraform/state_filter_test.go +++ b/terraform/state_filter_test.go @@ -38,6 +38,15 @@ func TestStateFilterFilter(t *testing.T) { }, }, + "single resource with similar names": { + "small_test_instance.tfstate", + []string{"test_instance.foo"}, + []string{ + "*terraform.ResourceState: test_instance.foo", + "*terraform.InstanceState: test_instance.foo", + }, + }, + "single instance": { "small.tfstate", []string{"aws_key_pair.onprem.primary"}, diff --git a/terraform/state_test.go b/terraform/state_test.go index 41e5f6bcf..34b2c362c 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -600,6 +600,13 @@ func TestStateRemove(t *testing.T) { ID: "foo", }, }, + + "test_instance.bar": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, }, }, }, @@ -607,8 +614,15 @@ func TestStateRemove(t *testing.T) { &State{ Modules: []*ModuleState{ &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{}, + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "test_instance.bar": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, }, }, }, diff --git a/terraform/test-fixtures/state-filter/small_test_instance.tfstate b/terraform/test-fixtures/state-filter/small_test_instance.tfstate new file mode 100644 index 000000000..7d6283389 --- /dev/null +++ b/terraform/test-fixtures/state-filter/small_test_instance.tfstate @@ -0,0 +1,27 @@ +{ + "version": 1, + "serial": 12, + "modules": [ + { + "path": [ + "root" + ], + "resources": { + "test_instance.foo": { + "type": "test_instance", + "primary": { + "id": "foo" + } + } + }, + "resources": { + "test_instance.bar": { + "type": "test_instance", + "primary": { + "id": "foo" + } + } + } + } + ] +} diff --git a/website/source/docs/commands/state/rm.html.md b/website/source/docs/commands/state/rm.html.md new file mode 100644 index 000000000..206d49695 --- /dev/null +++ b/website/source/docs/commands/state/rm.html.md @@ -0,0 +1,65 @@ +--- +layout: "commands-state" +page_title: "Command: state rm" +sidebar_current: "docs-state-sub-rm" +description: |- + The `terraform state rm` command removes items from the Terraform state. +--- + +# Command: state rm + +The `terraform state rm` command is used to remove items from the +[Terraform state](/docs/state/index.html). This command can remove +single resources, since instances of a resource, entire modules, +and more. + +## Usage + +Usage: `terraform state rm [options] ADDRESS...` + +The command will remove all the items matched by the addresses given. + +Items removed from the Terraform state are _not physically destroyed_. +Items removed from the Terraform state are only no longer managed by +Terraform. For example, if you remove an AWS instance from the state, the AWS +instance will continue running, but `terraform plan` will no longer see that +instance. + +There are various use cases for removing items from a Terraform state +file. The most common is refactoring a configuration to no longer manage +that resource (perhaps moving it to another Terraform configuration/state). + +The state will only be saved on successful removal of all addresses. +If any specific address errors for any reason (such as a syntax error), +the state will not be modified at all. + +This command will output a backup copy of the state prior to saving any +changes. The backup cannot be disabled. Due to the destructive nature +of this command, backups are required. + +This command requires one or more addresses that point to a resources in the +state. Addresses are +in [resource addressing format](/docs/commands/state/addressing.html). + +The command-line flags are all optional. The list of available flags are: + +* `-backup=path` - Path to a backup file Defaults to the state path plus + a timestamp with the ".backup" extension. + +* `-state=path` - Path to the state file. Defaults to "terraform.tfstate". + +## Example: Remove a Resource + +The example below removes a single resource in a module: + +``` +$ terraform state rm module.foo.packet_device.worker[0] +``` + +## Example: Remove a Module + +The example below removes an entire module: + +``` +$ terraform state rm module.foo +``` diff --git a/website/source/layouts/commands-state.erb b/website/source/layouts/commands-state.erb index 14cc64416..e012e6f61 100644 --- a/website/source/layouts/commands-state.erb +++ b/website/source/layouts/commands-state.erb @@ -24,7 +24,7 @@