github.com/opentofu/opentofu@v1.7.1/internal/command/untaint.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  // UntaintCommand is a cli.Command implementation that manually untaints
    22  // a resource, marking it as primary and ready for service.
    23  type UntaintCommand struct {
    24  	Meta
    25  }
    26  
    27  func (c *UntaintCommand) Run(args []string) int {
    28  	args = c.Meta.process(args)
    29  	var allowMissing bool
    30  	cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("untaint")
    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 untaint
    46  	args = cmdFlags.Args()
    47  	if len(args) != 1 {
    48  		c.Ui.Error("The untaint 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  	// Load the encryption configuration
    61  	enc, encDiags := c.Encryption()
    62  	diags = diags.Append(encDiags)
    63  	if encDiags.HasErrors() {
    64  		c.showDiagnostics(diags)
    65  		return 1
    66  	}
    67  
    68  	// Load the backend
    69  	b, backendDiags := c.Backend(nil, enc.State())
    70  	diags = diags.Append(backendDiags)
    71  	if backendDiags.HasErrors() {
    72  		c.showDiagnostics(diags)
    73  		return 1
    74  	}
    75  
    76  	// Determine the workspace name
    77  	workspace, err := c.Workspace()
    78  	if err != nil {
    79  		c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
    80  		return 1
    81  	}
    82  
    83  	// Check remote OpenTofu version is compatible
    84  	remoteVersionDiags := c.remoteVersionCheck(b, workspace)
    85  	diags = diags.Append(remoteVersionDiags)
    86  	c.showDiagnostics(diags)
    87  	if diags.HasErrors() {
    88  		return 1
    89  	}
    90  
    91  	// Get the state
    92  	stateMgr, err := b.StateMgr(workspace)
    93  	if err != nil {
    94  		c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
    95  		return 1
    96  	}
    97  
    98  	if c.stateLock {
    99  		stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
   100  		if diags := stateLocker.Lock(stateMgr, "untaint"); diags.HasErrors() {
   101  			c.showDiagnostics(diags)
   102  			return 1
   103  		}
   104  		defer func() {
   105  			if diags := stateLocker.Unlock(); diags.HasErrors() {
   106  				c.showDiagnostics(diags)
   107  			}
   108  		}()
   109  	}
   110  
   111  	if err := stateMgr.RefreshState(); err != nil {
   112  		c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
   113  		return 1
   114  	}
   115  
   116  	// Get the actual state structure
   117  	state := stateMgr.State()
   118  	if state.Empty() {
   119  		if allowMissing {
   120  			return c.allowMissingExit(addr)
   121  		}
   122  
   123  		diags = diags.Append(tfdiags.Sourceless(
   124  			tfdiags.Error,
   125  			"No such resource instance",
   126  			"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.",
   127  		))
   128  		c.showDiagnostics(diags)
   129  		return 1
   130  	}
   131  
   132  	ss := state.SyncWrapper()
   133  
   134  	// Get the resource and instance we're going to taint
   135  	rs := ss.Resource(addr.ContainingResource())
   136  	is := ss.ResourceInstance(addr)
   137  	if is == nil {
   138  		if allowMissing {
   139  			return c.allowMissingExit(addr)
   140  		}
   141  
   142  		diags = diags.Append(tfdiags.Sourceless(
   143  			tfdiags.Error,
   144  			"No such resource instance",
   145  			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),
   146  		))
   147  		c.showDiagnostics(diags)
   148  		return 1
   149  	}
   150  
   151  	obj := is.Current
   152  	if obj == nil {
   153  		if len(is.Deposed) != 0 {
   154  			diags = diags.Append(tfdiags.Sourceless(
   155  				tfdiags.Error,
   156  				"No such resource instance",
   157  				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),
   158  			))
   159  		} else {
   160  			// Don't know why we're here, but we'll produce a generic error message anyway.
   161  			diags = diags.Append(tfdiags.Sourceless(
   162  				tfdiags.Error,
   163  				"No such resource instance",
   164  				fmt.Sprintf("Resource instance %s does not currently have a remote object associated with it, so it cannot be tainted.", addr),
   165  			))
   166  		}
   167  		c.showDiagnostics(diags)
   168  		return 1
   169  	}
   170  
   171  	if obj.Status != states.ObjectTainted {
   172  		diags = diags.Append(tfdiags.Sourceless(
   173  			tfdiags.Error,
   174  			"Resource instance is not tainted",
   175  			fmt.Sprintf("Resource instance %s is not currently tainted, and so it cannot be untainted.", addr),
   176  		))
   177  		c.showDiagnostics(diags)
   178  		return 1
   179  	}
   180  
   181  	// Get schemas, if possible, before writing state
   182  	var schemas *tofu.Schemas
   183  	if isCloudMode(b) {
   184  		var schemaDiags tfdiags.Diagnostics
   185  		schemas, schemaDiags = c.MaybeGetSchemas(state, nil)
   186  		diags = diags.Append(schemaDiags)
   187  	}
   188  
   189  	obj.Status = states.ObjectReady
   190  	ss.SetResourceInstanceCurrent(addr, obj, rs.ProviderConfig)
   191  
   192  	if err := stateMgr.WriteState(state); err != nil {
   193  		c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
   194  		return 1
   195  	}
   196  	if err := stateMgr.PersistState(schemas); err != nil {
   197  		c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
   198  		return 1
   199  	}
   200  
   201  	c.showDiagnostics(diags)
   202  	c.Ui.Output(fmt.Sprintf("Resource instance %s has been successfully untainted.", addr))
   203  	return 0
   204  }
   205  
   206  func (c *UntaintCommand) Help() string {
   207  	helpText := `
   208  Usage: tofu [global options] untaint [options] name
   209  
   210    OpenTofu uses the term "tainted" to describe a resource instance
   211    which may not be fully functional, either because its creation
   212    partially failed or because you've manually marked it as such using
   213    the "tofu taint" command.
   214  
   215    This command removes that state from a resource instance, causing
   216    OpenTofu to see it as fully-functional and not in need of
   217    replacement.
   218  
   219    This will not modify your infrastructure directly. It only avoids
   220    OpenTofu planning to replace a tainted instance in a future operation.
   221  
   222  Options:
   223  
   224    -allow-missing          If specified, the command will succeed (exit code 0)
   225                            even if the resource is missing.
   226  
   227    -lock=false             Don't hold a state lock during the operation. This is
   228                            dangerous if others might concurrently run commands
   229                            against the same workspace.
   230  
   231    -lock-timeout=0s        Duration to retry a state lock.
   232  
   233    -ignore-remote-version  A rare option used for the remote backend only. See
   234                            the remote backend documentation for more information.
   235  
   236    -state, state-out, and -backup are legacy options supported for the local
   237    backend only. For more information, see the local backend's documentation.
   238  
   239  `
   240  	return strings.TrimSpace(helpText)
   241  }
   242  
   243  func (c *UntaintCommand) Synopsis() string {
   244  	return "Remove the 'tainted' state from a resource instance"
   245  }
   246  
   247  func (c *UntaintCommand) allowMissingExit(name addrs.AbsResourceInstance) int {
   248  	c.showDiagnostics(tfdiags.Sourceless(
   249  		tfdiags.Warning,
   250  		"No such resource instance",
   251  		fmt.Sprintf("Resource instance %s was not found, but this is not an error because -allow-missing was set.", name),
   252  	))
   253  	return 0
   254  }