mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
command/taint: new command
This commit is contained in:
parent
b3cd1bd5bc
commit
4ec31ecb95
@ -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")
|
||||||
|
119
command/taint.go
Normal file
119
command/taint.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TaintCommand is a cli.Command implementation that refreshes the state
|
||||||
|
// file.
|
||||||
|
type TaintCommand struct {
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TaintCommand) Run(args []string) int {
|
||||||
|
args = c.Meta.process(args, false)
|
||||||
|
|
||||||
|
cmdFlags := c.Meta.flagSet("taint")
|
||||||
|
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
||||||
|
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
||||||
|
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
||||||
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require the one argument for the resource to taint
|
||||||
|
args = cmdFlags.Args()
|
||||||
|
if len(args) != 1 {
|
||||||
|
c.Ui.Error("The taint command expects exactly one argument.")
|
||||||
|
cmdFlags.Usage()
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
// Get the state that we'll be modifying
|
||||||
|
state, err := c.State()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the actual state structure
|
||||||
|
s := state.State()
|
||||||
|
if s.Empty() {
|
||||||
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"The state is empty. The most common reason for this is that\n" +
|
||||||
|
"an invalid state file path was given or Terraform has never\n " +
|
||||||
|
"been run for this infrastructure. Infrastructure must exist\n" +
|
||||||
|
"for it to be tainted."))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mod := s.RootModule()
|
||||||
|
|
||||||
|
// If there are no resources in this module, it is an error
|
||||||
|
if len(mod.Resources) == 0 {
|
||||||
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"The module %s has no resources. There is nothing to taint.",
|
||||||
|
strings.Join(mod.Path, ".")))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the resource we're looking for
|
||||||
|
rs, ok := mod.Resources[name]
|
||||||
|
if !ok {
|
||||||
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"The resource %s couldn't be found in the module %s.",
|
||||||
|
name,
|
||||||
|
strings.Join(mod.Path, ".")))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taint the resource
|
||||||
|
rs.Taint()
|
||||||
|
|
||||||
|
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
||||||
|
if err := c.Meta.PersistState(s); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TaintCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: terraform taint [options] name
|
||||||
|
|
||||||
|
Manually mark a resource as tainted, forcing a destroy and recreate
|
||||||
|
on the next plan/apply.
|
||||||
|
|
||||||
|
This will not modify your infrastructure. This command changes your
|
||||||
|
state to mark a resource as tainted so that during the next plan or
|
||||||
|
apply, that resource will be destroyed and recreated. This command on
|
||||||
|
its own will not modify infrastructure. This command can be undone by
|
||||||
|
reverting the state backup file that is created.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-backup=path Path to backup the existing state file before
|
||||||
|
modifying. Defaults to the "-state-out" path with
|
||||||
|
".backup" extension. Set to "-" to disable backup.
|
||||||
|
|
||||||
|
-no-color If specified, output won't contain any color.
|
||||||
|
|
||||||
|
-state=path Path to read and save state (unless state-out
|
||||||
|
is specified). Defaults to "terraform.tfstate".
|
||||||
|
|
||||||
|
-state-out=path Path to write updated state file. By default, the
|
||||||
|
"-state" path will be used.
|
||||||
|
|
||||||
|
`
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TaintCommand) Synopsis() string {
|
||||||
|
return "Manually mark a resource for recreation"
|
||||||
|
}
|
241
command/taint_test.go
Normal file
241
command/taint_test.go
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTaint(t *testing.T) {
|
||||||
|
state := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
statePath := testStateFile(t, state)
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &TaintCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state", statePath,
|
||||||
|
"test_instance.foo",
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
testStateOutput(t, statePath, testTaintStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaint_backup(t *testing.T) {
|
||||||
|
// Get a temp cwd
|
||||||
|
tmp, cwd := testCwd(t)
|
||||||
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
||||||
|
// Write the temp state
|
||||||
|
state := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
path := testStateFileDefault(t, state)
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &TaintCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"test_instance.foo",
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
testStateOutput(t, path+".backup", testTaintDefaultStr)
|
||||||
|
testStateOutput(t, path, testTaintStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaint_backupDisable(t *testing.T) {
|
||||||
|
// Get a temp cwd
|
||||||
|
tmp, cwd := testCwd(t)
|
||||||
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
||||||
|
// Write the temp state
|
||||||
|
state := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
path := testStateFileDefault(t, state)
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &TaintCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-backup", "-",
|
||||||
|
"test_instance.foo",
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(path + ".backup"); err == nil {
|
||||||
|
t.Fatal("backup path should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
testStateOutput(t, path, testTaintStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaint_badState(t *testing.T) {
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &TaintCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state", "i-should-not-exist-ever",
|
||||||
|
"foo",
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 1 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaint_defaultState(t *testing.T) {
|
||||||
|
// Get a temp cwd
|
||||||
|
tmp, cwd := testCwd(t)
|
||||||
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
||||||
|
// Write the temp state
|
||||||
|
state := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
path := testStateFileDefault(t, state)
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &TaintCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"test_instance.foo",
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
testStateOutput(t, path, testTaintStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaint_stateOut(t *testing.T) {
|
||||||
|
// Get a temp cwd
|
||||||
|
tmp, cwd := testCwd(t)
|
||||||
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
||||||
|
// Write the temp state
|
||||||
|
state := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
path := testStateFileDefault(t, state)
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &TaintCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state-out", "foo",
|
||||||
|
"test_instance.foo",
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
testStateOutput(t, path, testTaintDefaultStr)
|
||||||
|
testStateOutput(t, "foo", testTaintStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
const testTaintStr = `
|
||||||
|
test_instance.foo: (1 tainted)
|
||||||
|
ID = <not created>
|
||||||
|
Tainted ID 1 = bar
|
||||||
|
`
|
||||||
|
|
||||||
|
const testTaintDefaultStr = `
|
||||||
|
test_instance.foo:
|
||||||
|
ID = bar
|
||||||
|
`
|
@ -260,6 +260,10 @@ func (s *State) GoString() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) String() string {
|
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()
|
||||||
|
Loading…
Reference in New Issue
Block a user