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  `