Merge pull request #18760 from hashicorp/f-remote-backend

backend/migrations: migrate the default state
This commit is contained in:
Sander van Harmelen 2018-08-30 11:16:25 +02:00 committed by GitHub
commit ce2869dced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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"
}