| package command |
| |
| import ( |
| "fmt" |
| "strings" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/backend" |
| "github.com/hashicorp/terraform/internal/command/arguments" |
| "github.com/hashicorp/terraform/internal/command/clistate" |
| "github.com/hashicorp/terraform/internal/command/views" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/terraform" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/mitchellh/cli" |
| ) |
| |
| // StateMvCommand is a Command implementation that shows a single resource. |
| type StateMvCommand struct { |
| StateMeta |
| } |
| |
| func (c *StateMvCommand) Run(args []string) int { |
| args = c.Meta.process(args) |
| // We create two metas to track the two states |
| var backupPathOut, statePathOut string |
| |
| var dryRun bool |
| cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state mv") |
| cmdFlags.BoolVar(&dryRun, "dry-run", false, "dry run") |
| cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup") |
| cmdFlags.StringVar(&backupPathOut, "backup-out", "-", "backup") |
| cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock states") |
| cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") |
| cmdFlags.StringVar(&c.statePath, "state", "", "path") |
| cmdFlags.StringVar(&statePathOut, "state-out", "", "path") |
| if err := cmdFlags.Parse(args); err != nil { |
| c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) |
| return 1 |
| } |
| args = cmdFlags.Args() |
| if len(args) != 2 { |
| c.Ui.Error("Exactly two arguments expected.\n") |
| return cli.RunResultHelp |
| } |
| |
| if diags := c.Meta.checkRequiredVersion(); diags != nil { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| // If backup or backup-out options are set |
| // and the state option is not set, make sure |
| // the backend is local |
| backupOptionSetWithoutStateOption := c.backupPath != "-" && c.statePath == "" |
| backupOutOptionSetWithoutStateOption := backupPathOut != "-" && c.statePath == "" |
| |
| var setLegacyLocalBackendOptions []string |
| if backupOptionSetWithoutStateOption { |
| setLegacyLocalBackendOptions = append(setLegacyLocalBackendOptions, "-backup") |
| } |
| if backupOutOptionSetWithoutStateOption { |
| setLegacyLocalBackendOptions = append(setLegacyLocalBackendOptions, "-backup-out") |
| } |
| |
| if len(setLegacyLocalBackendOptions) > 0 { |
| currentBackend, diags := c.backendFromConfig(&BackendOpts{}) |
| if diags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| // If currentBackend is nil and diags didn't have errors, |
| // this means we have an implicit local backend |
| _, isLocalBackend := currentBackend.(backend.Local) |
| if currentBackend != nil && !isLocalBackend { |
| diags = diags.Append( |
| tfdiags.Sourceless( |
| tfdiags.Error, |
| fmt.Sprintf("Invalid command line options: %s", strings.Join(setLegacyLocalBackendOptions[:], ", ")), |
| "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.", |
| ), |
| ) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| } |
| |
| // Read the from state |
| stateFromMgr, err := c.State() |
| if err != nil { |
| c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) |
| return 1 |
| } |
| |
| if c.stateLock { |
| stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) |
| if diags := stateLocker.Lock(stateFromMgr, "state-mv"); diags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| defer func() { |
| if diags := stateLocker.Unlock(); diags.HasErrors() { |
| c.showDiagnostics(diags) |
| } |
| }() |
| } |
| |
| if err := stateFromMgr.RefreshState(); err != nil { |
| c.Ui.Error(fmt.Sprintf("Failed to refresh source state: %s", err)) |
| return 1 |
| } |
| |
| stateFrom := stateFromMgr.State() |
| if stateFrom == nil { |
| c.Ui.Error(errStateNotFound) |
| return 1 |
| } |
| |
| // Read the destination state |
| stateToMgr := stateFromMgr |
| stateTo := stateFrom |
| |
| if statePathOut != "" { |
| c.statePath = statePathOut |
| c.backupPath = backupPathOut |
| |
| stateToMgr, err = c.State() |
| if err != nil { |
| c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) |
| return 1 |
| } |
| |
| if c.stateLock { |
| stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) |
| if diags := stateLocker.Lock(stateToMgr, "state-mv"); diags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| defer func() { |
| if diags := stateLocker.Unlock(); diags.HasErrors() { |
| c.showDiagnostics(diags) |
| } |
| }() |
| } |
| |
| if err := stateToMgr.RefreshState(); err != nil { |
| c.Ui.Error(fmt.Sprintf("Failed to refresh destination state: %s", err)) |
| return 1 |
| } |
| |
| stateTo = stateToMgr.State() |
| if stateTo == nil { |
| stateTo = states.NewState() |
| } |
| } |
| |
| var diags tfdiags.Diagnostics |
| sourceAddr, moreDiags := c.lookupSingleStateObjectAddr(stateFrom, args[0]) |
| diags = diags.Append(moreDiags) |
| destAddr, moreDiags := c.lookupSingleStateObjectAddr(stateFrom, args[1]) |
| diags = diags.Append(moreDiags) |
| if diags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| prefix := "Move" |
| if dryRun { |
| prefix = "Would move" |
| } |
| |
| const msgInvalidSource = "Invalid source address" |
| const msgInvalidTarget = "Invalid target address" |
| |
| var moved int |
| ssFrom := stateFrom.SyncWrapper() |
| sourceAddrs := c.sourceObjectAddrs(stateFrom, sourceAddr) |
| if len(sourceAddrs) == 0 { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidSource, |
| fmt.Sprintf("Cannot move %s: does not match anything in the current state.", sourceAddr), |
| )) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| for _, rawAddrFrom := range sourceAddrs { |
| switch addrFrom := rawAddrFrom.(type) { |
| case addrs.ModuleInstance: |
| search := sourceAddr.(addrs.ModuleInstance) |
| addrTo, ok := destAddr.(addrs.ModuleInstance) |
| if !ok { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidTarget, |
| fmt.Sprintf("Cannot move %s to %s: the target must also be a module.", addrFrom, destAddr), |
| )) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| if len(search) < len(addrFrom) { |
| n := make(addrs.ModuleInstance, 0, len(addrTo)+len(addrFrom)-len(search)) |
| n = append(n, addrTo...) |
| n = append(n, addrFrom[len(search):]...) |
| addrTo = n |
| } |
| |
| if stateTo.Module(addrTo) != nil { |
| c.Ui.Error(fmt.Sprintf(errStateMv, "destination module already exists")) |
| return 1 |
| } |
| |
| ms := ssFrom.Module(addrFrom) |
| if ms == nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidSource, |
| fmt.Sprintf("The current state does not contain %s.", addrFrom), |
| )) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| moved++ |
| c.Ui.Output(fmt.Sprintf("%s %q to %q", prefix, addrFrom.String(), addrTo.String())) |
| if !dryRun { |
| ssFrom.RemoveModule(addrFrom) |
| |
| // Update the address before adding it to the state. |
| ms.Addr = addrTo |
| stateTo.Modules[addrTo.String()] = ms |
| } |
| |
| case addrs.AbsResource: |
| addrTo, ok := destAddr.(addrs.AbsResource) |
| if !ok { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidTarget, |
| 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), |
| )) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| diags = diags.Append(c.validateResourceMove(addrFrom, addrTo)) |
| |
| if stateTo.Resource(addrTo) != nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidTarget, |
| fmt.Sprintf("Cannot move to %s: there is already a resource at that address in the current state.", addrTo), |
| )) |
| } |
| |
| rs := ssFrom.Resource(addrFrom) |
| if rs == nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidSource, |
| fmt.Sprintf("The current state does not contain %s.", addrFrom), |
| )) |
| } |
| |
| if diags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| moved++ |
| c.Ui.Output(fmt.Sprintf("%s %q to %q", prefix, addrFrom.String(), addrTo.String())) |
| if !dryRun { |
| ssFrom.RemoveResource(addrFrom) |
| |
| // Update the address before adding it to the state. |
| rs.Addr = addrTo |
| stateTo.EnsureModule(addrTo.Module).Resources[addrTo.Resource.String()] = rs |
| } |
| |
| case addrs.AbsResourceInstance: |
| addrTo, ok := destAddr.(addrs.AbsResourceInstance) |
| if !ok { |
| ra, ok := destAddr.(addrs.AbsResource) |
| if !ok { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidTarget, |
| fmt.Sprintf("Cannot move %s to %s: the target must also be a resource instance.", addrFrom, destAddr), |
| )) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| addrTo = ra.Instance(addrs.NoKey) |
| } |
| |
| diags = diags.Append(c.validateResourceMove(addrFrom.ContainingResource(), addrTo.ContainingResource())) |
| |
| if stateTo.Module(addrTo.Module) == nil { |
| // moving something to a mew module, so we need to ensure it exists |
| stateTo.EnsureModule(addrTo.Module) |
| } |
| if stateTo.ResourceInstance(addrTo) != nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidTarget, |
| fmt.Sprintf("Cannot move to %s: there is already a resource instance at that address in the current state.", addrTo), |
| )) |
| } |
| |
| is := ssFrom.ResourceInstance(addrFrom) |
| if is == nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidSource, |
| fmt.Sprintf("The current state does not contain %s.", addrFrom), |
| )) |
| } |
| |
| if diags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| moved++ |
| c.Ui.Output(fmt.Sprintf("%s %q to %q", prefix, addrFrom.String(), args[1])) |
| if !dryRun { |
| fromResourceAddr := addrFrom.ContainingResource() |
| fromResource := ssFrom.Resource(fromResourceAddr) |
| fromProviderAddr := fromResource.ProviderConfig |
| ssFrom.ForgetResourceInstanceAll(addrFrom) |
| ssFrom.RemoveResourceIfEmpty(fromResourceAddr) |
| |
| rs := stateTo.Resource(addrTo.ContainingResource()) |
| if rs == nil { |
| // If we're moving to an address without an index then that |
| // suggests the user's intent is to establish both the |
| // resource and the instance at the same time (since the |
| // address covers both). If there's an index in the |
| // target then allow creating the new instance here. |
| resourceAddr := addrTo.ContainingResource() |
| stateTo.SyncWrapper().SetResourceProvider( |
| resourceAddr, |
| fromProviderAddr, // in this case, we bring the provider along as if we were moving the whole resource |
| ) |
| rs = stateTo.Resource(resourceAddr) |
| } |
| |
| rs.Instances[addrTo.Resource.Key] = is |
| } |
| default: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidSource, |
| fmt.Sprintf("Cannot move %s: Terraform doesn't know how to move this object.", rawAddrFrom), |
| )) |
| } |
| |
| // Look for any dependencies that may be effected and |
| // remove them to ensure they are recreated in full. |
| for _, mod := range stateTo.Modules { |
| for _, res := range mod.Resources { |
| for _, ins := range res.Instances { |
| if ins.Current == nil { |
| continue |
| } |
| |
| for _, dep := range ins.Current.Dependencies { |
| // check both directions here, since we may be moving |
| // an instance which is in a resource, or a module |
| // which can contain a resource. |
| if dep.TargetContains(rawAddrFrom) || rawAddrFrom.TargetContains(dep) { |
| ins.Current.Dependencies = nil |
| break |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| if dryRun { |
| if moved == 0 { |
| c.Ui.Output("Would have moved nothing.") |
| } |
| return 0 // This is as far as we go in dry-run mode |
| } |
| |
| b, backendDiags := c.Backend(nil) |
| diags = diags.Append(backendDiags) |
| if backendDiags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| // Get schemas, if possible, before writing state |
| var schemas *terraform.Schemas |
| if isCloudMode(b) { |
| var schemaDiags tfdiags.Diagnostics |
| schemas, schemaDiags = c.MaybeGetSchemas(stateTo, nil) |
| diags = diags.Append(schemaDiags) |
| } |
| |
| // Write the new state |
| if err := stateToMgr.WriteState(stateTo); err != nil { |
| c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) |
| return 1 |
| } |
| if err := stateToMgr.PersistState(schemas); err != nil { |
| c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) |
| return 1 |
| } |
| |
| // Write the old state if it is different |
| if stateTo != stateFrom { |
| if err := stateFromMgr.WriteState(stateFrom); err != nil { |
| c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) |
| return 1 |
| } |
| if err := stateFromMgr.PersistState(schemas); err != nil { |
| c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) |
| return 1 |
| } |
| } |
| |
| c.showDiagnostics(diags) |
| |
| if moved == 0 { |
| c.Ui.Output("No matching objects found.") |
| } else { |
| c.Ui.Output(fmt.Sprintf("Successfully moved %d object(s).", moved)) |
| } |
| return 0 |
| } |
| |
| // sourceObjectAddrs takes a single source object address and expands it to |
| // potentially multiple objects that need to be handled within it. |
| // |
| // In particular, this handles the case where a module is requested directly: |
| // if it has any child modules, then they must also be moved. It also resolves |
| // the ambiguity that an index-less resource address could either be a resource |
| // address or a resource instance address, by making a decision about which |
| // is intended based on the current state of the resource in question. |
| func (c *StateMvCommand) sourceObjectAddrs(state *states.State, matched addrs.Targetable) []addrs.Targetable { |
| var ret []addrs.Targetable |
| |
| switch addr := matched.(type) { |
| case addrs.ModuleInstance: |
| for _, mod := range state.Modules { |
| if len(mod.Addr) < len(addr) { |
| continue // can't possibly be our selection or a child of it |
| } |
| if !mod.Addr[:len(addr)].Equal(addr) { |
| continue |
| } |
| ret = append(ret, mod.Addr) |
| } |
| case addrs.AbsResource: |
| // If this refers to a resource without "count" or "for_each" set then |
| // we'll assume the user intended it to be a resource instance |
| // address instead, to allow for requests like this: |
| // terraform state mv aws_instance.foo aws_instance.bar[1] |
| // That wouldn't be allowed if aws_instance.foo had multiple instances |
| // since we can't move multiple instances into one. |
| if rs := state.Resource(addr); rs != nil { |
| if _, ok := rs.Instances[addrs.NoKey]; ok { |
| ret = append(ret, addr.Instance(addrs.NoKey)) |
| } else { |
| ret = append(ret, addr) |
| } |
| } |
| default: |
| ret = append(ret, matched) |
| } |
| |
| return ret |
| } |
| |
| func (c *StateMvCommand) validateResourceMove(addrFrom, addrTo addrs.AbsResource) tfdiags.Diagnostics { |
| const msgInvalidRequest = "Invalid state move request" |
| |
| var diags tfdiags.Diagnostics |
| if addrFrom.Resource.Mode != addrTo.Resource.Mode { |
| switch addrFrom.Resource.Mode { |
| case addrs.ManagedResourceMode: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidRequest, |
| fmt.Sprintf("Cannot move %s to %s: a managed resource can be moved only to another managed resource address.", addrFrom, addrTo), |
| )) |
| case addrs.DataResourceMode: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidRequest, |
| fmt.Sprintf("Cannot move %s to %s: a data resource can be moved only to another data resource address.", addrFrom, addrTo), |
| )) |
| default: |
| // In case a new mode is added in future, this unhelpful error is better than nothing. |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidRequest, |
| fmt.Sprintf("Cannot move %s to %s: cannot change resource mode.", addrFrom, addrTo), |
| )) |
| } |
| } |
| if addrFrom.Resource.Type != addrTo.Resource.Type { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| msgInvalidRequest, |
| fmt.Sprintf("Cannot move %s to %s: resource types don't match.", addrFrom, addrTo), |
| )) |
| } |
| return diags |
| } |
| |
| func (c *StateMvCommand) Help() string { |
| helpText := ` |
| Usage: terraform [global options] state mv [options] SOURCE DESTINATION |
| |
| This command will move an item matched by the address given to the |
| destination address. This command can also move to a destination address |
| in a completely different state file. |
| |
| This can be used for simple resource renaming, moving items to and from |
| a module, moving entire modules, and more. And because this command can also |
| move data to a completely new state, it can also be used for refactoring |
| one configuration into multiple separately managed Terraform configurations. |
| |
| This command will output a backup copy of the state prior to saving any |
| changes. The backup cannot be disabled. Due to the destructive nature |
| of this command, backups are required. |
| |
| If you're moving an item to a different state file, a backup will be created |
| for each state file. |
| |
| Options: |
| |
| -dry-run If set, prints out what would've been moved but doesn't |
| actually move anything. |
| |
| -lock=false Don't hold a state lock during the operation. This is |
| dangerous if others might concurrently run commands |
| against the same workspace. |
| |
| -lock-timeout=0s Duration to retry a state lock. |
| |
| -ignore-remote-version A rare option used for the remote backend only. See |
| the remote backend documentation for more information. |
| |
| -state, state-out, and -backup are legacy options supported for the local |
| backend only. For more information, see the local backend's documentation. |
| |
| ` |
| return strings.TrimSpace(helpText) |
| } |
| |
| func (c *StateMvCommand) Synopsis() string { |
| return "Move an item in the state" |
| } |
| |
| const errStateMv = `Error moving state: %s |
| |
| Please ensure your addresses and state paths are valid. No |
| state was persisted. Your existing states are untouched.` |