command: Add `terraform untaint`
- [x] Docs - [x] Command Unit Tests - [x] State Unit Tests Closes #4820
This commit is contained in:
parent
5d9637ab1a
commit
c7f5450a96
|
@ -202,7 +202,7 @@ func testStateOutput(t *testing.T, path string, expected string) {
|
||||||
actual := strings.TrimSpace(newState.String())
|
actual := strings.TrimSpace(newState.String())
|
||||||
expected = strings.TrimSpace(expected)
|
expected = strings.TrimSpace(expected)
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
t.Fatalf("bad:\n\n%s", actual)
|
t.Fatalf("expected:\n%s\nactual:\n%s", expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
`))
|
||||||
|
}
|
|
@ -125,6 +125,12 @@ func init() {
|
||||||
CheckFunc: commandVersionCheck,
|
CheckFunc: commandVersionCheck,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"untaint": func() (cli.Command, error) {
|
||||||
|
return &command.UntaintCommand{
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -872,6 +872,36 @@ func (r *ResourceState) Taint() {
|
||||||
r.Primary = nil
|
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() {
|
func (r *ResourceState) init() {
|
||||||
if r.Primary == nil {
|
if r.Primary == nil {
|
||||||
r.Primary = &InstanceState{}
|
r.Primary = &InstanceState{}
|
||||||
|
|
|
@ -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) {
|
func TestInstanceStateEmpty(t *testing.T) {
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
In *InstanceState
|
In *InstanceState
|
||||||
|
|
|
@ -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.
|
|
@ -111,6 +111,9 @@
|
||||||
<a href="/docs/commands/validate.html">validate</a>
|
<a href="/docs/commands/validate.html">validate</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-commands-untaint") %>>
|
||||||
|
<a href="/docs/commands/untaint.html">untaint</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue