backend/migrations: migrate the default state

Certain backends (currently only the `remote` backend) do not support using both the default and named workspaces at the same time.

To make the migration easier for users that currently use both types of workspaces, this commit adds logic to ask the user for a new workspace name during the migration process.
This commit is contained in:
Sander van Harmelen 2018-08-28 14:02:18 +02:00
parent 4612a71414
commit f410a5bb26
4 changed files with 182 additions and 38 deletions

View File

@ -22,15 +22,15 @@ const DefaultStateName = "default"
// This must be returned rather than a custom error so that the Terraform
// CLI can detect it and handle it appropriately.
var (
// ErrDefaultStateNotSupported is returned when an operation does not support
// using the default state, but requires a named state to be selected.
ErrDefaultStateNotSupported = errors.New("default state not supported\n" +
"You can create a new workspace with the \"workspace new\" command.")
// ErrNamedStatesNotSupported is returned when a named state operation
// isn't supported.
ErrNamedStatesNotSupported = errors.New("named states not supported")
// ErrDefaultStateNotSupported is returned when an operation does not support
// using the default state, but requires a named state to be selected.
ErrDefaultStateNotSupported = errors.New("default state not supported\n\n" +
"You can create a new workspace wth the \"workspace new\" command")
// ErrOperationNotSupported is returned when an unsupported operation
// is detected by the configured backend.
ErrOperationNotSupported = errors.New("operation not supported")

View File

@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/hashicorp/terraform/backend"
@ -16,6 +17,17 @@ import (
"github.com/hashicorp/terraform/terraform"
)
type backendMigrateOpts struct {
OneType, TwoType string
One, Two backend.Backend
// Fields below are set internally when migrate is called
oneEnv string // source env
twoEnv string // dest env
force bool // if true, won't ask for confirmation
}
// backendMigrateState handles migrating (copying) state from one backend
// to another. This function handles asking the user for confirmation
// as well as the copy itself.
@ -157,7 +169,56 @@ func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error {
}
}
return nil
// Its possible that the currently selected workspace is not migrated,
// so we call selectWorkspace to ensure a valid workspace is selected.
return m.selectWorkspace(opts.Two)
}
// selectWorkspace gets a list of migrated workspaces and then checks
// if the currently selected workspace is valid. If not, it will ask
// the user to select a workspace from the list.
func (m *Meta) selectWorkspace(b backend.Backend) error {
workspaces, err := b.States()
if err != nil {
return fmt.Errorf("Failed to get migrated workspaces: %s", err)
}
if len(workspaces) == 0 {
return fmt.Errorf(errBackendNoMigratedWorkspaces)
}
// Get the currently selected workspace.
workspace := m.Workspace()
// Check if any of the migrated workspaces match the selected workspace
// and create a numbered list with migrated workspaces.
var list strings.Builder
for i, w := range workspaces {
if w == workspace {
return nil
}
fmt.Fprintf(&list, "%d. %s\n", i+1, w)
}
// If the selected workspace is not migrated, ask the user to select
// a workspace from the list of migrated workspaces.
v, err := m.UIInput().Input(&terraform.InputOpts{
Id: "select-workspace",
Query: fmt.Sprintf(
"[reset][bold][yellow]The currently selected workspace (%s) is not migrated.[reset]",
workspace),
Description: fmt.Sprintf(
strings.TrimSpace(inputBackendSelectWorkspace), list.String()),
})
if err != nil {
return fmt.Errorf("Error asking to select workspace: %s", err)
}
idx, err := strconv.Atoi(v)
if err != nil || (idx < 1 || idx > len(workspaces)) {
return fmt.Errorf("Error selecting workspace: input not a valid number")
}
return m.SetWorkspace(workspaces[idx-1])
}
// Multi-state to single state.
@ -209,15 +270,48 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
errMigrateSingleLoadDefault), opts.OneType, err)
}
// Do not migrate workspaces without state.
if stateOne.State() == nil {
return nil
}
stateTwo, err := opts.Two.State(opts.twoEnv)
if err == backend.ErrDefaultStateNotSupported {
// If the backend doesn't support using the default state, we ask the user
// for a new name and migrate the default state to the given named state.
stateTwo, err = func() (state.State, error) {
name, err := m.UIInput().Input(&terraform.InputOpts{
Id: "new-state-name",
Query: fmt.Sprintf(
"[reset][bold][yellow]The %q backend configuration only allows "+
"named workspaces![reset]",
opts.TwoType),
Description: strings.TrimSpace(inputBackendNewWorkspaceName),
})
if err != nil {
return nil, fmt.Errorf("Error asking for new state name: %s", err)
}
// Update the name of the target state.
opts.twoEnv = name
stateTwo, err := opts.Two.State(opts.twoEnv)
if err != nil {
return nil, err
}
// If the currently selected workspace is the default workspace, then set
// the named workspace as the new selected workspace.
if m.Workspace() == backend.DefaultStateName {
if err := m.SetWorkspace(opts.twoEnv); err != nil {
return nil, fmt.Errorf("Failed to set new workspace: %s", err)
}
}
return stateTwo, nil
}()
}
if err != nil {
if err == backend.ErrDefaultStateNotSupported && stateOne.State() == nil {
// When using named workspaces it is common that the default
// workspace is not actually used. So we first check if there
// actually is a state to be migrated, if not we just return
// and silently ignore the unused default worksopace.
return nil
}
return fmt.Errorf(strings.TrimSpace(
errMigrateSingleLoadDefault), opts.TwoType, err)
}
@ -392,17 +486,6 @@ func (m *Meta) backendMigrateNonEmptyConfirm(
return m.confirm(inputOpts)
}
type backendMigrateOpts struct {
OneType, TwoType string
One, Two backend.Backend
// Fields below are set internally when migrate is called
oneEnv string // source env
twoEnv string // dest env
force bool // if true, won't ask for confirmation
}
const errMigrateLoadStates = `
Error inspecting states in the %q backend:
%s
@ -447,6 +530,14 @@ The state in the previous backend remains intact and unmodified. Please resolve
the error above and try again.
`
const errBackendNoMigratedWorkspaces = `
No workspaces are migrated. Use the "terraform workspace" command to create
and select a new workspace.
If the backend already contains existing workspaces, you may need to update
the workspace name or prefix in the backend configuration.
`
const inputBackendMigrateEmpty = `
Pre-existing state was found while migrating the previous %q backend to the
newly configured %q backend. No existing state was found in the newly
@ -478,9 +569,9 @@ up, or cancel altogether, answer "no" and Terraform will abort.
`
const inputBackendMigrateMultiToMulti = `
Both the existing %[1]q backend and the newly configured %[2]q backend support
workspaces. When migrating between backends, Terraform will copy all
workspaces (with the same names). THIS WILL OVERWRITE any conflicting
Both the existing %[1]q backend and the newly configured %[2]q backend
support workspaces. When migrating between backends, Terraform will copy
all workspaces (with the same names). THIS WILL OVERWRITE any conflicting
states in the destination.
Terraform initialization doesn't currently migrate only select workspaces.
@ -490,3 +581,15 @@ pull and push those states.
If you answer "yes", Terraform will migrate all states. If you answer
"no", Terraform will abort.
`
const inputBackendNewWorkspaceName = `
Please provide a new workspace name (e.g. dev, test) that will be used
to migrate the existing default workspace.
`
const inputBackendSelectWorkspace = `
This is expected behavior when the selected workspace did not have an
existing non-empty state. Please enter a number to select a workspace:
%s
`

