| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| 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" | |
| ) | |
| // StateReplaceProviderCommand is a Command implementation that allows users | |
| // to change the provider associated with existing resources. This is only | |
| // likely to be useful if a provider is forked or changes its fully-qualified | |
| // name. | |
| type StateReplaceProviderCommand struct { | |
| StateMeta | |
| } | |
| func (c *StateReplaceProviderCommand) Run(args []string) int { | |
| parsedArgs, parseDiags := arguments.ParseStateReplaceProvider(c.Meta.process(args)) | |
| if parseDiags.HasErrors() { | |
| c.showDiagnostics(parseDiags) | |
| return 1 | |
| } | |
| c.backupPath = parsedArgs.BackupPath | |
| c.Meta.stateLock = parsedArgs.StateLock | |
| c.Meta.stateLockTimeout = parsedArgs.StateLockTimeout | |
| c.statePath = parsedArgs.StatePath | |
| c.Meta.ignoreRemoteVersion = parsedArgs.IgnoreRemoteVersion | |
| loader, err := c.initConfigLoader() | |
| if err != nil { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(err) | |
| c.showDiagnostics(diags) | |
| return 1 | |
| } | |
| var varDiags tfdiags.Diagnostics | |
| c.VariableValues, varDiags = parsedArgs.Vars.CollectValues(func(filename string, src []byte) { | |
| loader.Parser().ForceFileSource(filename, src) | |
| }) | |
| if varDiags.HasErrors() { | |
| c.showDiagnostics(varDiags) | |
| return 1 | |
| } | |
| if diags := c.Meta.checkRequiredVersion(); diags != nil { | |
| c.showDiagnostics(diags) | |
| return 1 | |
| } | |
| var diags tfdiags.Diagnostics | |
| // Parse from/to arguments into providers | |
| from, fromDiags := addrs.ParseProviderSourceString(parsedArgs.FromProviderAddr) | |
| if fromDiags.HasErrors() { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| fmt.Sprintf(`Invalid "from" provider %q`, parsedArgs.FromProviderAddr), | |
| fromDiags.Err().Error(), | |
| )) | |
| } | |
| to, toDiags := addrs.ParseProviderSourceString(parsedArgs.ToProviderAddr) | |
| if toDiags.HasErrors() { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| fmt.Sprintf(`Invalid "to" provider %q`, parsedArgs.ToProviderAddr), | |
| toDiags.Err().Error(), | |
| )) | |
| } | |
| if diags.HasErrors() { | |
| c.showDiagnostics(diags) | |
| return 1 | |
| } | |
| // Initialize the state manager as configured | |
| view := arguments.ViewHuman | |
| stateMgr, err := c.State(view) | |
| if err != nil { | |
| c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) | |
| return 1 | |
| } | |
| // Acquire lock if requested | |
| if c.stateLock { | |
| stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) | |
| if diags := stateLocker.Lock(stateMgr, "state-replace-provider"); diags.HasErrors() { | |
| c.showDiagnostics(diags) | |
| return 1 | |
| } | |
| defer func() { | |
| if diags := stateLocker.Unlock(); diags.HasErrors() { | |
| c.showDiagnostics(diags) | |
| } | |
| }() | |
| } | |
| // Refresh and load state | |
| if err := stateMgr.RefreshState(); err != nil { | |
| c.Ui.Error(fmt.Sprintf("Failed to refresh source state: %s", err)) | |
| return 1 | |
| } | |
| state := stateMgr.State() | |
| if state == nil { | |
| c.Ui.Error(errStateNotFound) | |
| return 1 | |
| } | |
| // Fetch all resources from the state | |
| resources, diags := c.lookupAllResources(state) | |
| if diags.HasErrors() { | |
| c.showDiagnostics(diags) | |
| return 1 | |
| } | |
| var willReplace []*states.Resource | |
| // Update all matching resources with new provider | |
| for _, resource := range resources { | |
| if resource.ProviderConfig.Provider.Equals(from) { | |
| willReplace = append(willReplace, resource) | |
| } | |
| } | |
| c.showDiagnostics(diags) | |
| if len(willReplace) == 0 { | |
| c.Ui.Output("No matching resources found.") | |
| return 0 | |
| } | |
| // Explain the changes | |
| colorize := c.Colorize() | |
| c.Ui.Output("Terraform will perform the following actions:\n") | |
| c.Ui.Output(colorize.Color(" [yellow]~[reset] Updating provider:")) | |
| c.Ui.Output(colorize.Color(fmt.Sprintf(" [red]-[reset] %s", from))) | |
| c.Ui.Output(colorize.Color(fmt.Sprintf(" [green]+[reset] %s\n", to))) | |
| c.Ui.Output(colorize.Color(fmt.Sprintf("[bold]Changing[reset] %d resources:\n", len(willReplace)))) | |
| for _, resource := range willReplace { | |
| c.Ui.Output(colorize.Color(fmt.Sprintf(" %s", resource.Addr))) | |
| } | |
| // Confirm | |
| if !parsedArgs.AutoApprove { | |
| c.Ui.Output(colorize.Color( | |
| "\n[bold]Do you want to make these changes?[reset]\n" + | |
| "Only 'yes' will be accepted to continue.\n", | |
| )) | |
| v, err := c.Ui.Ask("Enter a value:") | |
| if err != nil { | |
| c.Ui.Error(fmt.Sprintf("Error asking for approval: %s", err)) | |
| return 1 | |
| } | |
| if v != "yes" { | |
| c.Ui.Output("Cancelled replacing providers.") | |
| return 0 | |
| } | |
| } | |
| // Update the provider for each resource | |
| for _, resource := range willReplace { | |
| resource.ProviderConfig.Provider = to | |
| } | |
| // Load the backend | |
| b, backendDiags := c.backend(".", view) | |
| diags = diags.Append(backendDiags) | |
| if backendDiags.HasErrors() { | |
| 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) | |
| } | |
| // Write the updated state | |
| if err := stateMgr.WriteState(state); err != nil { | |
| c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) | |
| return 1 | |
| } | |
| if err := stateMgr.PersistState(schemas); err != nil { | |
| c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) | |
| return 1 | |
| } | |
| c.showDiagnostics(diags) | |
| c.Ui.Output(fmt.Sprintf("\nSuccessfully replaced provider for %d resources.", len(willReplace))) | |
| return 0 | |
| } | |
| func (c *StateReplaceProviderCommand) Help() string { | |
| helpText := ` | |
| Usage: terraform [global options] state replace-provider [options] FROM_PROVIDER_FQN TO_PROVIDER_FQN | |
| Replace provider for resources in the Terraform state. | |
| Options: | |
| -auto-approve Skip interactive approval. | |
| -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. | |
| -var 'foo=bar' Set a value for one of the input variables in the root | |
| module of the configuration. Use this option more than | |
| once to set more than one variable. | |
| -var-file=filename Load variable values from the given file, in addition | |
| to the default files terraform.tfvars and *.auto.tfvars. | |
| Use this option more than once to include more than one | |
| variables file. | |
| -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 *StateReplaceProviderCommand) Synopsis() string { | |
| return "Replace provider in the state" | |
| } |