commit
ac6efa5e57
|
@ -4,6 +4,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
|
@ -131,6 +132,43 @@ func testStateFile(t *testing.T, s *terraform.State) string {
|
||||||
return path
|
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 {
|
func testProvider() *terraform.MockResourceProvider {
|
||||||
p := new(terraform.MockResourceProvider)
|
p := new(terraform.MockResourceProvider)
|
||||||
p.DiffReturn = &terraform.InstanceDiff{}
|
p.DiffReturn = &terraform.InstanceDiff{}
|
||||||
|
@ -175,7 +213,7 @@ func testTempDir(t *testing.T) string {
|
||||||
return d
|
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
|
// into a test directory that should be remoted after
|
||||||
func testCwd(t *testing.T) (string, string) {
|
func testCwd(t *testing.T) (string, string) {
|
||||||
tmp, err := ioutil.TempDir("", "tf")
|
tmp, err := ioutil.TempDir("", "tf")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
`
|
|
@ -110,6 +110,12 @@ func init() {
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"taint": func() (cli.Command, error) {
|
||||||
|
return &command.TaintCommand{
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
|
||||||
"version": func() (cli.Command, error) {
|
"version": func() (cli.Command, error) {
|
||||||
return &command.VersionCommand{
|
return &command.VersionCommand{
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
|
|
|
@ -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) {
|
func TestContext2Apply_unknownAttribute(t *testing.T) {
|
||||||
m := testModule(t, "apply-unknown")
|
m := testModule(t, "apply-unknown")
|
||||||
p := testProvider("aws")
|
p := testProvider("aws")
|
||||||
|
@ -4967,6 +5093,12 @@ func testApplyFn(
|
||||||
|
|
||||||
result := &InstanceState{
|
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 {
|
if d != nil {
|
||||||
|
|
|
@ -260,6 +260,10 @@ func (s *State) GoString() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) String() string {
|
func (s *State) String() string {
|
||||||
|
if s == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
for _, m := range s.Modules {
|
for _, m := range s.Modules {
|
||||||
mStr := m.String()
|
mStr := m.String()
|
||||||
|
@ -699,6 +703,19 @@ func (s *ResourceState) Equal(other *ResourceState) bool {
|
||||||
return true
|
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() {
|
func (r *ResourceState) init() {
|
||||||
if r.Primary == nil {
|
if r.Primary == nil {
|
||||||
r.Primary = &InstanceState{}
|
r.Primary = &InstanceState{}
|
||||||
|
@ -710,16 +727,24 @@ func (r *ResourceState) deepcopy() *ResourceState {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
n := &ResourceState{
|
n := &ResourceState{
|
||||||
Type: r.Type,
|
Type: r.Type,
|
||||||
Dependencies: make([]string, len(r.Dependencies)),
|
Dependencies: nil,
|
||||||
Primary: r.Primary.deepcopy(),
|
Primary: r.Primary.deepcopy(),
|
||||||
Tainted: make([]*InstanceState, 0, len(r.Tainted)),
|
Tainted: nil,
|
||||||
}
|
}
|
||||||
|
if r.Dependencies != nil {
|
||||||
|
n.Dependencies = make([]string, len(r.Dependencies))
|
||||||
copy(n.Dependencies, r.Dependencies)
|
copy(n.Dependencies, r.Dependencies)
|
||||||
|
}
|
||||||
|
if r.Tainted != nil {
|
||||||
|
n.Tainted = make([]*InstanceState, 0, len(r.Tainted))
|
||||||
for _, inst := range r.Tainted {
|
for _, inst := range r.Tainted {
|
||||||
n.Tainted = append(n.Tainted, inst.deepcopy())
|
n.Tainted = append(n.Tainted, inst.deepcopy())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
func TestInstanceStateEqual(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Result bool
|
Result bool
|
||||||
|
|
|
@ -433,6 +433,36 @@ aws_instance.bar:
|
||||||
type = aws_instance
|
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 = `
|
const testTerraformApplyOutputStr = `
|
||||||
aws_instance.bar:
|
aws_instance.bar:
|
||||||
ID = foo
|
ID = foo
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -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}"
|
||||||
|
}
|
|
@ -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.
|
|
@ -98,6 +98,10 @@
|
||||||
<li<%= sidebar_current("docs-commands-show") %>>
|
<li<%= sidebar_current("docs-commands-show") %>>
|
||||||
<a href="/docs/commands/show.html">show</a>
|
<a href="/docs/commands/show.html">show</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-commands-taint") %>>
|
||||||
|
<a href="/docs/commands/taint.html">taint</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue