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 }