command: initial work on migrating envs, basic cases first
This commit is contained in:
parent
177400dbbf
commit
1d8b76c89d
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
@ -758,16 +739,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()
|
||||||
|
|
||||||
|
@ -791,22 +762,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
|
||||||
|
@ -894,33 +856,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
|
||||||
|
@ -975,20 +916,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
|
||||||
|
@ -1080,33 +1013,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
|
||||||
|
@ -1244,33 +1156,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
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"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 +25,131 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
panic("YO")
|
||||||
|
return m.backendMigrateState_s_s(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("unhandled")
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("unhandled")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Single state to single state, assumed default state name.
|
||||||
|
func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
|
||||||
|
stateOne, err := opts.One.State(backend.DefaultStateName)
|
||||||
|
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(backend.DefaultStateName)
|
||||||
|
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():
|
||||||
|
@ -73,7 +175,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
@ -82,11 +184,11 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 +197,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 +226,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 +291,28 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 errBackendStateCopy = `
|
const errBackendStateCopy = `
|
||||||
Error copying state from %q to %q: %s
|
Error copying state from %q to %q: %s
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"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 +953,61 @@ 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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-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