diff --git a/backend/backend.go b/backend/backend.go index 50cf14465..f6c567c71 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -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. diff --git a/backend/local/backend.go b/backend/local/backend.go index 0b592e33d..61df56bde 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -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 diff --git a/backend/local/testing.go b/backend/local/testing.go index e5920f36d..67048766f 100644 --- a/backend/local/testing.go +++ b/backend/local/testing.go @@ -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 { diff --git a/command/command_test.go b/command/command_test.go index cb89f5e75..21a5a8d41 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -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. diff --git a/command/meta_backend.go b/command/meta_backend.go index f991e29f0..ab39348a5 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -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 diff --git a/command/meta_backend_migrate.go b/command/meta_backend_migrate.go index 03a73e2b6..1531b4a92 100644 --- a/command/meta_backend_migrate.go +++ b/command/meta_backend_migrate.go @@ -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. +` diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index 4ce7c1b80..02a1d2c40 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -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 diff --git a/command/test-fixtures/backend-change-multi-default-to-single/.terraform/terraform.tfstate b/command/test-fixtures/backend-change-multi-default-to-single/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/backend-change-multi-default-to-single/.terraform/terraform.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": [] + } + ] +} diff --git a/command/test-fixtures/backend-change-multi-default-to-single/local-state.tfstate b/command/test-fixtures/backend-change-multi-default-to-single/local-state.tfstate new file mode 100644 index 000000000..88c1d86ec --- /dev/null +++ b/command/test-fixtures/backend-change-multi-default-to-single/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-change" +} diff --git a/command/test-fixtures/backend-change-multi-default-to-single/main.tf b/command/test-fixtures/backend-change-multi-default-to-single/main.tf new file mode 100644 index 000000000..2f67c6f1b --- /dev/null +++ b/command/test-fixtures/backend-change-multi-default-to-single/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local-single" { + path = "local-state-2.tfstate" + } +} diff --git a/command/test-fixtures/backend-change-multi-to-multi/.terraform/terraform.tfstate b/command/test-fixtures/backend-change-multi-to-multi/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-multi/.terraform/terraform.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": [] + } + ] +} diff --git a/command/test-fixtures/backend-change-multi-to-multi/local-state.tfstate b/command/test-fixtures/backend-change-multi-to-multi/local-state.tfstate new file mode 100644 index 000000000..88c1d86ec --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-multi/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-change" +} diff --git a/command/test-fixtures/backend-change-multi-to-multi/main.tf b/command/test-fixtures/backend-change-multi-to-multi/main.tf new file mode 100644 index 000000000..04b76b378 --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-multi/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + environment_dir = "envdir-new" + } +} diff --git a/command/test-fixtures/backend-change-multi-to-multi/terraform.tfstate.d/env2/terraform.tfstate b/command/test-fixtures/backend-change-multi-to-multi/terraform.tfstate.d/env2/terraform.tfstate new file mode 100644 index 000000000..855a27f4c --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-multi/terraform.tfstate.d/env2/terraform.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-change-env2" +} diff --git a/command/test-fixtures/backend-change-multi-to-single/.terraform/terraform.tfstate b/command/test-fixtures/backend-change-multi-to-single/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-single/.terraform/terraform.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": [] + } + ] +} diff --git a/command/test-fixtures/backend-change-multi-to-single/local-state.tfstate b/command/test-fixtures/backend-change-multi-to-single/local-state.tfstate new file mode 100644 index 000000000..88c1d86ec --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-single/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-change" +} diff --git a/command/test-fixtures/backend-change-multi-to-single/main.tf b/command/test-fixtures/backend-change-multi-to-single/main.tf new file mode 100644 index 000000000..2f67c6f1b --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-single/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local-single" { + path = "local-state-2.tfstate" + } +} diff --git a/command/test-fixtures/backend-change-multi-to-single/terraform.tfstate.d/env2/terraform.tfstate b/command/test-fixtures/backend-change-multi-to-single/terraform.tfstate.d/env2/terraform.tfstate new file mode 100644 index 000000000..855a27f4c --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-single/terraform.tfstate.d/env2/terraform.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-change-env2" +} diff --git a/command/test-fixtures/backend-change-single-to-single/.terraform/terraform.tfstate b/command/test-fixtures/backend-change-single-to-single/.terraform/terraform.tfstate new file mode 100644 index 000000000..be6ed26d9 --- /dev/null +++ b/command/test-fixtures/backend-change-single-to-single/.terraform/terraform.tfstate @@ -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": [] + } + ] +} diff --git a/command/test-fixtures/backend-change-single-to-single/local-state.tfstate b/command/test-fixtures/backend-change-single-to-single/local-state.tfstate new file mode 100644 index 000000000..88c1d86ec --- /dev/null +++ b/command/test-fixtures/backend-change-single-to-single/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-change" +} diff --git a/command/test-fixtures/backend-change-single-to-single/main.tf b/command/test-fixtures/backend-change-single-to-single/main.tf new file mode 100644 index 000000000..2f67c6f1b --- /dev/null +++ b/command/test-fixtures/backend-change-single-to-single/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local-single" { + path = "local-state-2.tfstate" + } +} diff --git a/command/ui_input.go b/command/ui_input.go index fad684d72..9c8873d46 100644 --- a/command/ui_input.go +++ b/command/ui_input.go @@ -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