Merge pull request #1065 from hashicorp/f-taint

Add "taint" command
This commit is contained in:
Mitchell Hashimoto 2015-02-26 23:55:20 -08:00
commit ac6efa5e57
12 changed files with 909 additions and 7 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")

171
command/taint.go Normal file
View File

@ -0,0 +1,171 @@
package command
import (
"fmt"
"log"
"strings"
)
// TaintCommand is a cli.Command implementation that manually taints
// a resource, marking it for recreation.
type TaintCommand struct {
Meta
}
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")
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]
if module == "" {
module = "root"
} else {
module = "root." + module
}
// 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() {
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 " +
"been run for this infrastructure. Infrastructure must exist\n" +
"for it to be tainted."))
return 1
}
// Get the proper module we want to taint
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))
return 1
}
// 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))
return 1
}
// 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,
module))
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
}
c.Ui.Output(fmt.Sprintf(
"The resource %s in the module %s has been marked as tainted!",
name, module))
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:
-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.
-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
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"
}
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
}

368
command/taint_test.go Normal file
View File

@ -0,0 +1,368 @@
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_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)
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)
}
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 = <not created>
Tainted ID 1 = bar
`
const testTaintDefaultStr = `
test_instance.foo:
ID = bar
`
const testTaintModuleStr = `
test_instance.foo:
ID = bar
module.child:
test_instance.blah: (1 tainted)
ID = <not created>
Tainted ID 1 = blah
`

View File

@ -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,

View File

@ -4714,6 +4714,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")
@ -4966,7 +5092,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 {

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()
@ -699,6 +703,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 +727,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
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,61 @@
---
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:
* `-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 "-".
* `-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".
* `-state-out=path` - Path to write updated state file. By default, the
`-state` path will be used.

View File

@ -98,6 +98,10 @@
<li<%= sidebar_current("docs-commands-show") %>>
<a href="/docs/commands/show.html">show</a>
</li>
<li<%= sidebar_current("docs-commands-taint") %>>
<a href="/docs/commands/taint.html">taint</a>
</li>
</ul>
</li>