Skip to content

Commit 2c70fbb

Browse files
authored
Merge pull request #24523 from hashicorp/alisdair/terraform-state-replace-provider
command: Add state replace-provider subcommand
2 parents f3bed40 + cadc133 commit 2c70fbb

File tree

5 files changed

+532
-1
lines changed

5 files changed

+532
-1
lines changed

addrs/provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ func ParseProviderSourceString(str string) (Provider, tfdiags.Diagnostics) {
192192
diags = diags.Append(&hcl.Diagnostic{
193193
Severity: hcl.DiagError,
194194
Summary: "Invalid provider type",
195-
Detail: fmt.Sprintf(`Invalid provider type %q in source %q: %s"`, name, str, err),
195+
Detail: fmt.Sprintf(`Invalid provider type %q in source %q: %s"`, givenName, str, err),
196196
})
197197
return ret, diags
198198
}

command/state_meta.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,14 @@ func (c *StateMeta) collectResourceInstances(moduleAddr addrs.ModuleInstance, rs
196196
}
197197
return ret
198198
}
199+
200+
func (c *StateMeta) lookupAllResources(state *states.State) ([]*states.Resource, tfdiags.Diagnostics) {
201+
var ret []*states.Resource
202+
var diags tfdiags.Diagnostics
203+
for _, ms := range state.Modules {
204+
for _, resource := range ms.Resources {
205+
ret = append(ret, resource)
206+
}
207+
}
208+
return ret, diags
209+
}