View File

@ -1098,7 +1098,6 @@ func TestMetaBackend_configuredChangeCopy_singleState(t *testing.T) {
// Ask input
defer testInputMap(t, map[string]string{
"backend-migrate-to-new": "yes",
"backend-migrate-copy-to-empty": "yes",
})()
@ -1154,7 +1153,6 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleDefault(t *testing.T) {
// Ask input
defer testInputMap(t, map[string]string{
"backend-migrate-to-new": "yes",
"backend-migrate-copy-to-empty": "yes",
})()
@ -1209,7 +1207,6 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) {
// Ask input
defer testInputMap(t, map[string]string{
"backend-migrate-to-new": "yes",
"backend-migrate-multistate-to-single": "yes",
"backend-migrate-copy-to-empty": "yes",
})()
@ -1276,7 +1273,6 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T)
// Ask input
defer testInputMap(t, map[string]string{
"backend-migrate-to-new": "yes",
"backend-migrate-multistate-to-single": "yes",
"backend-migrate-copy-to-empty": "yes",
})()
@ -1339,7 +1335,6 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) {
// Ask input
defer testInputMap(t, map[string]string{
"backend-migrate-to-new": "yes",
"backend-migrate-multistate-to-multistate": "yes",
})()
@ -1438,17 +1433,63 @@ func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithDefault(t *testing
// Ask input
defer testInputMap(t, map[string]string{
"backend-migrate-to-new": "yes",
"backend-migrate-multistate-to-multistate": "yes",
"new-state-name": "env1",
})()
// Setup the meta
m := testMetaBackend(t, nil)
// Get the backend
_, err := m.Backend(&BackendOpts{Init: true})
if err == nil || !strings.Contains(err.Error(), "default state not supported") {
t.Fatalf("expected error to contain %q\ngot: %s", "default state not supported", err)
b, err := m.Backend(&BackendOpts{Init: true})
if err != nil {
t.Fatalf("bad: %s", err)
}
// Check resulting states
states, err := b.States()
if err != nil {
t.Fatalf("bad: %s", err)
}
sort.Strings(states)
expected := []string{"env1", "env2"}
if !reflect.DeepEqual(states, expected) {
t.Fatalf("bad: %#v", states)
}
{
// Check the renamed default state
s, err := b.State("env1")
if err != nil {
t.Fatalf("bad: %s", err)
}
if err := s.RefreshState(); err != nil {
t.Fatalf("bad: %s", err)
}
state := s.State()
if state == nil {
t.Fatal("state should not be nil")
}
if state.Lineage != "backend-change-env1" {
t.Fatalf("bad: %#v", state)
}
}
{
// Verify existing workspaces exist
envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
}
{
// Verify new workspaces exist
envPath := filepath.Join("envdir-new", "env2", backendLocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
}
}
@ -1468,8 +1509,8 @@ func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithoutDefault(t *test
// Ask input
defer testInputMap(t, map[string]string{
"backend-migrate-to-new": "yes",
"backend-migrate-multistate-to-multistate": "yes",
"select-workspace": "1",
})()
// Setup the meta

View File

@ -2,5 +2,5 @@
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "backend-change"
"lineage": "backend-change-env1"
}