command/taint: new command

This commit is contained in:
Mitchell Hashimoto 2015-02-26 10:29:23 -08:00
parent b3cd1bd5bc
commit 4ec31ecb95
4 changed files with 403 additions and 1 deletions

View File

@ -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")

119
command/taint.go Normal file
View File

@ -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"
}

241
command/taint_test.go Normal file
View File

@ -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 = <not created>
Tainted ID 1 = bar
`
const testTaintDefaultStr = `
test_instance.foo:
ID = bar
`

View File

@ -260,6 +260,10 @@ func (s *State) GoString() string {
}
func (s *State) String() string {
if s == nil {
return "<nil>"
}
var buf bytes.Buffer
for _, m := range s.Modules {
mStr := m.String()