noop migrate copy, add -lock and -input

A couple commits got rebased together here, and it's easier to enumerate
them in a single commit.

Skip copying of states during migration if they are the same state. This
can happen when trying to reconfigure a backend's options, or if the
state was manually transferred. This can fail unexpectedly with locking
enabled.

Honor the `-input` flag for all confirmations (the new test hit some
more). Also unify where we reference the Meta.forceInitCopy and transfer
the value to the existing backendMigrateOpts.force field.
This commit is contained in:
James Bardin 2017-03-30 15:53:21 -04:00
parent aad143b6d1
commit fb4a365d12
3 changed files with 86 additions and 14 deletions

View File

@ -237,7 +237,6 @@ Options:
-force-copy Suppress prompts about copying state data. This is -force-copy Suppress prompts about copying state data. This is
equivalent to providing a "yes" to all confirmation equivalent to providing a "yes" to all confirmation
prompts. prompts.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }

View File

@ -2,6 +2,7 @@ package command
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -53,7 +54,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
// Setup defaults // Setup defaults
opts.oneEnv = backend.DefaultStateName opts.oneEnv = backend.DefaultStateName
opts.twoEnv = backend.DefaultStateName opts.twoEnv = backend.DefaultStateName
opts.force = false opts.force = m.forceInitCopy
// Determine migration behavior based on whether the source/destionation // Determine migration behavior based on whether the source/destionation
// supports multi-state. // supports multi-state.
@ -163,7 +164,7 @@ func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error {
func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
currentEnv := m.Env() currentEnv := m.Env()
migrate := m.forceInitCopy migrate := opts.force
if !migrate { if !migrate {
var err error var err error
// Ask the user if they want to migrate their existing remote state // Ask the user if they want to migrate their existing remote state
@ -218,6 +219,19 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
errMigrateSingleLoadDefault), opts.TwoType, err) errMigrateSingleLoadDefault), opts.TwoType, err)
} }
// Check if we need migration at all.
// This is before taking a lock, because they may also correspond to the same lock.
one := stateOne.State()
two := stateTwo.State()
// no reason to migrate if the state is already there
if one.Equal(two) {
// Equal isn't identical; it doesn't check lineage.
if one != nil && two != nil && one.Lineage == two.Lineage {
return nil
}
}
if m.stateLock { if m.stateLock {
lockCtx, cancel := context.WithTimeout(context.Background(), m.stateLockTimeout) lockCtx, cancel := context.WithTimeout(context.Background(), m.stateLockTimeout)
defer cancel() defer cancel()
@ -241,10 +255,21 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
return fmt.Errorf("Error locking destination state: %s", err) return fmt.Errorf("Error locking destination state: %s", err)
} }
defer clistate.Unlock(stateTwo, lockIDTwo, m.Ui, m.Colorize()) defer clistate.Unlock(stateTwo, lockIDTwo, m.Ui, m.Colorize())
// We now own a lock, so double check that we have the version
// corresponding to the lock.
if err := stateOne.RefreshState(); err != nil {
return fmt.Errorf(strings.TrimSpace(
errMigrateSingleLoadDefault), opts.OneType, err)
}
if err := stateTwo.RefreshState(); err != nil {
return fmt.Errorf(strings.TrimSpace(
errMigrateSingleLoadDefault), opts.OneType, err)
} }
one := stateOne.State() one = stateOne.State()
two := stateTwo.State() two = stateTwo.State()
}
// Clear the legacy remote state in both cases. If we're at the migration // Clear the legacy remote state in both cases. If we're at the migration
// step then this won't be used anymore. // step then this won't be used anymore.
@ -281,6 +306,11 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
} }
if !opts.force { if !opts.force {
// Abort if we can't ask for input.
if !m.input {
return errors.New("error asking for state migration action: inptut disabled")
}
// Confirm with the user whether we want to copy state over // Confirm with the user whether we want to copy state over
confirm, err := confirmFunc(stateOne, stateTwo, opts) confirm, err := confirmFunc(stateOne, stateTwo, opts)
if err != nil { if err != nil {
@ -306,10 +336,6 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
} }
func (m *Meta) backendMigrateEmptyConfirm(one, two state.State, opts *backendMigrateOpts) (bool, error) { func (m *Meta) backendMigrateEmptyConfirm(one, two state.State, opts *backendMigrateOpts) (bool, error) {
if m.forceInitCopy {
return true, nil
}
inputOpts := &terraform.InputOpts{ inputOpts := &terraform.InputOpts{
Id: "backend-migrate-copy-to-empty", Id: "backend-migrate-copy-to-empty",
Query: fmt.Sprintf( Query: fmt.Sprintf(
@ -372,10 +398,6 @@ func (m *Meta) backendMigrateNonEmptyConfirm(
return false, fmt.Errorf("Error saving temporary state: %s", err) return false, fmt.Errorf("Error saving temporary state: %s", err)
} }
if m.forceInitCopy {
return true, nil
}
// Ask for confirmation // Ask for confirmation
inputOpts := &terraform.InputOpts{ inputOpts := &terraform.InputOpts{
Id: "backend-migrate-to-backend", Id: "backend-migrate-to-backend",

View File

@ -426,6 +426,57 @@ func TestMetaBackend_configureNewWithState(t *testing.T) {
} }
} }
// Newly configured backend with matching local and remote state doesn't prompt
// for copy.
func TestMetaBackend_configureNewWithoutCopy(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("backend-new-migrate"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
if err := copy.CopyFile(DefaultStateFilename, "local-state.tfstate"); err != nil {
t.Fatal(err)
}
// Setup the meta
m := testMetaBackend(t, nil)
m.input = false
// init the backend
_, err := m.Backend(&BackendOpts{Init: true})
if err != nil {
t.Fatalf("bad: %s", err)
}
// Verify the state is where we expect
f, err := os.Open("local-state.tfstate")
if err != nil {
t.Fatalf("err: %s", err)
}
actual, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
if actual.Lineage != "backend-new-migrate" {
t.Fatalf("incorrect state lineage: %q", actual.Lineage)
}
// Verify the default paths don't exist
if !isEmptyState(DefaultStateFilename) {
data, _ := ioutil.ReadFile(DefaultStateFilename)
t.Fatal("state should not exist, but contains:\n", string(data))
}
// Verify a backup does exist
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup state is empty or missing")
}
}
// Newly configured backend with prior local state and no remote state, // Newly configured backend with prior local state and no remote state,
// but opting to not migrate. // but opting to not migrate.
func TestMetaBackend_configureNewWithStateNoMigrate(t *testing.T) { func TestMetaBackend_configureNewWithStateNoMigrate(t *testing.T) {