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/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 d47d6c513..5d1e36ff1 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 @@ -758,16 +739,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() @@ -791,22 +762,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 @@ -894,33 +856,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 @@ -975,20 +916,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 @@ -1080,33 +1013,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 @@ -1244,33 +1156,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..66e885bd8 100644 --- a/command/meta_backend_migrate.go +++ b/command/meta_backend_migrate.go @@ -7,6 +7,7 @@ import ( "path/filepath" "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 +25,131 @@ 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) + } + + // 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.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(): @@ -73,7 +175,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { } // Confirm with the user whether we want to copy state over - confirm, err := confirmFunc(opts) + confirm, err := confirmFunc(stateOne, stateTwo, opts) if err != nil { return err } @@ -82,11 +184,11 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { } // 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 +197,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 +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 - 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 +291,28 @@ func (m *Meta) backendMigrateNonEmptyConfirm(opts *backendMigrateOpts) (bool, er type backendMigrateOpts struct { 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 = ` Error copying state from %q to %q: %s diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index 4ce7c1b80..66a59c14b 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -8,6 +8,8 @@ import ( "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 +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 func TestMetaBackend_configuredUnset(t *testing.T) { // Create a temporary working directory that is empty 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