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