command: multistate to multistate conversions

This commit is contained in:
Mitchell Hashimoto 2017-03-01 12:35:59 -08:00
parent c82d7dd56c
commit 1e3d452613
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
6 changed files with 225 additions and 8 deletions

View File

@ -5,6 +5,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/hashicorp/terraform/backend"
@ -51,6 +52,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
// 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.
@ -85,7 +87,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
return m.backendMigrateState_s_s(opts)
}
panic("unhandled")
return m.backendMigrateState_S_S(opts)
}
return nil
@ -107,6 +109,55 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
//
//-------------------------------------------------------------------
// 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()
@ -205,13 +256,15 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
panic("confirmFunc must not be nil")
}
// 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
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.
@ -328,6 +381,7 @@ type backendMigrateOpts struct {
oneEnv string // source env
twoEnv string // dest env
force bool // if true, won't ask for confirmation
}
const errMigrateLoadStates = `
@ -349,6 +403,20 @@ 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
@ -382,3 +450,17 @@ If you continue, Terraform will offer to copy your current environment
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.
`

View File

@ -4,6 +4,8 @@ import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
@ -1193,6 +1195,100 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T)
}
}
// 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

View File

@ -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": []
}
]
}

View File

@ -0,0 +1,6 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "backend-change"
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local" {
environment_dir = "envdir-new"
}
}

View File

@ -0,0 +1,6 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "backend-change-env2"
}