github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/state_mv.go (about)

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