| package command |
| |
| import ( |
| "fmt" |
| "strings" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/command/arguments" |
| "github.com/hashicorp/terraform/internal/command/clistate" |
| "github.com/hashicorp/terraform/internal/command/views" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/terraform" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| ) |
| |
| // UntaintCommand is a cli.Command implementation that manually untaints |
| // a resource, marking it as primary and ready for service. |
| type UntaintCommand struct { |
| Meta |
| } |
| |
| func (c *UntaintCommand) Run(args []string) int { |
| args = c.Meta.process(args) |
| var allowMissing bool |
| cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("untaint") |
| cmdFlags.BoolVar(&allowMissing, "allow-missing", false, "allow missing") |
| cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") |
| cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") |
| cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") |
| cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") |
| cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") |
| cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } |
| if err := cmdFlags.Parse(args); err != nil { |
| c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) |
| return 1 |
| } |
| |
| var diags tfdiags.Diagnostics |
| |
| // Require the one argument for the resource to untaint |
| args = cmdFlags.Args() |
| if len(args) != 1 { |
| c.Ui.Error("The untaint command expects exactly one argument.") |
| cmdFlags.Usage() |
| return 1 |
| } |
| |
| addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0]) |
| diags = diags.Append(addrDiags) |
| if addrDiags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| // Load the backend |
| b, backendDiags := c.Backend(nil) |
| diags = diags.Append(backendDiags) |
| if backendDiags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| // Determine the workspace name |
| workspace, err := c.Workspace() |
| if err != nil { |
| c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) |
| return 1 |
| } |
| |
| // Check remote Terraform version is compatible |
| remoteVersionDiags := c.remoteVersionCheck(b, workspace) |
| diags = diags.Append(remoteVersionDiags) |
| c.showDiagnostics(diags) |
| if diags.HasErrors() { |
| return 1 |
| } |
| |
| // Get the state |
| stateMgr, err := b.StateMgr(workspace) |
| if err != nil { |
| c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) |
| return 1 |
| } |
| |
| if c.stateLock { |
| stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) |
| if diags := stateLocker.Lock(stateMgr, "untaint"); diags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| defer func() { |
| if diags := stateLocker.Unlock(); diags.HasErrors() { |
| c.showDiagnostics(diags) |
| } |
| }() |
| } |
| |
| if err := stateMgr.RefreshState(); err != nil { |
| c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) |
| return 1 |
| } |
| |
| // Get the actual state structure |
| state := stateMgr.State() |
| if state.Empty() { |
| if allowMissing { |
| return c.allowMissingExit(addr) |
| } |
| |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "No such resource instance", |
| "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.", |
| )) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| ss := state.SyncWrapper() |
| |
| // Get the resource and instance we're going to taint |
| rs := ss.Resource(addr.ContainingResource()) |
| is := ss.ResourceInstance(addr) |
| if is == nil { |
| if allowMissing { |
| return c.allowMissingExit(addr) |
| } |
| |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "No such resource instance", |
| 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), |
| )) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| obj := is.Current |
| if obj == nil { |
| if len(is.Deposed) != 0 { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "No such resource instance", |
| 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), |
| )) |
| } else { |
| // Don't know why we're here, but we'll produce a generic error message anyway. |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "No such resource instance", |
| fmt.Sprintf("Resource instance %s does not currently have a remote object associated with it, so it cannot be tainted.", addr), |
| )) |
| } |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| if obj.Status != states.ObjectTainted { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Resource instance is not tainted", |
| fmt.Sprintf("Resource instance %s is not currently tainted, and so it cannot be untainted.", addr), |
| )) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| // Get schemas, if possible, before writing state |
| var schemas *terraform.Schemas |
| if isCloudMode(b) { |
| var schemaDiags tfdiags.Diagnostics |
| schemas, schemaDiags = c.MaybeGetSchemas(state, nil) |
| diags = diags.Append(schemaDiags) |
| } |
| |
| obj.Status = states.ObjectReady |
| ss.SetResourceInstanceCurrent(addr, obj, rs.ProviderConfig) |
| |
| if err := stateMgr.WriteState(state); err != nil { |
| c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) |
| return 1 |
| } |
| if err := stateMgr.PersistState(schemas); err != nil { |
| c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) |
| return 1 |
| } |
| |
| c.showDiagnostics(diags) |
| c.Ui.Output(fmt.Sprintf("Resource instance %s has been successfully untainted.", addr)) |
| return 0 |
| } |
| |
| func (c *UntaintCommand) Help() string { |
| helpText := ` |
| Usage: terraform [global options] untaint [options] name |
| |
| Terraform uses the term "tainted" to describe a resource instance |
| which may not be fully functional, either because its creation |
| partially failed or because you've manually marked it as such using |
| the "terraform taint" command. |
| |
| This command removes that state from a resource instance, causing |
| Terraform to see it as fully-functional and not in need of |
| replacement. |
| |
| This will not modify your infrastructure directly. It only avoids |
| Terraform planning to replace a tainted instance in a future operation. |
| |
| Options: |
| |
| -allow-missing If specified, the command will succeed (exit code 0) |
| even if the resource is missing. |
| |
| -lock=false Don't hold a state lock during the operation. This is |
| dangerous if others might concurrently run commands |
| against the same workspace. |
| |
| -lock-timeout=0s Duration to retry a state lock. |
| |
| -ignore-remote-version A rare option used for the remote backend only. See |
| the remote backend documentation for more information. |
| |
| -state, state-out, and -backup are legacy options supported for the local |
| backend only. For more information, see the local backend's documentation. |
| |
| ` |
| return strings.TrimSpace(helpText) |
| } |
| |
| func (c *UntaintCommand) Synopsis() string { |
| return "Remove the 'tainted' state from a resource instance" |
| } |
| |
| func (c *UntaintCommand) allowMissingExit(name addrs.AbsResourceInstance) int { |
| c.showDiagnostics(tfdiags.Sourceless( |
| tfdiags.Warning, |
| "No such resource instance", |
| fmt.Sprintf("Resource instance %s was not found, but this is not an error because -allow-missing was set.", name), |
| )) |
| return 0 |
| } |