github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/command/state_mv.go (about)

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