// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 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" "github.com/mitchellh/cli" ) // 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 { args = c.Meta.process(args) var autoApprove bool cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state replace-provider") cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of replacements") cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup") cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock states") cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") cmdFlags.StringVar(&c.statePath, "state", "", "path") if err := cmdFlags.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) return cli.RunResultHelp } args = cmdFlags.Args() if len(args) != 2 { c.Ui.Error("Exactly two arguments expected.\n") return cli.RunResultHelp } 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(args[0]) if fromDiags.HasErrors() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf(`Invalid "from" provider %q`, args[0]), fromDiags.Err().Error(), )) } to, toDiags := addrs.ParseProviderSourceString(args[1]) if toDiags.HasErrors() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf(`Invalid "to" provider %q`, args[1]), toDiags.Err().Error(), )) } if diags.HasErrors() { c.showDiagnostics(diags) return 1 } // Initialize the state manager as configured stateMgr, err := c.State() 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 !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 } b, backendDiags := c.Backend(nil) 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" }