terraform/command/meta_backend.go

1753 lines
52 KiB
Go

package command
// This file contains all the Backend-related function calls on Meta,
// exported and private.
import (
"context"
"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/command/clistate"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
backendinit "github.com/hashicorp/terraform/backend/init"
backendlocal "github.com/hashicorp/terraform/backend/local"
)
// 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
// ConfigExtra is extra configuration to merge into the backend
// configuration after the extra file above.
ConfigExtra map[string]interface{}
// 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{}
}
// 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)
}
// Setup the CLI opts we pass into backends that support it
cliOpts := &backend.CLIOpts{
CLI: m.Ui,
CLIColor: m.Colorize(),
StatePath: m.statePath,
StateOutPath: m.stateOutPath,
StateBackupPath: m.backupPath,
ContextOpts: m.contextOpts(),
Input: m.Input(),
}
// Don't validate if we have a plan. Validation is normally harmless here,
// but validation requires interpolation, and `file()` function calls may
// not have the original files in the current execution context.
cliOpts.Validation = opts.Plan == nil
// If the backend supports CLI initialization, do it.
if cli, ok := b.(backend.CLI); ok {
if err := cli.CLIInit(cliOpts); err != nil {
return nil, fmt.Errorf(
"Error initializing backend %T: %s\n\n"+
"This is a bug, please report it to the backend developer",
b, err)
}
}
// 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
local := &backendlocal.Local{Backend: b}
if err := local.CLIInit(cliOpts); err != nil {
// Local backend isn't allowed to fail. It would be a bug.
panic(err)
}
return local, nil
}
// IsLocalBackend returns true if the backend is a local backend. We use this
// for some checks that require a remote backend.
func (m *Meta) IsLocalBackend(b backend.Backend) bool {
// Is it a local backend?
bLocal, ok := b.(*backendlocal.Local)
// If it is, does it not have an alternate state backend?
if ok {
ok = bLocal.Backend == nil
}
return ok
}
// 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(),
Environment: m.Env(),
StateLockTimeout: m.stateLockTimeout,
}
}
// 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)
}
// If we have extra config values, merge that
if len(opts.ConfigExtra) > 0 {
log.Printf(
"[DEBUG] command: adding extra backend config from CLI")
rc, err := config.NewRawConfig(opts.ConfigExtra)
if err != nil {
return nil, fmt.Errorf(
"Error adding extra configuration file for backend: %s", err)
}
// Merge in the configuration
backend.RawConfig = backend.RawConfig.Merge(rc)
}
// Validate the backend early. We have to do this before the normal
// config validation pass since backend loading happens earlier.
if errs := backend.Validate(); len(errs) > 0 {
return nil, multierror.Append(nil, errs...)
}
// 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)
}
// cHash defaults to zero unless c is set
var cHash uint64
if c != nil {
// We need to rehash to get the value since we may have merged the
// config with an extra ConfigFile. We don't do this when merging
// because we do want the ORIGINAL value on c so that we store
// that to not detect drift. This is covered in tests.
cHash = c.Rehash()
}
// 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() {
hash := s.Backend.Hash
// on init we need an updated hash containing any extra options
// that were added after merging.
if opts.Init {
hash = s.Backend.Rehash()
}
if hash == cHash {
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, cHash)
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.
hash := s.Backend.Hash
if opts.Init {
hash = s.Backend.Rehash()
}
if hash == cHash {
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.")
}
planBackend := opts.Plan.Backend
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() && planBackend.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() || !planBackend.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() && planBackend.Empty():
log.Printf("[INFO] command: initializing local backend from plan (not set)")
// Get the local backend
b, err = m.Backend(&BackendOpts{ForceLocal: true})
// New backend configuration set
case planState.Remote.Empty() && !planBackend.Empty():
log.Printf(
"[INFO] command: initializing backend from plan: %s",
planBackend.Type)
b, err = m.backendInitFromSaved(planBackend)
// Legacy remote state set
case !planState.Remote.Empty() && planBackend.Empty():
log.Printf(
"[INFO] command: initializing legacy remote backend from plan: %s",
planState.Remote.Type)
// 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() && !planBackend.Empty():
return nil, fmt.Errorf(strings.TrimSpace(errBackendPlanBoth))
}
// If we had an error, return that
if err != nil {
return nil, err
}
env := m.Env()
// Get the state so we can determine the effect of using this plan
realMgr, err := b.State(env)
if err != nil {
return nil, fmt.Errorf("Error reading state: %s", err)
}
if m.stateLock {
lockCtx, cancel := context.WithTimeout(context.Background(), m.stateLockTimeout)
defer cancel()
// Lock the state if we can
lockInfo := state.NewLockInfo()
lockInfo.Operation = "backend from plan"
lockID, err := clistate.Lock(lockCtx, realMgr, lockInfo, m.Ui, m.Colorize())
if err != nil {
return nil, fmt.Errorf("Error locking state: %s", err)
}
defer clistate.Unlock(realMgr, lockID, m.Ui, m.Colorize())
}
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
}
// realMgr locked above
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
copy := m.forceInitCopy
if !copy {
var err error
// 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)
}
// 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)
}
// Perform the migration
err = m.backendMigrateState(&backendMigrateOpts{
OneType: s.Backend.Type,
TwoType: "local",
One: b,
Two: localB,
})
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)
// Get the backend
f := backendinit.Backend(s.Remote.Type)
if f == nil {
return nil, fmt.Errorf(strings.TrimSpace(errBackendLegacyUnknown), s.Remote.Type)
}
b := f()
// 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)
}
// Grab the state
s := sMgr.State()
// Ask the user if they want to migrate their existing remote state
copy := m.forceInitCopy
if !copy {
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
}
// Perform the migration
err = m.backendMigrateState(&backendMigrateOpts{
OneType: s.Remote.Type,
TwoType: "local",
One: oldB,
Two: localB,
})
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 := m.forceInitCopy
if !copy {
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
}
// Perform the migration
err = m.backendMigrateState(&backendMigrateOpts{
OneType: s.Remote.Type,
TwoType: c.Type,
One: oldB,
Two: b,
})
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)
}
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)
}
// 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() {
// Perform the migration
err = m.backendMigrateState(&backendMigrateOpts{
OneType: "local",
TwoType: c.Type,
One: localB,
Two: b,
})
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)
}
}
if m.stateLock {
lockCtx, cancel := context.WithTimeout(context.Background(), m.stateLockTimeout)
defer cancel()
// Lock the state if we can
lockInfo := state.NewLockInfo()
lockInfo.Operation = "backend from config"
lockID, err := clistate.Lock(lockCtx, sMgr, lockInfo, m.Ui, m.Colorize())
if err != nil {
return nil, fmt.Errorf("Error locking state: %s", err)
}
defer clistate.Unlock(sMgr, lockID, m.Ui, m.Colorize())
}
// 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 old state
s := sMgr.State()
// 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 := m.forceInitCopy
if !copy {
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(fmt.Sprintf(inputBackendMigrateChange, c.Type, s.Backend.Type)),
})
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)
}
// Perform the migration
err = m.backendMigrateState(&backendMigrateOpts{
OneType: s.Backend.Type,
TwoType: c.Type,
One: oldB,
Two: b,
})
if err != nil {
return nil, err
}
}
if m.stateLock {
lockCtx, cancel := context.WithTimeout(context.Background(), m.stateLockTimeout)
defer cancel()
// Lock the state if we can
lockInfo := state.NewLockInfo()
lockInfo.Operation = "backend from config"
lockID, err := clistate.Lock(lockCtx, sMgr, lockInfo, m.Ui, m.Colorize())
if err != nil {
return nil, fmt.Errorf("Error locking state: %s", err)
}
defer clistate.Unlock(sMgr, lockID, m.Ui, m.Colorize())
}
// 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()
// it's possible for a backend to be unchanged, and the config itself to
// have changed by moving a paramter from the config to `-backend-config`
// In this case we only need to update the Hash.
if c != nil && s.Backend.Hash != c.Hash {
s.Backend.Hash = c.Hash
if err := sMgr.WriteState(s); err != nil {
return nil, fmt.Errorf(errBackendWriteSaved, err)
}
}
// 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 := backendinit.Backend(s.Backend.Type)
if f == nil {
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 := m.forceInitCopy
if !copy {
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
}
// Perform the migration
err = m.backendMigrateState(&backendMigrateOpts{
OneType: s.Remote.Type,
TwoType: s.Backend.Type,
One: oldB,
Two: b,
})
if err != nil {
return nil, err
}
}
if m.stateLock {
lockCtx, cancel := context.WithTimeout(context.Background(), m.stateLockTimeout)
defer cancel()
// Lock the state if we can
lockInfo := state.NewLockInfo()
lockInfo.Operation = "backend from config"
lockID, err := clistate.Lock(lockCtx, sMgr, lockInfo, m.Ui, m.Colorize())
if err != nil {
return nil, fmt.Errorf("Error locking state: %s", err)
}
defer clistate.Unlock(sMgr, lockID, m.Ui, m.Colorize())
}
// 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 := backendinit.Backend(c.Type)
if f == nil {
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)
// Get the backend
f := backendinit.Backend(s.Type)
if f == nil {
return nil, fmt.Errorf(strings.TrimSpace(errBackendLegacyUnknown), s.Type)
}
b := f()
// 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 := backendinit.Backend(s.Type)
if f == nil {
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
//-------------------------------------------------------------------
// 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:
https://www.terraform.io/docs/backends/index.html
The error(s) configuring the legacy remote state:
%s
`
const errBackendLegacyUnknown = `
The legacy remote state type %q could not be found.
Terraform 0.9.0 shipped with backwards compatibility for all built-in
legacy remote state types. This error may mean that you were using a
custom Terraform build that perhaps supported a different type of
remote state.
Please check with the creator of the remote state above and try again.
`
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
determine 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 %q backend? 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.
You can find a guide for upgrading here:
https://www.terraform.io/docs/backends/legacy-0-8.html
`