github.com/opentofu/opentofu@v1.7.1/internal/command/taint.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package command
     7  
     8  import (
     9  	"fmt"
    10  	"strings"
    11  
    12  	"github.com/opentofu/opentofu/internal/addrs"
    13  	"github.com/opentofu/opentofu/internal/command/arguments"
    14  	"github.com/opentofu/opentofu/internal/command/clistate"
    15  	"github.com/opentofu/opentofu/internal/command/views"
    16  	"github.com/opentofu/opentofu/internal/states"
    17  	"github.com/opentofu/opentofu/internal/tfdiags"
    18  	"github.com/opentofu/opentofu/internal/tofu"
    19  )
    20  
    21  // TaintCommand is a cli.Command implementation that manually taints
    22  // a resource, marking it for recreation.
    23  type TaintCommand struct {
    24  	Meta
    25  }
    26  
    27  func (c *TaintCommand) Run(args []string) int {
    28  	args = c.Meta.process(args)
    29  	var allowMissing bool
    30  	cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("taint")
    31  	cmdFlags.BoolVar(&allowMissing, "allow-missing", false, "allow missing")
    32  	cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
    33  	cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
    34  	cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
    35  	cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
    36  	cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
    37  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    38  	if err := cmdFlags.Parse(args); err != nil {
    39  		c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
    40  		return 1
    41  	}
    42  
    43  	var diags tfdiags.Diagnostics
    44  
    45  	// Require the one argument for the resource to taint
    46  	args = cmdFlags.Args()
    47  	if len(args) != 1 {
    48  		c.Ui.Error("The taint command expects exactly one argument.")
    49  		cmdFlags.Usage()
    50  		return 1
    51  	}
    52  
    53  	addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0])
    54  	diags = diags.Append(addrDiags)
    55  	if addrDiags.HasErrors() {
    56  		c.showDiagnostics(diags)
    57  		return 1
    58  	}
    59  
    60  	if addr.Resource.Resource.Mode != addrs.ManagedResourceMode {
    61  		c.Ui.Error(fmt.Sprintf("Resource instance %s cannot be tainted", addr))
    62  		return 1
    63  	}
    64  
    65  	if diags := c.Meta.checkRequiredVersion(); diags != nil {
    66  		c.showDiagnostics(diags)
    67  		return 1
    68  	}
    69  
    70  	// Load the encryption configuration
    71  	enc, encDiags := c.Encryption()
    72  	if encDiags.HasErrors() {
    73  		c.showDiagnostics(encDiags)
    74  		return 1
    75  	}
    76  
    77  	// Load the backend
    78  	b, backendDiags := c.Backend(nil, enc.State())
    79  	diags = diags.Append(backendDiags)
    80  	if backendDiags.HasErrors() {
    81  		c.showDiagnostics(diags)
    82  		return 1
    83  	}
    84  
    85  	// Determine the workspace name
    86  	workspace, err := c.Workspace()
    87  	if err != nil {
    88  		c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
    89  		return 1
    90  	}
    91  
    92  	// Check remote OpenTofu version is compatible
    93  	remoteVersionDiags := c.remoteVersionCheck(b, workspace)
    94  	diags = diags.Append(remoteVersionDiags)
    95  	c.showDiagnostics(diags)
    96  	if diags.HasErrors() {
    97  		return 1
    98  	}
    99  
   100  	// Get the state
   101  	stateMgr, err := b.StateMgr(workspace)
   102  	if err != nil {
   103  		c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
   104  		return 1
   105  	}
   106  
   107  	if c.stateLock {
   108  		stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
   109  		if diags := stateLocker.Lock(stateMgr, "taint"); diags.HasErrors() {
   110  			c.showDiagnostics(diags)
   111  			return 1
   112  		}
   113  		defer func() {
   114  			if diags := stateLocker.Unlock(); diags.HasErrors() {
   115  				c.showDiagnostics(diags)
   116  			}
   117  		}()
   118  	}
   119  
   120  	if err := stateMgr.RefreshState(); err != nil {
   121  		c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
   122  		return 1
   123  	}
   124  
   125  	// Get the actual state structure
   126  	state := stateMgr.State()
   127  	if state.Empty() {
   128  		if allowMissing {
   129  			return c.allowMissingExit(addr)
   130  		}
   131  
   132  		diags = diags.Append(tfdiags.Sourceless(
   133  			tfdiags.Error,
   134  			"No such resource instance",
   135  			"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.",
   136  		))
   137  		c.showDiagnostics(diags)
   138  		return 1
   139  	}
   140  
   141  	// Get schemas, if possible, before writing state
   142  	var schemas *tofu.Schemas
   143  	if isCloudMode(b) {
   144  		var schemaDiags tfdiags.Diagnostics
   145  		schemas, schemaDiags = c.MaybeGetSchemas(state, nil)
   146  		diags = diags.Append(schemaDiags)
   147  	}
   148  
   149  	ss := state.SyncWrapper()
   150  
   151  	// Get the resource and instance we're going to taint
   152  	rs := ss.Resource(addr.ContainingResource())
   153  	is := ss.ResourceInstance(addr)
   154  	if is == nil {
   155  		if allowMissing {
   156  			return c.allowMissingExit(addr)
   157  		}
   158  
   159  		diags = diags.Append(tfdiags.Sourceless(
   160  			tfdiags.Error,
   161  			"No such resource instance",
   162  			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 \"tofu apply\" once to create the corresponding instance(s) before they can be tainted.", addr),
   163  		))
   164  		c.showDiagnostics(diags)
   165  		return 1
   166  	}
   167  
   168  	obj := is.Current
   169  	if obj == nil {
   170  		if len(is.Deposed) != 0 {
   171  			diags = diags.Append(tfdiags.Sourceless(
   172  				tfdiags.Error,
   173  				"No such resource instance",
   174  				fmt.Sprintf("Resource instance %s is currently part-way through a create_before_destroy replacement action. Run \"tofu apply\" to complete its replacement before tainting it.", addr),
   175  			))
   176  		} else {
   177  			// Don't know why we're here, but we'll produce a generic error message anyway.
   178  			diags = diags.Append(tfdiags.Sourceless(
   179  				tfdiags.Error,
   180  				"No such resource instance",
   181  				fmt.Sprintf("Resource instance %s does not currently have a remote object associated with it, so it cannot be tainted.", addr),
   182  			))
   183  		}
   184  		c.showDiagnostics(diags)
   185  		return 1
   186  	}
   187  
   188  	obj.Status = states.ObjectTainted
   189  	ss.SetResourceInstanceCurrent(addr, obj, rs.ProviderConfig)
   190  
   191  	if err := stateMgr.WriteState(state); err != nil {
   192  		c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
   193  		return 1
   194  	}
   195  	if err := stateMgr.PersistState(schemas); err != nil {
   196  		c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
   197  		return 1
   198  	}
   199  
   200  	c.showDiagnostics(diags)
   201  	c.Ui.Output(fmt.Sprintf("Resource instance %s has been marked as tainted.", addr))
   202  	return 0
   203  }
   204  
   205  func (c *TaintCommand) Help() string {
   206  	helpText := `
   207  Usage: tofu [global options] taint [options] <address>
   208  
   209    OpenTofu uses the term "tainted" to describe a resource instance
   210    which may not be fully functional, either because its creation
   211    partially failed or because you've manually marked it as such using
   212    this command.
   213  
   214    This will not modify your infrastructure directly, but subsequent
   215    OpenTofu plans will include actions to destroy the remote object
   216    and create a new object to replace it.
   217  
   218    You can remove the "taint" state from a resource instance using
   219    the "tofu untaint" command.
   220  
   221    The address is in the usual resource address syntax, such as:
   222      aws_instance.foo
   223      aws_instance.bar[1]
   224      module.foo.module.bar.aws_instance.baz
   225  
   226    Use your shell's quoting or escaping syntax to ensure that the
   227    address will reach OpenTofu correctly, without any special
   228    interpretation.
   229  
   230  Options:
   231  
   232    -allow-missing          If specified, the command will succeed (exit code 0)
   233                            even if the resource is missing.
   234  
   235    -lock=false             Don't hold a state lock during the operation. This is
   236                            dangerous if others might concurrently run commands
   237                            against the same workspace.
   238  
   239    -lock-timeout=0s        Duration to retry a state lock.
   240  
   241    -ignore-remote-version  A rare option used for the remote backend only. See
   242                            the remote backend documentation for more information.
   243  
   244    -state, state-out, and -backup are legacy options supported for the local
   245    backend only. For more information, see the local backend's documentation.
   246  
   247  `
   248  	return strings.TrimSpace(helpText)
   249  }
   250  
   251  func (c *TaintCommand) Synopsis() string {
   252  	return "Mark a resource instance as not fully functional"
   253  }
   254  
   255  func (c *TaintCommand) allowMissingExit(name addrs.AbsResourceInstance) int {
   256  	c.showDiagnostics(tfdiags.Sourceless(
   257  		tfdiags.Warning,
   258  		"No such resource instance",
   259  		fmt.Sprintf("Resource instance %s was not found, but this is not an error because -allow-missing was set.", name),
   260  	))
   261  	return 0
   262  }