github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/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/cycloidio/terraform/backend" 16 "github.com/cycloidio/terraform/backend/remote" 17 "github.com/cycloidio/terraform/cloud" 18 "github.com/cycloidio/terraform/command/arguments" 19 "github.com/cycloidio/terraform/command/clistate" 20 "github.com/cycloidio/terraform/command/views" 21 "github.com/cycloidio/terraform/states" 22 "github.com/cycloidio/terraform/states/statemgr" 23 "github.com/cycloidio/terraform/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 if err := destinationState.PersistState(); err != nil { 442 return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), 443 opts.SourceType, opts.DestinationType, err) 444 } 445 446 // And we're done. 447 return nil 448 } 449 450 func (m *Meta) backendMigrateEmptyConfirm(source, destination statemgr.Full, opts *backendMigrateOpts) (bool, error) { 451 var inputOpts *terraform.InputOpts 452 if opts.DestinationType == "cloud" { 453 inputOpts = &terraform.InputOpts{ 454 Id: "backend-migrate-copy-to-empty-cloud", 455 Query: "Do you want to copy existing state to Terraform Cloud?", 456 Description: fmt.Sprintf(strings.TrimSpace(inputBackendMigrateEmptyCloud), opts.SourceType), 457 } 458 } else { 459 inputOpts = &terraform.InputOpts{ 460 Id: "backend-migrate-copy-to-empty", 461 Query: "Do you want to copy existing state to the new backend?", 462 Description: fmt.Sprintf( 463 strings.TrimSpace(inputBackendMigrateEmpty), 464 opts.SourceType, opts.DestinationType), 465 } 466 } 467 468 return m.confirm(inputOpts) 469 } 470 471 func (m *Meta) backendMigrateNonEmptyConfirm( 472 sourceState, destinationState statemgr.Full, opts *backendMigrateOpts) (bool, error) { 473 // We need to grab both states so we can write them to a file 474 source := sourceState.State() 475 destination := destinationState.State() 476 477 // Save both to a temporary 478 td, err := ioutil.TempDir("", "terraform") 479 if err != nil { 480 return false, fmt.Errorf("Error creating temporary directory: %s", err) 481 } 482 defer os.RemoveAll(td) 483 484 // Helper to write the state 485 saveHelper := func(n, path string, s *states.State) error { 486 mgr := statemgr.NewFilesystem(path) 487 return mgr.WriteState(s) 488 } 489 490 // Write the states 491 sourcePath := filepath.Join(td, fmt.Sprintf("1-%s.tfstate", opts.SourceType)) 492 destinationPath := filepath.Join(td, fmt.Sprintf("2-%s.tfstate", opts.DestinationType)) 493 if err := saveHelper(opts.SourceType, sourcePath, source); err != nil { 494 return false, fmt.Errorf("Error saving temporary state: %s", err) 495 } 496 if err := saveHelper(opts.DestinationType, destinationPath, destination); err != nil { 497 return false, fmt.Errorf("Error saving temporary state: %s", err) 498 } 499 500 // Ask for confirmation 501 var inputOpts *terraform.InputOpts 502 if opts.DestinationType == "cloud" { 503 inputOpts = &terraform.InputOpts{ 504 Id: "backend-migrate-to-tfc", 505 Query: "Do you want to copy existing state to Terraform Cloud?", 506 Description: fmt.Sprintf( 507 strings.TrimSpace(inputBackendMigrateNonEmptyCloud), 508 opts.SourceType, sourcePath, destinationPath), 509 } 510 } else { 511 inputOpts = &terraform.InputOpts{ 512 Id: "backend-migrate-to-backend", 513 Query: "Do you want to copy existing state to the new backend?", 514 Description: fmt.Sprintf( 515 strings.TrimSpace(inputBackendMigrateNonEmpty), 516 opts.SourceType, opts.DestinationType, sourcePath, destinationPath), 517 } 518 } 519 520 // Confirm with the user that the copy should occur 521 return m.confirm(inputOpts) 522 } 523 524 func retrieveWorkspaces(back backend.Backend, sourceType string) ([]string, bool, error) { 525 var singleState bool 526 var err error 527 workspaces, err := back.Workspaces() 528 if err == backend.ErrWorkspacesNotSupported { 529 singleState = true 530 err = nil 531 } 532 if err != nil { 533 return nil, singleState, fmt.Errorf(strings.TrimSpace( 534 errMigrateLoadStates), sourceType, err) 535 } 536 537 return workspaces, singleState, err 538 } 539 540 func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { 541 _, sourceTFC := opts.Source.(*cloud.Cloud) 542 cloudBackendDestination, destinationTFC := opts.Destination.(*cloud.Cloud) 543 544 sourceWorkspaces, sourceSingleState, err := retrieveWorkspaces(opts.Source, opts.SourceType) 545 if err != nil { 546 return err 547 } 548 //to be used below, not yet implamented 549 // destinationWorkspaces, destinationSingleState 550 _, _, err = retrieveWorkspaces(opts.Destination, opts.SourceType) 551 if err != nil { 552 return err 553 } 554 555 // from TFC to non-TFC backend 556 if sourceTFC && !destinationTFC { 557 // From Terraform Cloud to another backend. This is not yet implemented, and 558 // we recommend people to use the TFC API. 559 return fmt.Errorf(strings.TrimSpace(errTFCMigrateNotYetImplemented)) 560 } 561 562 // Everything below, by the above two conditionals, now assumes that the 563 // destination is always Terraform Cloud (TFC). 564 565 sourceSingle := sourceSingleState || (len(sourceWorkspaces) == 1) 566 if sourceSingle { 567 if cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy { 568 // If we know the name via WorkspaceNameStrategy, then set the 569 // destinationWorkspace to the new Name and skip the user prompt. Here the 570 // destinationWorkspace is not set to `default` thereby we will create it 571 // in TFC if it does not exist. 572 opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name 573 } 574 575 currentWorkspace, err := m.Workspace() 576 if err != nil { 577 return err 578 } 579 opts.sourceWorkspace = currentWorkspace 580 581 log.Printf("[INFO] backendMigrateTFC: single-to-single migration from source %s to destination %q", opts.sourceWorkspace, opts.destinationWorkspace) 582 583 // If the current workspace is has no state we do not need to ask 584 // if they want to migrate the state. 585 sourceState, err := opts.Source.StateMgr(currentWorkspace) 586 if err != nil { 587 return err 588 } 589 if err := sourceState.RefreshState(); err != nil { 590 return err 591 } 592 if sourceState.State().Empty() { 593 log.Printf("[INFO] backendMigrateTFC: skipping migration because source %s is empty", opts.sourceWorkspace) 594 return nil 595 } 596 597 // Run normal single-to-single state migration. 598 // This will handle both situations where the new cloud backend 599 // configuration is using a workspace.name strategy or workspace.tags 600 // strategy. 601 // 602 // We do prompt first though, because state migration is mandatory 603 // for moving to Cloud and the user should get an opportunity to 604 // confirm that first. 605 if migrate, err := m.promptSingleToCloudSingleStateMigration(opts); err != nil { 606 return err 607 } else if !migrate { 608 return nil //skip migrating but return successfully 609 } 610 611 return m.backendMigrateState_s_s(opts) 612 } 613 614 destinationTagsStrategy := cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceTagsStrategy 615 destinationNameStrategy := cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy 616 617 multiSource := !sourceSingleState && len(sourceWorkspaces) > 1 618 if multiSource && destinationNameStrategy { 619 currentWorkspace, err := m.Workspace() 620 if err != nil { 621 return err 622 } 623 624 opts.sourceWorkspace = currentWorkspace 625 opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name 626 if err := m.promptMultiToSingleCloudMigration(opts); err != nil { 627 return err 628 } 629 630 log.Printf("[INFO] backendMigrateTFC: multi-to-single migration from source %s to destination %q", opts.sourceWorkspace, opts.destinationWorkspace) 631 632 return m.backendMigrateState_s_s(opts) 633 } 634 635 // Multiple sources, and using tags strategy. So migrate every source 636 // workspace over to new one, prompt for workspace name pattern (*), 637 // and start migrating, and create tags for each workspace. 638 if multiSource && destinationTagsStrategy { 639 log.Printf("[INFO] backendMigrateTFC: multi-to-multi migration from source workspaces %q", sourceWorkspaces) 640 return m.backendMigrateState_S_TFC(opts, sourceWorkspaces) 641 } 642 643 // TODO(omar): after the check for sourceSingle is done, everything following 644 // it has to be multi. So rework the code to not need to check for multi, adn 645 // return m.backendMigrateState_S_TFC here. 646 return nil 647 } 648 649 // migrates a multi-state backend to Terraform Cloud 650 func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspaces []string) error { 651 log.Print("[TRACE] backendMigrateState: migrating all named workspaces") 652 653 currentWorkspace, err := m.Workspace() 654 if err != nil { 655 return err 656 } 657 newCurrentWorkspace := "" 658 659 // This map is used later when doing the migration per source/destination. 660 // If a source has 'default' and has state, then we ask what the new name should be. 661 // And further down when we actually run state migration for each 662 // source/destination workspace, we use this new name (where source is 'default') 663 // and set as destinationWorkspace. If the default workspace does not have 664 // state we will not prompt the user for a new name because empty workspaces 665 // do not get migrated. 666 defaultNewName := map[string]string{} 667 for i := 0; i < len(sourceWorkspaces); i++ { 668 if sourceWorkspaces[i] == backend.DefaultStateName { 669 // For the default workspace we want to look to see if there is any state 670 // before we ask for a workspace name to migrate the default workspace into. 671 sourceState, err := opts.Source.StateMgr(backend.DefaultStateName) 672 if err != nil { 673 return fmt.Errorf(strings.TrimSpace( 674 errMigrateSingleLoadDefault), opts.SourceType, err) 675 } 676 // RefreshState is what actually pulls the state to be evaluated. 677 if err := sourceState.RefreshState(); err != nil { 678 return fmt.Errorf(strings.TrimSpace( 679 errMigrateSingleLoadDefault), opts.SourceType, err) 680 } 681 if !sourceState.State().Empty() { 682 newName, err := m.promptNewWorkspaceName(opts.DestinationType) 683 if err != nil { 684 return err 685 } 686 defaultNewName[sourceWorkspaces[i]] = newName 687 } 688 } 689 } 690 691 // Fetch the pattern that will be used to rename the workspaces for Terraform Cloud. 692 // 693 // * For the general case, this will be a pattern provided by the user. 694 // 695 // * Specifically for a migration from the "remote" backend using 'prefix', we will 696 // instead 'migrate' the workspaces using a pattern based on the old prefix+name, 697 // not allowing a user to accidentally input the wrong pattern to line up with 698 // what the the remote backend was already using before (which presumably already 699 // meets the naming considerations for Terraform Cloud). 700 // In other words, this is a fast-track migration path from the remote backend, retaining 701 // how things already are in Terraform Cloud with no user intervention needed. 702 pattern := "" 703 if remoteBackend, ok := opts.Source.(*remote.Remote); ok { 704 if err := m.promptRemotePrefixToCloudTagsMigration(opts); err != nil { 705 return err 706 } 707 pattern = remoteBackend.WorkspaceNamePattern() 708 log.Printf("[TRACE] backendMigrateTFC: Remote backend reports workspace name pattern as: %q", pattern) 709 } 710 711 if pattern == "" { 712 pattern, err = m.promptMultiStateMigrationPattern(opts.SourceType) 713 if err != nil { 714 return err 715 } 716 } 717 718 // Go through each and migrate 719 for _, name := range sourceWorkspaces { 720 721 // Copy the same names 722 opts.sourceWorkspace = name 723 if newName, ok := defaultNewName[name]; ok { 724 // this has to be done before setting destinationWorkspace 725 name = newName 726 } 727 opts.destinationWorkspace = strings.Replace(pattern, "*", name, -1) 728 729 // Force it, we confirmed above 730 opts.force = true 731 732 // Perform the migration 733 log.Printf("[INFO] backendMigrateTFC: multi-to-multi migration, source workspace %q to destination workspace %q", opts.sourceWorkspace, opts.destinationWorkspace) 734 if err := m.backendMigrateState_s_s(opts); err != nil { 735 return fmt.Errorf(strings.TrimSpace( 736 errMigrateMulti), name, opts.SourceType, opts.DestinationType, err) 737 } 738 739 if currentWorkspace == opts.sourceWorkspace { 740 newCurrentWorkspace = opts.destinationWorkspace 741 } 742 } 743 744 // After migrating multiple workspaces, we need to reselect the current workspace as it may 745 // have been renamed. Query the backend first to be sure it now exists. 746 workspaces, err := opts.Destination.Workspaces() 747 if err != nil { 748 return err 749 } 750 751 var workspacePresent bool 752 for _, name := range workspaces { 753 if name == newCurrentWorkspace { 754 workspacePresent = true 755 } 756 } 757 758 // If we couldn't select the workspace automatically from the backend (maybe it was empty 759 // and wasn't migrated, for instance), ask the user to select one instead and be done. 760 if !workspacePresent { 761 if err = m.selectWorkspace(opts.Destination); err != nil { 762 return err 763 } 764 return nil 765 } 766 767 // The newly renamed current workspace is present, so we'll automatically select it for the 768 // user, as well as display the equivalent of 'workspace list' to show how the workspaces 769 // were changed (as well as the newly selected current workspace). 770 if err = m.SetWorkspace(newCurrentWorkspace); err != nil { 771 return err 772 } 773 774 m.Ui.Output(m.Colorize().Color("[reset][bold]Migration complete! Your workspaces are as follows:[reset]")) 775 var out bytes.Buffer 776 for _, name := range workspaces { 777 if name == newCurrentWorkspace { 778 out.WriteString("* ") 779 } else { 780 out.WriteString(" ") 781 } 782 out.WriteString(name + "\n") 783 } 784 785 m.Ui.Output(out.String()) 786 787 return nil 788 } 789 790 func (m *Meta) promptSingleToCloudSingleStateMigration(opts *backendMigrateOpts) (bool, error) { 791 if !m.input { 792 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 793 return false, errors.New(strings.TrimSpace(errInteractiveInputDisabled)) 794 } 795 migrate := opts.force 796 if !migrate { 797 var err error 798 migrate, err = m.confirm(&terraform.InputOpts{ 799 Id: "backend-migrate-state-single-to-cloud-single", 800 Query: "Do you wish to proceed?", 801 Description: strings.TrimSpace(tfcInputBackendMigrateStateSingleToCloudSingle), 802 }) 803 if err != nil { 804 return false, fmt.Errorf("Error asking for state migration action: %s", err) 805 } 806 } 807 808 return migrate, nil 809 } 810 811 func (m *Meta) promptRemotePrefixToCloudTagsMigration(opts *backendMigrateOpts) error { 812 if !m.input { 813 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 814 return errors.New(strings.TrimSpace(errInteractiveInputDisabled)) 815 } 816 migrate := opts.force 817 if !migrate { 818 var err error 819 migrate, err = m.confirm(&terraform.InputOpts{ 820 Id: "backend-migrate-remote-multistate-to-cloud", 821 Query: "Do you wish to proceed?", 822 Description: strings.TrimSpace(tfcInputBackendMigrateRemoteMultiToCloud), 823 }) 824 if err != nil { 825 return fmt.Errorf("Error asking for state migration action: %s", err) 826 } 827 } 828 829 if !migrate { 830 return fmt.Errorf("Migration aborted by user.") 831 } 832 833 return nil 834 } 835 836 // Multi-state to single state. 837 func (m *Meta) promptMultiToSingleCloudMigration(opts *backendMigrateOpts) error { 838 if !m.input { 839 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 840 return errors.New(strings.TrimSpace(errInteractiveInputDisabled)) 841 } 842 migrate := opts.force 843 if !migrate { 844 var err error 845 // Ask the user if they want to migrate their existing remote state 846 migrate, err = m.confirm(&terraform.InputOpts{ 847 Id: "backend-migrate-multistate-to-single", 848 Query: "Do you want to copy only your current workspace?", 849 Description: fmt.Sprintf( 850 strings.TrimSpace(tfcInputBackendMigrateMultiToSingle), 851 opts.SourceType, opts.destinationWorkspace), 852 }) 853 if err != nil { 854 return fmt.Errorf("Error asking for state migration action: %s", err) 855 } 856 } 857 858 if !migrate { 859 return fmt.Errorf("Migration aborted by user.") 860 } 861 862 return nil 863 } 864 865 func (m *Meta) promptNewWorkspaceName(destinationType string) (string, error) { 866 message := fmt.Sprintf("[reset][bold][yellow]The %q backend configuration only allows "+ 867 "named workspaces![reset]", destinationType) 868 if destinationType == "cloud" { 869 if !m.input { 870 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 871 return "", errors.New(strings.TrimSpace(errInteractiveInputDisabled)) 872 } 873 message = `[reset][bold][yellow]Terraform Cloud requires all workspaces to be given an explicit name.[reset]` 874 } 875 name, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ 876 Id: "new-state-name", 877 Query: message, 878 Description: strings.TrimSpace(inputBackendNewWorkspaceName), 879 }) 880 if err != nil { 881 return "", fmt.Errorf("Error asking for new state name: %s", err) 882 } 883 884 return name, nil 885 } 886 887 func (m *Meta) promptMultiStateMigrationPattern(sourceType string) (string, error) { 888 // This is not the first prompt a user would be presented with in the migration to TFC, so no 889 // guard on m.input is needed here. 890 renameWorkspaces, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ 891 Id: "backend-migrate-multistate-to-tfc", 892 Query: fmt.Sprintf("[reset][bold][yellow]%s[reset]", "Would you like to rename your workspaces?"), 893 Description: fmt.Sprintf(strings.TrimSpace(tfcInputBackendMigrateMultiToMulti), sourceType), 894 }) 895 if err != nil { 896 return "", fmt.Errorf("Error asking for state migration action: %s", err) 897 } 898 if renameWorkspaces != "2" && renameWorkspaces != "1" { 899 return "", fmt.Errorf("Please select 1 or 2 as part of this option.") 900 } 901 if renameWorkspaces == "2" { 902 // this means they did not want to rename their workspaces, and we are 903 // returning a generic '*' that means use the same workspace name during 904 // migration. 905 return "*", nil 906 } 907 908 pattern, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ 909 Id: "backend-migrate-multistate-to-tfc-pattern", 910 Query: fmt.Sprintf("[reset][bold][yellow]%s[reset]", "How would you like to rename your workspaces?"), 911 Description: strings.TrimSpace(tfcInputBackendMigrateMultiToMultiPattern), 912 }) 913 if err != nil { 914 return "", fmt.Errorf("Error asking for state migration action: %s", err) 915 } 916 if !strings.Contains(pattern, "*") { 917 return "", fmt.Errorf("The pattern must have an '*'") 918 } 919 920 if count := strings.Count(pattern, "*"); count > 1 { 921 return "", fmt.Errorf("The pattern '*' cannot be used more than once.") 922 } 923 924 return pattern, nil 925 } 926 927 const errMigrateLoadStates = ` 928 Error inspecting states in the %q backend: 929 %s 930 931 Prior to changing backends, Terraform inspects the source and destination 932 states to determine what kind of migration steps need to be taken, if any. 933 Terraform failed to load the states. The data in both the source and the 934 destination remain unmodified. Please resolve the above error and try again. 935 ` 936 937 const errMigrateSingleLoadDefault = ` 938 Error loading state: 939 %[2]s 940 941 Terraform failed to load the default state from the %[1]q backend. 942 State migration cannot occur unless the state can be loaded. Backend 943 modification and state migration has been aborted. The state in both the 944 source and the destination remain unmodified. Please resolve the 945 above error and try again. 946 ` 947 948 const errMigrateMulti = ` 949 Error migrating the workspace %q from the previous %q backend 950 to the newly configured %q backend: 951 %s 952 953 Terraform copies workspaces in alphabetical order. Any workspaces 954 alphabetically earlier than this one have been copied. Any workspaces 955 later than this haven't been modified in the destination. No workspaces 956 in the source state have been modified. 957 958 Please resolve the error above and run the initialization command again. 959 This will attempt to copy (with permission) all workspaces again. 960 ` 961 962 const errBackendStateCopy = ` 963 Error copying state from the previous %q backend to the newly configured 964 %q backend: 965 %s 966 967 The state in the previous backend remains intact and unmodified. Please resolve 968 the error above and try again. 969 ` 970 971 const errTFCMigrateNotYetImplemented = ` 972 Migrating state from Terraform Cloud to another backend is not yet implemented. 973 974 Please use the API to do this: https://www.terraform.io/docs/cloud/api/state-versions.html 975 ` 976 977 const errInteractiveInputDisabled = ` 978 Can't ask approval for state migration when interactive input is disabled. 979 980 Please remove the "-input=false" option and try again. 981 ` 982 983 const tfcInputBackendMigrateMultiToMultiPattern = ` 984 Enter a pattern with an asterisk (*) to rename all workspaces based on their 985 previous names. The asterisk represents the current workspace name. 986 987 For example, if a workspace is currently named 'prod', the pattern 'app-*' would yield 988 'app-prod' for a new workspace name; 'app-*-region1' would yield 'app-prod-region1'. 989 ` 990 991 const tfcInputBackendMigrateMultiToMulti = ` 992 Unlike typical Terraform workspaces representing an environment associated with a particular 993 configuration (e.g. production, staging, development), Terraform Cloud workspaces are named uniquely 994 across all configurations used within an organization. A typical strategy to start with is 995 <COMPONENT>-<ENVIRONMENT>-<REGION> (e.g. networking-prod-us-east, networking-staging-us-east). 996 997 For more information on workspace naming, see https://www.terraform.io/docs/cloud/workspaces/naming.html 998 999 When migrating existing workspaces from the backend %[1]q to Terraform Cloud, would you like to 1000 rename your workspaces? Enter 1 or 2. 1001 1002 1. Yes, I'd like to rename all workspaces according to a pattern I will provide. 1003 2. No, I would not like to rename my workspaces. Migrate them as currently named. 1004 ` 1005 1006 const tfcInputBackendMigrateMultiToSingle = ` 1007 The previous backend %[1]q has multiple workspaces, but Terraform Cloud has 1008 been configured to use a single workspace (%[2]q). By continuing, you will 1009 only migrate your current workspace. If you wish to migrate all workspaces 1010 from the previous backend, you may cancel this operation and use the 'tags' 1011 strategy in your workspace configuration block instead. 1012 1013 Enter "yes" to proceed or "no" to cancel. 1014 ` 1015 1016 const tfcInputBackendMigrateStateSingleToCloudSingle = ` 1017 As part of migrating to Terraform Cloud, Terraform can optionally copy your 1018 current workspace state to the configured Terraform Cloud workspace. 1019 1020 Answer "yes" to copy the latest state snapshot to the configured 1021 Terraform Cloud workspace. 1022 1023 Answer "no" to ignore the existing state and just activate the configured 1024 Terraform Cloud workspace with its existing state, if any. 1025 1026 Should Terraform migrate your existing state? 1027 ` 1028 1029 const tfcInputBackendMigrateRemoteMultiToCloud = ` 1030 When migrating from the 'remote' backend to Terraform's native integration 1031 with Terraform Cloud, Terraform will automatically create or use existing 1032 workspaces based on the previous backend configuration's 'prefix' value. 1033 1034 When the migration is complete, workspace names in Terraform will match the 1035 fully qualified Terraform Cloud workspace name. If necessary, the workspace 1036 tags configured in the 'cloud' option block will be added to the associated 1037 Terraform Cloud workspaces. 1038 1039 Enter "yes" to proceed or "no" to cancel. 1040 ` 1041 1042 const inputBackendMigrateEmpty = ` 1043 Pre-existing state was found while migrating the previous %q backend to the 1044 newly configured %q backend. No existing state was found in the newly 1045 configured %[2]q backend. Do you want to copy this state to the new %[2]q 1046 backend? Enter "yes" to copy and "no" to start with an empty state. 1047 ` 1048 1049 const inputBackendMigrateEmptyCloud = ` 1050 Pre-existing state was found while migrating the previous %q backend to Terraform Cloud. 1051 No existing state was found in Terraform Cloud. Do you want to copy this state to Terraform Cloud? 1052 Enter "yes" to copy and "no" to start with an empty state. 1053 ` 1054 1055 const inputBackendMigrateNonEmpty = ` 1056 Pre-existing state was found while migrating the previous %q backend to the 1057 newly configured %q backend. An existing non-empty state already exists in 1058 the new backend. The two states have been saved to temporary files that will be 1059 removed after responding to this query. 1060 1061 Previous (type %[1]q): %[3]s 1062 New (type %[2]q): %[4]s 1063 1064 Do you want to overwrite the state in the new backend with the previous state? 1065 Enter "yes" to copy and "no" to start with the existing state in the newly 1066 configured %[2]q backend. 1067 ` 1068 1069 const inputBackendMigrateNonEmptyCloud = ` 1070 Pre-existing state was found while migrating the previous %q backend to 1071 Terraform Cloud. An existing non-empty state already exists in Terraform Cloud. 1072 The two states have been saved to temporary files that will be removed after 1073 responding to this query. 1074 1075 Previous (type %[1]q): %[2]s 1076 New (Terraform Cloud): %[3]s 1077 1078 Do you want to overwrite the state in Terraform Cloud with the previous state? 1079 Enter "yes" to copy and "no" to start with the existing state in Terraform Cloud. 1080 ` 1081 1082 const inputBackendMigrateMultiToSingle = ` 1083 The existing %[1]q backend supports workspaces and you currently are 1084 using more than one. The newly configured %[2]q backend doesn't support 1085 workspaces. If you continue, Terraform will copy your current workspace %[3]q 1086 to the default workspace in the new backend. Your existing workspaces in the 1087 source backend won't be modified. If you want to switch workspaces, back them 1088 up, or cancel altogether, answer "no" and Terraform will abort. 1089 ` 1090 1091 const inputBackendMigrateMultiToMulti = ` 1092 Both the existing %[1]q backend and the newly configured %[2]q backend 1093 support workspaces. When migrating between backends, Terraform will copy 1094 all workspaces (with the same names). THIS WILL OVERWRITE any conflicting 1095 states in the destination. 1096 1097 Terraform initialization doesn't currently migrate only select workspaces. 1098 If you want to migrate a select number of workspaces, you must manually 1099 pull and push those states. 1100 1101 If you answer "yes", Terraform will migrate all states. If you answer 1102 "no", Terraform will abort. 1103 ` 1104 1105 const inputBackendNewWorkspaceName = ` 1106 Please provide a new workspace name (e.g. dev, test) that will be used 1107 to migrate the existing default workspace. 1108 ` 1109 1110 const inputBackendSelectWorkspace = ` 1111 This is expected behavior when the selected workspace did not have an 1112 existing non-empty state. Please enter a number to select a workspace: 1113 1114 %s 1115 `