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