command: Add `terraform untaint`

- [x] Docs
 - [x] Command Unit Tests
 - [x] State Unit Tests

Closes #4820
This commit is contained in:
Paul Hinze 2016-03-08 14:37:34 -06:00
parent 5d9637ab1a
commit c7f5450a96
8 changed files with 860 additions and 1 deletions

View File

@ -202,7 +202,7 @@ func testStateOutput(t *testing.T, path string, expected string) {
actual := strings.TrimSpace(newState.String())
expected = strings.TrimSpace(expected)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
t.Fatalf("expected:\n%s\nactual:\n%s", expected, actual)
}
}

184
command/untaint.go Normal file
View File

@ -0,0 +1,184 @@
package command
import (
"fmt"
"log"
"strings"
)
// UntaintCommand is a cli.Command implementation that manually untaints
// a resource, marking it as primary and ready for service.
type UntaintCommand struct {
Meta
}
func (c *UntaintCommand) Run(args []string) int {
args = c.Meta.process(args, false)
var allowMissing bool
var module string
var index int
cmdFlags := c.Meta.flagSet("untaint")
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.IntVar(&index, "index", -1, "index")
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 untaint
args = cmdFlags.Args()
if len(args) != 1 {
c.Ui.Error("The untaint 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 untainted."))
return 1
}
// Get the proper module holding the resource we want to untaint
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 untaint.",
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 untaint.",
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
}
// Untaint the resource
if err := rs.Untaint(index); err != nil {
c.Ui.Error(fmt.Sprintf("Error untainting %s: %s", name, err))
c.Ui.Error("You can use `terraform show` to inspect the current state.")
return 1
}
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 successfully untainted!",
name, module))
return 0
}
func (c *UntaintCommand) Help() string {
helpText := `
Usage: terraform untaint [options] name
Manually unmark a resource as tainted, restoring it as the primary
instance in the state. This reverses either a manual 'terraform taint'
or the result of provisioners failing on a resource.
This will not modify your infrastructure. This command changes your
state to unmark a resource as tainted. This command can be undone by
reverting the state backup file that is created, or by running
'terraform taint' on the resource.
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.
-index=n Selects a single tainted instance when there are more
than one tainted instances present in the state for a
given resource. This flag is required when multiple
tainted instances are present. The vast majority of the
time, there is a maxiumum of one tainted instance per
resource, so this flag can be safely omitted.
-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 *UntaintCommand) Synopsis() string {
return "Manually unmark a resource as tainted"
}
func (c *UntaintCommand) 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
}

478
command/untaint_test.go Normal file
View File

@ -0,0 +1,478 @@
package command
import (
"os"
"strings"
"testing"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func TestUntaint(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",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
},
},
},
},
},
}
statePath := testStateFile(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
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())
}
expected := strings.TrimSpace(`
test_instance.foo:
ID = bar
`)
testStateOutput(t, statePath, expected)
}
func TestUntaint_indexRequired(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",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
&terraform.InstanceState{ID: "bar2"},
},
},
},
},
},
}
statePath := testStateFile(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
Meta: Meta{
Ui: ui,
},
}
args := []string{
"-state", statePath,
"test_instance.foo",
}
if code := c.Run(args); code == 0 {
t.Fatalf("Expected non-zero exit. Output:\n\n%s", ui.OutputWriter.String())
}
// Nothing should have gotten untainted
expected := strings.TrimSpace(`
test_instance.foo: (2 tainted)
ID = <not created>
Tainted ID 1 = bar
Tainted ID 2 = bar2
`)
testStateOutput(t, statePath, expected)
// Should have gotten an error message mentioning index
errOut := ui.ErrorWriter.String()
errContains := "please specify an index"
if !strings.Contains(errOut, errContains) {
t.Fatalf("Expected err output: %s, to contain: %s", errOut, errContains)
}
}
func TestUntaint_indexSpecified(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",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
&terraform.InstanceState{ID: "bar2"},
},
},
},
},
},
}
statePath := testStateFile(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
Meta: Meta{
Ui: ui,
},
}
args := []string{
"-state", statePath,
"-index", "1",
"test_instance.foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Nothing should have gotten untainted
expected := strings.TrimSpace(`
test_instance.foo: (1 tainted)
ID = bar2
Tainted ID 1 = bar
`)
testStateOutput(t, statePath, expected)
}
func TestUntaint_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",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
},
},
},
},
},
}
path := testStateFileDefault(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
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())
}
// Backup is still tainted
testStateOutput(t, path+".backup", strings.TrimSpace(`
test_instance.foo: (1 tainted)
ID = <not created>
Tainted ID 1 = bar
`))
// State is untainted
testStateOutput(t, path, strings.TrimSpace(`
test_instance.foo:
ID = bar
`))
}
func TestUntaint_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",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
},
},
},
},
},
}
path := testStateFileDefault(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
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, strings.TrimSpace(`
test_instance.foo:
ID = bar
`))
}
func TestUntaint_badState(t *testing.T) {
ui := new(cli.MockUi)
c := &UntaintCommand{
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 TestUntaint_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",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
},
},
},
},
},
}
path := testStateFileDefault(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
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, strings.TrimSpace(`
test_instance.foo:
ID = bar
`))
}
func TestUntaint_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",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
},
},
},
},
},
}
statePath := testStateFile(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
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 TestUntaint_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",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
},
},
},
},
},
}
statePath := testStateFile(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
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 TestUntaint_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",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
},
},
},
},
},
}
path := testStateFileDefault(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
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, strings.TrimSpace(`
test_instance.foo: (1 tainted)
ID = <not created>
Tainted ID 1 = bar
`))
testStateOutput(t, "foo", strings.TrimSpace(`
test_instance.foo:
ID = bar
`))
}
func TestUntaint_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",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
},
},
},
},
&terraform.ModuleState{
Path: []string{"root", "child"},
Resources: map[string]*terraform.ResourceState{
"test_instance.blah": &terraform.ResourceState{
Type: "test_instance",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
},
},
},
},
},
}
statePath := testStateFile(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
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, strings.TrimSpace(`
test_instance.foo: (1 tainted)
ID = <not created>
Tainted ID 1 = bar
module.child:
test_instance.blah:
ID = bar
`))
}

View File

@ -125,6 +125,12 @@ func init() {
CheckFunc: commandVersionCheck,
}, nil
},
"untaint": func() (cli.Command, error) {
return &command.UntaintCommand{
Meta: meta,
}, nil
},
}
}

View File

@ -872,6 +872,36 @@ func (r *ResourceState) Taint() {
r.Primary = nil
}
// Untaint takes a tainted InstanceState and marks it as primary.
// The index argument is used to select a single InstanceState from the
// array of Tainted when there are more than one. If index is -1, the
// first Tainted InstanceState will be untainted iff there is only one
// Tainted InstanceState. Index must be >= 0 to specify an InstanceState
// when Tainted has more than one member.
func (r *ResourceState) Untaint(index int) error {
if len(r.Tainted) == 0 {
return fmt.Errorf("Nothing to untaint.")
}
if r.Primary != nil {
return fmt.Errorf("Resource has a primary instance in the state that would be overwritten by untainting. If you want to restore a tainted resource to primary, taint the existing primary instance first.")
}
if index == -1 && len(r.Tainted) > 1 {
return fmt.Errorf("There are %d tainted instances for this resource, please specify an index to select which one to untaint.", len(r.Tainted))
}
if index == -1 {
index = 0
}
if index >= len(r.Tainted) {
return fmt.Errorf("There are %d tainted instances for this resource, the index specified (%d) is out of range.", len(r.Tainted), index)
}
// Perform the untaint
r.Primary = r.Tainted[index]
r.Tainted = append(r.Tainted[:index], r.Tainted[index+1:]...)
return nil
}
func (r *ResourceState) init() {
if r.Primary == nil {
r.Primary = &InstanceState{}

View File

@ -502,6 +502,103 @@ func TestResourceStateTaint(t *testing.T) {
}
}
func TestResourceStateUntaint(t *testing.T) {
cases := map[string]struct {
Input *ResourceState
Index func() int
ExpectedOutput *ResourceState
ExpectedErrMsg string
}{
"no primary, no tainted, err": {
Input: &ResourceState{},
ExpectedOutput: &ResourceState{},
ExpectedErrMsg: "Nothing to untaint",
},
"one tainted, no primary": {
Input: &ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
},
},
ExpectedOutput: &ResourceState{
Primary: &InstanceState{ID: "foo"},
Tainted: []*InstanceState{},
},
},
"one tainted, existing primary error": {
Input: &ResourceState{
Primary: &InstanceState{ID: "foo"},
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
},
},
ExpectedErrMsg: "Resource has a primary",
},
"multiple tainted, no index": {
Input: &ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "bar"},
&InstanceState{ID: "foo"},
},
},
ExpectedErrMsg: "please specify an index",
},
"multiple tainted, with index": {
Input: &ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "bar"},
&InstanceState{ID: "foo"},
},
},
Index: func() int { return 1 },
ExpectedOutput: &ResourceState{
Primary: &InstanceState{ID: "foo"},
Tainted: []*InstanceState{
&InstanceState{ID: "bar"},
},
},
},
"index out of bounds error": {
Input: &ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "bar"},
&InstanceState{ID: "foo"},
},
},
Index: func() int { return 2 },
ExpectedErrMsg: "out of range",
},
}
for k, tc := range cases {
index := -1
if tc.Index != nil {
index = tc.Index()
}
err := tc.Input.Untaint(index)
if tc.ExpectedErrMsg == "" && err != nil {
t.Fatalf("[%s] unexpected err: %s", k, err)
}
if tc.ExpectedErrMsg != "" {
if strings.Contains(err.Error(), tc.ExpectedErrMsg) {
continue
}
t.Fatalf("[%s] expected err: %s to contain: %s",
k, err, tc.ExpectedErrMsg)
}
if !reflect.DeepEqual(tc.Input, tc.ExpectedOutput) {
t.Fatalf(
"Failure: %s\n\nExpected: %#v\n\nGot: %#v",
k, tc.ExpectedOutput, tc.Input)
}
}
}
func TestInstanceStateEmpty(t *testing.T) {
cases := map[string]struct {
In *InstanceState

View File

@ -0,0 +1,61 @@
---
layout: "docs"
page_title: "Command: untaint"
sidebar_current: "docs-commands-untaint"
description: |-
The `terraform untaint` command manually unmarks a Terraform-managed resource as tainted, restoring it as the primary instance in the state.
---
# Command: untaint
The `terraform untaint` command manually unmarks a Terraform-managed resource
as tainted, restoring it as the primary instance in the state. This reverses
either a manual `terraform taint` or the result of provisioners failing on a
resource.
This command _will not_ modify infrastructure, but does modify the state file
in order to unmark a resource as tainted.
~> **NOTE on Tainted Indexes:** In certain edge cases, more than one tainted
instance can be present for a single resource. When this happens, the `-index`
flag is required to select which of the tainted instances to restore as
primary. You can use the `terraform show` command to inspect the state and
determine which index holds the instance you'd like to restore. In the vast
majority of cases, there will only be one tainted instance, and the `-index`
flag can be omitted.
## Usage
Usage: `terraform untaint [options] name`
The `name` argument is the name of the resource to mark as untainted. The
format of this argument is `TYPE.NAME`, such as `aws_instance.foo`.
The command-line flags are all optional (with the exception of `-index` in
certain cases, see above note). 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 "-".
* `-index=n` - Selects a single tainted instance when there are more than one
tainted instances present in the state for a given resource. This flag is
required when multiple tainted instances are present. The vast majority of the
time, there is a maxiumum of one tainted instance per resource, so this flag
can be safely omitted.
* `-module=path` - The module path where the resource to untaint 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

@ -111,6 +111,9 @@
<a href="/docs/commands/validate.html">validate</a>
</li>
<li<%= sidebar_current("docs-commands-untaint") %>>
<a href="/docs/commands/untaint.html">untaint</a>
</li>
</ul>
</li>