kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/command/meta_backend_migrate.go (about) 1 package command 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "log" 9 "os" 10 "path/filepath" 11 "sort" 12 "strings" 13 14 "kubeform.dev/terraform-backend-sdk/backend" 15 "kubeform.dev/terraform-backend-sdk/command/arguments" 16 "kubeform.dev/terraform-backend-sdk/command/clistate" 17 "kubeform.dev/terraform-backend-sdk/command/views" 18 "kubeform.dev/terraform-backend-sdk/states" 19 "kubeform.dev/terraform-backend-sdk/states/statemgr" 20 "kubeform.dev/terraform-backend-sdk/terraform" 21 ) 22 23 type backendMigrateOpts struct { 24 OneType, TwoType string 25 One, Two backend.Backend 26 27 // Fields below are set internally when migrate is called 28 29 oneEnv string // source env 30 twoEnv string // dest env 31 force bool // if true, won't ask for confirmation 32 } 33 34 // backendMigrateState handles migrating (copying) state from one backend 35 // to another. This function handles asking the user for confirmation 36 // as well as the copy itself. 37 // 38 // This function can handle all scenarios of state migration regardless 39 // of the existence of state in either backend. 40 // 41 // After migrating the state, the existing state in the first backend 42 // remains untouched. 43 // 44 // This will attempt to lock both states for the migration. 45 func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { 46 log.Printf("[TRACE] backendMigrateState: need to migrate from %q to %q backend config", opts.OneType, opts.TwoType) 47 // We need to check what the named state status is. If we're converting 48 // from multi-state to single-state for example, we need to handle that. 49 var oneSingle, twoSingle bool 50 oneStates, err := opts.One.Workspaces() 51 if err == backend.ErrWorkspacesNotSupported { 52 oneSingle = true 53 err = nil 54 } 55 if err != nil { 56 return fmt.Errorf(strings.TrimSpace( 57 errMigrateLoadStates), opts.OneType, err) 58 } 59 60 twoWorkspaces, err := opts.Two.Workspaces() 61 if err == backend.ErrWorkspacesNotSupported { 62 twoSingle = true 63 err = nil 64 } 65 if err != nil { 66 return fmt.Errorf(strings.TrimSpace( 67 errMigrateLoadStates), opts.TwoType, err) 68 } 69 70 // Set up defaults 71 opts.oneEnv = backend.DefaultStateName 72 opts.twoEnv = backend.DefaultStateName 73 opts.force = m.forceInitCopy 74 75 // Disregard remote Terraform version for the state source backend. If it's a 76 // Terraform Cloud remote backend, we don't care about the remote version, 77 // as we are migrating away and will not break a remote workspace. 78 m.ignoreRemoteBackendVersionConflict(opts.One) 79 80 for _, twoWorkspace := range twoWorkspaces { 81 // Check the remote Terraform version for the state destination backend. If 82 // it's a Terraform Cloud remote backend, we want to ensure that we don't 83 // break the workspace by uploading an incompatible state file. 84 diags := m.remoteBackendVersionCheck(opts.Two, twoWorkspace) 85 if diags.HasErrors() { 86 return diags.Err() 87 } 88 } 89 90 // Determine migration behavior based on whether the source/destination 91 // supports multi-state. 92 switch { 93 // Single-state to single-state. This is the easiest case: we just 94 // copy the default state directly. 95 case oneSingle && twoSingle: 96 return m.backendMigrateState_s_s(opts) 97 98 // Single-state to multi-state. This is easy since we just copy 99 // the default state and ignore the rest in the destination. 100 case oneSingle && !twoSingle: 101 return m.backendMigrateState_s_s(opts) 102 103 // Multi-state to single-state. If the source has more than the default 104 // state this is complicated since we have to ask the user what to do. 105 case !oneSingle && twoSingle: 106 // If the source only has one state and it is the default, 107 // treat it as if it doesn't support multi-state. 108 if len(oneStates) == 1 && oneStates[0] == backend.DefaultStateName { 109 return m.backendMigrateState_s_s(opts) 110 } 111 112 return m.backendMigrateState_S_s(opts) 113 114 // Multi-state to multi-state. We merge the states together (migrating 115 // each from the source to the destination one by one). 116 case !oneSingle && !twoSingle: 117 // If the source only has one state and it is the default, 118 // treat it as if it doesn't support multi-state. 119 if len(oneStates) == 1 && oneStates[0] == backend.DefaultStateName { 120 return m.backendMigrateState_s_s(opts) 121 } 122 123 return m.backendMigrateState_S_S(opts) 124 } 125 126 return nil 127 } 128 129 //------------------------------------------------------------------- 130 // State Migration Scenarios 131 // 132 // The functions below cover handling all the various scenarios that 133 // can exist when migrating state. They are named in an immediately not 134 // obvious format but is simple: 135 // 136 // Format: backendMigrateState_s1_s2[_suffix] 137 // 138 // When s1 or s2 is lower case, it means that it is a single state backend. 139 // When either is uppercase, it means that state is a multi-state backend. 140 // The suffix is used to disambiguate multiple cases with the same type of 141 // states. 142 // 143 //------------------------------------------------------------------- 144 145 // Multi-state to multi-state. 146 func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error { 147 log.Print("[TRACE] backendMigrateState: migrating all named workspaces") 148 149 migrate := opts.force 150 if !migrate { 151 var err error 152 // Ask the user if they want to migrate their existing remote state 153 migrate, err = m.confirm(&terraform.InputOpts{ 154 Id: "backend-migrate-multistate-to-multistate", 155 Query: fmt.Sprintf( 156 "Do you want to migrate all workspaces to %q?", 157 opts.TwoType), 158 Description: fmt.Sprintf( 159 strings.TrimSpace(inputBackendMigrateMultiToMulti), 160 opts.OneType, opts.TwoType), 161 }) 162 if err != nil { 163 return fmt.Errorf( 164 "Error asking for state migration action: %s", err) 165 } 166 } 167 if !migrate { 168 return fmt.Errorf("Migration aborted by user.") 169 } 170 171 // Read all the states 172 oneStates, err := opts.One.Workspaces() 173 if err != nil { 174 return fmt.Errorf(strings.TrimSpace( 175 errMigrateLoadStates), opts.OneType, err) 176 } 177 178 // Sort the states so they're always copied alphabetically 179 sort.Strings(oneStates) 180 181 // Go through each and migrate 182 for _, name := range oneStates { 183 // Copy the same names 184 opts.oneEnv = name 185 opts.twoEnv = name 186 187 // Force it, we confirmed above 188 opts.force = true 189 190 // Perform the migration 191 if err := m.backendMigrateState_s_s(opts); err != nil { 192 return fmt.Errorf(strings.TrimSpace( 193 errMigrateMulti), name, opts.OneType, opts.TwoType, err) 194 } 195 } 196 197 return nil 198 } 199 200 // Multi-state to single state. 201 func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { 202 log.Printf("[TRACE] backendMigrateState: target backend type %q does not support named workspaces", opts.TwoType) 203 204 currentEnv, err := m.Workspace() 205 if err != nil { 206 return err 207 } 208 209 migrate := opts.force 210 if !migrate { 211 var err error 212 // Ask the user if they want to migrate their existing remote state 213 migrate, err = m.confirm(&terraform.InputOpts{ 214 Id: "backend-migrate-multistate-to-single", 215 Query: fmt.Sprintf( 216 "Destination state %q doesn't support workspaces.\n"+ 217 "Do you want to copy only your current workspace?", 218 opts.TwoType), 219 Description: fmt.Sprintf( 220 strings.TrimSpace(inputBackendMigrateMultiToSingle), 221 opts.OneType, opts.TwoType, currentEnv), 222 }) 223 if err != nil { 224 return fmt.Errorf( 225 "Error asking for state migration action: %s", err) 226 } 227 } 228 229 if !migrate { 230 return fmt.Errorf("Migration aborted by user.") 231 } 232 233 // Copy the default state 234 opts.oneEnv = currentEnv 235 236 // now switch back to the default env so we can acccess the new backend 237 m.SetWorkspace(backend.DefaultStateName) 238 239 return m.backendMigrateState_s_s(opts) 240 } 241 242 // Single state to single state, assumed default state name. 243 func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { 244 log.Printf("[TRACE] backendMigrateState: migrating %q workspace to %q workspace", opts.oneEnv, opts.twoEnv) 245 246 stateOne, err := opts.One.StateMgr(opts.oneEnv) 247 if err != nil { 248 return fmt.Errorf(strings.TrimSpace( 249 errMigrateSingleLoadDefault), opts.OneType, err) 250 } 251 if err := stateOne.RefreshState(); err != nil { 252 return fmt.Errorf(strings.TrimSpace( 253 errMigrateSingleLoadDefault), opts.OneType, err) 254 } 255 256 // Do not migrate workspaces without state. 257 if stateOne.State().Empty() { 258 log.Print("[TRACE] backendMigrateState: source workspace has empty state, so nothing to migrate") 259 return nil 260 } 261 262 stateTwo, err := opts.Two.StateMgr(opts.twoEnv) 263 if err == backend.ErrDefaultWorkspaceNotSupported { 264 // If the backend doesn't support using the default state, we ask the user 265 // for a new name and migrate the default state to the given named state. 266 stateTwo, err = func() (statemgr.Full, error) { 267 log.Print("[TRACE] backendMigrateState: target doesn't support a default workspace, so we must prompt for a new name") 268 name, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ 269 Id: "new-state-name", 270 Query: fmt.Sprintf( 271 "[reset][bold][yellow]The %q backend configuration only allows "+ 272 "named workspaces![reset]", 273 opts.TwoType), 274 Description: strings.TrimSpace(inputBackendNewWorkspaceName), 275 }) 276 if err != nil { 277 return nil, fmt.Errorf("Error asking for new state name: %s", err) 278 } 279 280 // Update the name of the target state. 281 opts.twoEnv = name 282 283 stateTwo, err := opts.Two.StateMgr(opts.twoEnv) 284 if err != nil { 285 return nil, err 286 } 287 288 // Ignore invalid workspace name as it is irrelevant in this context. 289 workspace, _ := m.Workspace() 290 291 // If the currently selected workspace is the default workspace, then set 292 // the named workspace as the new selected workspace. 293 if workspace == backend.DefaultStateName { 294 if err := m.SetWorkspace(opts.twoEnv); err != nil { 295 return nil, fmt.Errorf("Failed to set new workspace: %s", err) 296 } 297 } 298 299 return stateTwo, nil 300 }() 301 } 302 if err != nil { 303 return fmt.Errorf(strings.TrimSpace( 304 errMigrateSingleLoadDefault), opts.TwoType, err) 305 } 306 if err := stateTwo.RefreshState(); err != nil { 307 return fmt.Errorf(strings.TrimSpace( 308 errMigrateSingleLoadDefault), opts.TwoType, err) 309 } 310 311 // Check if we need migration at all. 312 // This is before taking a lock, because they may also correspond to the same lock. 313 one := stateOne.State() 314 two := stateTwo.State() 315 316 // no reason to migrate if the state is already there 317 if one.Equal(two) { 318 // Equal isn't identical; it doesn't check lineage. 319 sm1, _ := stateOne.(statemgr.PersistentMeta) 320 sm2, _ := stateTwo.(statemgr.PersistentMeta) 321 if one != nil && two != nil { 322 if sm1 == nil || sm2 == nil { 323 log.Print("[TRACE] backendMigrateState: both source and destination workspaces have no state, so no migration is needed") 324 return nil 325 } 326 if sm1.StateSnapshotMeta().Lineage == sm2.StateSnapshotMeta().Lineage { 327 log.Printf("[TRACE] backendMigrateState: both source and destination workspaces have equal state with lineage %q, so no migration is needed", sm1.StateSnapshotMeta().Lineage) 328 return nil 329 } 330 } 331 } 332 333 if m.stateLock { 334 lockCtx := context.Background() 335 336 view := views.NewStateLocker(arguments.ViewHuman, m.View) 337 locker := clistate.NewLocker(m.stateLockTimeout, view) 338 339 lockerOne := locker.WithContext(lockCtx) 340 if diags := lockerOne.Lock(stateOne, "migration source state"); diags.HasErrors() { 341 return diags.Err() 342 } 343 defer lockerOne.Unlock() 344 345 lockerTwo := locker.WithContext(lockCtx) 346 if diags := lockerTwo.Lock(stateTwo, "migration destination state"); diags.HasErrors() { 347 return diags.Err() 348 } 349 defer lockerTwo.Unlock() 350 351 // We now own a lock, so double check that we have the version 352 // corresponding to the lock. 353 log.Print("[TRACE] backendMigrateState: refreshing source workspace state") 354 if err := stateOne.RefreshState(); err != nil { 355 return fmt.Errorf(strings.TrimSpace( 356 errMigrateSingleLoadDefault), opts.OneType, err) 357 } 358 log.Print("[TRACE] backendMigrateState: refreshing target workspace state") 359 if err := stateTwo.RefreshState(); err != nil { 360 return fmt.Errorf(strings.TrimSpace( 361 errMigrateSingleLoadDefault), opts.OneType, err) 362 } 363 364 one = stateOne.State() 365 two = stateTwo.State() 366 } 367 368 var confirmFunc func(statemgr.Full, statemgr.Full, *backendMigrateOpts) (bool, error) 369 switch { 370 // No migration necessary 371 case one.Empty() && two.Empty(): 372 log.Print("[TRACE] backendMigrateState: both source and destination workspaces have empty state, so no migration is required") 373 return nil 374 375 // No migration necessary if we're inheriting state. 376 case one.Empty() && !two.Empty(): 377 log.Print("[TRACE] backendMigrateState: source workspace has empty state, so no migration is required") 378 return nil 379 380 // We have existing state moving into no state. Ask the user if 381 // they'd like to do this. 382 case !one.Empty() && two.Empty(): 383 log.Print("[TRACE] backendMigrateState: target workspace has empty state, so might copy source workspace state") 384 confirmFunc = m.backendMigrateEmptyConfirm 385 386 // Both states are non-empty, meaning we need to determine which 387 // state should be used and update accordingly. 388 case !one.Empty() && !two.Empty(): 389 log.Print("[TRACE] backendMigrateState: both source and destination workspaces have states, so might overwrite destination with source") 390 confirmFunc = m.backendMigrateNonEmptyConfirm 391 } 392 393 if confirmFunc == nil { 394 panic("confirmFunc must not be nil") 395 } 396 397 if !opts.force { 398 // Abort if we can't ask for input. 399 if !m.input { 400 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 401 return errors.New("error asking for state migration action: input disabled") 402 } 403 404 // Confirm with the user whether we want to copy state over 405 confirm, err := confirmFunc(stateOne, stateTwo, opts) 406 if err != nil { 407 log.Print("[TRACE] backendMigrateState: error reading input, so aborting migration") 408 return err 409 } 410 if !confirm { 411 log.Print("[TRACE] backendMigrateState: user cancelled at confirmation prompt, so aborting migration") 412 return nil 413 } 414 } 415 416 // Confirmed! We'll have the statemgr package handle the migration, which 417 // includes preserving any lineage/serial information where possible, if 418 // both managers support such metadata. 419 log.Print("[TRACE] backendMigrateState: migration confirmed, so migrating") 420 if err := statemgr.Migrate(stateTwo, stateOne); err != nil { 421 return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), 422 opts.OneType, opts.TwoType, err) 423 } 424 if err := stateTwo.PersistState(); err != nil { 425 return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), 426 opts.OneType, opts.TwoType, err) 427 } 428 429 // And we're done. 430 return nil 431 } 432 433 func (m *Meta) backendMigrateEmptyConfirm(one, two statemgr.Full, opts *backendMigrateOpts) (bool, error) { 434 inputOpts := &terraform.InputOpts{ 435 Id: "backend-migrate-copy-to-empty", 436 Query: "Do you want to copy existing state to the new backend?", 437 Description: fmt.Sprintf( 438 strings.TrimSpace(inputBackendMigrateEmpty), 439 opts.OneType, opts.TwoType), 440 } 441 442 return m.confirm(inputOpts) 443 } 444 445 func (m *Meta) backendMigrateNonEmptyConfirm( 446 stateOne, stateTwo statemgr.Full, opts *backendMigrateOpts) (bool, error) { 447 // We need to grab both states so we can write them to a file 448 one := stateOne.State() 449 two := stateTwo.State() 450 451 // Save both to a temporary 452 td, err := ioutil.TempDir("", "terraform") 453 if err != nil { 454 return false, fmt.Errorf("Error creating temporary directory: %s", err) 455 } 456 defer os.RemoveAll(td) 457 458 // Helper to write the state 459 saveHelper := func(n, path string, s *states.State) error { 460 mgr := statemgr.NewFilesystem(path) 461 return mgr.WriteState(s) 462 } 463 464 // Write the states 465 onePath := filepath.Join(td, fmt.Sprintf("1-%s.tfstate", opts.OneType)) 466 twoPath := filepath.Join(td, fmt.Sprintf("2-%s.tfstate", opts.TwoType)) 467 if err := saveHelper(opts.OneType, onePath, one); err != nil { 468 return false, fmt.Errorf("Error saving temporary state: %s", err) 469 } 470 if err := saveHelper(opts.TwoType, twoPath, two); err != nil { 471 return false, fmt.Errorf("Error saving temporary state: %s", err) 472 } 473 474 // Ask for confirmation 475 inputOpts := &terraform.InputOpts{ 476 Id: "backend-migrate-to-backend", 477 Query: "Do you want to copy existing state to the new backend?", 478 Description: fmt.Sprintf( 479 strings.TrimSpace(inputBackendMigrateNonEmpty), 480 opts.OneType, opts.TwoType, onePath, twoPath), 481 } 482 483 // Confirm with the user that the copy should occur 484 return m.confirm(inputOpts) 485 } 486 487 const errMigrateLoadStates = ` 488 Error inspecting states in the %q backend: 489 %s 490 491 Prior to changing backends, Terraform inspects the source and destination 492 states to determine what kind of migration steps need to be taken, if any. 493 Terraform failed to load the states. The data in both the source and the 494 destination remain unmodified. Please resolve the above error and try again. 495 ` 496 497 const errMigrateSingleLoadDefault = ` 498 Error loading state: 499 %[2]s 500 501 Terraform failed to load the default state from the %[1]q backend. 502 State migration cannot occur unless the state can be loaded. Backend 503 modification and state migration has been aborted. The state in both the 504 source and the destination remain unmodified. Please resolve the 505 above error and try again. 506 ` 507 508 const errMigrateMulti = ` 509 Error migrating the workspace %q from the previous %q backend 510 to the newly configured %q backend: 511 %s 512 513 Terraform copies workspaces in alphabetical order. Any workspaces 514 alphabetically earlier than this one have been copied. Any workspaces 515 later than this haven't been modified in the destination. No workspaces 516 in the source state have been modified. 517 518 Please resolve the error above and run the initialization command again. 519 This will attempt to copy (with permission) all workspaces again. 520 ` 521 522 const errBackendStateCopy = ` 523 Error copying state from the previous %q backend to the newly configured 524 %q backend: 525 %s 526 527 The state in the previous backend remains intact and unmodified. Please resolve 528 the error above and try again. 529 ` 530 531 const inputBackendMigrateEmpty = ` 532 Pre-existing state was found while migrating the previous %q backend to the 533 newly configured %q backend. No existing state was found in the newly 534 configured %[2]q backend. Do you want to copy this state to the new %[2]q 535 backend? Enter "yes" to copy and "no" to start with an empty state. 536 ` 537 538 const inputBackendMigrateNonEmpty = ` 539 Pre-existing state was found while migrating the previous %q backend to the 540 newly configured %q backend. An existing non-empty state already exists in 541 the new backend. The two states have been saved to temporary files that will be 542 removed after responding to this query. 543 544 Previous (type %[1]q): %[3]s 545 New (type %[2]q): %[4]s 546 547 Do you want to overwrite the state in the new backend with the previous state? 548 Enter "yes" to copy and "no" to start with the existing state in the newly 549 configured %[2]q backend. 550 ` 551 552 const inputBackendMigrateMultiToSingle = ` 553 The existing %[1]q backend supports workspaces and you currently are 554 using more than one. The newly configured %[2]q backend doesn't support 555 workspaces. If you continue, Terraform will copy your current workspace %[3]q 556 to the default workspace in the target backend. Your existing workspaces in the 557 source backend won't be modified. If you want to switch workspaces, back them 558 up, or cancel altogether, answer "no" and Terraform will abort. 559 ` 560 561 const inputBackendMigrateMultiToMulti = ` 562 Both the existing %[1]q backend and the newly configured %[2]q backend 563 support workspaces. When migrating between backends, Terraform will copy 564 all workspaces (with the same names). THIS WILL OVERWRITE any conflicting 565 states in the destination. 566 567 Terraform initialization doesn't currently migrate only select workspaces. 568 If you want to migrate a select number of workspaces, you must manually 569 pull and push those states. 570 571 If you answer "yes", Terraform will migrate all states. If you answer 572 "no", Terraform will abort. 573 ` 574 575 const inputBackendNewWorkspaceName = ` 576 Please provide a new workspace name (e.g. dev, test) that will be used 577 to migrate the existing default workspace. 578 ` 579 580 const inputBackendSelectWorkspace = ` 581 This is expected behavior when the selected workspace did not have an 582 existing non-empty state. Please enter a number to select a workspace: 583 584 %s 585 `