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