mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 16:31:10 -06:00
241 lines
7.3 KiB
Go
241 lines
7.3 KiB
Go
package command
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/command/arguments"
|
|
"github.com/hashicorp/terraform/internal/command/clistate"
|
|
"github.com/hashicorp/terraform/internal/command/views"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
// 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)
|
|
var allowMissing bool
|
|
cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("taint")
|
|
cmdFlags.BoolVar(&allowMissing, "allow-missing", false, "allow missing")
|
|
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
|
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
|
|
cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
|
|
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
|
|
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
|
if err := cmdFlags.Parse(args); err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
|
|
return 1
|
|
}
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// 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
|
|
}
|
|
|
|
addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0])
|
|
diags = diags.Append(addrDiags)
|
|
if addrDiags.HasErrors() {
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
if addr.Resource.Resource.Mode != addrs.ManagedResourceMode {
|
|
c.Ui.Error(fmt.Sprintf("Resource instance %s cannot be tainted", addr))
|
|
return 1
|
|
}
|
|
|
|
if diags := c.Meta.checkRequiredVersion(); diags != nil {
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// Load the backend
|
|
b, backendDiags := c.Backend(nil)
|
|
diags = diags.Append(backendDiags)
|
|
if backendDiags.HasErrors() {
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// Determine the workspace name
|
|
workspace, err := c.Workspace()
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
|
|
return 1
|
|
}
|
|
|
|
// Check remote Terraform version is compatible
|
|
remoteVersionDiags := c.remoteVersionCheck(b, workspace)
|
|
diags = diags.Append(remoteVersionDiags)
|
|
c.showDiagnostics(diags)
|
|
if diags.HasErrors() {
|
|
return 1
|
|
}
|
|
|
|
// Get the state
|
|
stateMgr, err := b.StateMgr(workspace)
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
|
return 1
|
|
}
|
|
|
|
if c.stateLock {
|
|
stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
|
|
if diags := stateLocker.Lock(stateMgr, "taint"); diags.HasErrors() {
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
defer func() {
|
|
if diags := stateLocker.Unlock(); diags.HasErrors() {
|
|
c.showDiagnostics(diags)
|
|
}
|
|
}()
|
|
}
|
|
|
|
if err := stateMgr.RefreshState(); err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
|
return 1
|
|
}
|
|
|
|
// Get the actual state structure
|
|
state := stateMgr.State()
|
|
if state.Empty() {
|
|
if allowMissing {
|
|
return c.allowMissingExit(addr)
|
|
}
|
|
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"No such resource instance",
|
|
"The state currently contains no resource instances whatsoever. This may occur if the configuration has never been applied or if it has recently been destroyed.",
|
|
))
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
ss := state.SyncWrapper()
|
|
|
|
// Get the resource and instance we're going to taint
|
|
rs := ss.Resource(addr.ContainingResource())
|
|
is := ss.ResourceInstance(addr)
|
|
if is == nil {
|
|
if allowMissing {
|
|
return c.allowMissingExit(addr)
|
|
}
|
|
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"No such resource instance",
|
|
fmt.Sprintf("There is no resource instance in the state with the address %s. If the resource configuration has just been added, you must run \"terraform apply\" once to create the corresponding instance(s) before they can be tainted.", addr),
|
|
))
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
obj := is.Current
|
|
if obj == nil {
|
|
if len(is.Deposed) != 0 {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"No such resource instance",
|
|
fmt.Sprintf("Resource instance %s is currently part-way through a create_before_destroy replacement action. Run \"terraform apply\" to complete its replacement before tainting it.", addr),
|
|
))
|
|
} else {
|
|
// Don't know why we're here, but we'll produce a generic error message anyway.
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"No such resource instance",
|
|
fmt.Sprintf("Resource instance %s does not currently have a remote object associated with it, so it cannot be tainted.", addr),
|
|
))
|
|
}
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
obj.Status = states.ObjectTainted
|
|
ss.SetResourceInstanceCurrent(addr, obj, rs.ProviderConfig)
|
|
|
|
if err := stateMgr.WriteState(state); err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
|
return 1
|
|
}
|
|
if err := stateMgr.PersistState(); err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
|
return 1
|
|
}
|
|
|
|
c.Ui.Output(fmt.Sprintf("Resource instance %s has been marked as tainted.", addr))
|
|
return 0
|
|
}
|
|
|
|
func (c *TaintCommand) Help() string {
|
|
helpText := `
|
|
Usage: terraform [global options] taint [options] <address>
|
|
|
|
Terraform uses the term "tainted" to describe a resource instance
|
|
which may not be fully functional, either because its creation
|
|
partially failed or because you've manually marked it as such using
|
|
this command.
|
|
|
|
This will not modify your infrastructure directly, but subsequent
|
|
Terraform plans will include actions to destroy the remote object
|
|
and create a new object to replace it.
|
|
|
|
You can remove the "taint" state from a resource instance using
|
|
the "terraform untaint" command.
|
|
|
|
The address is in the usual resource address syntax, such as:
|
|
aws_instance.foo
|
|
aws_instance.bar[1]
|
|
module.foo.module.bar.aws_instance.baz
|
|
|
|
Use your shell's quoting or escaping syntax to ensure that the
|
|
address will reach Terraform correctly, without any special
|
|
interpretation.
|
|
|
|
Options:
|
|
|
|
-allow-missing If specified, the command will succeed (exit code 0)
|
|
even if the resource is missing.
|
|
|
|
-lock=false Don't hold a state lock during the operation. This is
|
|
dangerous if others might concurrently run commands
|
|
against the same workspace.
|
|
|
|
-lock-timeout=0s Duration to retry a state lock.
|
|
|
|
-ignore-remote-version A rare option used for the remote backend only. See
|
|
the remote backend documentation for more information.
|
|
|
|
-state, state-out, and -backup are legacy options supported for the local
|
|
backend only. For more information, see the local backend's documentation.
|
|
|
|
`
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (c *TaintCommand) Synopsis() string {
|
|
return "Mark a resource instance as not fully functional"
|
|
}
|
|
|
|
func (c *TaintCommand) allowMissingExit(name addrs.AbsResourceInstance) int {
|
|
c.showDiagnostics(tfdiags.Sourceless(
|
|
tfdiags.Warning,
|
|
"No such resource instance",
|
|
fmt.Sprintf("Resource instance %s was not found, but this is not an error because -allow-missing was set.", name),
|
|
))
|
|
return 0
|
|
}
|