github.com/opentofu/opentofu@v1.7.1/internal/command/state_replace_provider.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package command
     7  
     8  import (
     9  	"fmt"
    10  	"strings"
    11  
    12  	"github.com/mitchellh/cli"
    13  
    14  	"github.com/opentofu/opentofu/internal/addrs"
    15  	"github.com/opentofu/opentofu/internal/command/arguments"
    16  	"github.com/opentofu/opentofu/internal/command/clistate"
    17  	"github.com/opentofu/opentofu/internal/command/views"
    18  	"github.com/opentofu/opentofu/internal/states"
    19  	"github.com/opentofu/opentofu/internal/tfdiags"
    20  	"github.com/opentofu/opentofu/internal/tofu"
    21  )
    22  
    23  // StateReplaceProviderCommand is a Command implementation that allows users
    24  // to change the provider associated with existing resources. This is only
    25  // likely to be useful if a provider is forked or changes its fully-qualified
    26  // name.
    27  
    28  type StateReplaceProviderCommand struct {
    29  	StateMeta
    30  }
    31  
    32  func (c *StateReplaceProviderCommand) Run(args []string) int {
    33  	args = c.Meta.process(args)
    34  
    35  	var autoApprove bool
    36  	cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state replace-provider")
    37  	cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of replacements")
    38  	cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup")
    39  	cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock states")
    40  	cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
    41  	cmdFlags.StringVar(&c.statePath, "state", "", "path")
    42  	if err := cmdFlags.Parse(args); err != nil {
    43  		c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
    44  		return cli.RunResultHelp
    45  	}
    46  	args = cmdFlags.Args()
    47  	if len(args) != 2 {
    48  		c.Ui.Error("Exactly two arguments expected.\n")
    49  		return cli.RunResultHelp
    50  	}
    51  
    52  	if diags := c.Meta.checkRequiredVersion(); diags != nil {
    53  		c.showDiagnostics(diags)
    54  		return 1
    55  	}
    56  
    57  	var diags tfdiags.Diagnostics
    58  
    59  	// Parse from/to arguments into providers
    60  	from, fromDiags := addrs.ParseProviderSourceString(args[0])
    61  	if fromDiags.HasErrors() {
    62  		diags = diags.Append(tfdiags.Sourceless(
    63  			tfdiags.Error,
    64  			fmt.Sprintf(`Invalid "from" provider %q`, args[0]),
    65  			fromDiags.Err().Error(),
    66  		))
    67  	}
    68  	to, toDiags := addrs.ParseProviderSourceString(args[1])
    69  	if toDiags.HasErrors() {
    70  		diags = diags.Append(tfdiags.Sourceless(
    71  			tfdiags.Error,
    72  			fmt.Sprintf(`Invalid "to" provider %q`, args[1]),
    73  			toDiags.Err().Error(),
    74  		))
    75  	}
    76  	if diags.HasErrors() {
    77  		c.showDiagnostics(diags)
    78  		return 1
    79  	}
    80  
    81  	// Load the encryption configuration
    82  	enc, encDiags := c.Encryption()
    83  	diags = diags.Append(encDiags)
    84  	if encDiags.HasErrors() {
    85  		c.showDiagnostics(diags)
    86  		return 1
    87  	}
    88  
    89  	// Initialize the state manager as configured
    90  	stateMgr, err := c.State(enc)
    91  	if err != nil {
    92  		c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
    93  		return 1
    94  	}
    95  
    96  	// Acquire lock if requested
    97  	if c.stateLock {
    98  		stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
    99  		if diags := stateLocker.Lock(stateMgr, "state-replace-provider"); diags.HasErrors() {
   100  			c.showDiagnostics(diags)
   101  			return 1
   102  		}
   103  		defer func() {
   104  			if diags := stateLocker.Unlock(); diags.HasErrors() {
   105  				c.showDiagnostics(diags)
   106  			}
   107  		}()
   108  	}
   109  
   110  	// Refresh and load state
   111  	if err := stateMgr.RefreshState(); err != nil {
   112  		c.Ui.Error(fmt.Sprintf("Failed to refresh source state: %s", err))
   113  		return 1
   114  	}
   115  
   116  	state := stateMgr.State()
   117  	if state == nil {
   118  		c.Ui.Error(errStateNotFound)
   119  		return 1
   120  	}
   121  
   122  	// Fetch all resources from the state
   123  	resources, diags := c.lookupAllResources(state)
   124  	if diags.HasErrors() {
   125  		c.showDiagnostics(diags)
   126  		return 1
   127  	}
   128  
   129  	var willReplace []*states.Resource
   130  
   131  	// Update all matching resources with new provider
   132  	for _, resource := range resources {
   133  		if resource.ProviderConfig.Provider.Equals(from) {
   134  			willReplace = append(willReplace, resource)
   135  		}
   136  	}
   137  	c.showDiagnostics(diags)
   138  
   139  	if len(willReplace) == 0 {
   140  		c.Ui.Output("No matching resources found.")
   141  		return 0
   142  	}
   143  
   144  	// Explain the changes
   145  	colorize := c.Colorize()
   146  	c.Ui.Output("OpenTofu will perform the following actions:\n")
   147  	c.Ui.Output(colorize.Color("  [yellow]~[reset] Updating provider:"))
   148  	c.Ui.Output(colorize.Color(fmt.Sprintf("    [red]-[reset] %s", from)))
   149  	c.Ui.Output(colorize.Color(fmt.Sprintf("    [green]+[reset] %s\n", to)))
   150  
   151  	c.Ui.Output(colorize.Color(fmt.Sprintf("[bold]Changing[reset] %d resources:\n", len(willReplace))))
   152  	for _, resource := range willReplace {
   153  		c.Ui.Output(colorize.Color(fmt.Sprintf("  %s", resource.Addr)))
   154  	}
   155  
   156  	// Confirm
   157  	if !autoApprove {
   158  		c.Ui.Output(colorize.Color(
   159  			"\n[bold]Do you want to make these changes?[reset]\n" +
   160  				"Only 'yes' will be accepted to continue.\n",
   161  		))
   162  		v, err := c.Ui.Ask("Enter a value:")
   163  		if err != nil {
   164  			c.Ui.Error(fmt.Sprintf("Error asking for approval: %s", err))
   165  			return 1
   166  		}
   167  		if v != "yes" {
   168  			c.Ui.Output("Cancelled replacing providers.")
   169  			return 0
   170  		}
   171  	}
   172  
   173  	// Update the provider for each resource
   174  	for _, resource := range willReplace {
   175  		resource.ProviderConfig.Provider = to
   176  	}
   177  
   178  	b, backendDiags := c.Backend(nil, enc.State())
   179  	diags = diags.Append(backendDiags)
   180  	if backendDiags.HasErrors() {
   181  		c.showDiagnostics(diags)
   182  		return 1
   183  	}
   184  
   185  	// Get schemas, if possible, before writing state
   186  	var schemas *tofu.Schemas
   187  	if isCloudMode(b) {
   188  		var schemaDiags tfdiags.Diagnostics
   189  		schemas, schemaDiags = c.MaybeGetSchemas(state, nil)
   190  		diags = diags.Append(schemaDiags)
   191  	}
   192  
   193  	// Write the updated state
   194  	if err := stateMgr.WriteState(state); err != nil {
   195  		c.Ui.Error(fmt.Sprintf(errStateRmPersist, err))
   196  		return 1
   197  	}
   198  	if err := stateMgr.PersistState(schemas); err != nil {
   199  		c.Ui.Error(fmt.Sprintf(errStateRmPersist, err))
   200  		return 1
   201  	}
   202  
   203  	c.showDiagnostics(diags)
   204  	c.Ui.Output(fmt.Sprintf("\nSuccessfully replaced provider for %d resources.", len(willReplace)))
   205  	return 0
   206  }
   207  
   208  func (c *StateReplaceProviderCommand) Help() string {
   209  	helpText := `
   210  Usage: tofu [global options] state replace-provider [options] FROM_PROVIDER_FQN TO_PROVIDER_FQN
   211  
   212    Replace provider for resources in the OpenTofu state.
   213  
   214  Options:
   215  
   216    -auto-approve           Skip interactive approval.
   217  
   218    -lock=false             Don't hold a state lock during the operation. This is
   219                            dangerous if others might concurrently run commands
   220                            against the same workspace.
   221  
   222    -lock-timeout=0s        Duration to retry a state lock.
   223  
   224    -ignore-remote-version  A rare option used for the remote backend only. See
   225                            the remote backend documentation for more information.
   226  
   227    -state, state-out, and -backup are legacy options supported for the local
   228    backend only. For more information, see the local backend's documentation.
   229  
   230  `
   231  	return strings.TrimSpace(helpText)
   232  }
   233  
   234  func (c *StateReplaceProviderCommand) Synopsis() string {
   235  	return "Replace provider in the state"
   236  }