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 }