github.com/pulumi/terraform@v1.4.0/pkg/command/state_mv.go (about)

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