From 341be226f4bcd9289f9ef4faf27530c9a369ec6a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2015 09:50:18 -0800 Subject: [PATCH 1/9] terraform: test for various taint cases --- terraform/context_test.go | 134 +++++++++++++++++- terraform/terraform_test.go | 30 ++++ .../apply-taint-dep-requires-new/main.tf | 10 ++ .../test-fixtures/apply-taint-dep/main.tf | 10 ++ 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 terraform/test-fixtures/apply-taint-dep-requires-new/main.tf create mode 100644 terraform/test-fixtures/apply-taint-dep/main.tf diff --git a/terraform/context_test.go b/terraform/context_test.go index 5ee9a8c71..9d21c21bc 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -4693,6 +4693,132 @@ func TestContext2Apply_taint(t *testing.T) { } } +func TestContext2Apply_taintDep(t *testing.T) { + m := testModule(t, "apply-taint-dep") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Tainted: []*InstanceState{ + &InstanceState{ + ID: "baz", + Attributes: map[string]string{ + "num": "2", + "type": "aws_instance", + }, + }, + }, + }, + "aws_instance.bar": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "foo": "baz", + "num": "2", + "type": "aws_instance", + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + if p, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } else { + t.Logf("plan: %s", p) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyTaintDepStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Apply_taintDepRequiresNew(t *testing.T) { + m := testModule(t, "apply-taint-dep-requires-new") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Tainted: []*InstanceState{ + &InstanceState{ + ID: "baz", + Attributes: map[string]string{ + "num": "2", + "type": "aws_instance", + }, + }, + }, + }, + "aws_instance.bar": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "foo": "baz", + "num": "2", + "type": "aws_instance", + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + if p, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } else { + t.Logf("plan: %s", p) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyTaintDepRequireNewStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + func TestContext2Apply_unknownAttribute(t *testing.T) { m := testModule(t, "apply-unknown") p := testProvider("aws") @@ -4945,7 +5071,13 @@ func testApplyFn( } result := &InstanceState{ - ID: id, + ID: id, + Attributes: make(map[string]string), + } + + // Copy all the prior attributes + for k, v := range s.Attributes { + result.Attributes[k] = v } if d != nil { diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 4372ff77b..d063555bf 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -433,6 +433,36 @@ aws_instance.bar: type = aws_instance ` +const testTerraformApplyTaintDepStr = ` +aws_instance.bar: + ID = bar + foo = foo + num = 2 + type = aws_instance + + Dependencies: + aws_instance.foo +aws_instance.foo: + ID = foo + num = 2 + type = aws_instance +` + +const testTerraformApplyTaintDepRequireNewStr = ` +aws_instance.bar: + ID = foo + foo = foo + require_new = yes + type = aws_instance + + Dependencies: + aws_instance.foo +aws_instance.foo: + ID = foo + num = 2 + type = aws_instance +` + const testTerraformApplyOutputStr = ` aws_instance.bar: ID = foo diff --git a/terraform/test-fixtures/apply-taint-dep-requires-new/main.tf b/terraform/test-fixtures/apply-taint-dep-requires-new/main.tf new file mode 100644 index 000000000..1295a1bca --- /dev/null +++ b/terraform/test-fixtures/apply-taint-dep-requires-new/main.tf @@ -0,0 +1,10 @@ +resource "aws_instance" "foo" { + id = "foo" + num = "2" +} + +resource "aws_instance" "bar" { + id = "bar" + foo = "${aws_instance.foo.id}" + require_new = "yes" +} diff --git a/terraform/test-fixtures/apply-taint-dep/main.tf b/terraform/test-fixtures/apply-taint-dep/main.tf new file mode 100644 index 000000000..1191781a9 --- /dev/null +++ b/terraform/test-fixtures/apply-taint-dep/main.tf @@ -0,0 +1,10 @@ +resource "aws_instance" "foo" { + id = "foo" + num = "2" +} + +resource "aws_instance" "bar" { + id = "bar" + num = "2" + foo = "${aws_instance.foo.id}" +} From b3cd1bd5bc3931f67b383f13f9da9b83740200de Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2015 09:58:56 -0800 Subject: [PATCH 2/9] terraform: add ResourceState.Taint --- terraform/state.go | 31 ++++++++++++++++++++++----- terraform/state_test.go | 47 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/terraform/state.go b/terraform/state.go index 5b695c33c..e3a26b7b2 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -699,6 +699,19 @@ func (s *ResourceState) Equal(other *ResourceState) bool { return true } +// Taint takes the primary state and marks it as tainted. If there is no +// primary state, this does nothing. +func (r *ResourceState) Taint() { + // If there is no primary, nothing to do + if r.Primary == nil { + return + } + + // Shuffle to the end of the taint list and set primary to nil + r.Tainted = append(r.Tainted, r.Primary) + r.Primary = nil +} + func (r *ResourceState) init() { if r.Primary == nil { r.Primary = &InstanceState{} @@ -710,16 +723,24 @@ func (r *ResourceState) deepcopy() *ResourceState { if r == nil { return nil } + n := &ResourceState{ Type: r.Type, - Dependencies: make([]string, len(r.Dependencies)), + Dependencies: nil, Primary: r.Primary.deepcopy(), - Tainted: make([]*InstanceState, 0, len(r.Tainted)), + Tainted: nil, } - copy(n.Dependencies, r.Dependencies) - for _, inst := range r.Tainted { - n.Tainted = append(n.Tainted, inst.deepcopy()) + if r.Dependencies != nil { + n.Dependencies = make([]string, len(r.Dependencies)) + copy(n.Dependencies, r.Dependencies) } + if r.Tainted != nil { + n.Tainted = make([]*InstanceState, 0, len(r.Tainted)) + for _, inst := range r.Tainted { + n.Tainted = append(n.Tainted, inst.deepcopy()) + } + } + return n } diff --git a/terraform/state_test.go b/terraform/state_test.go index ef2aa0b59..9dfbbbf04 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -275,6 +275,53 @@ func TestResourceStateEqual(t *testing.T) { } } +func TestResourceStateTaint(t *testing.T) { + cases := map[string]struct { + Input *ResourceState + Output *ResourceState + }{ + "no primary": { + &ResourceState{}, + &ResourceState{}, + }, + + "primary, no tainted": { + &ResourceState{ + Primary: &InstanceState{ID: "foo"}, + }, + &ResourceState{ + Tainted: []*InstanceState{ + &InstanceState{ID: "foo"}, + }, + }, + }, + + "primary, with tainted": { + &ResourceState{ + Primary: &InstanceState{ID: "foo"}, + Tainted: []*InstanceState{ + &InstanceState{ID: "bar"}, + }, + }, + &ResourceState{ + Tainted: []*InstanceState{ + &InstanceState{ID: "bar"}, + &InstanceState{ID: "foo"}, + }, + }, + }, + } + + for k, tc := range cases { + tc.Input.Taint() + if !reflect.DeepEqual(tc.Input, tc.Output) { + t.Fatalf( + "Failure: %s\n\nExpected: %#v\n\nGot: %#v", + k, tc.Output, tc.Input) + } + } +} + func TestInstanceStateEqual(t *testing.T) { cases := []struct { Result bool From 4ec31ecb9506a794f0c53f90d0cc529bef1aa70d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2015 10:29:23 -0800 Subject: [PATCH 3/9] command/taint: new command --- command/command_test.go | 40 ++++++- command/taint.go | 119 ++++++++++++++++++++ command/taint_test.go | 241 ++++++++++++++++++++++++++++++++++++++++ terraform/state.go | 4 + 4 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 command/taint.go create mode 100644 command/taint_test.go diff --git a/command/command_test.go b/command/command_test.go index cda23cf3f..303fc4b2b 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "testing" "github.com/hashicorp/terraform/config/module" @@ -131,6 +132,43 @@ func testStateFile(t *testing.T, s *terraform.State) string { return path } +// testStateFileDefault writes the state out to the default statefile +// in the cwd. Use `testCwd` to change into a temp cwd. +func testStateFileDefault(t *testing.T, s *terraform.State) string { + f, err := os.Create(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %s", err) + } + defer f.Close() + + if err := terraform.WriteState(s, f); err != nil { + t.Fatalf("err: %s", err) + } + + return DefaultStateFilename +} + +// testStateOutput tests that the state at the given path contains +// the expected state string. +func testStateOutput(t *testing.T, path string, expected string) { + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + + newState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(newState.String()) + expected = strings.TrimSpace(expected) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + func testProvider() *terraform.MockResourceProvider { p := new(terraform.MockResourceProvider) p.DiffReturn = &terraform.InstanceDiff{} @@ -175,7 +213,7 @@ func testTempDir(t *testing.T) string { return d } -// testCwdDir is used to change the current working directory +// testCwd is used to change the current working directory // into a test directory that should be remoted after func testCwd(t *testing.T) (string, string) { tmp, err := ioutil.TempDir("", "tf") diff --git a/command/taint.go b/command/taint.go new file mode 100644 index 000000000..ef6730f0f --- /dev/null +++ b/command/taint.go @@ -0,0 +1,119 @@ +package command + +import ( + "fmt" + "log" + "strings" +) + +// TaintCommand is a cli.Command implementation that refreshes the state +// file. +type TaintCommand struct { + Meta +} + +func (c *TaintCommand) Run(args []string) int { + args = c.Meta.process(args, false) + + cmdFlags := c.Meta.flagSet("taint") + cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") + cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + // Require the one argument for the resource to taint + args = cmdFlags.Args() + if len(args) != 1 { + c.Ui.Error("The taint command expects exactly one argument.") + cmdFlags.Usage() + return 1 + } + name := args[0] + + // Get the state that we'll be modifying + state, err := c.State() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + + // Get the actual state structure + s := state.State() + if s.Empty() { + c.Ui.Error(fmt.Sprintf( + "The state is empty. The most common reason for this is that\n" + + "an invalid state file path was given or Terraform has never\n " + + "been run for this infrastructure. Infrastructure must exist\n" + + "for it to be tainted.")) + return 1 + } + + mod := s.RootModule() + + // If there are no resources in this module, it is an error + if len(mod.Resources) == 0 { + c.Ui.Error(fmt.Sprintf( + "The module %s has no resources. There is nothing to taint.", + strings.Join(mod.Path, "."))) + return 1 + } + + // Get the resource we're looking for + rs, ok := mod.Resources[name] + if !ok { + c.Ui.Error(fmt.Sprintf( + "The resource %s couldn't be found in the module %s.", + name, + strings.Join(mod.Path, "."))) + return 1 + } + + // Taint the resource + rs.Taint() + + log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) + if err := c.Meta.PersistState(s); err != nil { + c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) + return 1 + } + + return 0 +} + +func (c *TaintCommand) Help() string { + helpText := ` +Usage: terraform taint [options] name + + Manually mark a resource as tainted, forcing a destroy and recreate + on the next plan/apply. + + This will not modify your infrastructure. This command changes your + state to mark a resource as tainted so that during the next plan or + apply, that resource will be destroyed and recreated. This command on + its own will not modify infrastructure. This command can be undone by + reverting the state backup file that is created. + +Options: + + -backup=path Path to backup the existing state file before + modifying. Defaults to the "-state-out" path with + ".backup" extension. Set to "-" to disable backup. + + -no-color If specified, output won't contain any color. + + -state=path Path to read and save state (unless state-out + is specified). Defaults to "terraform.tfstate". + + -state-out=path Path to write updated state file. By default, the + "-state" path will be used. + +` + return strings.TrimSpace(helpText) +} + +func (c *TaintCommand) Synopsis() string { + return "Manually mark a resource for recreation" +} diff --git a/command/taint_test.go b/command/taint_test.go new file mode 100644 index 000000000..a20378d9f --- /dev/null +++ b/command/taint_test.go @@ -0,0 +1,241 @@ +package command + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func TestTaint(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) + + 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.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + testStateOutput(t, statePath, testTaintStr) +} + +func TestTaint_backup(t *testing.T) { + // Get a temp cwd + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Write the temp state + 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", + }, + }, + }, + }, + }, + } + path := testStateFileDefault(t, state) + + ui := new(cli.MockUi) + c := &TaintCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{ + "test_instance.foo", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + testStateOutput(t, path+".backup", testTaintDefaultStr) + testStateOutput(t, path, testTaintStr) +} + +func TestTaint_backupDisable(t *testing.T) { + // Get a temp cwd + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Write the temp state + 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", + }, + }, + }, + }, + }, + } + path := testStateFileDefault(t, state) + + ui := new(cli.MockUi) + c := &TaintCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{ + "-backup", "-", + "test_instance.foo", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if _, err := os.Stat(path + ".backup"); err == nil { + t.Fatal("backup path should not exist") + } + + testStateOutput(t, path, testTaintStr) +} + +func TestTaint_badState(t *testing.T) { + ui := new(cli.MockUi) + c := &TaintCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{ + "-state", "i-should-not-exist-ever", + "foo", + } + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + +func TestTaint_defaultState(t *testing.T) { + // Get a temp cwd + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Write the temp state + 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", + }, + }, + }, + }, + }, + } + path := testStateFileDefault(t, state) + + ui := new(cli.MockUi) + c := &TaintCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{ + "test_instance.foo", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + testStateOutput(t, path, testTaintStr) +} + +func TestTaint_stateOut(t *testing.T) { + // Get a temp cwd + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Write the temp state + 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", + }, + }, + }, + }, + }, + } + path := testStateFileDefault(t, state) + + ui := new(cli.MockUi) + c := &TaintCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{ + "-state-out", "foo", + "test_instance.foo", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + testStateOutput(t, path, testTaintDefaultStr) + testStateOutput(t, "foo", testTaintStr) +} + +const testTaintStr = ` +test_instance.foo: (1 tainted) + ID = + Tainted ID 1 = bar +` + +const testTaintDefaultStr = ` +test_instance.foo: + ID = bar +` diff --git a/terraform/state.go b/terraform/state.go index e3a26b7b2..85492f31c 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -260,6 +260,10 @@ func (s *State) GoString() string { } func (s *State) String() string { + if s == nil { + return "" + } + var buf bytes.Buffer for _, m := range s.Modules { mStr := m.String() From b06a88d1aba69af84f3a5a7bf343b204fc47104d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2015 10:29:51 -0800 Subject: [PATCH 4/9] main: add the taint command --- commands.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/commands.go b/commands.go index aa0fc5e4c..8c10a9a06 100644 --- a/commands.go +++ b/commands.go @@ -110,6 +110,12 @@ func init() { }, nil }, + "taint": func() (cli.Command, error) { + return &command.TaintCommand{ + Meta: meta, + }, nil + }, + "version": func() (cli.Command, error) { return &command.VersionCommand{ Meta: meta, From fa9b655fd133ef25413c76fe74fe6d7f4da65a57 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2015 10:37:08 -0800 Subject: [PATCH 5/9] website: docs for tainted command --- command/taint.go | 3 ++ .../source/docs/commands/taint.html.markdown | 51 +++++++++++++++++++ website/source/layouts/docs.erb | 4 ++ 3 files changed, 58 insertions(+) create mode 100644 website/source/docs/commands/taint.html.markdown diff --git a/command/taint.go b/command/taint.go index ef6730f0f..35e298888 100644 --- a/command/taint.go +++ b/command/taint.go @@ -80,6 +80,9 @@ func (c *TaintCommand) Run(args []string) int { return 1 } + c.Ui.Output( + "The resource %s in the module %s has been marked as tainted!", + name, strings.Join(mod.Path, ".")) return 0 } diff --git a/website/source/docs/commands/taint.html.markdown b/website/source/docs/commands/taint.html.markdown new file mode 100644 index 000000000..3116c0fce --- /dev/null +++ b/website/source/docs/commands/taint.html.markdown @@ -0,0 +1,51 @@ +--- +layout: "docs" +page_title: "Command: taint" +sidebar_current: "docs-commands-taint" +description: |- + The `terraform taint` command manually marks a Terraform-managed resource as tainted, forcing it to be destroyed and recreated on the next apply. +--- + +# Command: taint + +The `terraform taint` command manually marks a Terraform-managed resource +as tainted, forcing it to be destroyed and recreated on the next apply. + +This command _will not_ modify infrastructure, but does modify the +state file in order to mark a resource as tainted. Once a resource is +marked as tainted, the next +[plan](/docs/commands/plan.html) will show that the resource will +be destroyed and recreated and the next +[apply](/docs/commands/apply.html) will implement this change. + +Forcing the recreation of a resource is useful when you want a certain +side effect of recreation that is not visible in the attributes of a resource. +For example: re-running provisioners will cause the node to be different +or rebooting the machine from a base image will cause new startup scripts +to run. + +Note that tainting a resource for recreation may affect resources that +depend on the newly tainted resource. For example, a DNS resource that +uses the IP address of a server may need to be modified to reflect +the potentially new IP address of a tainted server. The +[plan command](/docs/commands/plan.html) will show this if this is +the case. + +## Usage + +Usage: `terraform taint [options] name` + +The `name` argument is the name of the resource to mark as tainted. +The format of this argument is `TYPE.NAME`, such as `aws_instance.foo`. + +The command-line flags are all optional. The list of available flags are: + +* `-backup=path` - Path to the backup file. Defaults to `-state-out` with + the ".backup" extension. Disabled by setting to "-". + +* `-no-color` - Disables output with coloring + +* `-state=path` - Path to read and write the state file to. Defaults to "terraform.tfstate". + +* `-state-out=path` - Path to write updated state file. By default, the + `-state` path will be used. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index c71ac5a2e..80aef8dd0 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -98,6 +98,10 @@ > show + + > + taint + From 01aa4236c0d90c1006ad2d0164acfa81fb1a752d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2015 10:44:25 -0800 Subject: [PATCH 6/9] command/taint: support tainting resources in modules --- command/taint.go | 29 ++++++++++++++++++---- command/taint_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/command/taint.go b/command/taint.go index 35e298888..4876b2eb3 100644 --- a/command/taint.go +++ b/command/taint.go @@ -15,7 +15,9 @@ type TaintCommand struct { func (c *TaintCommand) Run(args []string) int { args = c.Meta.process(args, false) + var module string cmdFlags := c.Meta.flagSet("taint") + cmdFlags.StringVar(&module, "module", "", "module") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") @@ -51,13 +53,26 @@ func (c *TaintCommand) Run(args []string) int { return 1 } - mod := s.RootModule() + // Get the proper module we want to taint + if module == "" { + module = "root" + } else { + module = "root." + module + } + modPath := strings.Split(module, ".") + mod := s.ModuleByPath(modPath) + if mod == nil { + c.Ui.Error(fmt.Sprintf( + "The module %s could not be found. There is nothing to taint.", + module)) + return 1 + } // If there are no resources in this module, it is an error if len(mod.Resources) == 0 { c.Ui.Error(fmt.Sprintf( "The module %s has no resources. There is nothing to taint.", - strings.Join(mod.Path, "."))) + module)) return 1 } @@ -67,7 +82,7 @@ func (c *TaintCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf( "The resource %s couldn't be found in the module %s.", name, - strings.Join(mod.Path, "."))) + module)) return 1 } @@ -80,9 +95,9 @@ func (c *TaintCommand) Run(args []string) int { return 1 } - c.Ui.Output( + c.Ui.Output(fmt.Sprintf( "The resource %s in the module %s has been marked as tainted!", - name, strings.Join(mod.Path, ".")) + name, module)) return 0 } @@ -105,6 +120,10 @@ Options: modifying. Defaults to the "-state-out" path with ".backup" extension. Set to "-" to disable backup. + -module=path The module path where the resource lives. By + default this will be root. Child modules can be specified + by names. Ex. "consul" or "consul.vpc" (nested modules). + -no-color If specified, output won't contain any color. -state=path Path to read and save state (unless state-out diff --git a/command/taint_test.go b/command/taint_test.go index a20378d9f..f6e79a50c 100644 --- a/command/taint_test.go +++ b/command/taint_test.go @@ -229,6 +229,54 @@ func TestTaint_stateOut(t *testing.T) { testStateOutput(t, "foo", testTaintStr) } +func TestTaint_module(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", + }, + }, + }, + }, + &terraform.ModuleState{ + Path: []string{"root", "child"}, + Resources: map[string]*terraform.ResourceState{ + "test_instance.blah": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "blah", + }, + }, + }, + }, + }, + } + statePath := testStateFile(t, state) + + ui := new(cli.MockUi) + c := &TaintCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{ + "-module=child", + "-state", statePath, + "test_instance.blah", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + testStateOutput(t, statePath, testTaintModuleStr) +} + const testTaintStr = ` test_instance.foo: (1 tainted) ID = @@ -239,3 +287,13 @@ const testTaintDefaultStr = ` test_instance.foo: ID = bar ` + +const testTaintModuleStr = ` +test_instance.foo: + ID = bar + +module.child: + test_instance.blah: (1 tainted) + ID = + Tainted ID 1 = blah +` From d43c88f5f3ba1efdee1252c94bf200e8fb0f158d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2015 10:45:39 -0800 Subject: [PATCH 7/9] website: update docs for tainted --- website/source/docs/commands/taint.html.markdown | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/source/docs/commands/taint.html.markdown b/website/source/docs/commands/taint.html.markdown index 3116c0fce..c5685e2a7 100644 --- a/website/source/docs/commands/taint.html.markdown +++ b/website/source/docs/commands/taint.html.markdown @@ -43,6 +43,12 @@ The command-line flags are all optional. The list of available flags are: * `-backup=path` - Path to the backup file. Defaults to `-state-out` with the ".backup" extension. Disabled by setting to "-". +* `-module=path` - The module path where the resource to taint exists. + By default this is the root path. Other modules can be specified by + a period-separated list. Example: "foo" would reference the module + "foo" but "foo.bar" would reference the "bar" module in the "foo" + module. + * `-no-color` - Disables output with coloring * `-state=path` - Path to read and write the state file to. Defaults to "terraform.tfstate". From d411e2939f809ffbd18439b541e871b3ccdd99b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2015 10:56:45 -0800 Subject: [PATCH 8/9] command/taint: -allow-missing --- command/taint.go | 40 +++++++++-- command/taint_test.go | 69 +++++++++++++++++++ .../source/docs/commands/taint.html.markdown | 4 ++ 3 files changed, 108 insertions(+), 5 deletions(-) diff --git a/command/taint.go b/command/taint.go index 4876b2eb3..02227d5b6 100644 --- a/command/taint.go +++ b/command/taint.go @@ -15,8 +15,10 @@ type TaintCommand struct { func (c *TaintCommand) Run(args []string) int { args = c.Meta.process(args, false) + var allowMissing bool var module string cmdFlags := c.Meta.flagSet("taint") + cmdFlags.BoolVar(&allowMissing, "allow-missing", false, "module") cmdFlags.StringVar(&module, "module", "", "module") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") @@ -33,7 +35,13 @@ func (c *TaintCommand) Run(args []string) int { cmdFlags.Usage() return 1 } + name := args[0] + if module == "" { + module = "root" + } else { + module = "root." + module + } // Get the state that we'll be modifying state, err := c.State() @@ -45,6 +53,10 @@ func (c *TaintCommand) Run(args []string) int { // Get the actual state structure s := state.State() if s.Empty() { + if allowMissing { + return c.allowMissingExit(name, module) + } + c.Ui.Error(fmt.Sprintf( "The state is empty. The most common reason for this is that\n" + "an invalid state file path was given or Terraform has never\n " + @@ -54,14 +66,13 @@ func (c *TaintCommand) Run(args []string) int { } // Get the proper module we want to taint - if module == "" { - module = "root" - } else { - module = "root." + module - } modPath := strings.Split(module, ".") mod := s.ModuleByPath(modPath) if mod == nil { + if allowMissing { + return c.allowMissingExit(name, module) + } + c.Ui.Error(fmt.Sprintf( "The module %s could not be found. There is nothing to taint.", module)) @@ -70,6 +81,10 @@ func (c *TaintCommand) Run(args []string) int { // If there are no resources in this module, it is an error if len(mod.Resources) == 0 { + if allowMissing { + return c.allowMissingExit(name, module) + } + c.Ui.Error(fmt.Sprintf( "The module %s has no resources. There is nothing to taint.", module)) @@ -79,6 +94,10 @@ func (c *TaintCommand) Run(args []string) int { // Get the resource we're looking for rs, ok := mod.Resources[name] if !ok { + if allowMissing { + return c.allowMissingExit(name, module) + } + c.Ui.Error(fmt.Sprintf( "The resource %s couldn't be found in the module %s.", name, @@ -116,6 +135,9 @@ Usage: terraform taint [options] name Options: + -allow-missing If specified, the command will succeed (exit code 0) + even if the resource is missing. + -backup=path Path to backup the existing state file before modifying. Defaults to the "-state-out" path with ".backup" extension. Set to "-" to disable backup. @@ -139,3 +161,11 @@ Options: func (c *TaintCommand) Synopsis() string { return "Manually mark a resource for recreation" } + +func (c *TaintCommand) allowMissingExit(name, module string) int { + c.Ui.Output(fmt.Sprintf( + "The resource %s in the module %s was not found, but\n"+ + "-allow-missing is set, so we're exiting successfully.", + name, module)) + return 0 +} diff --git a/command/taint_test.go b/command/taint_test.go index f6e79a50c..617555b00 100644 --- a/command/taint_test.go +++ b/command/taint_test.go @@ -187,6 +187,75 @@ func TestTaint_defaultState(t *testing.T) { testStateOutput(t, path, testTaintStr) } +func TestTaint_missing(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) + + ui := new(cli.MockUi) + c := &TaintCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "test_instance.bar", + } + if code := c.Run(args); code == 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.OutputWriter.String()) + } +} + +func TestTaint_missingAllow(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) + + ui := new(cli.MockUi) + c := &TaintCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{ + "-allow-missing", + "-state", statePath, + "test_instance.bar", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + func TestTaint_stateOut(t *testing.T) { // Get a temp cwd tmp, cwd := testCwd(t) diff --git a/website/source/docs/commands/taint.html.markdown b/website/source/docs/commands/taint.html.markdown index c5685e2a7..05ea6301a 100644 --- a/website/source/docs/commands/taint.html.markdown +++ b/website/source/docs/commands/taint.html.markdown @@ -40,6 +40,10 @@ The format of this argument is `TYPE.NAME`, such as `aws_instance.foo`. The command-line flags are all optional. The list of available flags are: +* `-allow-missing` - If specified, the command will succeed (exit code 0) + even if the resource is missing. The command can still error, but only + in critically erroneous cases. + * `-backup=path` - Path to the backup file. Defaults to `-state-out` with the ".backup" extension. Disabled by setting to "-". From 6f9a358cc432e1b29da00ff364f741293743ff75 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2015 14:30:02 -0800 Subject: [PATCH 9/9] command/taint: fix comment --- command/taint.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/taint.go b/command/taint.go index 02227d5b6..54aa00146 100644 --- a/command/taint.go +++ b/command/taint.go @@ -6,8 +6,8 @@ import ( "strings" ) -// TaintCommand is a cli.Command implementation that refreshes the state -// file. +// TaintCommand is a cli.Command implementation that manually taints +// a resource, marking it for recreation. type TaintCommand struct { Meta }