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"
|
||||
)
|
||||
|
||||
// This is the name of the default, initial state that every backend
|
||||
// must have. This state cannot be deleted.
|
||||
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")
|
||||
|
||||
// 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.
|
||||
// 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
|
||||
StateOutPath string
|
||||
StateBackupPath string
|
||||
StateEnvDir string
|
||||
|
||||
// We only want to create a single instance of a local state, so store them
|
||||
// here as they're loaded.
|
||||
|
@ -266,6 +270,13 @@ func (b *Local) init() {
|
|||
"path": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
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
|
||||
}
|
||||
|
||||
if raw, ok := d.GetOk("environment_dir"); ok {
|
||||
path := raw.(string)
|
||||
if path != "" {
|
||||
b.StateEnvDir = path
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -302,12 +320,17 @@ func (b *Local) StatePaths(name string) (string, string, string) {
|
|||
name = backend.DefaultStateName
|
||||
}
|
||||
|
||||
envDir := DefaultEnvDir
|
||||
if b.StateEnvDir != "" {
|
||||
envDir = b.StateEnvDir
|
||||
}
|
||||
|
||||
if name == backend.DefaultStateName {
|
||||
if statePath == "" {
|
||||
statePath = DefaultStateFilename
|
||||
}
|
||||
} else {
|
||||
statePath = filepath.Join(DefaultEnvDir, name, DefaultStateFilename)
|
||||
statePath = filepath.Join(envDir, name, DefaultStateFilename)
|
||||
}
|
||||
|
||||
if stateOutPath == "" {
|
||||
|
@ -330,7 +353,12 @@ func (b *Local) createState(name string) error {
|
|||
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)
|
||||
if err == nil && s.IsDir() {
|
||||
// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
|
@ -56,6 +58,38 @@ func TestLocalProvider(t *testing.T, b *Local, name string) *terraform.MockResou
|
|||
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 {
|
||||
d, err := ioutil.TempDir("", "tf")
|
||||
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
|
||||
// backend. This returns the complete state that can be saved. Use
|
||||
// `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)
|
||||
}
|
||||
|
||||
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
|
||||
b, err := m.backend_C_r_S_unchanged(c, sMgr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
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
|
||||
err = m.backendMigrateState(&backendMigrateOpts{
|
||||
OneType: s.Backend.Type,
|
||||
TwoType: "local",
|
||||
One: backendState,
|
||||
Two: localState,
|
||||
One: b,
|
||||
Two: localB,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -762,16 +743,6 @@ func (m *Meta) backend_c_R_S(
|
|||
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
|
||||
s := sMgr.State()
|
||||
|
||||
|
@ -795,22 +766,13 @@ func (m *Meta) backend_c_R_S(
|
|||
if err != nil {
|
||||
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
|
||||
err = m.backendMigrateState(&backendMigrateOpts{
|
||||
OneType: s.Remote.Type,
|
||||
TwoType: "local",
|
||||
One: oldState,
|
||||
Two: localState,
|
||||
One: oldB,
|
||||
Two: localB,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -898,33 +860,12 @@ func (m *Meta) backend_C_R_s(
|
|||
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
|
||||
err = m.backendMigrateState(&backendMigrateOpts{
|
||||
OneType: s.Remote.Type,
|
||||
TwoType: c.Type,
|
||||
One: oldState,
|
||||
Two: newState,
|
||||
One: oldB,
|
||||
Two: b,
|
||||
})
|
||||
if err != nil {
|
||||
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
|
||||
// state migration to the new backend (with user permission).
|
||||
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
|
||||
err = m.backendMigrateState(&backendMigrateOpts{
|
||||
OneType: "local",
|
||||
TwoType: c.Type,
|
||||
One: localState,
|
||||
Two: backendState,
|
||||
One: localB,
|
||||
Two: b,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -1084,33 +1017,12 @@ func (m *Meta) backend_C_r_S_changed(
|
|||
"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
|
||||
err = m.backendMigrateState(&backendMigrateOpts{
|
||||
OneType: s.Backend.Type,
|
||||
TwoType: c.Type,
|
||||
One: oldState,
|
||||
Two: newState,
|
||||
One: oldB,
|
||||
Two: b,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -1248,33 +1160,12 @@ func (m *Meta) backend_C_R_S_unchanged(
|
|||
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
|
||||
err = m.backendMigrateState(&backendMigrateOpts{
|
||||
OneType: s.Remote.Type,
|
||||
TwoType: s.Backend.Type,
|
||||
One: oldState,
|
||||
Two: newState,
|
||||
One: oldB,
|
||||
Two: b,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -5,8 +5,10 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
clistate "github.com/hashicorp/terraform/command/state"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
|
@ -24,30 +26,212 @@ import (
|
|||
//
|
||||
// This will attempt to lock both states for the migration.
|
||||
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.Operation = "migration"
|
||||
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 {
|
||||
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.Operation = "migration"
|
||||
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 {
|
||||
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()
|
||||
two := opts.Two.State()
|
||||
one := stateOne.State()
|
||||
two := stateTwo.State()
|
||||
|
||||
var confirmFunc func(opts *backendMigrateOpts) (bool, error)
|
||||
var confirmFunc func(state.State, state.State, *backendMigrateOpts) (bool, error)
|
||||
switch {
|
||||
// No migration necessary
|
||||
case one.Empty() && two.Empty():
|
||||
|
@ -72,21 +256,23 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
|
|||
panic("confirmFunc must not be nil")
|
||||
}
|
||||
|
||||
// Confirm with the user whether we want to copy state over
|
||||
confirm, err := confirmFunc(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirm {
|
||||
return nil
|
||||
if !opts.force {
|
||||
// Confirm with the user whether we want to copy state over
|
||||
confirm, err := confirmFunc(stateOne, stateTwo, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirm {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmed! Write.
|
||||
if err := opts.Two.WriteState(one); err != nil {
|
||||
if err := stateTwo.WriteState(one); err != nil {
|
||||
return fmt.Errorf(strings.TrimSpace(errBackendStateCopy),
|
||||
opts.OneType, opts.TwoType, err)
|
||||
}
|
||||
if err := opts.Two.PersistState(); err != nil {
|
||||
if err := stateTwo.PersistState(); err != nil {
|
||||
return fmt.Errorf(strings.TrimSpace(errBackendStateCopy),
|
||||
opts.OneType, opts.TwoType, err)
|
||||
}
|
||||
|
@ -95,9 +281,9 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
|
|||
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{
|
||||
Id: "backend-migrate-to-backend",
|
||||
Id: "backend-migrate-copy-to-empty",
|
||||
Query: fmt.Sprintf(
|
||||
"Do you want to copy state from %q to %q?",
|
||||
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
|
||||
one := opts.One.State()
|
||||
two := opts.Two.State()
|
||||
one := stateOne.State()
|
||||
two := stateTwo.State()
|
||||
|
||||
// Save both to a temporary
|
||||
td, err := ioutil.TempDir("", "terraform")
|
||||
|
@ -188,9 +375,48 @@ func (m *Meta) backendMigrateNonEmptyConfirm(opts *backendMigrateOpts) (bool, er
|
|||
|
||||
type backendMigrateOpts struct {
|
||||
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 = `
|
||||
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
|
||||
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"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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/state"
|
||||
"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
|
||||
func TestMetaBackend_configuredUnset(t *testing.T) {
|
||||
// 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 defaultInputWriter io.Writer
|
||||
var testInputResponse []string
|
||||
var testInputResponseMap map[string]string
|
||||
|
||||
// UIInput is an implementation of terraform.UIInput that asks the CLI
|
||||
// for input stdin.
|
||||
|
@ -65,13 +66,25 @@ func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) {
|
|||
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 {
|
||||
v := testInputResponse[0]
|
||||
testInputResponse = testInputResponse[1:]
|
||||
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)
|
||||
|
||||
// Listen for interrupts so we can cancel the input ask
|
||||
|
|
Loading…
Reference in New Issue