github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/untaint.go (about)

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