Merge pull request #12347 from hashicorp/b-env-migrate
command: migrating envs when changing backends
This commit is contained in:
commit
bdde7d845a
|
@ -13,9 +13,13 @@ import (
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// This is the name of the default, initial state that every backend
|
||||||
|
// must have. This state cannot be deleted.
|
||||||
const DefaultStateName = "default"
|
const DefaultStateName = "default"
|
||||||
|
|
||||||
// Error value to return when a named state operation isn't supported
|
// Error value to return when a named state operation isn't supported.
|
||||||
|
// This must be returned rather than a custom error so that the Terraform
|
||||||
|
// CLI can detect it and handle it appropriately.
|
||||||
var ErrNamedStatesNotSupported = errors.New("named states not supported")
|
var ErrNamedStatesNotSupported = errors.New("named states not supported")
|
||||||
|
|
||||||
// Backend is the minimal interface that must be implemented to enable Terraform.
|
// Backend is the minimal interface that must be implemented to enable Terraform.
|
||||||
|
|
|
@ -47,9 +47,13 @@ type Local struct {
|
||||||
//
|
//
|
||||||
// StateBackupPath is the local path where a backup file will be written.
|
// StateBackupPath is the local path where a backup file will be written.
|
||||||
// Set this to "-" to disable state backup.
|
// Set this to "-" to disable state backup.
|
||||||
|
//
|
||||||
|
// StateEnvPath is the path to the folder containing environments. This
|
||||||
|
// defaults to DefaultEnvDir if not set.
|
||||||
StatePath string
|
StatePath string
|
||||||
StateOutPath string
|
StateOutPath string
|
||||||
StateBackupPath string
|
StateBackupPath string
|
||||||
|
StateEnvDir string
|
||||||
|
|
||||||
// We only want to create a single instance of a local state, so store them
|
// We only want to create a single instance of a local state, so store them
|
||||||
// here as they're loaded.
|
// here as they're loaded.
|
||||||
|
@ -266,6 +270,13 @@ func (b *Local) init() {
|
||||||
"path": &schema.Schema{
|
"path": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
"environment_dir": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -288,6 +299,13 @@ func (b *Local) schemaConfigure(ctx context.Context) error {
|
||||||
b.StateOutPath = path
|
b.StateOutPath = path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if raw, ok := d.GetOk("environment_dir"); ok {
|
||||||
|
path := raw.(string)
|
||||||
|
if path != "" {
|
||||||
|
b.StateEnvDir = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,12 +320,17 @@ func (b *Local) StatePaths(name string) (string, string, string) {
|
||||||
name = backend.DefaultStateName
|
name = backend.DefaultStateName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
envDir := DefaultEnvDir
|
||||||
|
if b.StateEnvDir != "" {
|
||||||
|
envDir = b.StateEnvDir
|
||||||
|
}
|
||||||
|
|
||||||
if name == backend.DefaultStateName {
|
if name == backend.DefaultStateName {
|
||||||
if statePath == "" {
|
if statePath == "" {
|
||||||
statePath = DefaultStateFilename
|
statePath = DefaultStateFilename
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
statePath = filepath.Join(DefaultEnvDir, name, DefaultStateFilename)
|
statePath = filepath.Join(envDir, name, DefaultStateFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
if stateOutPath == "" {
|
if stateOutPath == "" {
|
||||||
|
@ -330,7 +353,12 @@ func (b *Local) createState(name string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
stateDir := filepath.Join(DefaultEnvDir, name)
|
envDir := DefaultEnvDir
|
||||||
|
if b.StateEnvDir != "" {
|
||||||
|
envDir = b.StateEnvDir
|
||||||
|
}
|
||||||
|
|
||||||
|
stateDir := filepath.Join(envDir, name)
|
||||||
s, err := os.Stat(stateDir)
|
s, err := os.Stat(stateDir)
|
||||||
if err == nil && s.IsDir() {
|
if err == nil && s.IsDir() {
|
||||||
// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
|
// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,6 +58,38 @@ func TestLocalProvider(t *testing.T, b *Local, name string) *terraform.MockResou
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNewLocalSingle is a factory for creating a TestLocalSingleState.
|
||||||
|
// This function matches the signature required for backend/init.
|
||||||
|
func TestNewLocalSingle() backend.Backend {
|
||||||
|
return &TestLocalSingleState{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalSingleState is a backend implementation that wraps Local
|
||||||
|
// and modifies it to only support single states (returns
|
||||||
|
// ErrNamedStatesNotSupported for multi-state operations).
|
||||||
|
//
|
||||||
|
// This isn't an actual use case, this is exported just to provide a
|
||||||
|
// easy way to test that behavior.
|
||||||
|
type TestLocalSingleState struct {
|
||||||
|
Local
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TestLocalSingleState) State(name string) (state.State, error) {
|
||||||
|
if name != backend.DefaultStateName {
|
||||||
|
return nil, backend.ErrNamedStatesNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Local.State(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TestLocalSingleState) States() ([]string, error) {
|
||||||
|
return nil, backend.ErrNamedStatesNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TestLocalSingleState) DeleteState(string) error {
|
||||||
|
return backend.ErrNamedStatesNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
func testTempDir(t *testing.T) string {
|
func testTempDir(t *testing.T) string {
|
||||||
d, err := ioutil.TempDir("", "tf")
|
d, err := ioutil.TempDir("", "tf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -449,6 +449,28 @@ func testInteractiveInput(t *testing.T, answers []string) func() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testInputMap configures tests so that the given answers are returned
|
||||||
|
// for calls to Input when the right question is asked. The key is the
|
||||||
|
// question "Id" that is used.
|
||||||
|
func testInputMap(t *testing.T, answers map[string]string) func() {
|
||||||
|
// Disable test mode so input is called
|
||||||
|
test = false
|
||||||
|
|
||||||
|
// Setup reader/writers
|
||||||
|
defaultInputReader = bytes.NewBufferString("")
|
||||||
|
defaultInputWriter = new(bytes.Buffer)
|
||||||
|
|
||||||
|
// Setup answers
|
||||||
|
testInputResponse = nil
|
||||||
|
testInputResponseMap = answers
|
||||||
|
|
||||||
|
// Return the cleanup
|
||||||
|
return func() {
|
||||||
|
test = true
|
||||||
|
testInputResponseMap = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// testBackendState is used to make a test HTTP server to test a configured
|
// testBackendState is used to make a test HTTP server to test a configured
|
||||||
// backend. This returns the complete state that can be saved. Use
|
// backend. This returns the complete state that can be saved. Use
|
||||||
// `testStateFileRemote` to write the returned state.
|
// `testStateFileRemote` to write the returned state.
|
||||||
|
|
|
@ -646,38 +646,19 @@ func (m *Meta) backend_c_r_S(
|
||||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendLocalRead), err)
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendLocalRead), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
env := m.Env()
|
|
||||||
|
|
||||||
localState, err := localB.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendLocalRead), err)
|
|
||||||
}
|
|
||||||
if err := localState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendLocalRead), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the configured backend
|
// Initialize the configured backend
|
||||||
b, err := m.backend_C_r_S_unchanged(c, sMgr)
|
b, err := m.backend_C_r_S_unchanged(c, sMgr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
||||||
}
|
}
|
||||||
backendState, err := b.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
|
||||||
}
|
|
||||||
if err := backendState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the migration
|
// Perform the migration
|
||||||
err = m.backendMigrateState(&backendMigrateOpts{
|
err = m.backendMigrateState(&backendMigrateOpts{
|
||||||
OneType: s.Backend.Type,
|
OneType: s.Backend.Type,
|
||||||
TwoType: "local",
|
TwoType: "local",
|
||||||
One: backendState,
|
One: b,
|
||||||
Two: localState,
|
Two: localB,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -762,16 +743,6 @@ func (m *Meta) backend_c_R_S(
|
||||||
return nil, fmt.Errorf(errBackendLocalRead, err)
|
return nil, fmt.Errorf(errBackendLocalRead, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
env := m.Env()
|
|
||||||
|
|
||||||
localState, err := localB.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(errBackendLocalRead, err)
|
|
||||||
}
|
|
||||||
if err := localState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(errBackendLocalRead, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab the state
|
// Grab the state
|
||||||
s := sMgr.State()
|
s := sMgr.State()
|
||||||
|
|
||||||
|
@ -795,22 +766,13 @@ func (m *Meta) backend_c_R_S(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
oldState, err := oldB.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
|
||||||
}
|
|
||||||
if err := oldState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the migration
|
// Perform the migration
|
||||||
err = m.backendMigrateState(&backendMigrateOpts{
|
err = m.backendMigrateState(&backendMigrateOpts{
|
||||||
OneType: s.Remote.Type,
|
OneType: s.Remote.Type,
|
||||||
TwoType: "local",
|
TwoType: "local",
|
||||||
One: oldState,
|
One: oldB,
|
||||||
Two: localState,
|
Two: localB,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -898,33 +860,12 @@ func (m *Meta) backend_C_R_s(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
env := m.Env()
|
|
||||||
|
|
||||||
oldState, err := oldB.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
|
||||||
}
|
|
||||||
if err := oldState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the new state
|
|
||||||
newState, err := b.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewRead), err)
|
|
||||||
}
|
|
||||||
if err := newState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewRead), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the migration
|
// Perform the migration
|
||||||
err = m.backendMigrateState(&backendMigrateOpts{
|
err = m.backendMigrateState(&backendMigrateOpts{
|
||||||
OneType: s.Remote.Type,
|
OneType: s.Remote.Type,
|
||||||
TwoType: c.Type,
|
TwoType: c.Type,
|
||||||
One: oldState,
|
One: oldB,
|
||||||
Two: newState,
|
Two: b,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -979,20 +920,12 @@ func (m *Meta) backend_C_r_s(
|
||||||
// If the local state is not empty, we need to potentially do a
|
// If the local state is not empty, we need to potentially do a
|
||||||
// state migration to the new backend (with user permission).
|
// state migration to the new backend (with user permission).
|
||||||
if localS := localState.State(); !localS.Empty() {
|
if localS := localState.State(); !localS.Empty() {
|
||||||
backendState, err := b.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(errBackendRemoteRead, err)
|
|
||||||
}
|
|
||||||
if err := backendState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(errBackendRemoteRead, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the migration
|
// Perform the migration
|
||||||
err = m.backendMigrateState(&backendMigrateOpts{
|
err = m.backendMigrateState(&backendMigrateOpts{
|
||||||
OneType: "local",
|
OneType: "local",
|
||||||
TwoType: c.Type,
|
TwoType: c.Type,
|
||||||
One: localState,
|
One: localB,
|
||||||
Two: backendState,
|
Two: b,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1084,33 +1017,12 @@ func (m *Meta) backend_C_r_S_changed(
|
||||||
"Error loading previously configured backend: %s", err)
|
"Error loading previously configured backend: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
env := m.Env()
|
|
||||||
|
|
||||||
oldState, err := oldB.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
|
||||||
}
|
|
||||||
if err := oldState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the new state
|
|
||||||
newState, err := b.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewRead), err)
|
|
||||||
}
|
|
||||||
if err := newState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewRead), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the migration
|
// Perform the migration
|
||||||
err = m.backendMigrateState(&backendMigrateOpts{
|
err = m.backendMigrateState(&backendMigrateOpts{
|
||||||
OneType: s.Backend.Type,
|
OneType: s.Backend.Type,
|
||||||
TwoType: c.Type,
|
TwoType: c.Type,
|
||||||
One: oldState,
|
One: oldB,
|
||||||
Two: newState,
|
Two: b,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1248,33 +1160,12 @@ func (m *Meta) backend_C_R_S_unchanged(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
env := m.Env()
|
|
||||||
|
|
||||||
oldState, err := oldB.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Remote.Type, err)
|
|
||||||
}
|
|
||||||
if err := oldState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
strings.TrimSpace(errBackendSavedUnsetConfig), s.Remote.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the new state
|
|
||||||
newState, err := b.State(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewRead), err)
|
|
||||||
}
|
|
||||||
if err := newState.RefreshState(); err != nil {
|
|
||||||
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewRead), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the migration
|
// Perform the migration
|
||||||
err = m.backendMigrateState(&backendMigrateOpts{
|
err = m.backendMigrateState(&backendMigrateOpts{
|
||||||
OneType: s.Remote.Type,
|
OneType: s.Remote.Type,
|
||||||
TwoType: s.Backend.Type,
|
TwoType: s.Backend.Type,
|
||||||
One: oldState,
|
One: oldB,
|
||||||
Two: newState,
|
Two: b,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -5,8 +5,10 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
clistate "github.com/hashicorp/terraform/command/state"
|
clistate "github.com/hashicorp/terraform/command/state"
|
||||||
"github.com/hashicorp/terraform/state"
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
@ -24,30 +26,212 @@ import (
|
||||||
//
|
//
|
||||||
// This will attempt to lock both states for the migration.
|
// This will attempt to lock both states for the migration.
|
||||||
func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
|
func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
|
||||||
|
// We need to check what the named state status is. If we're converting
|
||||||
|
// from multi-state to single-state for example, we need to handle that.
|
||||||
|
var oneSingle, twoSingle bool
|
||||||
|
oneStates, err := opts.One.States()
|
||||||
|
if err == backend.ErrNamedStatesNotSupported {
|
||||||
|
oneSingle = true
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(strings.TrimSpace(
|
||||||
|
errMigrateLoadStates), opts.OneType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = opts.Two.States()
|
||||||
|
if err == backend.ErrNamedStatesNotSupported {
|
||||||
|
twoSingle = true
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(strings.TrimSpace(
|
||||||
|
errMigrateLoadStates), opts.TwoType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup defaults
|
||||||
|
opts.oneEnv = backend.DefaultStateName
|
||||||
|
opts.twoEnv = backend.DefaultStateName
|
||||||
|
opts.force = false
|
||||||
|
|
||||||
|
// Determine migration behavior based on whether the source/destionation
|
||||||
|
// supports multi-state.
|
||||||
|
switch {
|
||||||
|
// Single-state to single-state. This is the easiest case: we just
|
||||||
|
// copy the default state directly.
|
||||||
|
case oneSingle && twoSingle:
|
||||||
|
return m.backendMigrateState_s_s(opts)
|
||||||
|
|
||||||
|
// Single-state to multi-state. This is easy since we just copy
|
||||||
|
// the default state and ignore the rest in the destination.
|
||||||
|
case oneSingle && !twoSingle:
|
||||||
|
return m.backendMigrateState_s_s(opts)
|
||||||
|
|
||||||
|
// Multi-state to single-state. If the source has more than the default
|
||||||
|
// state this is complicated since we have to ask the user what to do.
|
||||||
|
case !oneSingle && twoSingle:
|
||||||
|
// If the source only has one state and it is the default,
|
||||||
|
// treat it as if it doesn't support multi-state.
|
||||||
|
if len(oneStates) == 1 && oneStates[0] == backend.DefaultStateName {
|
||||||
|
return m.backendMigrateState_s_s(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.backendMigrateState_S_s(opts)
|
||||||
|
|
||||||
|
// Multi-state to multi-state. We merge the states together (migrating
|
||||||
|
// each from the source to the destination one by one).
|
||||||
|
case !oneSingle && !twoSingle:
|
||||||
|
// If the source only has one state and it is the default,
|
||||||
|
// treat it as if it doesn't support multi-state.
|
||||||
|
if len(oneStates) == 1 && oneStates[0] == backend.DefaultStateName {
|
||||||
|
return m.backendMigrateState_s_s(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.backendMigrateState_S_S(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
// State Migration Scenarios
|
||||||
|
//
|
||||||
|
// The functions below cover handling all the various scenarios that
|
||||||
|
// can exist when migrating state. They are named in an immediately not
|
||||||
|
// obvious format but is simple:
|
||||||
|
//
|
||||||
|
// Format: backendMigrateState_s1_s2[_suffix]
|
||||||
|
//
|
||||||
|
// When s1 or s2 is lower case, it means that it is a single state backend.
|
||||||
|
// When either is uppercase, it means that state is a multi-state backend.
|
||||||
|
// The suffix is used to disambiguate multiple cases with the same type of
|
||||||
|
// states.
|
||||||
|
//
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Multi-state to multi-state.
|
||||||
|
func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error {
|
||||||
|
// Ask the user if they want to migrate their existing remote state
|
||||||
|
migrate, err := m.confirm(&terraform.InputOpts{
|
||||||
|
Id: "backend-migrate-multistate-to-multistate",
|
||||||
|
Query: fmt.Sprintf(
|
||||||
|
"Do you want to migrate all environments to %q?",
|
||||||
|
opts.TwoType),
|
||||||
|
Description: fmt.Sprintf(
|
||||||
|
strings.TrimSpace(inputBackendMigrateMultiToMulti),
|
||||||
|
opts.OneType, opts.TwoType),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Error asking for state migration action: %s", err)
|
||||||
|
}
|
||||||
|
if !migrate {
|
||||||
|
return fmt.Errorf("Migration aborted by user.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all the states
|
||||||
|
oneStates, err := opts.One.States()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(strings.TrimSpace(
|
||||||
|
errMigrateLoadStates), opts.OneType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the states so they're always copied alphabetically
|
||||||
|
sort.Strings(oneStates)
|
||||||
|
|
||||||
|
// Go through each and migrate
|
||||||
|
for _, name := range oneStates {
|
||||||
|
// Copy the same names
|
||||||
|
opts.oneEnv = name
|
||||||
|
opts.twoEnv = name
|
||||||
|
|
||||||
|
// Force it, we confirmed above
|
||||||
|
opts.force = true
|
||||||
|
|
||||||
|
// Perform the migration
|
||||||
|
if err := m.backendMigrateState_s_s(opts); err != nil {
|
||||||
|
return fmt.Errorf(strings.TrimSpace(
|
||||||
|
errMigrateMulti), name, opts.OneType, opts.TwoType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-state to single state.
|
||||||
|
func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
|
||||||
|
currentEnv := m.Env()
|
||||||
|
|
||||||
|
// Ask the user if they want to migrate their existing remote state
|
||||||
|
migrate, err := m.confirm(&terraform.InputOpts{
|
||||||
|
Id: "backend-migrate-multistate-to-single",
|
||||||
|
Query: fmt.Sprintf(
|
||||||
|
"Destination state %q doesn't support environments (named states).\n"+
|
||||||
|
"Do you want to copy only your current environment?",
|
||||||
|
opts.TwoType),
|
||||||
|
Description: fmt.Sprintf(
|
||||||
|
strings.TrimSpace(inputBackendMigrateMultiToSingle),
|
||||||
|
opts.OneType, opts.TwoType, currentEnv),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Error asking for state migration action: %s", err)
|
||||||
|
}
|
||||||
|
if !migrate {
|
||||||
|
return fmt.Errorf("Migration aborted by user.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the default state
|
||||||
|
opts.oneEnv = currentEnv
|
||||||
|
return m.backendMigrateState_s_s(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single state to single state, assumed default state name.
|
||||||
|
func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
|
||||||
|
stateOne, err := opts.One.State(opts.oneEnv)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(strings.TrimSpace(
|
||||||
|
errMigrateSingleLoadDefault), opts.OneType, err)
|
||||||
|
}
|
||||||
|
if err := stateOne.RefreshState(); err != nil {
|
||||||
|
return fmt.Errorf(strings.TrimSpace(
|
||||||
|
errMigrateSingleLoadDefault), opts.OneType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateTwo, err := opts.Two.State(opts.twoEnv)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(strings.TrimSpace(
|
||||||
|
errMigrateSingleLoadDefault), opts.TwoType, err)
|
||||||
|
}
|
||||||
|
if err := stateTwo.RefreshState(); err != nil {
|
||||||
|
return fmt.Errorf(strings.TrimSpace(
|
||||||
|
errMigrateSingleLoadDefault), opts.TwoType, err)
|
||||||
|
}
|
||||||
|
|
||||||
lockInfoOne := state.NewLockInfo()
|
lockInfoOne := state.NewLockInfo()
|
||||||
lockInfoOne.Operation = "migration"
|
lockInfoOne.Operation = "migration"
|
||||||
lockInfoOne.Info = "source state"
|
lockInfoOne.Info = "source state"
|
||||||
|
|
||||||
lockIDOne, err := clistate.Lock(opts.One, lockInfoOne, m.Ui, m.Colorize())
|
lockIDOne, err := clistate.Lock(stateOne, lockInfoOne, m.Ui, m.Colorize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error locking source state: %s", err)
|
return fmt.Errorf("Error locking source state: %s", err)
|
||||||
}
|
}
|
||||||
defer clistate.Unlock(opts.One, lockIDOne, m.Ui, m.Colorize())
|
defer clistate.Unlock(stateOne, lockIDOne, m.Ui, m.Colorize())
|
||||||
|
|
||||||
lockInfoTwo := state.NewLockInfo()
|
lockInfoTwo := state.NewLockInfo()
|
||||||
lockInfoTwo.Operation = "migration"
|
lockInfoTwo.Operation = "migration"
|
||||||
lockInfoTwo.Info = "destination state"
|
lockInfoTwo.Info = "destination state"
|
||||||
|
|
||||||
lockIDTwo, err := clistate.Lock(opts.Two, lockInfoTwo, m.Ui, m.Colorize())
|
lockIDTwo, err := clistate.Lock(stateTwo, lockInfoTwo, m.Ui, m.Colorize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error locking destination state: %s", err)
|
return fmt.Errorf("Error locking destination state: %s", err)
|
||||||
}
|
}
|
||||||
defer clistate.Unlock(opts.Two, lockIDTwo, m.Ui, m.Colorize())
|
defer clistate.Unlock(stateTwo, lockIDTwo, m.Ui, m.Colorize())
|
||||||
|
|
||||||
one := opts.One.State()
|
one := stateOne.State()
|
||||||
two := opts.Two.State()
|
two := stateTwo.State()
|
||||||
|
|
||||||
var confirmFunc func(opts *backendMigrateOpts) (bool, error)
|
var confirmFunc func(state.State, state.State, *backendMigrateOpts) (bool, error)
|
||||||
switch {
|
switch {
|
||||||
// No migration necessary
|
// No migration necessary
|
||||||
case one.Empty() && two.Empty():
|
case one.Empty() && two.Empty():
|
||||||
|
@ -72,21 +256,23 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
|
||||||
panic("confirmFunc must not be nil")
|
panic("confirmFunc must not be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !opts.force {
|
||||||
// Confirm with the user whether we want to copy state over
|
// Confirm with the user whether we want to copy state over
|
||||||
confirm, err := confirmFunc(opts)
|
confirm, err := confirmFunc(stateOne, stateTwo, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !confirm {
|
if !confirm {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Confirmed! Write.
|
// Confirmed! Write.
|
||||||
if err := opts.Two.WriteState(one); err != nil {
|
if err := stateTwo.WriteState(one); err != nil {
|
||||||
return fmt.Errorf(strings.TrimSpace(errBackendStateCopy),
|
return fmt.Errorf(strings.TrimSpace(errBackendStateCopy),
|
||||||
opts.OneType, opts.TwoType, err)
|
opts.OneType, opts.TwoType, err)
|
||||||
}
|
}
|
||||||
if err := opts.Two.PersistState(); err != nil {
|
if err := stateTwo.PersistState(); err != nil {
|
||||||
return fmt.Errorf(strings.TrimSpace(errBackendStateCopy),
|
return fmt.Errorf(strings.TrimSpace(errBackendStateCopy),
|
||||||
opts.OneType, opts.TwoType, err)
|
opts.OneType, opts.TwoType, err)
|
||||||
}
|
}
|
||||||
|
@ -95,9 +281,9 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Meta) backendMigrateEmptyConfirm(opts *backendMigrateOpts) (bool, error) {
|
func (m *Meta) backendMigrateEmptyConfirm(one, two state.State, opts *backendMigrateOpts) (bool, error) {
|
||||||
inputOpts := &terraform.InputOpts{
|
inputOpts := &terraform.InputOpts{
|
||||||
Id: "backend-migrate-to-backend",
|
Id: "backend-migrate-copy-to-empty",
|
||||||
Query: fmt.Sprintf(
|
Query: fmt.Sprintf(
|
||||||
"Do you want to copy state from %q to %q?",
|
"Do you want to copy state from %q to %q?",
|
||||||
opts.OneType, opts.TwoType),
|
opts.OneType, opts.TwoType),
|
||||||
|
@ -124,10 +310,11 @@ func (m *Meta) backendMigrateEmptyConfirm(opts *backendMigrateOpts) (bool, error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Meta) backendMigrateNonEmptyConfirm(opts *backendMigrateOpts) (bool, error) {
|
func (m *Meta) backendMigrateNonEmptyConfirm(
|
||||||
|
stateOne, stateTwo state.State, opts *backendMigrateOpts) (bool, error) {
|
||||||
// We need to grab both states so we can write them to a file
|
// We need to grab both states so we can write them to a file
|
||||||
one := opts.One.State()
|
one := stateOne.State()
|
||||||
two := opts.Two.State()
|
two := stateTwo.State()
|
||||||
|
|
||||||
// Save both to a temporary
|
// Save both to a temporary
|
||||||
td, err := ioutil.TempDir("", "terraform")
|
td, err := ioutil.TempDir("", "terraform")
|
||||||
|
@ -188,9 +375,48 @@ func (m *Meta) backendMigrateNonEmptyConfirm(opts *backendMigrateOpts) (bool, er
|
||||||
|
|
||||||
type backendMigrateOpts struct {
|
type backendMigrateOpts struct {
|
||||||
OneType, TwoType string
|
OneType, TwoType string
|
||||||
One, Two state.State
|
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 state in %q: %s
|
||||||
|
|
||||||
|
Prior to changing backends, Terraform inspects the source and destionation
|
||||||
|
states to determine what kind of migration steps need to be taken, if any.
|
||||||
|
Terraform failed to load the states. The data in both the source and the
|
||||||
|
destination remain unmodified. Please resolve the above error and try again.
|
||||||
|
`
|
||||||
|
|
||||||
|
const errMigrateSingleLoadDefault = `
|
||||||
|
Error loading state from %q: %s
|
||||||
|
|
||||||
|
Terraform failed to load the default state from %[1]q.
|
||||||
|
State migration cannot occur unless the state can be loaded. Backend
|
||||||
|
modification and state migration has been aborted. The state in both the
|
||||||
|
source and the destination remain unmodified. Please resolve the
|
||||||
|
above error and try again.
|
||||||
|
`
|
||||||
|
|
||||||
|
const errMigrateMulti = `
|
||||||
|
Error migrating the environment %q from %q to %q:
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
Terraform copies environments in alphabetical order. Any environments
|
||||||
|
alphabetically earlier than this one have been copied. Any environments
|
||||||
|
later than this haven't been modified in the destination. No environments
|
||||||
|
in the source state have been modified.
|
||||||
|
|
||||||
|
Please resolve the error above and run the initialization command again.
|
||||||
|
This will attempt to copy (with permission) all environments again.
|
||||||
|
`
|
||||||
|
|
||||||
const errBackendStateCopy = `
|
const errBackendStateCopy = `
|
||||||
Error copying state from %q to %q: %s
|
Error copying state from %q to %q: %s
|
||||||
|
|
||||||
|
@ -215,3 +441,26 @@ Two (%[2]q): %[4]s
|
||||||
Do you want to copy the state from %[1]q to %[2]q? Enter "yes" to copy
|
Do you want to copy the state from %[1]q to %[2]q? Enter "yes" to copy
|
||||||
and "no" to start with the existing state in %[2]q.
|
and "no" to start with the existing state in %[2]q.
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const inputBackendMigrateMultiToSingle = `
|
||||||
|
The existing backend %[1]q supports environments and you currently are
|
||||||
|
using more than one. The target backend %[2]q doesn't support environments.
|
||||||
|
If you continue, Terraform will offer to copy your current environment
|
||||||
|
%[3]q to the default environment in the target. Your existing environments
|
||||||
|
in the source backend won't be modified. If you want to switch environments,
|
||||||
|
back them up, or cancel altogether, answer "no" and Terraform will abort.
|
||||||
|
`
|
||||||
|
|
||||||
|
const inputBackendMigrateMultiToMulti = `
|
||||||
|
Both the existing backend %[1]q and the target backend %[2]q support
|
||||||
|
environments. When migrating between backends, Terraform will copy all
|
||||||
|
environments (with the same names). THIS WILL OVERWRITE any conflicting
|
||||||
|
states in the destination.
|
||||||
|
|
||||||
|
Terraform initialization doesn't currently migrate only select environments.
|
||||||
|
If you want to migrate a select number of environments, you must manually
|
||||||
|
pull and push those states.
|
||||||
|
|
||||||
|
If you answer "yes", Terraform will migrate all states. If you answer
|
||||||
|
"no", Terraform will abort.
|
||||||
|
`
|
||||||
|
|
|
@ -4,10 +4,14 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
backendinit "github.com/hashicorp/terraform/backend/init"
|
||||||
|
backendlocal "github.com/hashicorp/terraform/backend/local"
|
||||||
"github.com/hashicorp/terraform/helper/copy"
|
"github.com/hashicorp/terraform/helper/copy"
|
||||||
"github.com/hashicorp/terraform/state"
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
@ -951,6 +955,340 @@ func TestMetaBackend_configuredChangeCopy(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Changing a configured backend that supports only single states to another
|
||||||
|
// backend that only supports single states.
|
||||||
|
func TestMetaBackend_configuredChangeCopy_singleState(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("backend-change-single-to-single"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
// Register the single-state backend
|
||||||
|
backendinit.Set("local-single", backendlocal.TestNewLocalSingle)
|
||||||
|
defer backendinit.Set("local-single", nil)
|
||||||
|
|
||||||
|
// Ask input
|
||||||
|
defer testInputMap(t, map[string]string{
|
||||||
|
"backend-migrate-to-new": "yes",
|
||||||
|
"backend-migrate-copy-to-empty": "yes",
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Setup the meta
|
||||||
|
m := testMetaBackend(t, nil)
|
||||||
|
|
||||||
|
// Get the backend
|
||||||
|
b, err := m.Backend(&BackendOpts{Init: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the state
|
||||||
|
s, err := b.State(backend.DefaultStateName)
|
||||||
|
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" {
|
||||||
|
t.Fatalf("bad: %#v", state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no local state
|
||||||
|
if _, err := os.Stat(DefaultStateFilename); err == nil {
|
||||||
|
t.Fatal("file should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no local backup
|
||||||
|
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
|
||||||
|
t.Fatal("file should not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing a configured backend that supports multi-state to a
|
||||||
|
// backend that only supports single states. The multi-state only has
|
||||||
|
// a default state.
|
||||||
|
func TestMetaBackend_configuredChangeCopy_multiToSingleDefault(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("backend-change-multi-default-to-single"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
// Register the single-state backend
|
||||||
|
backendinit.Set("local-single", backendlocal.TestNewLocalSingle)
|
||||||
|
defer backendinit.Set("local-single", nil)
|
||||||
|
|
||||||
|
// Ask input
|
||||||
|
defer testInputMap(t, map[string]string{
|
||||||
|
"backend-migrate-to-new": "yes",
|
||||||
|
"backend-migrate-copy-to-empty": "yes",
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Setup the meta
|
||||||
|
m := testMetaBackend(t, nil)
|
||||||
|
|
||||||
|
// Get the backend
|
||||||
|
b, err := m.Backend(&BackendOpts{Init: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the state
|
||||||
|
s, err := b.State(backend.DefaultStateName)
|
||||||
|
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" {
|
||||||
|
t.Fatalf("bad: %#v", state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no local state
|
||||||
|
if _, err := os.Stat(DefaultStateFilename); err == nil {
|
||||||
|
t.Fatal("file should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no local backup
|
||||||
|
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
|
||||||
|
t.Fatal("file should not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing a configured backend that supports multi-state to a
|
||||||
|
// backend that only supports single states.
|
||||||
|
func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("backend-change-multi-to-single"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
// Register the single-state backend
|
||||||
|
backendinit.Set("local-single", backendlocal.TestNewLocalSingle)
|
||||||
|
defer backendinit.Set("local-single", nil)
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Setup the meta
|
||||||
|
m := testMetaBackend(t, nil)
|
||||||
|
|
||||||
|
// Get the backend
|
||||||
|
b, err := m.Backend(&BackendOpts{Init: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the state
|
||||||
|
s, err := b.State(backend.DefaultStateName)
|
||||||
|
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" {
|
||||||
|
t.Fatalf("bad: %#v", state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no local state
|
||||||
|
if _, err := os.Stat(DefaultStateFilename); err == nil {
|
||||||
|
t.Fatal("file should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no local backup
|
||||||
|
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
|
||||||
|
t.Fatal("file should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify existing environments exist
|
||||||
|
envPath := filepath.Join(backendlocal.DefaultEnvDir, "env2", backendlocal.DefaultStateFilename)
|
||||||
|
if _, err := os.Stat(envPath); err != nil {
|
||||||
|
t.Fatal("env should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing a configured backend that supports multi-state to a
|
||||||
|
// backend that only supports single states.
|
||||||
|
func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("backend-change-multi-to-single"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
// Register the single-state backend
|
||||||
|
backendinit.Set("local-single", backendlocal.TestNewLocalSingle)
|
||||||
|
defer backendinit.Set("local-single", nil)
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Setup the meta
|
||||||
|
m := testMetaBackend(t, nil)
|
||||||
|
|
||||||
|
// Change env
|
||||||
|
if err := m.SetEnv("env2"); err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the backend
|
||||||
|
b, err := m.Backend(&BackendOpts{Init: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the state
|
||||||
|
s, err := b.State(backend.DefaultStateName)
|
||||||
|
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-env2" {
|
||||||
|
t.Fatalf("bad: %#v", state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no local state
|
||||||
|
if _, err := os.Stat(DefaultStateFilename); err == nil {
|
||||||
|
t.Fatal("file should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no local backup
|
||||||
|
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
|
||||||
|
t.Fatal("file should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify existing environments exist
|
||||||
|
envPath := filepath.Join(backendlocal.DefaultEnvDir, "env2", backendlocal.DefaultStateFilename)
|
||||||
|
if _, err := os.Stat(envPath); err != nil {
|
||||||
|
t.Fatal("env should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing a configured backend that supports multi-state to a
|
||||||
|
// backend that also supports multi-state.
|
||||||
|
func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("backend-change-multi-to-multi"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
// Ask input
|
||||||
|
defer testInputMap(t, map[string]string{
|
||||||
|
"backend-migrate-to-new": "yes",
|
||||||
|
"backend-migrate-multistate-to-multistate": "yes",
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Setup the meta
|
||||||
|
m := testMetaBackend(t, nil)
|
||||||
|
|
||||||
|
// Get the backend
|
||||||
|
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{"default", "env2"}
|
||||||
|
if !reflect.DeepEqual(states, expected) {
|
||||||
|
t.Fatalf("bad: %#v", states)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Check the default state
|
||||||
|
s, err := b.State(backend.DefaultStateName)
|
||||||
|
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" {
|
||||||
|
t.Fatalf("bad: %#v", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Check the other state
|
||||||
|
s, err := b.State("env2")
|
||||||
|
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-env2" {
|
||||||
|
t.Fatalf("bad: %#v", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no local backup
|
||||||
|
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
|
||||||
|
t.Fatal("file should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Verify existing environments exist
|
||||||
|
envPath := filepath.Join(backendlocal.DefaultEnvDir, "env2", backendlocal.DefaultStateFilename)
|
||||||
|
if _, err := os.Stat(envPath); err != nil {
|
||||||
|
t.Fatal("env should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Verify new environments exist
|
||||||
|
envPath := filepath.Join("envdir-new", "env2", backendlocal.DefaultStateFilename)
|
||||||
|
if _, err := os.Stat(envPath); err != nil {
|
||||||
|
t.Fatal("env should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Unsetting a saved backend
|
// Unsetting a saved backend
|
||||||
func TestMetaBackend_configuredUnset(t *testing.T) {
|
func TestMetaBackend_configuredUnset(t *testing.T) {
|
||||||
// Create a temporary working directory that is empty
|
// Create a temporary working directory that is empty
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-change"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local-single" {
|
||||||
|
path = "local-state-2.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-change"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
environment_dir = "envdir-new"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-change-env2"
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-change"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local-single" {
|
||||||
|
path = "local-state-2.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-change-env2"
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local-single",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-change"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local-single" {
|
||||||
|
path = "local-state-2.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import (
|
||||||
var defaultInputReader io.Reader
|
var defaultInputReader io.Reader
|
||||||
var defaultInputWriter io.Writer
|
var defaultInputWriter io.Writer
|
||||||
var testInputResponse []string
|
var testInputResponse []string
|
||||||
|
var testInputResponseMap map[string]string
|
||||||
|
|
||||||
// UIInput is an implementation of terraform.UIInput that asks the CLI
|
// UIInput is an implementation of terraform.UIInput that asks the CLI
|
||||||
// for input stdin.
|
// for input stdin.
|
||||||
|
@ -65,13 +66,25 @@ func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) {
|
||||||
return "", errors.New("interrupted")
|
return "", errors.New("interrupted")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have test results, return those
|
// If we have test results, return those. testInputResponse is the
|
||||||
|
// "old" way of doing it and we should remove that.
|
||||||
if testInputResponse != nil {
|
if testInputResponse != nil {
|
||||||
v := testInputResponse[0]
|
v := testInputResponse[0]
|
||||||
testInputResponse = testInputResponse[1:]
|
testInputResponse = testInputResponse[1:]
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testInputResponseMap is the new way for test responses, based on
|
||||||
|
// the query ID.
|
||||||
|
if testInputResponseMap != nil {
|
||||||
|
v, ok := testInputResponseMap[opts.Id]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unexpected input request in test: %s", opts.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] command: asking for input: %q", opts.Query)
|
log.Printf("[DEBUG] command: asking for input: %q", opts.Query)
|
||||||
|
|
||||||
// Listen for interrupts so we can cancel the input ask
|
// Listen for interrupts so we can cancel the input ask
|
||||||
|
|
Loading…
Reference in New Issue