github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/command/meta_backend_migrate.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io/ioutil" 9 "log" 10 "os" 11 "path/filepath" 12 "sort" 13 "strings" 14 15 "github.com/hashicorp/terraform/internal/backend" 16 "github.com/hashicorp/terraform/internal/backend/remote" 17 "github.com/hashicorp/terraform/internal/cloud" 18 "github.com/hashicorp/terraform/internal/command/arguments" 19 "github.com/hashicorp/terraform/internal/command/clistate" 20 "github.com/hashicorp/terraform/internal/command/views" 21 "github.com/hashicorp/terraform/internal/states" 22 "github.com/hashicorp/terraform/internal/states/statemgr" 23 "github.com/hashicorp/terraform/internal/terraform" 24 ) 25 26 type backendMigrateOpts struct { 27 SourceType, DestinationType string 28 Source, Destination backend.Backend 29 30 // Fields below are set internally when migrate is called 31 32 sourceWorkspace string 33 destinationWorkspace string 34 force bool // if true, won't ask for confirmation 35 } 36 37 // backendMigrateState handles migrating (copying) state from one backend 38 // to another. This function handles asking the user for confirmation 39 // as well as the copy itself. 40 // 41 // This function can handle all scenarios of state migration regardless 42 // of the existence of state in either backend. 43 // 44 // After migrating the state, the existing state in the first backend 45 // remains untouched. 46 // 47 // This will attempt to lock both states for the migration. 48 func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { 49 log.Printf("[INFO] backendMigrateState: need to migrate from %q to %q backend config", opts.SourceType, opts.DestinationType) 50 // We need to check what the named state status is. If we're converting 51 // from multi-state to single-state for example, we need to handle that. 52 var sourceSingleState, destinationSingleState, sourceTFC, destinationTFC bool 53 54 _, sourceTFC = opts.Source.(*cloud.Cloud) 55 _, destinationTFC = opts.Destination.(*cloud.Cloud) 56 57 sourceWorkspaces, sourceSingleState, err := retrieveWorkspaces(opts.Source, opts.SourceType) 58 if err != nil { 59 return err 60 } 61 destinationWorkspaces, destinationSingleState, err := retrieveWorkspaces(opts.Destination, opts.SourceType) 62 if err != nil { 63 return err 64 } 65 66 // Set up defaults 67 opts.sourceWorkspace = backend.DefaultStateName 68 opts.destinationWorkspace = backend.DefaultStateName 69 opts.force = m.forceInitCopy 70 71 // Disregard remote Terraform version for the state source backend. If it's a 72 // Terraform Cloud remote backend, we don't care about the remote version, 73 // as we are migrating away and will not break a remote workspace. 74 m.ignoreRemoteVersionConflict(opts.Source) 75 76 // Disregard remote Terraform version if instructed to do so via CLI flag. 77 if m.ignoreRemoteVersion { 78 m.ignoreRemoteVersionConflict(opts.Destination) 79 } else { 80 // Check the remote Terraform version for the state destination backend. If 81 // it's a Terraform Cloud remote backend, we want to ensure that we don't 82 // break the workspace by uploading an incompatible state file. 83 for _, workspace := range destinationWorkspaces { 84 diags := m.remoteVersionCheck(opts.Destination, workspace) 85 if diags.HasErrors() { 86 return diags.Err() 87 } 88 } 89 // If there are no specified destination workspaces, perform a remote 90 // backend version check with the default workspace. 91 // Ensure that we are not dealing with Terraform Cloud migrations, as it 92 // does not support the default name. 93 if len(destinationWorkspaces) == 0 && !destinationTFC { 94 diags := m.remoteVersionCheck(opts.Destination, backend.DefaultStateName) 95 if diags.HasErrors() { 96 return diags.Err() 97 } 98 } 99 } 100 101 // Determine migration behavior based on whether the source/destination 102 // supports multi-state. 103 switch { 104 case sourceTFC || destinationTFC: 105 return m.backendMigrateTFC(opts) 106 107 // Single-state to single-state. This is the easiest case: we just 108 // copy the default state directly. 109 case sourceSingleState && destinationSingleState: 110 return m.backendMigrateState_s_s(opts) 111 112 // Single-state to multi-state. This is easy since we just copy 113 // the default state and ignore the rest in the destination. 114 case sourceSingleState && !destinationSingleState: 115 return m.backendMigrateState_s_s(opts) 116 117 // Multi-state to single-state. If the source has more than the default 118 // state this is complicated since we have to ask the user what to do. 119 case !sourceSingleState && destinationSingleState: 120 // If the source only has one state and it is the default, 121 // treat it as if it doesn't support multi-state. 122 if len(sourceWorkspaces) == 1 && sourceWorkspaces[0] == backend.DefaultStateName { 123 return m.backendMigrateState_s_s(opts) 124 } 125 126 return m.backendMigrateState_S_s(opts) 127 128 // Multi-state to multi-state. We merge the states together (migrating 129 // each from the source to the destination one by one). 130 case !sourceSingleState && !destinationSingleState: 131 // If the source only has one state and it is the default, 132 // treat it as if it doesn't support multi-state. 133 if len(sourceWorkspaces) == 1 && sourceWorkspaces[0] == backend.DefaultStateName { 134 return m.backendMigrateState_s_s(opts) 135 } 136 137 return m.backendMigrateState_S_S(opts) 138 } 139 140 return nil 141 } 142 143 //------------------------------------------------------------------- 144 // State Migration Scenarios 145 // 146 // The functions below cover handling all the various scenarios that 147 // can exist when migrating state. They are named in an immediately not 148 // obvious format but is simple: 149 // 150 // Format: backendMigrateState_s1_s2[_suffix] 151 // 152 // When s1 or s2 is lower case, it means that it is a single state backend. 153 // When either is uppercase, it means that state is a multi-state backend. 154 // The suffix is used to disambiguate multiple cases with the same type of 155 // states. 156 // 157 //------------------------------------------------------------------- 158 159 // Multi-state to multi-state. 160 func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error { 161 log.Print("[INFO] backendMigrateState: migrating all named workspaces") 162 163 migrate := opts.force 164 if !migrate { 165 var err error 166 // Ask the user if they want to migrate their existing remote state 167 migrate, err = m.confirm(&terraform.InputOpts{ 168 Id: "backend-migrate-multistate-to-multistate", 169 Query: fmt.Sprintf( 170 "Do you want to migrate all workspaces to %q?", 171 opts.DestinationType), 172 Description: fmt.Sprintf( 173 strings.TrimSpace(inputBackendMigrateMultiToMulti), 174 opts.SourceType, opts.DestinationType), 175 }) 176 if err != nil { 177 return fmt.Errorf( 178 "Error asking for state migration action: %s", err) 179 } 180 } 181 if !migrate { 182 return fmt.Errorf("Migration aborted by user.") 183 } 184 185 // Read all the states 186 sourceWorkspaces, err := opts.Source.Workspaces() 187 if err != nil { 188 return fmt.Errorf(strings.TrimSpace( 189 errMigrateLoadStates), opts.SourceType, err) 190 } 191 192 // Sort the states so they're always copied alphabetically 193 sort.Strings(sourceWorkspaces) 194 195 // Go through each and migrate 196 for _, name := range sourceWorkspaces { 197 // Copy the same names 198 opts.sourceWorkspace = name 199 opts.destinationWorkspace = name 200 201 // Force it, we confirmed above 202 opts.force = true 203 204 // Perform the migration 205 if err := m.backendMigrateState_s_s(opts); err != nil { 206 return fmt.Errorf(strings.TrimSpace( 207 errMigrateMulti), name, opts.SourceType, opts.DestinationType, err) 208 } 209 } 210 211 return nil 212 } 213 214 // Multi-state to single state. 215 func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { 216 log.Printf("[INFO] backendMigrateState: destination backend type %q does not support named workspaces", opts.DestinationType) 217 218 currentWorkspace, err := m.Workspace() 219 if err != nil { 220 return err 221 } 222 223 migrate := opts.force 224 if !migrate { 225 var err error 226 // Ask the user if they want to migrate their existing remote state 227 migrate, err = m.confirm(&terraform.InputOpts{ 228 Id: "backend-migrate-multistate-to-single", 229 Query: fmt.Sprintf( 230 "Destination state %q doesn't support workspaces.\n"+ 231 "Do you want to copy only your current workspace?", 232 opts.DestinationType), 233 Description: fmt.Sprintf( 234 strings.TrimSpace(inputBackendMigrateMultiToSingle), 235 opts.SourceType, opts.DestinationType, currentWorkspace), 236 }) 237 if err != nil { 238 return fmt.Errorf( 239 "Error asking for state migration action: %s", err) 240 } 241 } 242 243 if !migrate { 244 return fmt.Errorf("Migration aborted by user.") 245 } 246 247 // Copy the default state 248 opts.sourceWorkspace = currentWorkspace 249 250 // now switch back to the default env so we can acccess the new backend 251 m.SetWorkspace(backend.DefaultStateName) 252 253 return m.backendMigrateState_s_s(opts) 254 } 255 256 // Single state to single state, assumed default state name. 257 func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { 258 log.Printf("[INFO] backendMigrateState: single-to-single migrating %q workspace to %q workspace", opts.sourceWorkspace, opts.destinationWorkspace) 259 260 sourceState, err := opts.Source.StateMgr(opts.sourceWorkspace) 261 if err != nil { 262 return fmt.Errorf(strings.TrimSpace( 263 errMigrateSingleLoadDefault), opts.SourceType, err) 264 } 265 if err := sourceState.RefreshState(); err != nil { 266 return fmt.Errorf(strings.TrimSpace( 267 errMigrateSingleLoadDefault), opts.SourceType, err) 268 } 269 270 // Do not migrate workspaces without state. 271 if sourceState.State().Empty() { 272 log.Print("[TRACE] backendMigrateState: source workspace has empty state, so nothing to migrate") 273 return nil 274 } 275 276 destinationState, err := opts.Destination.StateMgr(opts.destinationWorkspace) 277 if err == backend.ErrDefaultWorkspaceNotSupported { 278 // If the backend doesn't support using the default state, we ask the user 279 // for a new name and migrate the default state to the given named state. 280 destinationState, err = func() (statemgr.Full, error) { 281 log.Print("[TRACE] backendMigrateState: destination doesn't support a default workspace, so we must prompt for a new name") 282 name, err := m.promptNewWorkspaceName(opts.DestinationType) 283 if err != nil { 284 return nil, err 285 } 286 287 // Update the name of the destination state. 288 opts.destinationWorkspace = name 289 290 destinationState, err := opts.Destination.StateMgr(opts.destinationWorkspace) 291 if err != nil { 292 return nil, err 293 } 294 295 // Ignore invalid workspace name as it is irrelevant in this context. 296 workspace, _ := m.Workspace() 297 298 // If the currently selected workspace is the default workspace, then set 299 // the named workspace as the new selected workspace. 300 if workspace == backend.DefaultStateName { 301 if err := m.SetWorkspace(opts.destinationWorkspace); err != nil { 302 return nil, fmt.Errorf("Failed to set new workspace: %s", err) 303 } 304 } 305 306 return destinationState, nil 307 }() 308 } 309 if err != nil { 310 return fmt.Errorf(strings.TrimSpace( 311 errMigrateSingleLoadDefault), opts.DestinationType, err) 312 } 313 if err := destinationState.RefreshState(); err != nil { 314 return fmt.Errorf(strings.TrimSpace( 315 errMigrateSingleLoadDefault), opts.DestinationType, err) 316 } 317 318 // Check if we need migration at all. 319 // This is before taking a lock, because they may also correspond to the same lock. 320 source := sourceState.State() 321 destination := destinationState.State() 322 323 // no reason to migrate if the state is already there 324 if source.Equal(destination) { 325 // Equal isn't identical; it doesn't check lineage. 326 sm1, _ := sourceState.(statemgr.PersistentMeta) 327 sm2, _ := destinationState.(statemgr.PersistentMeta) 328 if source != nil && destination != nil { 329 if sm1 == nil || sm2 == nil { 330 log.Print("[TRACE] backendMigrateState: both source and destination workspaces have no state, so no migration is needed") 331 return nil 332 } 333 if sm1.StateSnapshotMeta().Lineage == sm2.StateSnapshotMeta().Lineage { 334 log.Printf("[TRACE] backendMigrateState: both source and destination workspaces have equal state with lineage %q, so no migration is needed", sm1.StateSnapshotMeta().Lineage) 335 return nil 336 } 337 } 338 } 339 340 if m.stateLock { 341 lockCtx := context.Background() 342 343 view := views.NewStateLocker(arguments.ViewHuman, m.View) 344 locker := clistate.NewLocker(m.stateLockTimeout, view) 345 346 lockerSource := locker.WithContext(lockCtx) 347 if diags := lockerSource.Lock(sourceState, "migration source state"); diags.HasErrors() { 348 return diags.Err() 349 } 350 defer lockerSource.Unlock() 351 352 lockerDestination := locker.WithContext(lockCtx) 353 if diags := lockerDestination.Lock(destinationState, "migration destination state"); diags.HasErrors() { 354 return diags.Err() 355 } 356 defer lockerDestination.Unlock() 357 358 // We now own a lock, so double check that we have the version 359 // corresponding to the lock. 360 log.Print("[TRACE] backendMigrateState: refreshing source workspace state") 361 if err := sourceState.RefreshState(); err != nil { 362 return fmt.Errorf(strings.TrimSpace( 363 errMigrateSingleLoadDefault), opts.SourceType, err) 364 } 365 log.Print("[TRACE] backendMigrateState: refreshing destination workspace state") 366 if err := destinationState.RefreshState(); err != nil { 367 return fmt.Errorf(strings.TrimSpace( 368 errMigrateSingleLoadDefault), opts.SourceType, err) 369 } 370 371 source = sourceState.State() 372 destination = destinationState.State() 373 } 374 375 var confirmFunc func(statemgr.Full, statemgr.Full, *backendMigrateOpts) (bool, error) 376 switch { 377 // No migration necessary 378 case source.Empty() && destination.Empty(): 379 log.Print("[TRACE] backendMigrateState: both source and destination workspaces have empty state, so no migration is required") 380 return nil 381 382 // No migration necessary if we're inheriting state. 383 case source.Empty() && !destination.Empty(): 384 log.Print("[TRACE] backendMigrateState: source workspace has empty state, so no migration is required") 385 return nil 386 387 // We have existing state moving into no state. Ask the user if 388 // they'd like to do this. 389 case !source.Empty() && destination.Empty(): 390 if opts.SourceType == "cloud" || opts.DestinationType == "cloud" { 391 // HACK: backendMigrateTFC has its own earlier prompt for 392 // whether to migrate state in the cloud case, so we'll skip 393 // this later prompt for Cloud, even though we do still need it 394 // for state backends. 395 confirmFunc = func(statemgr.Full, statemgr.Full, *backendMigrateOpts) (bool, error) { 396 return true, nil // the answer is implied to be "yes" if we reached this point 397 } 398 } else { 399 log.Print("[TRACE] backendMigrateState: destination workspace has empty state, so might copy source workspace state") 400 confirmFunc = m.backendMigrateEmptyConfirm 401 } 402 403 // Both states are non-empty, meaning we need to determine which 404 // state should be used and update accordingly. 405 case !source.Empty() && !destination.Empty(): 406 log.Print("[TRACE] backendMigrateState: both source and destination workspaces have states, so might overwrite destination with source") 407 confirmFunc = m.backendMigrateNonEmptyConfirm 408 } 409 410 if confirmFunc == nil { 411 panic("confirmFunc must not be nil") 412 } 413 414 if !opts.force { 415 // Abort if we can't ask for input. 416 if !m.input { 417 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 418 return errors.New(strings.TrimSpace(errInteractiveInputDisabled)) 419 } 420 421 // Confirm with the user whether we want to copy state over 422 confirm, err := confirmFunc(sourceState, destinationState, opts) 423 if err != nil { 424 log.Print("[TRACE] backendMigrateState: error reading input, so aborting migration") 425 return err 426 } 427 if !confirm { 428 log.Print("[TRACE] backendMigrateState: user cancelled at confirmation prompt, so aborting migration") 429 return nil 430 } 431 } 432 433 // Confirmed! We'll have the statemgr package handle the migration, which 434 // includes preserving any lineage/serial information where possible, if 435 // both managers support such metadata. 436 log.Print("[TRACE] backendMigrateState: migration confirmed, so migrating") 437 if err := statemgr.Migrate(destinationState, sourceState); err != nil { 438 return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), 439 opts.SourceType, opts.DestinationType, err) 440 } 441 // The backend is currently handled before providers are installed during init, 442 // so requiring schemas here could lead to a catch-22 where it requires some manual 443 // intervention to proceed far enough for provider installation. To avoid this, 444 // when migrating to TFC backend, the initial JSON varient of state won't be generated and stored. 445 if err := destinationState.PersistState(nil); err != nil { 446 return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), 447 opts.SourceType, opts.DestinationType, err) 448 } 449 450 // And we're done. 451 return nil 452 } 453 454 func (m *Meta) backendMigrateEmptyConfirm(source, destination statemgr.Full, opts *backendMigrateOpts) (bool, error) { 455 var inputOpts *terraform.InputOpts 456 if opts.DestinationType == "cloud" { 457 inputOpts = &terraform.InputOpts{ 458 Id: "backend-migrate-copy-to-empty-cloud", 459 Query: "Do you want to copy existing state to Terraform Cloud?", 460 Description: fmt.Sprintf(strings.TrimSpace(inputBackendMigrateEmptyCloud), opts.SourceType), 461 } 462 } else { 463 inputOpts = &terraform.InputOpts{ 464 Id: "backend-migrate-copy-to-empty", 465 Query: "Do you want to copy existing state to the new backend?", 466 Description: fmt.Sprintf( 467 strings.TrimSpace(inputBackendMigrateEmpty), 468 opts.SourceType, opts.DestinationType), 469 } 470 } 471 472 return m.confirm(inputOpts) 473 } 474 475 func (m *Meta) backendMigrateNonEmptyConfirm( 476 sourceState, destinationState statemgr.Full, opts *backendMigrateOpts) (bool, error) { 477 // We need to grab both states so we can write them to a file 478 source := sourceState.State() 479 destination := destinationState.State() 480 481 // Save both to a temporary 482 td, err := ioutil.TempDir("", "terraform") 483 if err != nil { 484 return false, fmt.Errorf("Error creating temporary directory: %s", err) 485 } 486 defer os.RemoveAll(td) 487 488 // Helper to write the state 489 saveHelper := func(n, path string, s *states.State) error { 490 mgr := statemgr.NewFilesystem(path) 491 return mgr.WriteState(s) 492 } 493 494 // Write the states 495 sourcePath := filepath.Join(td, fmt.Sprintf("1-%s.tfstate", opts.SourceType)) 496 destinationPath := filepath.Join(td, fmt.Sprintf("2-%s.tfstate", opts.DestinationType)) 497 if err := saveHelper(opts.SourceType, sourcePath, source); err != nil { 498 return false, fmt.Errorf("Error saving temporary state: %s", err) 499 } 500 if err := saveHelper(opts.DestinationType, destinationPath, destination); err != nil { 501 return false, fmt.Errorf("Error saving temporary state: %s", err) 502 } 503 504 // Ask for confirmation 505 var inputOpts *terraform.InputOpts 506 if opts.DestinationType == "cloud" { 507 inputOpts = &terraform.InputOpts{ 508 Id: "backend-migrate-to-tfc", 509 Query: "Do you want to copy existing state to Terraform Cloud?", 510 Description: fmt.Sprintf( 511 strings.TrimSpace(inputBackendMigrateNonEmptyCloud), 512 opts.SourceType, sourcePath, destinationPath), 513 } 514 } else { 515 inputOpts = &terraform.InputOpts{ 516 Id: "backend-migrate-to-backend", 517 Query: "Do you want to copy existing state to the new backend?", 518 Description: fmt.Sprintf( 519 strings.TrimSpace(inputBackendMigrateNonEmpty), 520 opts.SourceType, opts.DestinationType, sourcePath, destinationPath), 521 } 522 } 523 524 // Confirm with the user that the copy should occur 525 return m.confirm(inputOpts) 526 } 527 528 func retrieveWorkspaces(back backend.Backend, sourceType string) ([]string, bool, error) { 529 var singleState bool 530 var err error 531 workspaces, err := back.Workspaces() 532 if err == backend.ErrWorkspacesNotSupported { 533 singleState = true 534 err = nil 535 } 536 if err != nil { 537 return nil, singleState, fmt.Errorf(strings.TrimSpace( 538 errMigrateLoadStates), sourceType, err) 539 } 540 541 return workspaces, singleState, err 542 } 543 544 func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { 545 _, sourceTFC := opts.Source.(*cloud.Cloud) 546 cloudBackendDestination, destinationTFC := opts.Destination.(*cloud.Cloud) 547 548 sourceWorkspaces, sourceSingleState, err := retrieveWorkspaces(opts.Source, opts.SourceType) 549 if err != nil { 550 return err 551 } 552 //to be used below, not yet implamented 553 // destinationWorkspaces, destinationSingleState 554 _, _, err = retrieveWorkspaces(opts.Destination, opts.SourceType) 555 if err != nil { 556 return err 557 } 558 559 // from TFC to non-TFC backend 560 if sourceTFC && !destinationTFC { 561 // From Terraform Cloud to another backend. This is not yet implemented, and 562 // we recommend people to use the TFC API. 563 return fmt.Errorf(strings.TrimSpace(errTFCMigrateNotYetImplemented)) 564 } 565 566 // Everything below, by the above two conditionals, now assumes that the 567 // destination is always Terraform Cloud (TFC). 568 569 sourceSingle := sourceSingleState || (len(sourceWorkspaces) == 1) 570 if sourceSingle { 571 if cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy { 572 // If we know the name via WorkspaceNameStrategy, then set the 573 // destinationWorkspace to the new Name and skip the user prompt. Here the 574 // destinationWorkspace is not set to `default` thereby we will create it 575 // in TFC if it does not exist. 576 opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name 577 } 578 579 currentWorkspace, err := m.Workspace() 580 if err != nil { 581 return err 582 } 583 opts.sourceWorkspace = currentWorkspace 584 585 log.Printf("[INFO] backendMigrateTFC: single-to-single migration from source %s to destination %q", opts.sourceWorkspace, opts.destinationWorkspace) 586 587 // If the current workspace is has no state we do not need to ask 588 // if they want to migrate the state. 589 sourceState, err := opts.Source.StateMgr(currentWorkspace) 590 if err != nil { 591 return err 592 } 593 if err := sourceState.RefreshState(); err != nil { 594 return err 595 } 596 if sourceState.State().Empty() { 597 log.Printf("[INFO] backendMigrateTFC: skipping migration because source %s is empty", opts.sourceWorkspace) 598 return nil 599 } 600 601 // Run normal single-to-single state migration. 602 // This will handle both situations where the new cloud backend 603 // configuration is using a workspace.name strategy or workspace.tags 604 // strategy. 605 // 606 // We do prompt first though, because state migration is mandatory 607 // for moving to Cloud and the user should get an opportunity to 608 // confirm that first. 609 if migrate, err := m.promptSingleToCloudSingleStateMigration(opts); err != nil { 610 return err 611 } else if !migrate { 612 return nil //skip migrating but return successfully 613 } 614 615 return m.backendMigrateState_s_s(opts) 616 } 617 618 destinationTagsStrategy := cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceTagsStrategy 619 destinationNameStrategy := cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy 620 621 multiSource := !sourceSingleState && len(sourceWorkspaces) > 1 622 if multiSource && destinationNameStrategy { 623 currentWorkspace, err := m.Workspace() 624 if err != nil { 625 return err 626 } 627 628 opts.sourceWorkspace = currentWorkspace 629 opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name 630 if err := m.promptMultiToSingleCloudMigration(opts); err != nil { 631 return err 632 } 633 634 log.Printf("[INFO] backendMigrateTFC: multi-to-single migration from source %s to destination %q", opts.sourceWorkspace, opts.destinationWorkspace) 635 636 return m.backendMigrateState_s_s(opts) 637 } 638 639 // Multiple sources, and using tags strategy. So migrate every source 640 // workspace over to new one, prompt for workspace name pattern (*), 641 // and start migrating, and create tags for each workspace. 642 if multiSource && destinationTagsStrategy { 643 log.Printf("[INFO] backendMigrateTFC: multi-to-multi migration from source workspaces %q", sourceWorkspaces) 644 return m.backendMigrateState_S_TFC(opts, sourceWorkspaces) 645 } 646 647 // TODO(omar): after the check for sourceSingle is done, everything following 648 // it has to be multi. So rework the code to not need to check for multi, adn 649 // return m.backendMigrateState_S_TFC here. 650 return nil 651 } 652 653 // migrates a multi-state backend to Terraform Cloud 654 func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspaces []string) error { 655 log.Print("[TRACE] backendMigrateState: migrating all named workspaces") 656 657 currentWorkspace, err := m.Workspace() 658 if err != nil { 659 return err 660 } 661 newCurrentWorkspace := "" 662 663 // This map is used later when doing the migration per source/destination. 664 // If a source has 'default' and has state, then we ask what the new name should be. 665 // And further down when we actually run state migration for each 666 // source/destination workspace, we use this new name (where source is 'default') 667 // and set as destinationWorkspace. If the default workspace does not have 668 // state we will not prompt the user for a new name because empty workspaces 669 // do not get migrated. 670 defaultNewName := map[string]string{} 671 for i := 0; i < len(sourceWorkspaces); i++ { 672 if sourceWorkspaces[i] == backend.DefaultStateName { 673 // For the default workspace we want to look to see if there is any state 674 // before we ask for a workspace name to migrate the default workspace into. 675 sourceState, err := opts.Source.StateMgr(backend.DefaultStateName) 676 if err != nil { 677 return fmt.Errorf(strings.TrimSpace( 678 errMigrateSingleLoadDefault), opts.SourceType, err) 679 } 680 // RefreshState is what actually pulls the state to be evaluated. 681 if err := sourceState.RefreshState(); err != nil { 682 return fmt.Errorf(strings.TrimSpace( 683 errMigrateSingleLoadDefault), opts.SourceType, err) 684 } 685 if !sourceState.State().Empty() { 686 newName, err := m.promptNewWorkspaceName(opts.DestinationType) 687 if err != nil { 688 return err 689 } 690 defaultNewName[sourceWorkspaces[i]] = newName 691 } 692 } 693 } 694 695 // Fetch the pattern that will be used to rename the workspaces for Terraform Cloud. 696 // 697 // * For the general case, this will be a pattern provided by the user. 698 // 699 // * Specifically for a migration from the "remote" backend using 'prefix', we will 700 // instead 'migrate' the workspaces using a pattern based on the old prefix+name, 701 // not allowing a user to accidentally input the wrong pattern to line up with 702 // what the the remote backend was already using before (which presumably already 703 // meets the naming considerations for Terraform Cloud). 704 // In other words, this is a fast-track migration path from the remote backend, retaining 705 // how things already are in Terraform Cloud with no user intervention needed. 706 pattern := "" 707 if remoteBackend, ok := opts.Source.(*remote.Remote); ok { 708 if err := m.promptRemotePrefixToCloudTagsMigration(opts); err != nil { 709 return err 710 } 711 pattern = remoteBackend.WorkspaceNamePattern() 712 log.Printf("[TRACE] backendMigrateTFC: Remote backend reports workspace name pattern as: %q", pattern) 713 } 714 715 if pattern == "" { 716 pattern, err = m.promptMultiStateMigrationPattern(opts.SourceType) 717 if err != nil { 718 return err 719 } 720 } 721 722 // Go through each and migrate 723 for _, name := range sourceWorkspaces { 724 725 // Copy the same names 726 opts.sourceWorkspace = name 727 if newName, ok := defaultNewName[name]; ok { 728 // this has to be done before setting destinationWorkspace 729 name = newName 730 } 731 opts.destinationWorkspace = strings.Replace(pattern, "*", name, -1) 732 733 // Force it, we confirmed above 734 opts.force = true 735 736 // Perform the migration 737 log.Printf("[INFO] backendMigrateTFC: multi-to-multi migration, source workspace %q to destination workspace %q", opts.sourceWorkspace, opts.destinationWorkspace) 738 if err := m.backendMigrateState_s_s(opts); err != nil { 739 return fmt.Errorf(strings.TrimSpace( 740 errMigrateMulti), name, opts.SourceType, opts.DestinationType, err) 741 } 742 743 if currentWorkspace == opts.sourceWorkspace { 744 newCurrentWorkspace = opts.destinationWorkspace 745 } 746 } 747 748 // After migrating multiple workspaces, we need to reselect the current workspace as it may 749 // have been renamed. Query the backend first to be sure it now exists. 750 workspaces, err := opts.Destination.Workspaces() 751 if err != nil { 752 return err 753 } 754 755 var workspacePresent bool 756 for _, name := range workspaces { 757 if name == newCurrentWorkspace { 758 workspacePresent = true 759 } 760 } 761 762 // If we couldn't select the workspace automatically from the backend (maybe it was empty 763 // and wasn't migrated, for instance), ask the user to select one instead and be done. 764 if !workspacePresent { 765 if err = m.selectWorkspace(opts.Destination); err != nil { 766 return err 767 } 768 return nil 769 } 770 771 // The newly renamed current workspace is present, so we'll automatically select it for the 772 // user, as well as display the equivalent of 'workspace list' to show how the workspaces 773 // were changed (as well as the newly selected current workspace). 774 if err = m.SetWorkspace(newCurrentWorkspace); err != nil { 775 return err 776 } 777 778 m.Ui.Output(m.Colorize().Color("[reset][bold]Migration complete! Your workspaces are as follows:[reset]")) 779 var out bytes.Buffer 780 for _, name := range workspaces { 781 if name == newCurrentWorkspace { 782 out.WriteString("* ") 783 } else { 784 out.WriteString(" ") 785 } 786 out.WriteString(name + "\n") 787 } 788 789 m.Ui.Output(out.String()) 790 791 return nil 792 } 793 794 func (m *Meta) promptSingleToCloudSingleStateMigration(opts *backendMigrateOpts) (bool, error) { 795 if !m.input { 796 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 797 return false, errors.New(strings.TrimSpace(errInteractiveInputDisabled)) 798 } 799 migrate := opts.force 800 if !migrate { 801 var err error 802 migrate, err = m.confirm(&terraform.InputOpts{ 803 Id: "backend-migrate-state-single-to-cloud-single", 804 Query: "Do you wish to proceed?", 805 Description: strings.TrimSpace(tfcInputBackendMigrateStateSingleToCloudSingle), 806 }) 807 if err != nil { 808 return false, fmt.Errorf("Error asking for state migration action: %s", err) 809 } 810 } 811 812 return migrate, nil 813 } 814 815 func (m *Meta) promptRemotePrefixToCloudTagsMigration(opts *backendMigrateOpts) error { 816 if !m.input { 817 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 818 return errors.New(strings.TrimSpace(errInteractiveInputDisabled)) 819 } 820 migrate := opts.force 821 if !migrate { 822 var err error 823 migrate, err = m.confirm(&terraform.InputOpts{ 824 Id: "backend-migrate-remote-multistate-to-cloud", 825 Query: "Do you wish to proceed?", 826 Description: strings.TrimSpace(tfcInputBackendMigrateRemoteMultiToCloud), 827 }) 828 if err != nil { 829 return fmt.Errorf("Error asking for state migration action: %s", err) 830 } 831 } 832 833 if !migrate { 834 return fmt.Errorf("Migration aborted by user.") 835 } 836 837 return nil 838 } 839 840 // Multi-state to single state. 841 func (m *Meta) promptMultiToSingleCloudMigration(opts *backendMigrateOpts) error { 842 if !m.input { 843 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 844 return errors.New(strings.TrimSpace(errInteractiveInputDisabled)) 845 } 846 migrate := opts.force 847 if !migrate { 848 var err error 849 // Ask the user if they want to migrate their existing remote state 850 migrate, err = m.confirm(&terraform.InputOpts{ 851 Id: "backend-migrate-multistate-to-single", 852 Query: "Do you want to copy only your current workspace?", 853 Description: fmt.Sprintf( 854 strings.TrimSpace(tfcInputBackendMigrateMultiToSingle), 855 opts.SourceType, opts.destinationWorkspace), 856 }) 857 if err != nil { 858 return fmt.Errorf("Error asking for state migration action: %s", err) 859 } 860 } 861 862 if !migrate { 863 return fmt.Errorf("Migration aborted by user.") 864 } 865 866 return nil 867 } 868 869 func (m *Meta) promptNewWorkspaceName(destinationType string) (string, error) { 870 message := fmt.Sprintf("[reset][bold][yellow]The %q backend configuration only allows "+ 871 "named workspaces![reset]", destinationType) 872 if destinationType == "cloud" { 873 if !m.input { 874 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 875 return "", errors.New(strings.TrimSpace(errInteractiveInputDisabled)) 876 } 877 message = `[reset][bold][yellow]Terraform Cloud requires all workspaces to be given an explicit name.[reset]` 878 } 879 name, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ 880 Id: "new-state-name", 881 Query: message, 882 Description: strings.TrimSpace(inputBackendNewWorkspaceName), 883 }) 884 if err != nil { 885 return "", fmt.Errorf("Error asking for new state name: %s", err) 886 } 887 888 return name, nil 889 } 890 891 func (m *Meta) promptMultiStateMigrationPattern(sourceType string) (string, error) { 892 // This is not the first prompt a user would be presented with in the migration to TFC, so no 893 // guard on m.input is needed here. 894 renameWorkspaces, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ 895 Id: "backend-migrate-multistate-to-tfc", 896 Query: fmt.Sprintf("[reset][bold][yellow]%s[reset]", "Would you like to rename your workspaces?"), 897 Description: fmt.Sprintf(strings.TrimSpace(tfcInputBackendMigrateMultiToMulti), sourceType), 898 }) 899 if err != nil { 900 return "", fmt.Errorf("Error asking for state migration action: %s", err) 901 } 902 if renameWorkspaces != "2" && renameWorkspaces != "1" { 903 return "", fmt.Errorf("Please select 1 or 2 as part of this option.") 904 } 905 if renameWorkspaces == "2" { 906 // this means they did not want to rename their workspaces, and we are 907 // returning a generic '*' that means use the same workspace name during 908 // migration. 909 return "*", nil 910 } 911 912 pattern, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ 913 Id: "backend-migrate-multistate-to-tfc-pattern", 914 Query: fmt.Sprintf("[reset][bold][yellow]%s[reset]", "How would you like to rename your workspaces?"), 915 Description: strings.TrimSpace(tfcInputBackendMigrateMultiToMultiPattern), 916 }) 917 if err != nil { 918 return "", fmt.Errorf("Error asking for state migration action: %s", err) 919 } 920 if !strings.Contains(pattern, "*") { 921 return "", fmt.Errorf("The pattern must have an '*'") 922 } 923 924 if count := strings.Count(pattern, "*"); count > 1 { 925 return "", fmt.Errorf("The pattern '*' cannot be used more than once.") 926 } 927 928 return pattern, nil 929 } 930 931 const errMigrateLoadStates = ` 932 Error inspecting states in the %q backend: 933 %s 934 935 Prior to changing backends, Terraform inspects the source and destination 936 states to determine what kind of migration steps need to be taken, if any. 937 Terraform failed to load the states. The data in both the source and the 938 destination remain unmodified. Please resolve the above error and try again. 939 ` 940 941 const errMigrateSingleLoadDefault = ` 942 Error loading state: 943 %[2]s 944 945 Terraform failed to load the default state from the %[1]q backend. 946 State migration cannot occur unless the state can be loaded. Backend 947 modification and state migration has been aborted. The state in both the 948 source and the destination remain unmodified. Please resolve the 949 above error and try again. 950 ` 951 952 const errMigrateMulti = ` 953 Error migrating the workspace %q from the previous %q backend 954 to the newly configured %q backend: 955 %s 956 957 Terraform copies workspaces in alphabetical order. Any workspaces 958 alphabetically earlier than this one have been copied. Any workspaces 959 later than this haven't been modified in the destination. No workspaces 960 in the source state have been modified. 961 962 Please resolve the error above and run the initialization command again. 963 This will attempt to copy (with permission) all workspaces again. 964 ` 965 966 const errBackendStateCopy = ` 967 Error copying state from the previous %q backend to the newly configured 968 %q backend: 969 %s 970 971 The state in the previous backend remains intact and unmodified. Please resolve 972 the error above and try again. 973 ` 974 975 const errTFCMigrateNotYetImplemented = ` 976 Migrating state from Terraform Cloud to another backend is not yet implemented. 977 978 Please use the API to do this: https://www.terraform.io/docs/cloud/api/state-versions.html 979 ` 980 981 const errInteractiveInputDisabled = ` 982 Can't ask approval for state migration when interactive input is disabled. 983 984 Please remove the "-input=false" option and try again. 985 ` 986 987 const tfcInputBackendMigrateMultiToMultiPattern = ` 988 Enter a pattern with an asterisk (*) to rename all workspaces based on their 989 previous names. The asterisk represents the current workspace name. 990 991 For example, if a workspace is currently named 'prod', the pattern 'app-*' would yield 992 'app-prod' for a new workspace name; 'app-*-region1' would yield 'app-prod-region1'. 993 ` 994 995 const tfcInputBackendMigrateMultiToMulti = ` 996 Unlike typical Terraform workspaces representing an environment associated with a particular 997 configuration (e.g. production, staging, development), Terraform Cloud workspaces are named uniquely 998 across all configurations used within an organization. A typical strategy to start with is 999 <COMPONENT>-<ENVIRONMENT>-<REGION> (e.g. networking-prod-us-east, networking-staging-us-east). 1000 1001 For more information on workspace naming, see https://www.terraform.io/docs/cloud/workspaces/naming.html 1002 1003 When migrating existing workspaces from the backend %[1]q to Terraform Cloud, would you like to 1004 rename your workspaces? Enter 1 or 2. 1005 1006 1. Yes, I'd like to rename all workspaces according to a pattern I will provide. 1007 2. No, I would not like to rename my workspaces. Migrate them as currently named. 1008 ` 1009 1010 const tfcInputBackendMigrateMultiToSingle = ` 1011 The previous backend %[1]q has multiple workspaces, but Terraform Cloud has 1012 been configured to use a single workspace (%[2]q). By continuing, you will 1013 only migrate your current workspace. If you wish to migrate all workspaces 1014 from the previous backend, you may cancel this operation and use the 'tags' 1015 strategy in your workspace configuration block instead. 1016 1017 Enter "yes" to proceed or "no" to cancel. 1018 ` 1019 1020 const tfcInputBackendMigrateStateSingleToCloudSingle = ` 1021 As part of migrating to Terraform Cloud, Terraform can optionally copy your 1022 current workspace state to the configured Terraform Cloud workspace. 1023 1024 Answer "yes" to copy the latest state snapshot to the configured 1025 Terraform Cloud workspace. 1026 1027 Answer "no" to ignore the existing state and just activate the configured 1028 Terraform Cloud workspace with its existing state, if any. 1029 1030 Should Terraform migrate your existing state? 1031 ` 1032 1033 const tfcInputBackendMigrateRemoteMultiToCloud = ` 1034 When migrating from the 'remote' backend to Terraform's native integration 1035 with Terraform Cloud, Terraform will automatically create or use existing 1036 workspaces based on the previous backend configuration's 'prefix' value. 1037 1038 When the migration is complete, workspace names in Terraform will match the 1039 fully qualified Terraform Cloud workspace name. If necessary, the workspace 1040 tags configured in the 'cloud' option block will be added to the associated 1041 Terraform Cloud workspaces. 1042 1043 Enter "yes" to proceed or "no" to cancel. 1044 ` 1045 1046 const inputBackendMigrateEmpty = ` 1047 Pre-existing state was found while migrating the previous %q backend to the 1048 newly configured %q backend. No existing state was found in the newly 1049 configured %[2]q backend. Do you want to copy this state to the new %[2]q 1050 backend? Enter "yes" to copy and "no" to start with an empty state. 1051 ` 1052 1053 const inputBackendMigrateEmptyCloud = ` 1054 Pre-existing state was found while migrating the previous %q backend to Terraform Cloud. 1055 No existing state was found in Terraform Cloud. Do you want to copy this state to Terraform Cloud? 1056 Enter "yes" to copy and "no" to start with an empty state. 1057 ` 1058 1059 const inputBackendMigrateNonEmpty = ` 1060 Pre-existing state was found while migrating the previous %q backend to the 1061 newly configured %q backend. An existing non-empty state already exists in 1062 the new backend. The two states have been saved to temporary files that will be 1063 removed after responding to this query. 1064 1065 Previous (type %[1]q): %[3]s 1066 New (type %[2]q): %[4]s 1067 1068 Do you want to overwrite the state in the new backend with the previous state? 1069 Enter "yes" to copy and "no" to start with the existing state in the newly 1070 configured %[2]q backend. 1071 ` 1072 1073 const inputBackendMigrateNonEmptyCloud = ` 1074 Pre-existing state was found while migrating the previous %q backend to 1075 Terraform Cloud. An existing non-empty state already exists in Terraform Cloud. 1076 The two states have been saved to temporary files that will be removed after 1077 responding to this query. 1078 1079 Previous (type %[1]q): %[2]s 1080 New (Terraform Cloud): %[3]s 1081 1082 Do you want to overwrite the state in Terraform Cloud with the previous state? 1083 Enter "yes" to copy and "no" to start with the existing state in Terraform Cloud. 1084 ` 1085 1086 const inputBackendMigrateMultiToSingle = ` 1087 The existing %[1]q backend supports workspaces and you currently are 1088 using more than one. The newly configured %[2]q backend doesn't support 1089 workspaces. If you continue, Terraform will copy your current workspace %[3]q 1090 to the default workspace in the new backend. Your existing workspaces in the 1091 source backend won't be modified. If you want to switch workspaces, back them 1092 up, or cancel altogether, answer "no" and Terraform will abort. 1093 ` 1094 1095 const inputBackendMigrateMultiToMulti = ` 1096 Both the existing %[1]q backend and the newly configured %[2]q backend 1097 support workspaces. When migrating between backends, Terraform will copy 1098 all workspaces (with the same names). THIS WILL OVERWRITE any conflicting 1099 states in the destination. 1100 1101 Terraform initialization doesn't currently migrate only select workspaces. 1102 If you want to migrate a select number of workspaces, you must manually 1103 pull and push those states. 1104 1105 If you answer "yes", Terraform will migrate all states. If you answer 1106 "no", Terraform will abort. 1107 ` 1108 1109 const inputBackendNewWorkspaceName = ` 1110 Please provide a new workspace name (e.g. dev, test) that will be used 1111 to migrate the existing default workspace. 1112 ` 1113 1114 const inputBackendSelectWorkspace = ` 1115 This is expected behavior when the selected workspace did not have an 1116 existing non-empty state. Please enter a number to select a workspace: 1117 1118 %s 1119 `