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