github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/state_mv.go (about)

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