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()