Merge pull request #18760 from hashicorp/f-remote-backend
backend/migrations: migrate the default state
This commit is contained in:
commit
ce2869dced
|
@ -22,15 +22,15 @@ const DefaultStateName = "default"
|
||||||
// This must be returned rather than a custom error so that the Terraform
|
// This must be returned rather than a custom error so that the Terraform
|
||||||
// CLI can detect it and handle it appropriately.
|
// CLI can detect it and handle it appropriately.
|
||||||
var (
|
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
|
// ErrNamedStatesNotSupported is returned when a named state operation
|
||||||
// isn't supported.
|
// isn't supported.
|
||||||
ErrNamedStatesNotSupported = errors.New("named states not 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
|
// ErrOperationNotSupported is returned when an unsupported operation
|
||||||
// is detected by the configured backend.
|
// is detected by the configured backend.
|
||||||
ErrOperationNotSupported = errors.New("operation not supported")
|
ErrOperationNotSupported = errors.New("operation not supported")
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
@ -16,6 +17,17 @@ import (
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"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
|
// backendMigrateState handles migrating (copying) state from one backend
|
||||||
// to another. This function handles asking the user for confirmation
|
// to another. This function handles asking the user for confirmation
|
||||||
// as well as the copy itself.
|
// 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.
|
// Multi-state to single state.
|
||||||
|
@ -209,15 +270,48 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
|
||||||
errMigrateSingleLoadDefault), opts.OneType, err)
|
errMigrateSingleLoadDefault), opts.OneType, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do not migrate workspaces without state.
|
||||||
|
if stateOne.State() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
stateTwo, err := opts.Two.State(opts.twoEnv)
|
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 != 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(
|
return fmt.Errorf(strings.TrimSpace(
|
||||||
errMigrateSingleLoadDefault), opts.TwoType, err)
|
errMigrateSingleLoadDefault), opts.TwoType, err)
|
||||||
}
|
}
|
||||||
|
@ -392,17 +486,6 @@ func (m *Meta) backendMigrateNonEmptyConfirm(
|
||||||
return m.confirm(inputOpts)
|
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 = `
|
const errMigrateLoadStates = `
|
||||||
Error inspecting states in the %q backend:
|
Error inspecting states in the %q backend:
|
||||||
%s
|
%s
|
||||||
|
@ -447,6 +530,14 @@ The state in the previous backend remains intact and unmodified. Please resolve
|
||||||
the error above and try again.
|
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 = `
|
const inputBackendMigrateEmpty = `
|
||||||
Pre-existing state was found while migrating the previous %q backend to the
|
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
|
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 = `
|
const inputBackendMigrateMultiToMulti = `
|
||||||
Both the existing %[1]q backend and the newly configured %[2]q backend support
|
Both the existing %[1]q backend and the newly configured %[2]q backend
|
||||||
workspaces. When migrating between backends, Terraform will copy all
|
support workspaces. When migrating between backends, Terraform will copy
|
||||||
workspaces (with the same names). THIS WILL OVERWRITE any conflicting
|
all workspaces (with the same names). THIS WILL OVERWRITE any conflicting
|
||||||
states in the destination.
|
states in the destination.
|
||||||
|
|
||||||
Terraform initialization doesn't currently migrate only select workspaces.
|
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
|
If you answer "yes", Terraform will migrate all states. If you answer
|
||||||
"no", Terraform will abort.
|
"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
|
||||||
|
`
|
||||||
|
|
|
@ -1098,7 +1098,6 @@ func TestMetaBackend_configuredChangeCopy_singleState(t *testing.T) {
|
||||||
|
|
||||||
// Ask input
|
// Ask input
|
||||||
defer testInputMap(t, map[string]string{
|
defer testInputMap(t, map[string]string{
|
||||||
"backend-migrate-to-new": "yes",
|
|
||||||
"backend-migrate-copy-to-empty": "yes",
|
"backend-migrate-copy-to-empty": "yes",
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
@ -1154,7 +1153,6 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleDefault(t *testing.T) {
|
||||||
|
|
||||||
// Ask input
|
// Ask input
|
||||||
defer testInputMap(t, map[string]string{
|
defer testInputMap(t, map[string]string{
|
||||||
"backend-migrate-to-new": "yes",
|
|
||||||
"backend-migrate-copy-to-empty": "yes",
|
"backend-migrate-copy-to-empty": "yes",
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
@ -1209,7 +1207,6 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) {
|
||||||
|
|
||||||
// Ask input
|
// Ask input
|
||||||
defer testInputMap(t, map[string]string{
|
defer testInputMap(t, map[string]string{
|
||||||
"backend-migrate-to-new": "yes",
|
|
||||||
"backend-migrate-multistate-to-single": "yes",
|
"backend-migrate-multistate-to-single": "yes",
|
||||||
"backend-migrate-copy-to-empty": "yes",
|
"backend-migrate-copy-to-empty": "yes",
|
||||||
})()
|
})()
|
||||||
|
@ -1276,7 +1273,6 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T)
|
||||||
|
|
||||||
// Ask input
|
// Ask input
|
||||||
defer testInputMap(t, map[string]string{
|
defer testInputMap(t, map[string]string{
|
||||||
"backend-migrate-to-new": "yes",
|
|
||||||
"backend-migrate-multistate-to-single": "yes",
|
"backend-migrate-multistate-to-single": "yes",
|
||||||
"backend-migrate-copy-to-empty": "yes",
|
"backend-migrate-copy-to-empty": "yes",
|
||||||
})()
|
})()
|
||||||
|
@ -1339,7 +1335,6 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) {
|
||||||
|
|
||||||
// Ask input
|
// Ask input
|
||||||
defer testInputMap(t, map[string]string{
|
defer testInputMap(t, map[string]string{
|
||||||
"backend-migrate-to-new": "yes",
|
|
||||||
"backend-migrate-multistate-to-multistate": "yes",
|
"backend-migrate-multistate-to-multistate": "yes",
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
@ -1438,17 +1433,63 @@ func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithDefault(t *testing
|
||||||
|
|
||||||
// Ask input
|
// Ask input
|
||||||
defer testInputMap(t, map[string]string{
|
defer testInputMap(t, map[string]string{
|
||||||
"backend-migrate-to-new": "yes",
|
|
||||||
"backend-migrate-multistate-to-multistate": "yes",
|
"backend-migrate-multistate-to-multistate": "yes",
|
||||||
|
"new-state-name": "env1",
|
||||||
})()
|
})()
|
||||||
|
|
||||||
// Setup the meta
|
// Setup the meta
|
||||||
m := testMetaBackend(t, nil)
|
m := testMetaBackend(t, nil)
|
||||||
|
|
||||||
// Get the backend
|
// Get the backend
|
||||||
_, err := m.Backend(&BackendOpts{Init: true})
|
b, err := m.Backend(&BackendOpts{Init: true})
|
||||||
if err == nil || !strings.Contains(err.Error(), "default state not supported") {
|
if err != nil {
|
||||||
t.Fatalf("expected error to contain %q\ngot: %s", "default state not supported", err)
|
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
|
// Ask input
|
||||||
defer testInputMap(t, map[string]string{
|
defer testInputMap(t, map[string]string{
|
||||||
"backend-migrate-to-new": "yes",
|
|
||||||
"backend-migrate-multistate-to-multistate": "yes",
|
"backend-migrate-multistate-to-multistate": "yes",
|
||||||
|
"select-workspace": "1",
|
||||||
})()
|
})()
|
||||||
|
|
||||||
// Setup the meta
|
// Setup the meta
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"terraform_version": "0.8.2",
|
"terraform_version": "0.8.2",
|
||||||
"serial": 7,
|
"serial": 7,
|
||||||
"lineage": "backend-change"
|
"lineage": "backend-change-env1"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue