github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/state_mv.go (about)

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