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