github.com/jae-cisco/terraform@v0.11.12-beta1/command/meta_backend_migrate.go (about)

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