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