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  `