command/state_replace_provider.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package command
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/hashicorp/terraform/addrs"
9+
"github.com/hashicorp/terraform/command/clistate"
10+
"github.com/hashicorp/terraform/states"
11+
"github.com/hashicorp/terraform/tfdiags"
12+
"github.com/mitchellh/cli"
13+
)
14+
15+
// StateReplaceProviderCommand is a Command implementation that allows users
16+
// to change the provider associated with existing resources. This is only
17+
// likely to be useful if a provider is forked or changes its fully-qualified
18+
// name.
19+
20+
type StateReplaceProviderCommand struct {
21+
StateMeta
22+
}
23+
24+
func (c *StateReplaceProviderCommand) Run(args []string) int {
25+
args = c.Meta.process(args)
26+
27+
var autoApprove bool
28+
cmdFlags := c.Meta.defaultFlagSet("state replace-provider")
29+
cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of replacements")
30+
cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup")
31+
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock states")
32+
cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
33+
cmdFlags.StringVar(&c.statePath, "state", "", "path")
34+
if err := cmdFlags.Parse(args); err != nil {
35+
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
36+
return cli.RunResultHelp
37+
}
38+
args = cmdFlags.Args()
39+
if len(args) != 2 {
40+
c.Ui.Error("Exactly two arguments expected.\n")
41+
return cli.RunResultHelp
42+
}
43+
44+
var diags tfdiags.Diagnostics
45+
46+
// Parse from/to arguments into providers
47+
from, fromDiags := addrs.ParseProviderSourceString(args[0])
48+
if fromDiags.HasErrors() {
49+
diags = diags.Append(tfdiags.Sourceless(
50+
tfdiags.Error,
51+
fmt.Sprintf(`Invalid "from" provider %q`, args[0]),
52+
fromDiags.Err().Error(),
53+
))
54+
}
55+
to, toDiags := addrs.ParseProviderSourceString(args[1])
56+
if toDiags.HasErrors() {
57+
diags = diags.Append(tfdiags.Sourceless(
58+
tfdiags.Error,
59+
fmt.Sprintf(`Invalid "to" provider %q`, args[1]),
60+
toDiags.Err().Error(),
61+
))
62+
}
63+
if diags.HasErrors() {
64+
c.showDiagnostics(diags)
65+
return 1
66+
}
67+
68+
// Initialize the state manager as configured
69+
stateMgr, err := c.State()
70+
if err != nil {
71+
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
72+
return 1
73+
}
74+
75+
// Acquire lock if requested
76+
if c.stateLock {
77+
stateLocker := clistate.NewLocker(context.Background(), c.stateLockTimeout, c.Ui, c.Colorize())
78+
if err := stateLocker.Lock(stateMgr, "state-replace-provider"); err != nil {
79+
c.Ui.Error(fmt.Sprintf("Error locking source state: %s", err))
80+
return 1
81+
}
82+
defer stateLocker.Unlock(nil)
83+
}
84+
85+
// Refresh and load state
86+
if err := stateMgr.RefreshState(); err != nil {
87+
c.Ui.Error(fmt.Sprintf("Failed to refresh source state: %s", err))
88+
return 1
89+
}
90+
91+
state := stateMgr.State()
92+
if state == nil {
93+
c.Ui.Error(fmt.Sprintf(errStateNotFound))
94+
return 1
95+
}
96+
97+
// Fetch all resources from the state
98+
resources, diags := c.lookupAllResources(state)
99+
if diags.HasErrors() {
100+
c.showDiagnostics(diags)
101+
return 1
102+
}
103+
104+
var willReplace []*states.Resource
105+
106+
// Update all matching resources with new provider
107+
for _, resource := range resources {
108+
if resource.ProviderConfig.Provider.Equals(from) {
109+
willReplace = append(willReplace, resource)
110+
}
111+
}
112+
c.showDiagnostics(diags)
113+
114+
if len(willReplace) == 0 {
115+
c.Ui.Output("No matching resources found.")
116+
return 0
117+
}
118+
119+
// Explain the changes
120+
colorize := c.Colorize()
121+
c.Ui.Output("Terraform will perform the following actions:\n")
122+
c.Ui.Output(colorize.Color(fmt.Sprintf(" [yellow]~[reset] Updating provider:")))
123+
c.Ui.Output(colorize.Color(fmt.Sprintf(" [red]-[reset] %s", from)))
124+
c.Ui.Output(colorize.Color(fmt.Sprintf(" [green]+[reset] %s\n", to)))
125+
126+
c.Ui.Output(colorize.Color(fmt.Sprintf("[bold]Changing[reset] %d resources:\n", len(willReplace))))
127+
for _, resource := range willReplace {
128+
c.Ui.Output(colorize.Color(fmt.Sprintf(" %s", resource.Addr)))
129+
}
130+
131+
// Confirm
132+
if !autoApprove {
133+
c.Ui.Output(colorize.Color(
134+
"\n[bold]Do you want to make these changes?[reset]\n" +
135+
"Only 'yes' will be accepted to continue.\n",
136+
))
137+
v, err := c.Ui.Ask(fmt.Sprintf("Enter a value:"))
138+
if err != nil {
139+
c.Ui.Error(fmt.Sprintf("Error asking for approval: %s", err))
140+
return 1
141+
}
142+
if v != "yes" {
143+
c.Ui.Output("Cancelled replacing providers.")
144+
return 0
145+
}
146+
}
147+
148+
// Update the provider for each resource
149+
for _, resource := range willReplace {
150+
resource.ProviderConfig.Provider = to
151+
}
152+
153+
// Write the updated state
154+
if err := stateMgr.WriteState(state); err != nil {
155+
c.Ui.Error(fmt.Sprintf(errStateRmPersist, err))
156+
return 1
157+
}
158+
if err := stateMgr.PersistState(); err != nil {
159+
c.Ui.Error(fmt.Sprintf(errStateRmPersist, err))
160+
return 1
161+
}
162+
163+
c.Ui.Output(fmt.Sprintf("\nSuccessfully replaced provider for %d resources.", len(willReplace)))
164+
return 0
165+
}
166+
167+
func (c *StateReplaceProviderCommand) Help() string {
168+
helpText := `
169+
Usage: terraform state replace-provider [options] FROM_PROVIDER_FQN TO_PROVIDER_FQN
170+
171+
Replace provider for resources in the Terraform state.
172+
173+
An error will be returned if any of the resources or modules given as
174+
filter addresses do not exist in the state.
175+
176+
Options:
177+
178+
-auto-approve Skip interactive approval.
179+
180+
-backup=PATH Path where Terraform should write the backup for the
181+
state file. This can't be disabled. If not set, Terraform
182+
will write it to the same path as the state file with
183+
a ".backup" extension.
184+
185+
-lock=true Lock the state files when locking is supported.
186+
187+
-lock-timeout=0s Duration to retry a state lock.
188+
189+
-state=PATH Path to the state file to update. Defaults to the configured
190+
backend, or "terraform.tfstate"
191+
`
192+
return strings.TrimSpace(helpText)
193+
}
194+
195+
func (c *StateReplaceProviderCommand) Synopsis() string {
196+
return "Replace provider in the state"
197+
}

0 commit comments

Comments
 (0)