You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform/internal/command/state_replace_provider.go

220 lines
6.2 KiB

// 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
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.
-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"
}