2017-01-19 05:50:04 +01:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
2017-02-14 20:33:55 +01:00
|
|
|
clistate "github.com/hashicorp/terraform/command/state"
|
2017-01-19 05:50:04 +01:00
|
|
|
"github.com/hashicorp/terraform/state"
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
|
|
)
|
|
|
|
|
|
|
|
// backendMigrateState handles migrating (copying) state from one backend
|
|
|
|
// to another. This function handles asking the user for confirmation
|
|
|
|
// as well as the copy itself.
|
|
|
|
//
|
|
|
|
// This function can handle all scenarios of state migration regardless
|
|
|
|
// of the existence of state in either backend.
|
|
|
|
//
|
|
|
|
// After migrating the state, the existing state in the first backend
|
|
|
|
// remains untouched.
|
2017-02-09 21:35:49 +01:00
|
|
|
//
|
|
|
|
// This will attempt to lock both states for the migration.
|
2017-01-19 05:50:04 +01:00
|
|
|
func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
|
2017-02-15 00:51:32 +01:00
|
|
|
lockInfoOne := &state.LockInfo{
|
|
|
|
Info: "migration source state",
|
|
|
|
}
|
|
|
|
|
|
|
|
lockIDOne, err := clistate.Lock(opts.One, lockInfoOne, m.Ui, m.Colorize())
|
2017-02-09 21:35:49 +01:00
|
|
|
if err != nil {
|
2017-02-14 20:33:55 +01:00
|
|
|
return fmt.Errorf("Error locking source state: %s", err)
|
2017-02-09 21:35:49 +01:00
|
|
|
}
|
2017-02-15 00:51:32 +01:00
|
|
|
defer clistate.Unlock(opts.One, lockIDOne, m.Ui, m.Colorize())
|
|
|
|
|
|
|
|
lockInfoTwo := &state.LockInfo{
|
|
|
|
Info: "migration source state",
|
|
|
|
}
|
2017-02-09 21:35:49 +01:00
|
|
|
|
2017-02-15 00:51:32 +01:00
|
|
|
lockIDTwo, err := clistate.Lock(opts.Two, lockInfoTwo, m.Ui, m.Colorize())
|
2017-02-09 21:35:49 +01:00
|
|
|
if err != nil {
|
2017-02-14 20:33:55 +01:00
|
|
|
return fmt.Errorf("Error locking destination state: %s", err)
|
2017-02-09 21:35:49 +01:00
|
|
|
}
|
2017-02-15 00:51:32 +01:00
|
|
|
defer clistate.Unlock(opts.Two, lockIDTwo, m.Ui, m.Colorize())
|
2017-02-09 21:35:49 +01:00
|
|
|
|
2017-01-19 05:50:04 +01:00
|
|
|
one := opts.One.State()
|
|
|
|
two := opts.Two.State()
|
|
|
|
|
|
|
|
var confirmFunc func(opts *backendMigrateOpts) (bool, error)
|
|
|
|
switch {
|
|
|
|
// No migration necessary
|
|
|
|
case one.Empty() && two.Empty():
|
|
|
|
return nil
|
|
|
|
|
|
|
|
// No migration necessary if we're inheriting state.
|
|
|
|
case one.Empty() && !two.Empty():
|
|
|
|
return nil
|
|
|
|
|
|
|
|
// We have existing state moving into no state. Ask the user if
|
|
|
|
// they'd like to do this.
|
|
|
|
case !one.Empty() && two.Empty():
|
|
|
|
confirmFunc = m.backendMigrateEmptyConfirm
|
|
|
|
|
|
|
|
// Both states are non-empty, meaning we need to determine which
|
|
|
|
// state should be used and update accordingly.
|
|
|
|
case !one.Empty() && !two.Empty():
|
|
|
|
confirmFunc = m.backendMigrateNonEmptyConfirm
|
|
|
|
}
|
|
|
|
|
|
|
|
if confirmFunc == nil {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Confirmed! Write.
|
|
|
|
if err := opts.Two.WriteState(one); err != nil {
|
|
|
|
return fmt.Errorf(strings.TrimSpace(errBackendStateCopy),
|
|
|
|
opts.OneType, opts.TwoType, err)
|
|
|
|
}
|
|
|
|
if err := opts.Two.PersistState(); err != nil {
|
|
|
|
return fmt.Errorf(strings.TrimSpace(errBackendStateCopy),
|
|
|
|
opts.OneType, opts.TwoType, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// And we're done.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Meta) backendMigrateEmptyConfirm(opts *backendMigrateOpts) (bool, error) {
|
|
|
|
inputOpts := &terraform.InputOpts{
|
|
|
|
Id: "backend-migrate-to-backend",
|
|
|
|
Query: fmt.Sprintf(
|
|
|
|
"Do you want to copy state from %q to %q?",
|
|
|
|
opts.OneType, opts.TwoType),
|
|
|
|
Description: fmt.Sprintf(
|
|
|
|
strings.TrimSpace(inputBackendMigrateEmpty),
|
|
|
|
opts.OneType, opts.TwoType),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Confirm with the user that the copy should occur
|
|
|
|
for {
|
|
|
|
v, err := m.UIInput().Input(inputOpts)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf(
|
|
|
|
"Error asking for state copy action: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch strings.ToLower(v) {
|
|
|
|
case "no":
|
|
|
|
return false, nil
|
|
|
|
|
|
|
|
case "yes":
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Meta) backendMigrateNonEmptyConfirm(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()
|
|
|
|
|
|
|
|
// Save both to a temporary
|
|
|
|
td, err := ioutil.TempDir("", "terraform")
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("Error creating temporary directory: %s", err)
|
|
|
|
}
|
|
|
|
defer os.RemoveAll(td)
|
|
|
|
|
|
|
|
// Helper to write the state
|
|
|
|
saveHelper := func(n, path string, s *terraform.State) error {
|
|
|
|
f, err := os.Create(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
return terraform.WriteState(s, f)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the states
|
|
|
|
onePath := filepath.Join(td, fmt.Sprintf("1-%s.tfstate", opts.OneType))
|
|
|
|
twoPath := filepath.Join(td, fmt.Sprintf("2-%s.tfstate", opts.TwoType))
|
|
|
|
if err := saveHelper(opts.OneType, onePath, one); err != nil {
|
|
|
|
return false, fmt.Errorf("Error saving temporary state: %s", err)
|
|
|
|
}
|
|
|
|
if err := saveHelper(opts.TwoType, twoPath, two); err != nil {
|
|
|
|
return false, fmt.Errorf("Error saving temporary state: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ask for confirmation
|
|
|
|
inputOpts := &terraform.InputOpts{
|
|
|
|
Id: "backend-migrate-to-backend",
|
|
|
|
Query: fmt.Sprintf(
|
|
|
|
"Do you want to copy state from %q to %q?",
|
|
|
|
opts.OneType, opts.TwoType),
|
|
|
|
Description: fmt.Sprintf(
|
|
|
|
strings.TrimSpace(inputBackendMigrateNonEmpty),
|
|
|
|
opts.OneType, opts.TwoType, onePath, twoPath),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Confirm with the user that the copy should occur
|
|
|
|
for {
|
|
|
|
v, err := m.UIInput().Input(inputOpts)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf(
|
|
|
|
"Error asking for state copy action: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch strings.ToLower(v) {
|
|
|
|
case "no":
|
|
|
|
return false, nil
|
|
|
|
|
|
|
|
case "yes":
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type backendMigrateOpts struct {
|
|
|
|
OneType, TwoType string
|
|
|
|
One, Two state.State
|
|
|
|
}
|
|
|
|
|
|
|
|
const errBackendStateCopy = `
|
|
|
|
Error copying state from %q to %q: %s
|
|
|
|
|
|
|
|
The state in %[1]q remains intact and unmodified. Please resolve the
|
|
|
|
error above and try again.
|
|
|
|
`
|
|
|
|
|
|
|
|
const inputBackendMigrateEmpty = `
|
|
|
|
Pre-existing state was found in %q while migrating to %q. No existing
|
|
|
|
state was found in %[2]q. Do you want to copy the state from %[1]q to
|
|
|
|
%[2]q? Enter "yes" to copy and "no" to start with an empty state.
|
|
|
|
`
|
|
|
|
|
|
|
|
const inputBackendMigrateNonEmpty = `
|
|
|
|
Pre-existing state was found in %q while migrating to %q. An existing
|
|
|
|
non-empty state exists in %[2]q. The two states have been saved to temporary
|
|
|
|
files that will be removed after responding to this query.
|
|
|
|
|
|
|
|
One (%[1]q): %[3]s
|
|
|
|
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.
|
|
|
|
`
|