github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/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 "github.com/iaas-resource-provision/iaas-rpc/internal/backend" 15 "github.com/iaas-resource-provision/iaas-rpc/internal/command/arguments" 16 "github.com/iaas-resource-provision/iaas-rpc/internal/command/clistate" 17 "github.com/iaas-resource-provision/iaas-rpc/internal/command/views" 18 "github.com/iaas-resource-provision/iaas-rpc/internal/states" 19 "github.com/iaas-resource-provision/iaas-rpc/internal/states/statemgr" 20 "github.com/iaas-resource-provision/iaas-rpc/internal/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 // Ask the user if they want to migrate their existing remote state 150 migrate, err := m.confirm(&terraform.InputOpts{ 151 Id: "backend-migrate-multistate-to-multistate", 152 Query: fmt.Sprintf( 153 "Do you want to migrate all workspaces to %q?", 154 opts.TwoType), 155 Description: fmt.Sprintf( 156 strings.TrimSpace(inputBackendMigrateMultiToMulti), 157 opts.OneType, opts.TwoType), 158 }) 159 if err != nil { 160 return fmt.Errorf( 161 "Error asking for state migration action: %s", err) 162 } 163 if !migrate { 164 return fmt.Errorf("Migration aborted by user.") 165 } 166 167 // Read all the states 168 oneStates, err := opts.One.Workspaces() 169 if err != nil { 170 return fmt.Errorf(strings.TrimSpace( 171 errMigrateLoadStates), opts.OneType, err) 172 } 173 174 // Sort the states so they're always copied alphabetically 175 sort.Strings(oneStates) 176 177 // Go through each and migrate 178 for _, name := range oneStates { 179 // Copy the same names 180 opts.oneEnv = name 181 opts.twoEnv = name 182 183 // Force it, we confirmed above 184 opts.force = true 185 186 // Perform the migration 187 if err := m.backendMigrateState_s_s(opts); err != nil { 188 return fmt.Errorf(strings.TrimSpace( 189 errMigrateMulti), name, opts.OneType, opts.TwoType, err) 190 } 191 } 192 193 return nil 194 } 195 196 // Multi-state to single state. 197 func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { 198 log.Printf("[TRACE] backendMigrateState: target backend type %q does not support named workspaces", opts.TwoType) 199 200 currentEnv, err := m.Workspace() 201 if err != nil { 202 return err 203 } 204 205 migrate := opts.force 206 if !migrate { 207 var err error 208 // Ask the user if they want to migrate their existing remote state 209 migrate, err = m.confirm(&terraform.InputOpts{ 210 Id: "backend-migrate-multistate-to-single", 211 Query: fmt.Sprintf( 212 "Destination state %q doesn't support workspaces.\n"+ 213 "Do you want to copy only your current workspace?", 214 opts.TwoType), 215 Description: fmt.Sprintf( 216 strings.TrimSpace(inputBackendMigrateMultiToSingle), 217 opts.OneType, opts.TwoType, currentEnv), 218 }) 219 if err != nil { 220 return fmt.Errorf( 221 "Error asking for state migration action: %s", err) 222 } 223 } 224 225 if !migrate { 226 return fmt.Errorf("Migration aborted by user.") 227 } 228 229 // Copy the default state 230 opts.oneEnv = currentEnv 231 232 // now switch back to the default env so we can acccess the new backend 233 m.SetWorkspace(backend.DefaultStateName) 234 235 return m.backendMigrateState_s_s(opts) 236 } 237 238 // Single state to single state, assumed default state name. 239 func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { 240 log.Printf("[TRACE] backendMigrateState: migrating %q workspace to %q workspace", opts.oneEnv, opts.twoEnv) 241 242 stateOne, err := opts.One.StateMgr(opts.oneEnv) 243 if err != nil { 244 return fmt.Errorf(strings.TrimSpace( 245 errMigrateSingleLoadDefault), opts.OneType, err) 246 } 247 if err := stateOne.RefreshState(); err != nil { 248 return fmt.Errorf(strings.TrimSpace( 249 errMigrateSingleLoadDefault), opts.OneType, err) 250 } 251 252 // Do not migrate workspaces without state. 253 if stateOne.State().Empty() { 254 log.Print("[TRACE] backendMigrateState: source workspace has empty state, so nothing to migrate") 255 return nil 256 } 257 258 stateTwo, err := opts.Two.StateMgr(opts.twoEnv) 259 if err == backend.ErrDefaultWorkspaceNotSupported { 260 // If the backend doesn't support using the default state, we ask the user 261 // for a new name and migrate the default state to the given named state. 262 stateTwo, err = func() (statemgr.Full, error) { 263 log.Print("[TRACE] backendMigrateState: target doesn't support a default workspace, so we must prompt for a new name") 264 name, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ 265 Id: "new-state-name", 266 Query: fmt.Sprintf( 267 "[reset][bold][yellow]The %q backend configuration only allows "+ 268 "named workspaces![reset]", 269 opts.TwoType), 270 Description: strings.TrimSpace(inputBackendNewWorkspaceName), 271 }) 272 if err != nil { 273 return nil, fmt.Errorf("Error asking for new state name: %s", err) 274 } 275 276 // Update the name of the target state. 277 opts.twoEnv = name 278 279 stateTwo, err := opts.Two.StateMgr(opts.twoEnv) 280 if err != nil { 281 return nil, err 282 } 283 284 // Ignore invalid workspace name as it is irrelevant in this context. 285 workspace, _ := m.Workspace() 286 287 // If the currently selected workspace is the default workspace, then set 288 // the named workspace as the new selected workspace. 289 if workspace == backend.DefaultStateName { 290 if err := m.SetWorkspace(opts.twoEnv); err != nil { 291 return nil, fmt.Errorf("Failed to set new workspace: %s", err) 292 } 293 } 294 295 return stateTwo, nil 296 }() 297 } 298 if err != nil { 299 return fmt.Errorf(strings.TrimSpace( 300 errMigrateSingleLoadDefault), opts.TwoType, err) 301 } 302 if err := stateTwo.RefreshState(); err != nil { 303 return fmt.Errorf(strings.TrimSpace( 304 errMigrateSingleLoadDefault), opts.TwoType, err) 305 } 306 307 // Check if we need migration at all. 308 // This is before taking a lock, because they may also correspond to the same lock. 309 one := stateOne.State() 310 two := stateTwo.State() 311 312 // no reason to migrate if the state is already there 313 if one.Equal(two) { 314 // Equal isn't identical; it doesn't check lineage. 315 sm1, _ := stateOne.(statemgr.PersistentMeta) 316 sm2, _ := stateTwo.(statemgr.PersistentMeta) 317 if one != nil && two != nil { 318 if sm1 == nil || sm2 == nil { 319 log.Print("[TRACE] backendMigrateState: both source and destination workspaces have no state, so no migration is needed") 320 return nil 321 } 322 if sm1.StateSnapshotMeta().Lineage == sm2.StateSnapshotMeta().Lineage { 323 log.Printf("[TRACE] backendMigrateState: both source and destination workspaces have equal state with lineage %q, so no migration is needed", sm1.StateSnapshotMeta().Lineage) 324 return nil 325 } 326 } 327 } 328 329 if m.stateLock { 330 lockCtx := context.Background() 331 332 view := views.NewStateLocker(arguments.ViewHuman, m.View) 333 locker := clistate.NewLocker(m.stateLockTimeout, view) 334 335 lockerOne := locker.WithContext(lockCtx) 336 if diags := lockerOne.Lock(stateOne, "migration source state"); diags.HasErrors() { 337 return diags.Err() 338 } 339 defer lockerOne.Unlock() 340 341 lockerTwo := locker.WithContext(lockCtx) 342 if diags := lockerTwo.Lock(stateTwo, "migration destination state"); diags.HasErrors() { 343 return diags.Err() 344 } 345 defer lockerTwo.Unlock() 346 347 // We now own a lock, so double check that we have the version 348 // corresponding to the lock. 349 log.Print("[TRACE] backendMigrateState: refreshing source workspace state") 350 if err := stateOne.RefreshState(); err != nil { 351 return fmt.Errorf(strings.TrimSpace( 352 errMigrateSingleLoadDefault), opts.OneType, err) 353 } 354 log.Print("[TRACE] backendMigrateState: refreshing target workspace state") 355 if err := stateTwo.RefreshState(); err != nil { 356 return fmt.Errorf(strings.TrimSpace( 357 errMigrateSingleLoadDefault), opts.OneType, err) 358 } 359 360 one = stateOne.State() 361 two = stateTwo.State() 362 } 363 364 var confirmFunc func(statemgr.Full, statemgr.Full, *backendMigrateOpts) (bool, error) 365 switch { 366 // No migration necessary 367 case one.Empty() && two.Empty(): 368 log.Print("[TRACE] backendMigrateState: both source and destination workspaces have empty state, so no migration is required") 369 return nil 370 371 // No migration necessary if we're inheriting state. 372 case one.Empty() && !two.Empty(): 373 log.Print("[TRACE] backendMigrateState: source workspace has empty state, so no migration is required") 374 return nil 375 376 // We have existing state moving into no state. Ask the user if 377 // they'd like to do this. 378 case !one.Empty() && two.Empty(): 379 log.Print("[TRACE] backendMigrateState: target workspace has empty state, so might copy source workspace state") 380 confirmFunc = m.backendMigrateEmptyConfirm 381 382 // Both states are non-empty, meaning we need to determine which 383 // state should be used and update accordingly. 384 case !one.Empty() && !two.Empty(): 385 log.Print("[TRACE] backendMigrateState: both source and destination workspaces have states, so might overwrite destination with source") 386 confirmFunc = m.backendMigrateNonEmptyConfirm 387 } 388 389 if confirmFunc == nil { 390 panic("confirmFunc must not be nil") 391 } 392 393 if !opts.force { 394 // Abort if we can't ask for input. 395 if !m.input { 396 log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") 397 return errors.New("error asking for state migration action: input disabled") 398 } 399 400 // Confirm with the user whether we want to copy state over 401 confirm, err := confirmFunc(stateOne, stateTwo, opts) 402 if err != nil { 403 log.Print("[TRACE] backendMigrateState: error reading input, so aborting migration") 404 return err 405 } 406 if !confirm { 407 log.Print("[TRACE] backendMigrateState: user cancelled at confirmation prompt, so aborting migration") 408 return nil 409 } 410 } 411 412 // Confirmed! We'll have the statemgr package handle the migration, which 413 // includes preserving any lineage/serial information where possible, if 414 // both managers support such metadata. 415 log.Print("[TRACE] backendMigrateState: migration confirmed, so migrating") 416 if err := statemgr.Migrate(stateTwo, stateOne); err != nil { 417 return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), 418 opts.OneType, opts.TwoType, err) 419 } 420 if err := stateTwo.PersistState(); err != nil { 421 return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), 422 opts.OneType, opts.TwoType, err) 423 } 424 425 // And we're done. 426 return nil 427 } 428 429 func (m *Meta) backendMigrateEmptyConfirm(one, two statemgr.Full, opts *backendMigrateOpts) (bool, error) { 430 inputOpts := &terraform.InputOpts{ 431 Id: "backend-migrate-copy-to-empty", 432 Query: "Do you want to copy existing state to the new backend?", 433 Description: fmt.Sprintf( 434 strings.TrimSpace(inputBackendMigrateEmpty), 435 opts.OneType, opts.TwoType), 436 } 437 438 return m.confirm(inputOpts) 439 } 440 441 func (m *Meta) backendMigrateNonEmptyConfirm( 442 stateOne, stateTwo statemgr.Full, opts *backendMigrateOpts) (bool, error) { 443 // We need to grab both states so we can write them to a file 444 one := stateOne.State() 445 two := stateTwo.State() 446 447 // Save both to a temporary 448 td, err := ioutil.TempDir("", "terraform") 449 if err != nil { 450 return false, fmt.Errorf("Error creating temporary directory: %s", err) 451 } 452 defer os.RemoveAll(td) 453 454 // Helper to write the state 455 saveHelper := func(n, path string, s *states.State) error { 456 mgr := statemgr.NewFilesystem(path) 457 return mgr.WriteState(s) 458 } 459 460 // Write the states 461 onePath := filepath.Join(td, fmt.Sprintf("1-%s.tfstate", opts.OneType)) 462 twoPath := filepath.Join(td, fmt.Sprintf("2-%s.tfstate", opts.TwoType)) 463 if err := saveHelper(opts.OneType, onePath, one); err != nil { 464 return false, fmt.Errorf("Error saving temporary state: %s", err) 465 } 466 if err := saveHelper(opts.TwoType, twoPath, two); err != nil { 467 return false, fmt.Errorf("Error saving temporary state: %s", err) 468 } 469 470 // Ask for confirmation 471 inputOpts := &terraform.InputOpts{ 472 Id: "backend-migrate-to-backend", 473 Query: "Do you want to copy existing state to the new backend?", 474 Description: fmt.Sprintf( 475 strings.TrimSpace(inputBackendMigrateNonEmpty), 476 opts.OneType, opts.TwoType, onePath, twoPath), 477 } 478 479 // Confirm with the user that the copy should occur 480 return m.confirm(inputOpts) 481 } 482 483 const errMigrateLoadStates = ` 484 Error inspecting states in the %q backend: 485 %s 486 487 Prior to changing backends, Terraform inspects the source and destination 488 states to determine what kind of migration steps need to be taken, if any. 489 Terraform failed to load the states. The data in both the source and the 490 destination remain unmodified. Please resolve the above error and try again. 491 ` 492 493 const errMigrateSingleLoadDefault = ` 494 Error loading state: 495 %[2]s 496 497 Terraform failed to load the default state from the %[1]q backend. 498 State migration cannot occur unless the state can be loaded. Backend 499 modification and state migration has been aborted. The state in both the 500 source and the destination remain unmodified. Please resolve the 501 above error and try again. 502 ` 503 504 const errMigrateMulti = ` 505 Error migrating the workspace %q from the previous %q backend 506 to the newly configured %q backend: 507 %s 508 509 Terraform copies workspaces in alphabetical order. Any workspaces 510 alphabetically earlier than this one have been copied. Any workspaces 511 later than this haven't been modified in the destination. No workspaces 512 in the source state have been modified. 513 514 Please resolve the error above and run the initialization command again. 515 This will attempt to copy (with permission) all workspaces again. 516 ` 517 518 const errBackendStateCopy = ` 519 Error copying state from the previous %q backend to the newly configured 520 %q backend: 521 %s 522 523 The state in the previous backend remains intact and unmodified. Please resolve 524 the error above and try again. 525 ` 526 527 const inputBackendMigrateEmpty = ` 528 Pre-existing state was found while migrating the previous %q backend to the 529 newly configured %q backend. No existing state was found in the newly 530 configured %[2]q backend. Do you want to copy this state to the new %[2]q 531 backend? Enter "yes" to copy and "no" to start with an empty state. 532 ` 533 534 const inputBackendMigrateNonEmpty = ` 535 Pre-existing state was found while migrating the previous %q backend to the 536 newly configured %q backend. An existing non-empty state already exists in 537 the new backend. The two states have been saved to temporary files that will be 538 removed after responding to this query. 539 540 Previous (type %[1]q): %[3]s 541 New (type %[2]q): %[4]s 542 543 Do you want to overwrite the state in the new backend with the previous state? 544 Enter "yes" to copy and "no" to start with the existing state in the newly 545 configured %[2]q backend. 546 ` 547 548 const inputBackendMigrateMultiToSingle = ` 549 The existing %[1]q backend supports workspaces and you currently are 550 using more than one. The newly configured %[2]q backend doesn't support 551 workspaces. If you continue, Terraform will copy your current workspace %[3]q 552 to the default workspace in the target backend. Your existing workspaces in the 553 source backend won't be modified. If you want to switch workspaces, back them 554 up, or cancel altogether, answer "no" and Terraform will abort. 555 ` 556 557 const inputBackendMigrateMultiToMulti = ` 558 Both the existing %[1]q backend and the newly configured %[2]q backend 559 support workspaces. When migrating between backends, Terraform will copy 560 all workspaces (with the same names). THIS WILL OVERWRITE any conflicting 561 states in the destination. 562 563 Terraform initialization doesn't currently migrate only select workspaces. 564 If you want to migrate a select number of workspaces, you must manually 565 pull and push those states. 566 567 If you answer "yes", Terraform will migrate all states. If you answer 568 "no", Terraform will abort. 569 ` 570 571 const inputBackendNewWorkspaceName = ` 572 Please provide a new workspace name (e.g. dev, test) that will be used 573 to migrate the existing default workspace. 574 ` 575 576 const inputBackendSelectWorkspace = ` 577 This is expected behavior when the selected workspace did not have an 578 existing non-empty state. Please enter a number to select a workspace: 579 580 %s 581 `