1669 lines
50 KiB
Go
1669 lines
50 KiB
Go
|
package command
|
||
|
|
||
|
// This file contains all the Backend-related function calls on Meta,
|
||
|
// exported and private.
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"log"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/hashicorp/errwrap"
|
||
|
"github.com/hashicorp/go-multierror"
|
||
|
"github.com/hashicorp/hcl"
|
||
|
"github.com/hashicorp/terraform/backend"
|
||
|
"github.com/hashicorp/terraform/config"
|
||
|
"github.com/hashicorp/terraform/state"
|
||
|
"github.com/hashicorp/terraform/terraform"
|
||
|
"github.com/mitchellh/mapstructure"
|
||
|
|
||
|
backendlegacy "github.com/hashicorp/terraform/backend/legacy"
|
||
|
backendlocal "github.com/hashicorp/terraform/backend/local"
|
||
|
backendconsul "github.com/hashicorp/terraform/backend/remote-state/consul"
|
||
|
)
|
||
|
|
||
|
// BackendOpts are the options used to initialize a backend.Backend.
|
||
|
type BackendOpts struct {
|
||
|
// ConfigPath is a path to a file or directory containing the backend
|
||
|
// configuration (declaration).
|
||
|
ConfigPath string
|
||
|
|
||
|
// ConfigFile is a path to a file that contains configuration that
|
||
|
// is merged directly into the backend configuration when loaded
|
||
|
// from a file.
|
||
|
ConfigFile string
|
||
|
|
||
|
// Plan is a plan that is being used. If this is set, the backend
|
||
|
// configuration and output configuration will come from this plan.
|
||
|
Plan *terraform.Plan
|
||
|
|
||
|
// Init should be set to true if initialization is allowed. If this is
|
||
|
// false, then any configuration that requires configuration will show
|
||
|
// an error asking the user to reinitialize.
|
||
|
Init bool
|
||
|
|
||
|
// ForceLocal will force a purely local backend, including state.
|
||
|
// You probably don't want to set this.
|
||
|
ForceLocal bool
|
||
|
}
|
||
|
|
||
|
// Backend initializes and returns the backend for this CLI session.
|
||
|
//
|
||
|
// The backend is used to perform the actual Terraform operations. This
|
||
|
// abstraction enables easily sliding in new Terraform behavior such as
|
||
|
// remote state storage, remote operations, etc. while allowing the CLI
|
||
|
// to remain mostly identical.
|
||
|
//
|
||
|
// This will initialize a new backend for each call, which can carry some
|
||
|
// overhead with it. Please reuse the returned value for optimal behavior.
|
||
|
//
|
||
|
// Only one backend should be used per Meta. This function is stateful
|
||
|
// and is unsafe to create multiple backends used at once. This function
|
||
|
// can be called multiple times with each backend being "live" (usable)
|
||
|
// one at a time.
|
||
|
func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) {
|
||
|
// If no opts are set, then initialize
|
||
|
if opts == nil {
|
||
|
opts = &BackendOpts{}
|
||
|
}
|
||
|
|
||
|
// Setup the local state paths
|
||
|
statePath := m.statePath
|
||
|
stateOutPath := m.stateOutPath
|
||
|
backupPath := m.backupPath
|
||
|
if statePath == "" {
|
||
|
statePath = DefaultStateFilename
|
||
|
}
|
||
|
if stateOutPath == "" {
|
||
|
stateOutPath = statePath
|
||
|
}
|
||
|
if backupPath == "" {
|
||
|
backupPath = stateOutPath + DefaultBackupExtension
|
||
|
}
|
||
|
if backupPath == "-" {
|
||
|
// The local backend expects an empty string for not taking backups.
|
||
|
backupPath = ""
|
||
|
}
|
||
|
|
||
|
// Initialize a backend from the config unless we're forcing a purely
|
||
|
// local operation.
|
||
|
var b backend.Backend
|
||
|
if !opts.ForceLocal {
|
||
|
var err error
|
||
|
|
||
|
// If we have a plan then, we get the the backend from there. Otherwise,
|
||
|
// the backend comes from the configuration.
|
||
|
if opts.Plan != nil {
|
||
|
b, err = m.backendFromPlan(opts)
|
||
|
} else {
|
||
|
b, err = m.backendFromConfig(opts)
|
||
|
}
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
log.Printf("[INFO] command: backend initialized: %T", b)
|
||
|
}
|
||
|
|
||
|
// If the result of loading the backend is an enhanced backend,
|
||
|
// then return that as-is. This works even if b == nil (it will be !ok).
|
||
|
if enhanced, ok := b.(backend.Enhanced); ok {
|
||
|
return enhanced, nil
|
||
|
}
|
||
|
|
||
|
// We either have a non-enhanced backend or no backend configured at
|
||
|
// all. In either case, we use local as our enhanced backend and the
|
||
|
// non-enhanced (if any) as the state backend.
|
||
|
|
||
|
if !opts.ForceLocal {
|
||
|
log.Printf("[INFO] command: backend %T is not enhanced, wrapping in local", b)
|
||
|
}
|
||
|
|
||
|
// Build the local backend
|
||
|
return &backendlocal.Local{
|
||
|
CLI: m.Ui,
|
||
|
CLIColor: m.Colorize(),
|
||
|
StatePath: statePath,
|
||
|
StateOutPath: stateOutPath,
|
||
|
StateBackupPath: backupPath,
|
||
|
ContextOpts: m.contextOpts(),
|
||
|
OpInput: m.Input(),
|
||
|
OpValidation: true,
|
||
|
Backend: b,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// Operation initializes a new backend.Operation struct.
|
||
|
//
|
||
|
// This prepares the operation. After calling this, the caller is expected
|
||
|
// to modify fields of the operation such as Sequence to specify what will
|
||
|
// be called.
|
||
|
func (m *Meta) Operation() *backend.Operation {
|
||
|
return &backend.Operation{
|
||
|
PlanOutBackend: m.backendState,
|
||
|
Targets: m.targets,
|
||
|
UIIn: m.UIInput(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// backendConfig returns the local configuration for the backend
|
||
|
func (m *Meta) backendConfig(opts *BackendOpts) (*config.Backend, error) {
|
||
|
// If no explicit path was given then it is okay for there to be
|
||
|
// no backend configuration found.
|
||
|
emptyOk := opts.ConfigPath == ""
|
||
|
|
||
|
// Determine the path to the configuration.
|
||
|
path := opts.ConfigPath
|
||
|
|
||
|
// If we had no path set, it is an error. We can't initialize unset
|
||
|
if path == "" {
|
||
|
path = "."
|
||
|
}
|
||
|
|
||
|
// Expand the path
|
||
|
if !filepath.IsAbs(path) {
|
||
|
var err error
|
||
|
path, err = filepath.Abs(path)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error expanding path to backend config %q: %s", path, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
log.Printf("[DEBUG] command: loading backend config file: %s", path)
|
||
|
|
||
|
// We first need to determine if we're loading a file or a directory.
|
||
|
fi, err := os.Stat(path)
|
||
|
if err != nil {
|
||
|
if os.IsNotExist(err) && emptyOk {
|
||
|
log.Printf(
|
||
|
"[INFO] command: backend config not found, returning nil: %s",
|
||
|
path)
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
var f func(string) (*config.Config, error) = config.LoadFile
|
||
|
if fi.IsDir() {
|
||
|
f = config.LoadDir
|
||
|
}
|
||
|
|
||
|
// Load the configuration
|
||
|
c, err := f(path)
|
||
|
if err != nil {
|
||
|
// Check for the error where we have no config files and return nil
|
||
|
// as the configuration type.
|
||
|
if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) {
|
||
|
log.Printf(
|
||
|
"[INFO] command: backend config not found, returning nil: %s",
|
||
|
path)
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// If there is no Terraform configuration block, no backend config
|
||
|
if c.Terraform == nil {
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
// Get the configuration for the backend itself.
|
||
|
backend := c.Terraform.Backend
|
||
|
if backend == nil {
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
// If we have a config file set, load that and merge.
|
||
|
if opts.ConfigFile != "" {
|
||
|
log.Printf(
|
||
|
"[DEBUG] command: loading extra backend config from: %s",
|
||
|
opts.ConfigFile)
|
||
|
rc, err := m.backendConfigFile(opts.ConfigFile)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error loading extra configuration file for backend: %s", err)
|
||
|
}
|
||
|
|
||
|
// Merge in the configuration
|
||
|
backend.RawConfig = backend.RawConfig.Merge(rc)
|
||
|
}
|
||
|
|
||
|
// Return the configuration which may or may not be set
|
||
|
return backend, nil
|
||
|
}
|
||
|
|
||
|
// backendConfigFile loads the extra configuration to merge with the
|
||
|
// backend configuration from an extra file if specified by
|
||
|
// BackendOpts.ConfigFile.
|
||
|
func (m *Meta) backendConfigFile(path string) (*config.RawConfig, error) {
|
||
|
// Read the file
|
||
|
d, err := ioutil.ReadFile(path)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Parse it
|
||
|
hclRoot, err := hcl.Parse(string(d))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Decode it
|
||
|
var c map[string]interface{}
|
||
|
if err := hcl.DecodeObject(&c, hclRoot); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return config.NewRawConfig(c)
|
||
|
}
|
||
|
|
||
|
// backendFromConfig returns the initialized (not configured) backend
|
||
|
// directly from the config/state..
|
||
|
//
|
||
|
// This function handles any edge cases around backend config loading. For
|
||
|
// example: legacy remote state, new config changes, backend type changes,
|
||
|
// etc.
|
||
|
//
|
||
|
// This function may query the user for input unless input is disabled, in
|
||
|
// which case this function will error.
|
||
|
func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) {
|
||
|
// Get the local backend configuration.
|
||
|
c, err := m.backendConfig(opts)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Error loading backend config: %s", err)
|
||
|
}
|
||
|
|
||
|
// Get the path to where we store a local cache of backend configuration
|
||
|
// if we're using a remote backend. This may not yet exist which means
|
||
|
// we haven't used a non-local backend before. That is okay.
|
||
|
statePath := filepath.Join(m.DataDir(), DefaultStateFilename)
|
||
|
sMgr := &state.LocalState{Path: statePath}
|
||
|
if err := sMgr.RefreshState(); err != nil {
|
||
|
return nil, fmt.Errorf("Error loading state: %s", err)
|
||
|
}
|
||
|
|
||
|
// Load the state, it must be non-nil for the tests below but can be empty
|
||
|
s := sMgr.State()
|
||
|
if s == nil {
|
||
|
log.Printf("[DEBUG] command: no data state file found for backend config")
|
||
|
s = terraform.NewState()
|
||
|
}
|
||
|
|
||
|
// Upon return, we want to set the state we're using in-memory so that
|
||
|
// we can access it for commands.
|
||
|
m.backendState = nil
|
||
|
defer func() {
|
||
|
if s := sMgr.State(); s != nil && !s.Backend.Empty() {
|
||
|
m.backendState = s.Backend
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// This giant switch statement covers all eight possible combinations
|
||
|
// of state settings between: configuring new backends, saved (previously-
|
||
|
// configured) backends, and legacy remote state.
|
||
|
switch {
|
||
|
// No configuration set at all. Pure local state.
|
||
|
case c == nil && s.Remote.Empty() && s.Backend.Empty():
|
||
|
return nil, nil
|
||
|
|
||
|
// We're unsetting a backend (moving from backend => local)
|
||
|
case c == nil && s.Remote.Empty() && !s.Backend.Empty():
|
||
|
if !opts.Init {
|
||
|
initReason := fmt.Sprintf(
|
||
|
"Unsetting the previously set backend %q",
|
||
|
s.Backend.Type)
|
||
|
m.backendInitRequired(initReason)
|
||
|
return nil, errBackendInitRequired
|
||
|
}
|
||
|
|
||
|
return m.backend_c_r_S(c, sMgr, true)
|
||
|
|
||
|
// We have a legacy remote state configuration but no new backend config
|
||
|
case c == nil && !s.Remote.Empty() && s.Backend.Empty():
|
||
|
return m.backend_c_R_s(c, sMgr)
|
||
|
|
||
|
// We have a legacy remote state configuration simultaneously with a
|
||
|
// saved backend configuration while at the same time disabling backend
|
||
|
// configuration.
|
||
|
//
|
||
|
// This is a naturally impossible case: Terraform will never put you
|
||
|
// in this state, though it is theoretically possible through manual edits
|
||
|
case c == nil && !s.Remote.Empty() && !s.Backend.Empty():
|
||
|
if !opts.Init {
|
||
|
initReason := fmt.Sprintf(
|
||
|
"Unsetting the previously set backend %q",
|
||
|
s.Backend.Type)
|
||
|
m.backendInitRequired(initReason)
|
||
|
return nil, errBackendInitRequired
|
||
|
}
|
||
|
|
||
|
return m.backend_c_R_S(c, sMgr)
|
||
|
|
||
|
// Configuring a backend for the first time.
|
||
|
case c != nil && s.Remote.Empty() && s.Backend.Empty():
|
||
|
if !opts.Init {
|
||
|
initReason := fmt.Sprintf(
|
||
|
"Initial configuration of the requested backend %q",
|
||
|
c.Type)
|
||
|
m.backendInitRequired(initReason)
|
||
|
return nil, errBackendInitRequired
|
||
|
}
|
||
|
|
||
|
return m.backend_C_r_s(c, sMgr)
|
||
|
|
||
|
// Potentially changing a backend configuration
|
||
|
case c != nil && s.Remote.Empty() && !s.Backend.Empty():
|
||
|
// If our configuration is the same, then we're just initializing
|
||
|
// a previously configured remote backend.
|
||
|
if !s.Backend.Empty() && s.Backend.Hash == c.Hash {
|
||
|
return m.backend_C_r_S_unchanged(c, sMgr)
|
||
|
}
|
||
|
|
||
|
if !opts.Init {
|
||
|
initReason := fmt.Sprintf(
|
||
|
"Backend configuration changed for %q",
|
||
|
c.Type)
|
||
|
m.backendInitRequired(initReason)
|
||
|
return nil, errBackendInitRequired
|
||
|
}
|
||
|
|
||
|
log.Printf(
|
||
|
"[WARN] command: backend config change! saved: %d, new: %d",
|
||
|
s.Backend.Hash, c.Hash)
|
||
|
return m.backend_C_r_S_changed(c, sMgr, true)
|
||
|
|
||
|
// Configuring a backend for the first time while having legacy
|
||
|
// remote state. This is very possible if a Terraform user configures
|
||
|
// a backend prior to ever running Terraform on an old state.
|
||
|
case c != nil && !s.Remote.Empty() && s.Backend.Empty():
|
||
|
if !opts.Init {
|
||
|
initReason := fmt.Sprintf(
|
||
|
"Initial configuration for backend %q",
|
||
|
c.Type)
|
||
|
m.backendInitRequired(initReason)
|
||
|
return nil, errBackendInitRequired
|
||
|
}
|
||
|
|
||
|
return m.backend_C_R_s(c, sMgr)
|
||
|
|
||
|
// Configuring a backend with both a legacy remote state set
|
||
|
// and a pre-existing backend saved.
|
||
|
case c != nil && !s.Remote.Empty() && !s.Backend.Empty():
|
||
|
// If the hashes are the same, we have a legacy remote state with
|
||
|
// an unchanged stored backend state.
|
||
|
if s.Backend.Hash == c.Hash {
|
||
|
if !opts.Init {
|
||
|
initReason := fmt.Sprintf(
|
||
|
"Legacy remote state found with configured backend %q",
|
||
|
c.Type)
|
||
|
m.backendInitRequired(initReason)
|
||
|
return nil, errBackendInitRequired
|
||
|
}
|
||
|
|
||
|
return m.backend_C_R_S_unchanged(c, sMgr, true)
|
||
|
}
|
||
|
|
||
|
if !opts.Init {
|
||
|
initReason := fmt.Sprintf(
|
||
|
"Reconfiguring the backend %q",
|
||
|
c.Type)
|
||
|
m.backendInitRequired(initReason)
|
||
|
return nil, errBackendInitRequired
|
||
|
}
|
||
|
|
||
|
// We have change in all three
|
||
|
return m.backend_C_R_S_changed(c, sMgr)
|
||
|
default:
|
||
|
// This should be impossible since all state possibilties are
|
||
|
// tested above, but we need a default case anyways and we should
|
||
|
// protect against the scenario where a case is somehow removed.
|
||
|
return nil, fmt.Errorf(
|
||
|
"Unhandled backend configuration state. This is a bug. Please\n"+
|
||
|
"report this error with the following information.\n\n"+
|
||
|
"Config Nil: %v\n"+
|
||
|
"Saved Backend Empty: %v\n"+
|
||
|
"Legacy Remote Empty: %v\n",
|
||
|
c == nil, s.Backend.Empty(), s.Remote.Empty())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// backendFromPlan loads the backend from a given plan file.
|
||
|
func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) {
|
||
|
// Precondition check
|
||
|
if opts.Plan == nil {
|
||
|
panic("plan should not be nil")
|
||
|
}
|
||
|
|
||
|
// We currently don't allow "-state" to be specified.
|
||
|
if m.statePath != "" {
|
||
|
return nil, fmt.Errorf(
|
||
|
"State path cannot be specified with a plan file. The plan itself contains\n" +
|
||
|
"the state to use. If you wish to change that, please create a new plan\n" +
|
||
|
"and specify the state path when creating the plan.")
|
||
|
}
|
||
|
|
||
|
planState := opts.Plan.State
|
||
|
if planState == nil {
|
||
|
// The state can be nil, we just have to make it empty for the logic
|
||
|
// in this function.
|
||
|
planState = terraform.NewState()
|
||
|
}
|
||
|
|
||
|
// Validation only for non-local plans
|
||
|
local := planState.Remote.Empty() && planState.Backend.Empty()
|
||
|
if !local {
|
||
|
// We currently don't allow "-state-out" to be specified.
|
||
|
if m.stateOutPath != "" {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendPlanStateFlag))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
// Determine the path where we'd be writing state
|
||
|
path := DefaultStateFilename
|
||
|
if !planState.Remote.Empty() || !planState.Backend.Empty() {
|
||
|
path = filepath.Join(m.DataDir(), DefaultStateFilename)
|
||
|
}
|
||
|
|
||
|
// If the path exists, then we need to verify we're writing the same
|
||
|
// state lineage. If the path doesn't exist that's okay.
|
||
|
_, err := os.Stat(path)
|
||
|
if err != nil && !os.IsNotExist(err) {
|
||
|
return nil, fmt.Errorf("Error checking state destination: %s", err)
|
||
|
}
|
||
|
if err == nil {
|
||
|
// The file exists, we need to read it and compare
|
||
|
if err := m.backendFromPlan_compareStates(state, path); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
*/
|
||
|
|
||
|
// If we have a stateOutPath, we must also specify it as the
|
||
|
// input path so we can check it properly. We restore it after this
|
||
|
// function exits.
|
||
|
original := m.statePath
|
||
|
m.statePath = m.stateOutPath
|
||
|
defer func() { m.statePath = original }()
|
||
|
|
||
|
var b backend.Backend
|
||
|
var err error
|
||
|
switch {
|
||
|
// No remote state at all, all local
|
||
|
case planState.Remote.Empty() && planState.Backend.Empty():
|
||
|
// Get the local backend
|
||
|
b, err = m.Backend(&BackendOpts{ForceLocal: true})
|
||
|
|
||
|
// New backend configuration set
|
||
|
case planState.Remote.Empty() && !planState.Backend.Empty():
|
||
|
b, err = m.backendInitFromSaved(planState.Backend)
|
||
|
|
||
|
// Legacy remote state set
|
||
|
case !planState.Remote.Empty() && planState.Backend.Empty():
|
||
|
// Write our current state to an inmemory state just so that we
|
||
|
// have it in the format of state.State
|
||
|
inmem := &state.InmemState{}
|
||
|
inmem.WriteState(planState)
|
||
|
|
||
|
// Get the backend through the normal means of legacy state
|
||
|
b, err = m.backend_c_R_s(nil, inmem)
|
||
|
|
||
|
// Both set, this can't happen in a plan.
|
||
|
case !planState.Remote.Empty() && !planState.Backend.Empty():
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendPlanBoth))
|
||
|
}
|
||
|
|
||
|
// If we had an error, return that
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Get the state so we can determine the effect of using this plan
|
||
|
realMgr, err := b.State()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Error reading state: %s", err)
|
||
|
}
|
||
|
if err := realMgr.RefreshState(); err != nil {
|
||
|
return nil, fmt.Errorf("Error reading state: %s", err)
|
||
|
}
|
||
|
real := realMgr.State()
|
||
|
if real != nil {
|
||
|
// If they're not the same lineage, don't allow this
|
||
|
if !real.SameLineage(planState) {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendPlanLineageDiff))
|
||
|
}
|
||
|
|
||
|
// Compare ages
|
||
|
comp, err := real.CompareAges(planState)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Error comparing state ages for safety: %s", err)
|
||
|
}
|
||
|
switch comp {
|
||
|
case terraform.StateAgeEqual:
|
||
|
// State ages are equal, this is perfect
|
||
|
|
||
|
case terraform.StateAgeReceiverOlder:
|
||
|
// Real state is somehow older, this is okay.
|
||
|
|
||
|
case terraform.StateAgeReceiverNewer:
|
||
|
// If we have an older serial it is a problem but if we have a
|
||
|
// differing serial but are still identical, just let it through.
|
||
|
if real.Equal(planState) {
|
||
|
log.Printf(
|
||
|
"[WARN] command: state in plan has older serial, but Equal is true")
|
||
|
break
|
||
|
}
|
||
|
|
||
|
// The real state is newer, this is not allowed.
|
||
|
return nil, fmt.Errorf(
|
||
|
strings.TrimSpace(errBackendPlanOlder),
|
||
|
planState.Serial, real.Serial)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Write the state
|
||
|
newState := opts.Plan.State.DeepCopy()
|
||
|
if newState != nil {
|
||
|
newState.Remote = nil
|
||
|
newState.Backend = nil
|
||
|
}
|
||
|
if err := realMgr.WriteState(newState); err != nil {
|
||
|
return nil, fmt.Errorf("Error writing state: %s", err)
|
||
|
}
|
||
|
if err := realMgr.PersistState(); err != nil {
|
||
|
return nil, fmt.Errorf("Error writing state: %s", err)
|
||
|
}
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
//-------------------------------------------------------------------
|
||
|
// Backend Config Scenarios
|
||
|
//
|
||
|
// The functions below cover handling all the various scenarios that
|
||
|
// can exist when loading a backend. They are named in the format of
|
||
|
// "backend_C_R_S" where C, R, S may be upper or lowercase. Lowercase
|
||
|
// means it is false, uppercase means it is true. The full set of eight
|
||
|
// possible cases is handled.
|
||
|
//
|
||
|
// The fields are:
|
||
|
//
|
||
|
// * C - Backend configuration is set and changed in TF files
|
||
|
// * R - Legacy remote state is set
|
||
|
// * S - Backend configuration is set in the state
|
||
|
//
|
||
|
//-------------------------------------------------------------------
|
||
|
|
||
|
// Unconfiguring a backend (moving from backend => local).
|
||
|
func (m *Meta) backend_c_r_S(
|
||
|
c *config.Backend, sMgr state.State, output bool) (backend.Backend, error) {
|
||
|
s := sMgr.State()
|
||
|
|
||
|
// Get the backend type for output
|
||
|
backendType := s.Backend.Type
|
||
|
|
||
|
// Confirm with the user that the copy should occur
|
||
|
copy, err := m.confirm(&terraform.InputOpts{
|
||
|
Id: "backend-migrate-to-local",
|
||
|
Query: fmt.Sprintf("Do you want to copy the state from %q?", s.Backend.Type),
|
||
|
Description: fmt.Sprintf(
|
||
|
strings.TrimSpace(inputBackendMigrateLocal), s.Backend.Type),
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error asking for state copy action: %s", err)
|
||
|
}
|
||
|
|
||
|
// If we're copying, perform the migration
|
||
|
if copy {
|
||
|
// Grab a purely local backend to get the local state if it exists
|
||
|
localB, err := m.Backend(&BackendOpts{ForceLocal: true})
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendLocalRead), err)
|
||
|
}
|
||
|
localState, err := localB.State()
|
||
|
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()
|
||
|
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,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Remove the stored metadata
|
||
|
s.Backend = nil
|
||
|
if err := sMgr.WriteState(s); err != nil {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendClearSaved), err)
|
||
|
}
|
||
|
if err := sMgr.PersistState(); err != nil {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendClearSaved), err)
|
||
|
}
|
||
|
|
||
|
if output {
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset][green]\n\n"+
|
||
|
strings.TrimSpace(successBackendUnset), backendType)))
|
||
|
}
|
||
|
|
||
|
// Return no backend
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
// Legacy remote state
|
||
|
func (m *Meta) backend_c_R_s(
|
||
|
c *config.Backend, sMgr state.State) (backend.Backend, error) {
|
||
|
s := sMgr.State()
|
||
|
|
||
|
// Warn the user
|
||
|
m.Ui.Warn(strings.TrimSpace(warnBackendLegacy) + "\n")
|
||
|
|
||
|
// We need to convert the config to map[string]interface{} since that
|
||
|
// is what the backends expect.
|
||
|
var configMap map[string]interface{}
|
||
|
if err := mapstructure.Decode(s.Remote.Config, &configMap); err != nil {
|
||
|
return nil, fmt.Errorf("Error configuring remote state: %s", err)
|
||
|
}
|
||
|
|
||
|
// Create the config
|
||
|
rawC, err := config.NewRawConfig(configMap)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Error configuring remote state: %s", err)
|
||
|
}
|
||
|
config := terraform.NewResourceConfig(rawC)
|
||
|
|
||
|
// Initialize the legacy remote backend
|
||
|
b := &backendlegacy.Backend{Type: s.Remote.Type}
|
||
|
|
||
|
// Configure
|
||
|
if err := b.Configure(config); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendLegacyConfig, err)
|
||
|
}
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
// Unsetting backend, saved backend, legacy remote state
|
||
|
func (m *Meta) backend_c_R_S(
|
||
|
c *config.Backend, sMgr state.State) (backend.Backend, error) {
|
||
|
// Notify the user
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset]%s\n\n",
|
||
|
strings.TrimSpace(outputBackendUnsetWithLegacy))))
|
||
|
|
||
|
// Get the backend type for later
|
||
|
backendType := sMgr.State().Backend.Type
|
||
|
|
||
|
// First, perform the configured => local tranasition
|
||
|
if _, err := m.backend_c_r_S(c, sMgr, false); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Grab a purely local backend
|
||
|
localB, err := m.Backend(&BackendOpts{ForceLocal: true})
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(errBackendLocalRead, err)
|
||
|
}
|
||
|
localState, err := localB.State()
|
||
|
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()
|
||
|
|
||
|
// Ask the user if they want to migrate their existing remote state
|
||
|
copy, err := m.confirm(&terraform.InputOpts{
|
||
|
Id: "backend-migrate-to-new",
|
||
|
Query: fmt.Sprintf(
|
||
|
"Do you want to copy the legacy remote state from %q?",
|
||
|
s.Remote.Type),
|
||
|
Description: strings.TrimSpace(inputBackendMigrateLegacyLocal),
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error asking for state copy action: %s", err)
|
||
|
}
|
||
|
|
||
|
// If the user wants a copy, copy!
|
||
|
if copy {
|
||
|
// Initialize the legacy backend
|
||
|
oldB, err := m.backendInitFromLegacy(s.Remote)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
oldState, err := oldB.State()
|
||
|
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,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Unset the remote state
|
||
|
s = sMgr.State()
|
||
|
if s == nil {
|
||
|
s = terraform.NewState()
|
||
|
}
|
||
|
s.Remote = nil
|
||
|
if err := sMgr.WriteState(s); err != nil {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err)
|
||
|
}
|
||
|
if err := sMgr.PersistState(); err != nil {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err)
|
||
|
}
|
||
|
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset][green]\n\n"+
|
||
|
strings.TrimSpace(successBackendUnset), backendType)))
|
||
|
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
// Configuring a backend for the first time with legacy remote state.
|
||
|
func (m *Meta) backend_C_R_s(
|
||
|
c *config.Backend, sMgr state.State) (backend.Backend, error) {
|
||
|
// Notify the user
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset]%s\n\n",
|
||
|
strings.TrimSpace(outputBackendConfigureWithLegacy))))
|
||
|
|
||
|
// First, configure the new backend
|
||
|
b, err := m.backendInitFromConfig(c)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Next, save the new configuration. This will not overwrite our
|
||
|
// legacy remote state. We'll handle that after.
|
||
|
s := sMgr.State()
|
||
|
if s == nil {
|
||
|
s = terraform.NewState()
|
||
|
}
|
||
|
s.Backend = &terraform.BackendState{
|
||
|
Type: c.Type,
|
||
|
Config: c.RawConfig.Raw,
|
||
|
Hash: c.Hash,
|
||
|
}
|
||
|
if err := sMgr.WriteState(s); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendWriteSaved, err)
|
||
|
}
|
||
|
if err := sMgr.PersistState(); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendWriteSaved, err)
|
||
|
}
|
||
|
|
||
|
// I don't know how this is possible but if we don't have remote
|
||
|
// state config anymore somehow, just return the backend. This
|
||
|
// shouldn't be possible, though.
|
||
|
if s.Remote.Empty() {
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
// Finally, ask the user if they want to copy the state from
|
||
|
// their old remote state location.
|
||
|
copy, err := m.confirm(&terraform.InputOpts{
|
||
|
Id: "backend-migrate-to-new",
|
||
|
Query: fmt.Sprintf(
|
||
|
"Do you want to copy the legacy remote state from %q?",
|
||
|
s.Remote.Type),
|
||
|
Description: strings.TrimSpace(inputBackendMigrateLegacy),
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error asking for state copy action: %s", err)
|
||
|
}
|
||
|
|
||
|
// If the user wants a copy, copy!
|
||
|
if copy {
|
||
|
// Initialize the legacy backend
|
||
|
oldB, err := m.backendInitFromLegacy(s.Remote)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
oldState, err := oldB.State()
|
||
|
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()
|
||
|
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,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Unset the remote state
|
||
|
s = sMgr.State()
|
||
|
if s == nil {
|
||
|
s = terraform.NewState()
|
||
|
}
|
||
|
s.Remote = nil
|
||
|
if err := sMgr.WriteState(s); err != nil {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err)
|
||
|
}
|
||
|
if err := sMgr.PersistState(); err != nil {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err)
|
||
|
}
|
||
|
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset][green]\n\n"+
|
||
|
strings.TrimSpace(successBackendSet), s.Backend.Type)))
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
// Configuring a backend for the first time.
|
||
|
func (m *Meta) backend_C_r_s(
|
||
|
c *config.Backend, sMgr state.State) (backend.Backend, error) {
|
||
|
// Get the backend
|
||
|
b, err := m.backendInitFromConfig(c)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Grab a purely local backend to get the local state if it exists
|
||
|
localB, err := m.Backend(&BackendOpts{ForceLocal: true})
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(errBackendLocalRead, err)
|
||
|
}
|
||
|
localState, err := localB.State()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(errBackendLocalRead, err)
|
||
|
}
|
||
|
if err := localState.RefreshState(); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendLocalRead, err)
|
||
|
}
|
||
|
|
||
|
// 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()
|
||
|
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,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// We always delete the local state
|
||
|
if err := localState.WriteState(nil); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendMigrateLocalDelete, err)
|
||
|
}
|
||
|
if err := localState.PersistState(); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendMigrateLocalDelete, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Store the metadata in our saved state location
|
||
|
s := sMgr.State()
|
||
|
if s == nil {
|
||
|
s = terraform.NewState()
|
||
|
}
|
||
|
s.Backend = &terraform.BackendState{
|
||
|
Type: c.Type,
|
||
|
Config: c.RawConfig.Raw,
|
||
|
Hash: c.Hash,
|
||
|
}
|
||
|
if err := sMgr.WriteState(s); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendWriteSaved, err)
|
||
|
}
|
||
|
if err := sMgr.PersistState(); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendWriteSaved, err)
|
||
|
}
|
||
|
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset][green]\n\n"+
|
||
|
strings.TrimSpace(successBackendSet), s.Backend.Type)))
|
||
|
|
||
|
// Return the backend
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
// Changing a previously saved backend.
|
||
|
func (m *Meta) backend_C_r_S_changed(
|
||
|
c *config.Backend, sMgr state.State, output bool) (backend.Backend, error) {
|
||
|
if output {
|
||
|
// Notify the user
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset]%s\n\n",
|
||
|
strings.TrimSpace(outputBackendReconfigure))))
|
||
|
}
|
||
|
|
||
|
// Get the backend
|
||
|
b, err := m.backendInitFromConfig(c)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error initializing new backend: %s", err)
|
||
|
}
|
||
|
|
||
|
// Check with the user if we want to migrate state
|
||
|
copy, err := m.confirm(&terraform.InputOpts{
|
||
|
Id: "backend-migrate-to-new",
|
||
|
Query: fmt.Sprintf("Do you want to copy the state from %q?", c.Type),
|
||
|
Description: strings.TrimSpace(inputBackendMigrateChange),
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error asking for state copy action: %s", err)
|
||
|
}
|
||
|
|
||
|
// If we are, then we need to initialize the old backend and
|
||
|
// perform the copy.
|
||
|
if copy {
|
||
|
// Grab the existing backend
|
||
|
oldB, err := m.backend_C_r_S_unchanged(c, sMgr)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error loading previously configured backend: %s", err)
|
||
|
}
|
||
|
|
||
|
// Get the old state
|
||
|
s := sMgr.State()
|
||
|
oldState, err := oldB.State()
|
||
|
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()
|
||
|
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,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Update the backend state
|
||
|
s := sMgr.State()
|
||
|
if s == nil {
|
||
|
s = terraform.NewState()
|
||
|
}
|
||
|
s.Backend = &terraform.BackendState{
|
||
|
Type: c.Type,
|
||
|
Config: c.RawConfig.Raw,
|
||
|
Hash: c.Hash,
|
||
|
}
|
||
|
if err := sMgr.WriteState(s); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendWriteSaved, err)
|
||
|
}
|
||
|
if err := sMgr.PersistState(); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendWriteSaved, err)
|
||
|
}
|
||
|
|
||
|
if output {
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset][green]\n\n"+
|
||
|
strings.TrimSpace(successBackendSet), s.Backend.Type)))
|
||
|
}
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
// Initiailizing an unchanged saved backend
|
||
|
func (m *Meta) backend_C_r_S_unchanged(
|
||
|
c *config.Backend, sMgr state.State) (backend.Backend, error) {
|
||
|
s := sMgr.State()
|
||
|
|
||
|
// Create the config. We do this from the backend state since this
|
||
|
// has the complete configuration data whereas the config itself
|
||
|
// may require input.
|
||
|
rawC, err := config.NewRawConfig(s.Backend.Config)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Error configuring backend: %s", err)
|
||
|
}
|
||
|
config := terraform.NewResourceConfig(rawC)
|
||
|
|
||
|
// Get the backend
|
||
|
f, ok := Backends[s.Backend.Type]
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type)
|
||
|
}
|
||
|
b := f()
|
||
|
|
||
|
// Configure
|
||
|
if err := b.Configure(config); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendSavedConfig, s.Backend.Type, err)
|
||
|
}
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
// Initiailizing a changed saved backend with legacy remote state.
|
||
|
func (m *Meta) backend_C_R_S_changed(
|
||
|
c *config.Backend, sMgr state.State) (backend.Backend, error) {
|
||
|
// Notify the user
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset]%s\n\n",
|
||
|
strings.TrimSpace(outputBackendSavedWithLegacyChanged))))
|
||
|
|
||
|
// Reconfigure the backend first
|
||
|
if _, err := m.backend_C_r_S_changed(c, sMgr, false); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Handle the case where we have all set but unchanged
|
||
|
b, err := m.backend_C_R_S_unchanged(c, sMgr, false)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Output success message
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset][green]\n\n"+
|
||
|
strings.TrimSpace(successBackendReconfigureWithLegacy), c.Type)))
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
// Initiailizing an unchanged saved backend with legacy remote state.
|
||
|
func (m *Meta) backend_C_R_S_unchanged(
|
||
|
c *config.Backend, sMgr state.State, output bool) (backend.Backend, error) {
|
||
|
if output {
|
||
|
// Notify the user
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset]%s\n\n",
|
||
|
strings.TrimSpace(outputBackendSavedWithLegacy))))
|
||
|
}
|
||
|
|
||
|
// Load the backend from the state
|
||
|
s := sMgr.State()
|
||
|
b, err := m.backendInitFromSaved(s.Backend)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Ask if the user wants to move their legacy remote state
|
||
|
copy, err := m.confirm(&terraform.InputOpts{
|
||
|
Id: "backend-migrate-to-new",
|
||
|
Query: fmt.Sprintf(
|
||
|
"Do you want to copy the legacy remote state from %q?",
|
||
|
s.Remote.Type),
|
||
|
Description: strings.TrimSpace(inputBackendMigrateLegacy),
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error asking for state copy action: %s", err)
|
||
|
}
|
||
|
|
||
|
// If the user wants a copy, copy!
|
||
|
if copy {
|
||
|
// Initialize the legacy backend
|
||
|
oldB, err := m.backendInitFromLegacy(s.Remote)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
oldState, err := oldB.State()
|
||
|
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()
|
||
|
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,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Unset the remote state
|
||
|
s = sMgr.State()
|
||
|
if s == nil {
|
||
|
s = terraform.NewState()
|
||
|
}
|
||
|
s.Remote = nil
|
||
|
if err := sMgr.WriteState(s); err != nil {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err)
|
||
|
}
|
||
|
if err := sMgr.PersistState(); err != nil {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err)
|
||
|
}
|
||
|
|
||
|
if output {
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset][green]\n\n"+
|
||
|
strings.TrimSpace(successBackendLegacyUnset), s.Backend.Type)))
|
||
|
}
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
//-------------------------------------------------------------------
|
||
|
// Reusable helper functions for backend management
|
||
|
//-------------------------------------------------------------------
|
||
|
|
||
|
func (m *Meta) backendInitFromConfig(c *config.Backend) (backend.Backend, error) {
|
||
|
// Create the config.
|
||
|
config := terraform.NewResourceConfig(c.RawConfig)
|
||
|
|
||
|
// Get the backend
|
||
|
f, ok := Backends[c.Type]
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewUnknown), c.Type)
|
||
|
}
|
||
|
b := f()
|
||
|
|
||
|
// TODO: test
|
||
|
// Ask for input if we have input enabled
|
||
|
if m.Input() {
|
||
|
var err error
|
||
|
config, err = b.Input(m.UIInput(), config)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error asking for input to configure the backend %q: %s",
|
||
|
c.Type, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Validate
|
||
|
warns, errs := b.Validate(config)
|
||
|
if len(errs) > 0 {
|
||
|
return nil, fmt.Errorf(
|
||
|
"Error configuring the backend %q: %s",
|
||
|
c.Type, multierror.Append(nil, errs...))
|
||
|
}
|
||
|
if len(warns) > 0 {
|
||
|
// TODO: warnings are currently ignored
|
||
|
}
|
||
|
|
||
|
// Configure
|
||
|
if err := b.Configure(config); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendNewConfig, c.Type, err)
|
||
|
}
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
func (m *Meta) backendInitFromLegacy(s *terraform.RemoteState) (backend.Backend, error) {
|
||
|
// We need to convert the config to map[string]interface{} since that
|
||
|
// is what the backends expect.
|
||
|
var configMap map[string]interface{}
|
||
|
if err := mapstructure.Decode(s.Config, &configMap); err != nil {
|
||
|
return nil, fmt.Errorf("Error configuring remote state: %s", err)
|
||
|
}
|
||
|
|
||
|
// Create the config
|
||
|
rawC, err := config.NewRawConfig(configMap)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Error configuring remote state: %s", err)
|
||
|
}
|
||
|
config := terraform.NewResourceConfig(rawC)
|
||
|
|
||
|
// Initialize the legacy remote backend
|
||
|
b := &backendlegacy.Backend{Type: s.Type}
|
||
|
|
||
|
// Configure
|
||
|
if err := b.Configure(config); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendLegacyConfig, err)
|
||
|
}
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
func (m *Meta) backendInitFromSaved(s *terraform.BackendState) (backend.Backend, error) {
|
||
|
// Create the config. We do this from the backend state since this
|
||
|
// has the complete configuration data whereas the config itself
|
||
|
// may require input.
|
||
|
rawC, err := config.NewRawConfig(s.Config)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Error configuring backend: %s", err)
|
||
|
}
|
||
|
config := terraform.NewResourceConfig(rawC)
|
||
|
|
||
|
// Get the backend
|
||
|
f, ok := Backends[s.Type]
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Type)
|
||
|
}
|
||
|
b := f()
|
||
|
|
||
|
// Configure
|
||
|
if err := b.Configure(config); err != nil {
|
||
|
return nil, fmt.Errorf(errBackendSavedConfig, s.Type, err)
|
||
|
}
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
func (m *Meta) backendInitRequired(reason string) {
|
||
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
||
|
"[reset]"+strings.TrimSpace(errBackendInit)+"\n", reason)))
|
||
|
}
|
||
|
|
||
|
//-------------------------------------------------------------------
|
||
|
// Output constants and initialization code
|
||
|
//-------------------------------------------------------------------
|
||
|
|
||
|
// Backends is the list of available backends. This is currently a hardcoded
|
||
|
// list that can't be modified without recompiling Terraform. This is done
|
||
|
// because the API for backends uses complex structures and supporting that
|
||
|
// over the plugin system is currently prohibitively difficult. For those
|
||
|
// wanting to implement a custom backend, recompilation should not be a
|
||
|
// high barrier.
|
||
|
var Backends map[string]func() backend.Backend
|
||
|
|
||
|
func init() {
|
||
|
// Our hardcoded backends
|
||
|
Backends = map[string]func() backend.Backend{
|
||
|
"local": func() backend.Backend { return &backendlocal.Local{} },
|
||
|
"consul": func() backend.Backend { return backendconsul.New() },
|
||
|
}
|
||
|
|
||
|
// Add the legacy remote backends that haven't yet been convertd to
|
||
|
// the new backend API.
|
||
|
backendlegacy.Init(Backends)
|
||
|
}
|
||
|
|
||
|
// errBackendInitRequired is the final error message shown when reinit
|
||
|
// is required for some reason. The error message includes the reason.
|
||
|
var errBackendInitRequired = errors.New(
|
||
|
"Initialization required. Please see the error message above.")
|
||
|
|
||
|
const errBackendLegacyConfig = `
|
||
|
One or more errors occurred while configuring the legacy remote state.
|
||
|
If fixing these errors requires changing your remote state configuration,
|
||
|
you must switch your configuration to the new remote backend configuration.
|
||
|
You can learn more about remote backends at the URL below:
|
||
|
|
||
|
TODO: URL
|
||
|
|
||
|
The error(s) configuring the legacy remote state:
|
||
|
|
||
|
%s
|
||
|
`
|
||
|
|
||
|
const errBackendLocalRead = `
|
||
|
Error reading local state: %s
|
||
|
|
||
|
Terraform is trying to read your local state to determine if there is
|
||
|
state to migrate to your newly configured backend. Terraform can't continue
|
||
|
without this check because that would risk losing state. Please resolve the
|
||
|
error above and try again.
|
||
|
`
|
||
|
|
||
|
const errBackendMigrateLocalDelete = `
|
||
|
Error deleting local state after migration: %s
|
||
|
|
||
|
Your local state is deleted after successfully migrating it to the newly
|
||
|
configured backend. As part of the deletion process, a backup is made at
|
||
|
the standard backup path unless explicitly asked not to. To cleanly operate
|
||
|
with a backend, we must delete the local state file. Please resolve the
|
||
|
issue above and retry the command.
|
||
|
`
|
||
|
|
||
|
const errBackendMigrateNew = `
|
||
|
Error migrating local state to backend: %s
|
||
|
|
||
|
Your local state remains intact and unmodified. Please resolve the error
|
||
|
above and try again.
|
||
|
`
|
||
|
|
||
|
const errBackendNewConfig = `
|
||
|
Error configuring the backend %q: %s
|
||
|
|
||
|
Please update the configuration in your Terraform files to fix this error
|
||
|
then run this command again.
|
||
|
`
|
||
|
|
||
|
const errBackendNewRead = `
|
||
|
Error reading newly configured backend state: %s
|
||
|
|
||
|
Terraform is trying to read the state from your newly configured backend
|
||
|
to determine the copy process for your existing state. Backends are expected
|
||
|
to not error even if there is no state yet written. Please resolve the
|
||
|
error above and try again.
|
||
|
`
|
||
|
|
||
|
const errBackendNewUnknown = `
|
||
|
The backend %q could not be found.
|
||
|
|
||
|
This is the backend specified in your Terraform configuration file.
|
||
|
This error could be a simple typo in your configuration, but it can also
|
||
|
be caused by using a Terraform version that doesn't support the specified
|
||
|
backend type. Please check your configuration and your Terraform version.
|
||
|
|
||
|
If you'd like to run Terraform and store state locally, you can fix this
|
||
|
error by removing the backend configuration from your configuration.
|
||
|
`
|
||
|
|
||
|
const errBackendRemoteRead = `
|
||
|
Error reading backend state: %s
|
||
|
|
||
|
Terraform is trying to read the state from your configured backend to
|
||
|
determien if there is any migration steps necessary. Terraform can't continue
|
||
|
without this check because that would risk losing state. Please resolve the
|
||
|
error above and try again.
|
||
|
`
|
||
|
|
||
|
const errBackendSavedConfig = `
|
||
|
Error configuring the backend %q: %s
|
||
|
|
||
|
Please update the configuration in your Terraform files to fix this error.
|
||
|
If you'd like to update the configuration interactively without storing
|
||
|
the values in your configuration, run "terraform init".
|
||
|
`
|
||
|
|
||
|
const errBackendSavedUnsetConfig = `
|
||
|
Error configuring the existing backend %q: %s
|
||
|
|
||
|
Terraform must configure the existing backend in order to copy the state
|
||
|
from the existing backend, as requested. Please resolve the error and try
|
||
|
again. If you choose to not copy the existing state, Terraform will not
|
||
|
configure the backend. If the configuration is invalid, please update your
|
||
|
Terraform configuration with proper configuration for this backend first
|
||
|
before unsetting the backend.
|
||
|
`
|
||
|
|
||
|
const errBackendSavedUnknown = `
|
||
|
The backend %q could not be found.
|
||
|
|
||
|
This is the backend that this Terraform environment is configured to use
|
||
|
both in your configuration and saved locally as your last-used backend.
|
||
|
If it isn't found, it could mean an alternate version of Terraform was
|
||
|
used with this configuration. Please use the proper version of Terraform that
|
||
|
contains support for this backend.
|
||
|
|
||
|
If you'd like to force remove this backend, you must update your configuration
|
||
|
to not use the backend and run "terraform init" (or any other command) again.
|
||
|
`
|
||
|
|
||
|
const errBackendClearLegacy = `
|
||
|
Error clearing the legacy remote state configuration: %s
|
||
|
|
||
|
Terraform completed configuring your backend. It is now safe to remove
|
||
|
the legacy remote state configuration, but an error occurred while trying
|
||
|
to do so. Please look at the error above, resolve it, and try again.
|
||
|
`
|
||
|
|
||
|
const errBackendClearSaved = `
|
||
|
Error clearing the backend configuration: %s
|
||
|
|
||
|
Terraform removes the saved backend configuration when you're removing a
|
||
|
configured backend. This must be done so future Terraform runs know to not
|
||
|
use the backend configuration. Please look at the error above, resolve it,
|
||
|
and try again.
|
||
|
`
|
||
|
|
||
|
const errBackendInit = `
|
||
|
[reset][bold][yellow]Backend reinitialization required. Please run "terraform init".[reset]
|
||
|
[yellow]Reason: %s
|
||
|
|
||
|
The "backend" is the interface that Terraform uses to store state,
|
||
|
perform operations, etc. If this message is showing up, it means that the
|
||
|
Terraform configuration you're using is using a custom configuration for
|
||
|
the Terraform backend.
|
||
|
|
||
|
Changes to backend configurations require reinitialization. This allows
|
||
|
Terraform to setup the new configuration, copy existing state, etc. This is
|
||
|
only done during "terraform init". Please run that command now then try again.
|
||
|
|
||
|
If the change reason above is incorrect, please verify your configuration
|
||
|
hasn't changed and try again. At this point, no changes to your existing
|
||
|
configuration or state have been made.
|
||
|
`
|
||
|
|
||
|
const errBackendWriteSaved = `
|
||
|
Error saving the backend configuration: %s
|
||
|
|
||
|
Terraform saves the complete backend configuration in a local file for
|
||
|
configuring the backend on future operations. This cannot be disabled. Errors
|
||
|
are usually due to simple file permission errors. Please look at the error
|
||
|
above, resolve it, and try again.
|
||
|
`
|
||
|
|
||
|
const errBackendPlanBoth = `
|
||
|
The plan file contained both a legacy remote state and backend configuration.
|
||
|
This is not allowed. Please recreate the plan file with the latest version of
|
||
|
Terraform.
|
||
|
`
|
||
|
|
||
|
const errBackendPlanLineageDiff = `
|
||
|
The plan file contains a state with a differing lineage than the current
|
||
|
state. By continuing, your current state would be overwritten by the state
|
||
|
in the plan. Please either update the plan with the latest state or delete
|
||
|
your current state and try again.
|
||
|
|
||
|
"Lineage" is a unique identifier generated only once on the creation of
|
||
|
a new, empty state. If these values differ, it means they were created new
|
||
|
at different times. Therefore, Terraform must assume that they're completely
|
||
|
different states.
|
||
|
|
||
|
The most common cause of seeing this error is using a plan that was
|
||
|
created against a different state. Perhaps the plan is very old and the
|
||
|
state has since been recreated, or perhaps the plan was against a competely
|
||
|
different infrastructure.
|
||
|
`
|
||
|
|
||
|
const errBackendPlanStateFlag = `
|
||
|
The -state and -state-out flags cannot be set with a plan that has a remote
|
||
|
state. The plan itself contains the configuration for the remote backend to
|
||
|
store state. The state will be written there for consistency.
|
||
|
|
||
|
If you wish to change this behavior, please create a plan from local state.
|
||
|
You may use the state flags with plans from local state to affect where
|
||
|
the final state is written.
|
||
|
`
|
||
|
|
||
|
const errBackendPlanOlder = `
|
||
|
This plan was created against an older state than is current. Please create
|
||
|
a new plan file against the latest state and try again.
|
||
|
|
||
|
Terraform doesn't allow you to run plans that were created from older
|
||
|
states since it doesn't properly represent the latest changes Terraform
|
||
|
may have made, and can result in unsafe behavior.
|
||
|
|
||
|
Plan Serial: %[1]d
|
||
|
Current Serial: %[2]d
|
||
|
`
|
||
|
|
||
|
const inputBackendMigrateChange = `
|
||
|
Would you like to copy the state from your prior backend %q to the
|
||
|
newly configured backend %q? If you're reconfiguring the same backend,
|
||
|
answering "yes" or "no" shouldn't make a difference. Please answer exactly
|
||
|
"yes" or "no".
|
||
|
`
|
||
|
|
||
|
const inputBackendMigrateLegacy = `
|
||
|
Terraform can copy the existing state in your legacy remote state
|
||
|
backend to your newly configured backend. Please answer "yes" or "no".
|
||
|
`
|
||
|
|
||
|
const inputBackendMigrateLegacyLocal = `
|
||
|
Terraform can copy the existing state in your legacy remote state
|
||
|
backend to your local state. Please answer "yes" or "no".
|
||
|
`
|
||
|
|
||
|
const inputBackendMigrateLocal = `
|
||
|
Terraform has detected you're unconfiguring your previously set backend.
|
||
|
Would you like to copy the state from %q to local state? Please answer
|
||
|
"yes" or "no". If you answer "no", you will start with a blank local state.
|
||
|
`
|
||
|
|
||
|
const outputBackendConfigureWithLegacy = `
|
||
|
[reset][bold]New backend configuration detected with legacy remote state![reset]
|
||
|
|
||
|
Terraform has detected that you're attempting to configure a new backend.
|
||
|
At the same time, legacy remote state configuration was found. Terraform will
|
||
|
first configure the new backend, and then ask if you'd like to migrate
|
||
|
your remote state to the new backend.
|
||
|
`
|
||
|
|
||
|
const outputBackendReconfigure = `
|
||
|
[reset][bold]Backend configuration changed![reset]
|
||
|
|
||
|
Terraform has detected that the configuration specified for the backend
|
||
|
has changed. Terraform will now reconfigure for this backend. If you didn't
|
||
|
intend to reconfigure your backend please undo any changes to the "backend"
|
||
|
section in your Terraform configuration.
|
||
|
`
|
||
|
|
||
|
const outputBackendSavedWithLegacy = `
|
||
|
[reset][bold]Legacy remote state was detected![reset]
|
||
|
|
||
|
Terraform has detected you still have legacy remote state enabled while
|
||
|
also having a backend configured. Terraform will now ask if you want to
|
||
|
migrate your legacy remote state data to the configured backend.
|
||
|
`
|
||
|
|
||
|
const outputBackendSavedWithLegacyChanged = `
|
||
|
[reset][bold]Legacy remote state was detected while also changing your current backend!reset]
|
||
|
|
||
|
Terraform has detected that you have legacy remote state, a configured
|
||
|
current backend, and you're attempting to reconfigure your backend. To handle
|
||
|
all of these changes, Terraform will first reconfigure your backend. After
|
||
|
this, Terraform will handle optionally copying your legacy remote state
|
||
|
into the newly configured backend.
|
||
|
`
|
||
|
|
||
|
const outputBackendUnsetWithLegacy = `
|
||
|
[reset][bold]Detected a request to unset the backend with legacy remote state present![reset]
|
||
|
|
||
|
Terraform has detected that you're attempting to unset a previously configured
|
||
|
backend (by not having the "backend" configuration set in your Terraform files).
|
||
|
At the same time, legacy remote state was detected. To handle this complex
|
||
|
scenario, Terraform will first unset your configured backend, and then
|
||
|
ask you how to handle the legacy remote state. This will be multi-step
|
||
|
process.
|
||
|
`
|
||
|
|
||
|
const successBackendLegacyUnset = `
|
||
|
Terraform has successfully migrated from legacy remote state to your
|
||
|
configured remote state.
|
||
|
`
|
||
|
|
||
|
const successBackendReconfigureWithLegacy = `
|
||
|
Terraform has successfully reconfigured your backend and migrate
|
||
|
from legacy remote state to the new backend.
|
||
|
`
|
||
|
|
||
|
const successBackendUnset = `
|
||
|
Successfully unset the backend %q. Terraform will now operate locally.
|
||
|
`
|
||
|
|
||
|
const successBackendSet = `
|
||
|
Successfully configured the backend %q! Terraform will automatically
|
||
|
use this backend unless the backend configuration changes.
|
||
|
`
|
||
|
|
||
|
const warnBackendLegacy = `
|
||
|
Deprecation warning: This environment is configured to use legacy remote state.
|
||
|
Remote state changed significantly in Terraform 0.9. Please update your remote
|
||
|
state configuration to use the new 'backend' settings. For now, Terraform
|
||
|
will continue to use your existing settings. Legacy remote state support
|
||
|
will be removed in Terraform 0.11.
|
||
|
`
|