github.com/opentofu/opentofu@v1.7.1/internal/command/state_mv.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/backend"
    16  	"github.com/opentofu/opentofu/internal/command/arguments"
    17  	"github.com/opentofu/opentofu/internal/command/clistate"
    18  	"github.com/opentofu/opentofu/internal/command/views"
    19  	"github.com/opentofu/opentofu/internal/states"
    20  	"github.com/opentofu/opentofu/internal/tfdiags"
    21  	"github.com/opentofu/opentofu/internal/tofu"
    22  )
    23  
    24  // StateMvCommand is a Command implementation that shows a single resource.
    25  type StateMvCommand struct {
    26  	StateMeta
    27  }
    28  
    29  func (c *StateMvCommand) Run(args []string) int {
    30  	args = c.Meta.process(args)
    31  	// We create two metas to track the two states
    32  	var backupPathOut, statePathOut string
    33  
    34  	var dryRun bool
    35  	cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state mv")
    36  	cmdFlags.BoolVar(&dryRun, "dry-run", false, "dry run")
    37  	cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup")
    38  	cmdFlags.StringVar(&backupPathOut, "backup-out", "-", "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  	cmdFlags.StringVar(&statePathOut, "state-out", "", "path")
    43  	if err := cmdFlags.Parse(args); err != nil {
    44  		c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
    45  		return 1
    46  	}
    47  	args = cmdFlags.Args()
    48  	if len(args) != 2 {
    49  		c.Ui.Error("Exactly two arguments expected.\n")
    50  		return cli.RunResultHelp
    51  	}
    52  
    53  	if diags := c.Meta.checkRequiredVersion(); diags != nil {
    54  		c.showDiagnostics(diags)
    55  		return 1
    56  	}
    57  
    58  	// If backup or backup-out options are set
    59  	// and the state option is not set, make sure
    60  	// the backend is local
    61  	backupOptionSetWithoutStateOption := c.backupPath != "-" && c.statePath == ""
    62  	backupOutOptionSetWithoutStateOption := backupPathOut != "-" && c.statePath == ""
    63  
    64  	var setLegacyLocalBackendOptions []string
    65  	if backupOptionSetWithoutStateOption {
    66  		setLegacyLocalBackendOptions = append(setLegacyLocalBackendOptions, "-backup")
    67  	}
    68  	if backupOutOptionSetWithoutStateOption {
    69  		setLegacyLocalBackendOptions = append(setLegacyLocalBackendOptions, "-backup-out")
    70  	}
    71  
    72  	// Load the encryption configuration
    73  	enc, encDiags := c.Encryption()
    74  	if encDiags.HasErrors() {
    75  		c.showDiagnostics(encDiags)
    76  		return 1
    77  	}
    78  
    79  	if len(setLegacyLocalBackendOptions) > 0 {
    80  		currentBackend, diags := c.backendFromConfig(&BackendOpts{}, enc.State())
    81  		if diags.HasErrors() {
    82  			c.showDiagnostics(diags)
    83  			return 1
    84  		}
    85  
    86  		// If currentBackend is nil and diags didn't have errors,
    87  		// this means we have an implicit local backend
    88  		_, isLocalBackend := currentBackend.(backend.Local)
    89  		if currentBackend != nil && !isLocalBackend {
    90  			diags = diags.Append(
    91  				tfdiags.Sourceless(
    92  					tfdiags.Error,
    93  					fmt.Sprintf("Invalid command line options: %s", strings.Join(setLegacyLocalBackendOptions[:], ", ")),
    94  					"Command line options -backup and -backup-out are legacy options that operate on a local state file only. You must specify a local state file with the -state option or switch to the local backend.",
    95  				),
    96  			)
    97  			c.showDiagnostics(diags)
    98  			return 1
    99  		}
   100  	}
   101  
   102  	// Read the from state
   103  	stateFromMgr, err := c.State(enc)
   104  	if err != nil {
   105  		c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
   106  		return 1
   107  	}
   108  
   109  	if c.stateLock {
   110  		stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
   111  		if diags := stateLocker.Lock(stateFromMgr, "state-mv"); diags.HasErrors() {
   112  			c.showDiagnostics(diags)
   113  			return 1
   114  		}
   115  		defer func() {
   116  			if diags := stateLocker.Unlock(); diags.HasErrors() {
   117  				c.showDiagnostics(diags)
   118  			}
   119  		}()
   120  	}
   121  
   122  	if err := stateFromMgr.RefreshState(); err != nil {
   123  		c.Ui.Error(fmt.Sprintf("Failed to refresh source state: %s", err))
   124  		return 1
   125  	}
   126  
   127  	stateFrom := stateFromMgr.State()
   128  	if stateFrom == nil {
   129  		c.Ui.Error(errStateNotFound)
   130  		return 1
   131  	}
   132  
   133  	// Read the destination state
   134  	stateToMgr := stateFromMgr
   135  	stateTo := stateFrom
   136  
   137  	if statePathOut != "" {
   138  		c.statePath = statePathOut
   139  		c.backupPath = backupPathOut
   140  
   141  		stateToMgr, err = c.State(enc)
   142  		if err != nil {
   143  			c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
   144  			return 1
   145  		}
   146  
   147  		if c.stateLock {
   148  			stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
   149  			if diags := stateLocker.Lock(stateToMgr, "state-mv"); diags.HasErrors() {
   150  				c.showDiagnostics(diags)
   151  				return 1
   152  			}
   153  			defer func() {
   154  				if diags := stateLocker.Unlock(); diags.HasErrors() {
   155  					c.showDiagnostics(diags)
   156  				}
   157  			}()
   158  		}
   159  
   160  		if err := stateToMgr.RefreshState(); err != nil {
   161  			c.Ui.Error(fmt.Sprintf("Failed to refresh destination state: %s", err))
   162  			return 1
   163  		}
   164  
   165  		stateTo = stateToMgr.State()
   166  		if stateTo == nil {
   167  			stateTo = states.NewState()
   168  		}
   169  	}
   170  
   171  	var diags tfdiags.Diagnostics
   172  	sourceAddr, moreDiags := c.lookupSingleStateObjectAddr(stateFrom, args[0])
   173  	diags = diags.Append(moreDiags)
   174  	destAddr, moreDiags := c.lookupSingleStateObjectAddr(stateFrom, args[1])
   175  	diags = diags.Append(moreDiags)
   176  	if diags.HasErrors() {
   177  		c.showDiagnostics(diags)
   178  		return 1
   179  	}
   180  
   181  	prefix := "Move"
   182  	if dryRun {
   183  		prefix = "Would move"
   184  	}
   185  
   186  	const msgInvalidSource = "Invalid source address"
   187  	const msgInvalidTarget = "Invalid target address"
   188  
   189  	var moved int
   190  	ssFrom := stateFrom.SyncWrapper()
   191  	sourceAddrs := c.sourceObjectAddrs(stateFrom, sourceAddr)
   192  	if len(sourceAddrs) == 0 {
   193  		diags = diags.Append(tfdiags.Sourceless(
   194  			tfdiags.Error,
   195  			msgInvalidSource,
   196  			fmt.Sprintf("Cannot move %s: does not match anything in the current state.", sourceAddr),
   197  		))
   198  		c.showDiagnostics(diags)
   199  		return 1
   200  	}
   201  	for _, rawAddrFrom := range sourceAddrs {
   202  		switch addrFrom := rawAddrFrom.(type) {
   203  		case addrs.ModuleInstance:
   204  			search := sourceAddr.(addrs.ModuleInstance)
   205  			addrTo, ok := destAddr.(addrs.ModuleInstance)
   206  			if !ok {
   207  				diags = diags.Append(tfdiags.Sourceless(
   208  					tfdiags.Error,
   209  					msgInvalidTarget,
   210  					fmt.Sprintf("Cannot move %s to %s: the target must also be a module.", addrFrom, destAddr),
   211  				))
   212  				c.showDiagnostics(diags)
   213  				return 1
   214  			}
   215  
   216  			if len(search) < len(addrFrom) {
   217  				n := make(addrs.ModuleInstance, 0, len(addrTo)+len(addrFrom)-len(search))
   218  				n = append(n, addrTo...)
   219  				n = append(n, addrFrom[len(search):]...)
   220  				addrTo = n
   221  			}
   222  
   223  			if stateTo.Module(addrTo) != nil {
   224  				c.Ui.Error(fmt.Sprintf(errStateMv, "destination module already exists"))
   225  				return 1
   226  			}
   227  
   228  			ms := ssFrom.Module(addrFrom)
   229  			if ms == nil {
   230  				diags = diags.Append(tfdiags.Sourceless(
   231  					tfdiags.Error,
   232  					msgInvalidSource,
   233  					fmt.Sprintf("The current state does not contain %s.", addrFrom),
   234  				))
   235  				c.showDiagnostics(diags)
   236  				return 1
   237  			}
   238  
   239  			moved++
   240  			c.Ui.Output(fmt.Sprintf("%s %q to %q", prefix, addrFrom.String(), addrTo.String()))
   241  			if !dryRun {
   242  				ssFrom.RemoveModule(addrFrom)
   243  
   244  				// Update the address before adding it to the state.
   245  				ms.Addr = addrTo
   246  				stateTo.Modules[addrTo.String()] = ms
   247  			}
   248  
   249  		case addrs.AbsResource:
   250  			addrTo, ok := destAddr.(addrs.AbsResource)
   251  			if !ok {
   252  				diags = diags.Append(tfdiags.Sourceless(
   253  					tfdiags.Error,
   254  					msgInvalidTarget,
   255  					fmt.Sprintf("Cannot move %s to %s: the source is a whole resource (not a resource instance) so the target must also be a whole resource.", addrFrom, destAddr),
   256  				))
   257  				c.showDiagnostics(diags)
   258  				return 1
   259  			}
   260  			diags = diags.Append(c.validateResourceMove(addrFrom, addrTo))
   261  
   262  			if stateTo.Resource(addrTo) != nil {
   263  				diags = diags.Append(tfdiags.Sourceless(
   264  					tfdiags.Error,
   265  					msgInvalidTarget,
   266  					fmt.Sprintf("Cannot move to %s: there is already a resource at that address in the current state.", addrTo),
   267  				))
   268  			}
   269  
   270  			rs := ssFrom.Resource(addrFrom)
   271  			if rs == nil {
   272  				diags = diags.Append(tfdiags.Sourceless(
   273  					tfdiags.Error,
   274  					msgInvalidSource,
   275  					fmt.Sprintf("The current state does not contain %s.", addrFrom),
   276  				))
   277  			}
   278  
   279  			if diags.HasErrors() {
   280  				c.showDiagnostics(diags)
   281  				return 1
   282  			}
   283  
   284  			moved++
   285  			c.Ui.Output(fmt.Sprintf("%s %q to %q", prefix, addrFrom.String(), addrTo.String()))
   286  			if !dryRun {
   287  				ssFrom.RemoveResource(addrFrom)
   288  
   289  				// Update the address before adding it to the state.
   290  				rs.Addr = addrTo
   291  				stateTo.EnsureModule(addrTo.Module).Resources[addrTo.Resource.String()] = rs
   292  			}
   293  
   294  		case addrs.AbsResourceInstance:
   295  			addrTo, ok := destAddr.(addrs.AbsResourceInstance)
   296  			if !ok {
   297  				ra, ok := destAddr.(addrs.AbsResource)
   298  				if !ok {
   299  					diags = diags.Append(tfdiags.Sourceless(
   300  						tfdiags.Error,
   301  						msgInvalidTarget,
   302  						fmt.Sprintf("Cannot move %s to %s: the target must also be a resource instance.", addrFrom, destAddr),
   303  					))
   304  					c.showDiagnostics(diags)
   305  					return 1
   306  				}
   307  				addrTo = ra.Instance(addrs.NoKey)
   308  			}
   309  
   310  			diags = diags.Append(c.validateResourceMove(addrFrom.ContainingResource(), addrTo.ContainingResource()))
   311  
   312  			if stateTo.Module(addrTo.Module) == nil {
   313  				// moving something to a mew module, so we need to ensure it exists
   314  				stateTo.EnsureModule(addrTo.Module)
   315  			}
   316  			if stateTo.ResourceInstance(addrTo) != nil {
   317  				diags = diags.Append(tfdiags.Sourceless(
   318  					tfdiags.Error,
   319  					msgInvalidTarget,
   320  					fmt.Sprintf("Cannot move to %s: there is already a resource instance at that address in the current state.", addrTo),
   321  				))
   322  			}
   323  
   324  			is := ssFrom.ResourceInstance(addrFrom)
   325  			if is == nil {
   326  				diags = diags.Append(tfdiags.Sourceless(
   327  					tfdiags.Error,
   328  					msgInvalidSource,
   329  					fmt.Sprintf("The current state does not contain %s.", addrFrom),
   330  				))
   331  			}
   332  
   333  			if diags.HasErrors() {
   334  				c.showDiagnostics(diags)
   335  				return 1
   336  			}
   337  
   338  			moved++
   339  			c.Ui.Output(fmt.Sprintf("%s %q to %q", prefix, addrFrom.String(), args[1]))
   340  			if !dryRun {
   341  				fromResourceAddr := addrFrom.ContainingResource()
   342  				fromResource := ssFrom.Resource(fromResourceAddr)
   343  				fromProviderAddr := fromResource.ProviderConfig
   344  				ssFrom.ForgetResourceInstanceAll(addrFrom)
   345  				ssFrom.RemoveResourceIfEmpty(fromResourceAddr)
   346  
   347  				rs := stateTo.Resource(addrTo.ContainingResource())
   348  				if rs == nil {
   349  					// If we're moving to an address without an index then that
   350  					// suggests the user's intent is to establish both the
   351  					// resource and the instance at the same time (since the
   352  					// address covers both). If there's an index in the
   353  					// target then allow creating the new instance here.
   354  					resourceAddr := addrTo.ContainingResource()
   355  					stateTo.SyncWrapper().SetResourceProvider(
   356  						resourceAddr,
   357  						fromProviderAddr, // in this case, we bring the provider along as if we were moving the whole resource
   358  					)
   359  					rs = stateTo.Resource(resourceAddr)
   360  				}
   361  
   362  				rs.Instances[addrTo.Resource.Key] = is
   363  			}
   364  		default:
   365  			diags = diags.Append(tfdiags.Sourceless(
   366  				tfdiags.Error,
   367  				msgInvalidSource,
   368  				fmt.Sprintf("Cannot move %s: OpenTofu doesn't know how to move this object.", rawAddrFrom),
   369  			))
   370  		}
   371  
   372  		// Look for any dependencies that may be effected and
   373  		// remove them to ensure they are recreated in full.
   374  		for _, mod := range stateTo.Modules {
   375  			for _, res := range mod.Resources {
   376  				for _, ins := range res.Instances {
   377  					if ins.Current == nil {
   378  						continue
   379  					}
   380  
   381  					for _, dep := range ins.Current.Dependencies {
   382  						// check both directions here, since we may be moving
   383  						// an instance which is in a resource, or a module
   384  						// which can contain a resource.
   385  						if dep.TargetContains(rawAddrFrom) || rawAddrFrom.TargetContains(dep) {
   386  							ins.Current.Dependencies = nil
   387  							break
   388  						}
   389  					}
   390  				}
   391  			}
   392  		}
   393  	}
   394  
   395  	if dryRun {
   396  		if moved == 0 {
   397  			c.Ui.Output("Would have moved nothing.")
   398  		}
   399  		return 0 // This is as far as we go in dry-run mode
   400  	}
   401  
   402  	b, backendDiags := c.Backend(nil, enc.State())
   403  	diags = diags.Append(backendDiags)
   404  	if backendDiags.HasErrors() {
   405  		c.showDiagnostics(diags)
   406  		return 1
   407  	}
   408  
   409  	// Get schemas, if possible, before writing state
   410  	var schemas *tofu.Schemas
   411  	if isCloudMode(b) {
   412  		var schemaDiags tfdiags.Diagnostics
   413  		schemas, schemaDiags = c.MaybeGetSchemas(stateTo, nil)
   414  		diags = diags.Append(schemaDiags)
   415  	}
   416  
   417  	// Write the new state
   418  	if err := stateToMgr.WriteState(stateTo); err != nil {
   419  		c.Ui.Error(fmt.Sprintf(errStateRmPersist, err))
   420  		return 1
   421  	}
   422  	if err := stateToMgr.PersistState(schemas); err != nil {
   423  		c.Ui.Error(fmt.Sprintf(errStateRmPersist, err))
   424  		return 1
   425  	}
   426  
   427  	// Write the old state if it is different
   428  	if stateTo != stateFrom {
   429  		if err := stateFromMgr.WriteState(stateFrom); err != nil {
   430  			c.Ui.Error(fmt.Sprintf(errStateRmPersist, err))
   431  			return 1
   432  		}
   433  		if err := stateFromMgr.PersistState(schemas); err != nil {
   434  			c.Ui.Error(fmt.Sprintf(errStateRmPersist, err))
   435  			return 1
   436  		}
   437  	}
   438  
   439  	c.showDiagnostics(diags)
   440  
   441  	if moved == 0 {
   442  		c.Ui.Output("No matching objects found.")
   443  	} else {
   444  		c.Ui.Output(fmt.Sprintf("Successfully moved %d object(s).", moved))
   445  	}
   446  	return 0
   447  }
   448  
   449  // sourceObjectAddrs takes a single source object address and expands it to
   450  // potentially multiple objects that need to be handled within it.
   451  //
   452  // In particular, this handles the case where a module is requested directly:
   453  // if it has any child modules, then they must also be moved. It also resolves
   454  // the ambiguity that an index-less resource address could either be a resource
   455  // address or a resource instance address, by making a decision about which
   456  // is intended based on the current state of the resource in question.
   457  func (c *StateMvCommand) sourceObjectAddrs(state *states.State, matched addrs.Targetable) []addrs.Targetable {
   458  	var ret []addrs.Targetable
   459  
   460  	switch addr := matched.(type) {
   461  	case addrs.ModuleInstance:
   462  		for _, mod := range state.Modules {
   463  			if len(mod.Addr) < len(addr) {
   464  				continue // can't possibly be our selection or a child of it
   465  			}
   466  			if !mod.Addr[:len(addr)].Equal(addr) {
   467  				continue
   468  			}
   469  			ret = append(ret, mod.Addr)
   470  		}
   471  	case addrs.AbsResource:
   472  		// If this refers to a resource without "count" or "for_each" set then
   473  		// we'll assume the user intended it to be a resource instance
   474  		// address instead, to allow for requests like this:
   475  		//   tofu state mv aws_instance.foo aws_instance.bar[1]
   476  		// That wouldn't be allowed if aws_instance.foo had multiple instances
   477  		// since we can't move multiple instances into one.
   478  		if rs := state.Resource(addr); rs != nil {
   479  			if _, ok := rs.Instances[addrs.NoKey]; ok {
   480  				ret = append(ret, addr.Instance(addrs.NoKey))
   481  			} else {
   482  				ret = append(ret, addr)
   483  			}
   484  		}
   485  	default:
   486  		ret = append(ret, matched)
   487  	}
   488  
   489  	return ret
   490  }
   491  
   492  func (c *StateMvCommand) validateResourceMove(addrFrom, addrTo addrs.AbsResource) tfdiags.Diagnostics {
   493  	const msgInvalidRequest = "Invalid state move request"
   494  
   495  	var diags tfdiags.Diagnostics
   496  	if addrFrom.Resource.Mode != addrTo.Resource.Mode {
   497  		switch addrFrom.Resource.Mode {
   498  		case addrs.ManagedResourceMode:
   499  			diags = diags.Append(tfdiags.Sourceless(
   500  				tfdiags.Error,
   501  				msgInvalidRequest,
   502  				fmt.Sprintf("Cannot move %s to %s: a managed resource can be moved only to another managed resource address.", addrFrom, addrTo),
   503  			))
   504  		case addrs.DataResourceMode:
   505  			diags = diags.Append(tfdiags.Sourceless(
   506  				tfdiags.Error,
   507  				msgInvalidRequest,
   508  				fmt.Sprintf("Cannot move %s to %s: a data resource can be moved only to another data resource address.", addrFrom, addrTo),
   509  			))
   510  		default:
   511  			// In case a new mode is added in future, this unhelpful error is better than nothing.
   512  			diags = diags.Append(tfdiags.Sourceless(
   513  				tfdiags.Error,
   514  				msgInvalidRequest,
   515  				fmt.Sprintf("Cannot move %s to %s: cannot change resource mode.", addrFrom, addrTo),
   516  			))
   517  		}
   518  	}
   519  	if addrFrom.Resource.Type != addrTo.Resource.Type {
   520  		diags = diags.Append(tfdiags.Sourceless(
   521  			tfdiags.Error,
   522  			msgInvalidRequest,
   523  			fmt.Sprintf("Cannot move %s to %s: resource types don't match.", addrFrom, addrTo),
   524  		))
   525  	}
   526  	return diags
   527  }
   528  
   529  func (c *StateMvCommand) Help() string {
   530  	helpText := `
   531  Usage: tofu [global options] state (move|mv) [options] SOURCE DESTINATION
   532  
   533   This command will move an item matched by the address given to the
   534   destination address. This command can also move to a destination address
   535   in a completely different state file.
   536  
   537   This can be used for simple resource renaming, moving items to and from
   538   a module, moving entire modules, and more. And because this command can also
   539   move data to a completely new state, it can also be used for refactoring
   540   one configuration into multiple separately managed OpenTofu configurations.
   541  
   542   This command will output a backup copy of the state prior to saving any
   543   changes. The backup cannot be disabled. Due to the destructive nature
   544   of this command, backups are required.
   545  
   546   If you're moving an item to a different state file, a backup will be created
   547   for each state file.
   548  
   549  Options:
   550  
   551    -dry-run                If set, prints out what would've been moved but doesn't
   552                            actually move anything.
   553  
   554    -lock=false             Don't hold a state lock during the operation. This is
   555                            dangerous if others might concurrently run commands
   556                            against the same workspace.
   557  
   558    -lock-timeout=0s        Duration to retry a state lock.
   559  
   560    -ignore-remote-version  A rare option used for the remote backend only. See
   561                            the remote backend documentation for more information.
   562  
   563    -state, state-out, and -backup are legacy options supported for the local
   564    backend only. For more information, see the local backend's documentation.
   565  
   566  `
   567  	return strings.TrimSpace(helpText)
   568  }
   569  
   570  func (c *StateMvCommand) Synopsis() string {
   571  	return "Move an item in the state"
   572  }
   573  
   574  const errStateMv = `Error moving state: %s
   575  
   576  Please ensure your addresses and state paths are valid. No
   577  state was persisted. Your existing states are untouched.`