command/taint: new command
This commit is contained in:
parent
b3cd1bd5bc
commit
4ec31ecb95
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue