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