Skip to content

Commit

Permalink
Merge pull request #18760 from hashicorp/f-remote-backend
Browse files Browse the repository at this point in the history
backend/migrations: migrate the default state
  • Loading branch information
Sander van Harmelen authored Aug 30, 2018
2 parents b591cb6 + f410a5b commit ce2869d
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 38 deletions.
10 changes: 5 additions & 5 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
147 changes: 125 additions & 22 deletions command/meta_backend_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"

"github.com/hashicorp/terraform/backend"
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
`
61 changes: 51 additions & 10 deletions command/meta_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})()

Expand Down Expand Up @@ -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",
})()

Expand Down Expand Up @@ -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",
})()
Expand Down Expand Up @@ -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",
})()
Expand Down Expand Up @@ -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",
})()

Expand Down Expand Up @@ -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")
}
}
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "backend-change"
"lineage": "backend-change-env1"
}

0 comments on commit ce2869d

Please sign in to comment.