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