diff --git a/command/meta_backend.go b/command/meta_backend.go new file mode 100644 index 000000000..cc1c4ee4e --- /dev/null +++ b/command/meta_backend.go @@ -0,0 +1,1668 @@ +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. +` diff --git a/command/meta_backend_migrate.go b/command/meta_backend_migrate.go new file mode 100644 index 000000000..0aaf0d3de --- /dev/null +++ b/command/meta_backend_migrate.go @@ -0,0 +1,194 @@ +package command + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/terraform" +) + +// backendMigrateState handles migrating (copying) state from one backend +// to another. This function handles asking the user for confirmation +// as well as the copy itself. +// +// This function can handle all scenarios of state migration regardless +// of the existence of state in either backend. +// +// After migrating the state, the existing state in the first backend +// remains untouched. +func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { + one := opts.One.State() + two := opts.Two.State() + + var confirmFunc func(opts *backendMigrateOpts) (bool, error) + switch { + // No migration necessary + case one.Empty() && two.Empty(): + return nil + + // No migration necessary if we're inheriting state. + case one.Empty() && !two.Empty(): + return nil + + // We have existing state moving into no state. Ask the user if + // they'd like to do this. + case !one.Empty() && two.Empty(): + confirmFunc = m.backendMigrateEmptyConfirm + + // Both states are non-empty, meaning we need to determine which + // state should be used and update accordingly. + case !one.Empty() && !two.Empty(): + confirmFunc = m.backendMigrateNonEmptyConfirm + } + + if confirmFunc == nil { + panic("confirmFunc must not be nil") + } + + // Confirm with the user whether we want to copy state over + confirm, err := confirmFunc(opts) + if err != nil { + return err + } + if !confirm { + return nil + } + + // Confirmed! Write. + if err := opts.Two.WriteState(one); err != nil { + return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), + opts.OneType, opts.TwoType, err) + } + if err := opts.Two.PersistState(); err != nil { + return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), + opts.OneType, opts.TwoType, err) + } + + // And we're done. + return nil +} + +func (m *Meta) backendMigrateEmptyConfirm(opts *backendMigrateOpts) (bool, error) { + inputOpts := &terraform.InputOpts{ + Id: "backend-migrate-to-backend", + Query: fmt.Sprintf( + "Do you want to copy state from %q to %q?", + opts.OneType, opts.TwoType), + Description: fmt.Sprintf( + strings.TrimSpace(inputBackendMigrateEmpty), + opts.OneType, opts.TwoType), + } + + // Confirm with the user that the copy should occur + for { + v, err := m.UIInput().Input(inputOpts) + if err != nil { + return false, fmt.Errorf( + "Error asking for state copy action: %s", err) + } + + switch strings.ToLower(v) { + case "no": + return false, nil + + case "yes": + return true, nil + } + } +} + +func (m *Meta) backendMigrateNonEmptyConfirm(opts *backendMigrateOpts) (bool, error) { + // We need to grab both states so we can write them to a file + one := opts.One.State() + two := opts.Two.State() + + // Save both to a temporary + td, err := ioutil.TempDir("", "terraform") + if err != nil { + return false, fmt.Errorf("Error creating temporary directory: %s", err) + } + defer os.RemoveAll(td) + + // Helper to write the state + saveHelper := func(n, path string, s *terraform.State) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + return terraform.WriteState(s, f) + } + + // Write the states + onePath := filepath.Join(td, fmt.Sprintf("1-%s.tfstate", opts.OneType)) + twoPath := filepath.Join(td, fmt.Sprintf("2-%s.tfstate", opts.TwoType)) + if err := saveHelper(opts.OneType, onePath, one); err != nil { + return false, fmt.Errorf("Error saving temporary state: %s", err) + } + if err := saveHelper(opts.TwoType, twoPath, two); err != nil { + return false, fmt.Errorf("Error saving temporary state: %s", err) + } + + // Ask for confirmation + inputOpts := &terraform.InputOpts{ + Id: "backend-migrate-to-backend", + Query: fmt.Sprintf( + "Do you want to copy state from %q to %q?", + opts.OneType, opts.TwoType), + Description: fmt.Sprintf( + strings.TrimSpace(inputBackendMigrateNonEmpty), + opts.OneType, opts.TwoType, onePath, twoPath), + } + + // Confirm with the user that the copy should occur + for { + v, err := m.UIInput().Input(inputOpts) + if err != nil { + return false, fmt.Errorf( + "Error asking for state copy action: %s", err) + } + + switch strings.ToLower(v) { + case "no": + return false, nil + + case "yes": + return true, nil + } + } +} + +type backendMigrateOpts struct { + OneType, TwoType string + One, Two state.State +} + +const errBackendStateCopy = ` +Error copying state from %q to %q: %s + +The state in %[1]q remains intact and unmodified. Please resolve the +error above and try again. +` + +const inputBackendMigrateEmpty = ` +Pre-existing state was found in %q while migrating to %q. No existing +state was found in %[2]q. Do you want to copy the state from %[1]q to +%[2]q? Enter "yes" to copy and "no" to start with an empty state. +` + +const inputBackendMigrateNonEmpty = ` +Pre-existing state was found in %q while migrating to %q. An existing +non-empty state exists in %[2]q. The two states have been saved to temporary +files that will be removed after responding to this query. + +One (%[1]q): %[3]s +Two (%[2]q): %[4]s + +Do you want to copy the state from %[1]q to %[2]q? Enter "yes" to copy +and "no" to start with the existing state in %[2]q. +` diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go new file mode 100644 index 000000000..89456e38f --- /dev/null +++ b/command/meta_backend_test.go @@ -0,0 +1,2816 @@ +package command + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/copy" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +// Test empty directory with no config/state creates a local state. +func TestMetaBackend_emptyDir(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + os.MkdirAll(td, 0755) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Get the backend + m := testMetaBackend(t, nil) + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Write some state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + s.WriteState(testState()) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify it exists where we expect it to + if _, err := os.Stat(DefaultStateFilename); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify no backup since it was empty to start + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no backend state was made + if _, err := os.Stat(filepath.Join(m.DataDir(), DefaultStateFilename)); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Test a directory with a legacy state and no config continues to +// use the legacy state. +func TestMetaBackend_emptyWithDefaultState(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + os.MkdirAll(td, 0755) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Write the legacy state + statePath := DefaultStateFilename + { + f, err := os.Create(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + err = terraform.WriteState(testState(), f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + } + + // Get the backend + m := testMetaBackend(t, nil) + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if actual := s.State().String(); actual != testState().String() { + t.Fatalf("bad: %s", actual) + } + + // Verify it exists where we expect it to + if _, err := os.Stat(DefaultStateFilename); err != nil { + t.Fatalf("err: %s", err) + } + if _, err := os.Stat(filepath.Join(m.DataDir(), DefaultStateFilename)); err == nil { + t.Fatalf("err: %s", err) + } + + // Write some state + next := testState() + next.Modules[0].Outputs["foo"] = &terraform.OutputState{Value: "bar"} + s.WriteState(testState()) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify a backup was made since we're modifying a pre-existing state + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// Test an empty directory with an explicit state path (outside the dir) +func TestMetaBackend_emptyWithExplicitState(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + os.MkdirAll(td, 0755) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Create another directory to store our state + stateDir := tempDir(t) + os.MkdirAll(stateDir, 0755) + defer os.RemoveAll(stateDir) + + // Write the legacy state + statePath := filepath.Join(stateDir, "foo") + { + f, err := os.Create(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + err = terraform.WriteState(testState(), f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + } + + // Setup the meta + m := testMetaBackend(t, nil) + m.statePath = statePath + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if actual := s.State().String(); actual != testState().String() { + t.Fatalf("bad: %s", actual) + } + + // Verify neither defaults exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + if _, err := os.Stat(filepath.Join(m.DataDir(), DefaultStateFilename)); err == nil { + t.Fatalf("err: %s", err) + } + + // Write some state + next := testState() + next.Modules[0].Outputs["foo"] = &terraform.OutputState{Value: "bar"} + s.WriteState(testState()) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify a backup was made since we're modifying a pre-existing state + if _, err := os.Stat(statePath + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// Empty directory with legacy remote state +func TestMetaBackend_emptyLegacyRemote(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + os.MkdirAll(td, 0755) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Create some legacy remote state + legacyState := testState() + _, srv := testRemoteState(t, legacyState, 200) + defer srv.Close() + statePath := testStateFileRemote(t, legacyState) + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if actual := state.String(); actual != legacyState.String() { + t.Fatalf("bad: %s", actual) + } + + // Verify we didn't setup the backend state + if !state.Backend.Empty() { + t.Fatal("shouldn't configure backend") + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + if _, err := os.Stat(statePath + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Newly configured backend +func TestMetaBackend_configureNew(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-new"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state != nil { + t.Fatal("state should be nil") + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Newly configured backend with prior local state and no remote state +func TestMetaBackend_configureNewWithState(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-new-migrate"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"yes"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "backend-new-migrate" { + t.Fatalf("bad: %#v", state) + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup does exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// Newly configured backend with prior local state and no remote state, +// but opting to not migrate. +func TestMetaBackend_configureNewWithStateNoMigrate(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-new-migrate"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"no"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + if state := s.State(); state != nil { + t.Fatal("state is not nil") + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup does exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// Newly configured backend with prior local state and remote state +func TestMetaBackend_configureNewWithStateExisting(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-new-migrate-existing"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"yes"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "local" { + t.Fatalf("bad: %#v", state) + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup does exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// Newly configured backend with prior local state and remote state +func TestMetaBackend_configureNewWithStateExistingNoMigrate(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-new-migrate-existing"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"no"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "remote" { + t.Fatalf("bad: %#v", state) + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup does exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// Newly configured backend with lgacy +func TestMetaBackend_configureNewLegacy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-new-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"no"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state != nil { + t.Fatal("state should be nil") + } + + // Verify we have no configured legacy + { + path := filepath.Join(m.DataDir(), DefaultStateFilename) + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Newly configured backend with legacy +func TestMetaBackend_configureNewLegacyCopy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-new-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"yes", "yes"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("nil state") + } + if state.Lineage != "backend-new-legacy" { + t.Fatalf("bad: %#v", state) + } + + // Verify we have no configured legacy + { + path := filepath.Join(m.DataDir(), DefaultStateFilename) + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Saved backend state matching config +func TestMetaBackend_configuredUnchanged(t *testing.T) { + defer testChdir(t, testFixturePath("backend-unchanged"))() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("nil state") + } + if state.Lineage != "configuredUnchanged" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Changing a configured backend +func TestMetaBackend_configuredChange(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-change"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"no"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state != nil { + t.Fatal("state should be nil") + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state-2.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no local state + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Changing a configured backend, copying state +func TestMetaBackend_configuredChangeCopy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-change"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"yes", "yes"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state should not be nil") + } + if state.Lineage != "backend-change" { + t.Fatalf("bad: %#v", state) + } + + // Verify no local state + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Unsetting a saved backend +func TestMetaBackend_configuredUnset(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-unset"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"no"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state != nil { + t.Fatal("state should be nil") + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if !actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + s.WriteState(testState()) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify it exists where we expect it to + if _, err := os.Stat(DefaultStateFilename); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify no backup since it was empty to start + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Unsetting a saved backend and copying the remote state +func TestMetaBackend_configuredUnsetCopy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-unset"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"yes", "yes"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "configuredUnset" { + t.Fatalf("bad: %#v", state) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if !actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + s.WriteState(testState()) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify it exists where we expect it to + if _, err := os.Stat(DefaultStateFilename); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup since it wasn't empty to start + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// Saved backend state matching config, with legacy +func TestMetaBackend_configuredUnchangedLegacy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-unchanged-with-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"no"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "configured" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured legacy + { + path := filepath.Join(m.DataDir(), DefaultStateFilename) + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no local state + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Saved backend state matching config, with legacy +func TestMetaBackend_configuredUnchangedLegacyCopy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-unchanged-with-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"yes", "yes"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "backend-unchanged-with-legacy" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured legacy + { + path := filepath.Join(m.DataDir(), DefaultStateFilename) + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no local state + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Saved backend state, new config, legacy remote state +func TestMetaBackend_configuredChangedLegacy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-changed-with-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"no", "no"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state != nil { + t.Fatal("state should be nil") + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured legacy + { + path := filepath.Join(m.DataDir(), DefaultStateFilename) + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state-2.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no local state + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Saved backend state, new config, legacy remote state +func TestMetaBackend_configuredChangedLegacyCopyBackend(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-changed-with-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"yes", "yes", "no"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "configured" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured legacy + { + path := filepath.Join(m.DataDir(), DefaultStateFilename) + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state-2.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no local state + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Saved backend state, new config, legacy remote state +func TestMetaBackend_configuredChangedLegacyCopyLegacy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-changed-with-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"no", "yes", "yes"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "legacy" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured legacy + { + path := filepath.Join(m.DataDir(), DefaultStateFilename) + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state-2.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no local state + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Saved backend state, new config, legacy remote state +func TestMetaBackend_configuredChangedLegacyCopyBoth(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-changed-with-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"yes", "yes", "yes", "yes"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "legacy" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default paths don't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured legacy + { + path := filepath.Join(m.DataDir(), DefaultStateFilename) + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state-2.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no local state + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// Saved backend state, unset config, legacy remote state +func TestMetaBackend_configuredUnsetWithLegacyNoCopy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-unset-with-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"no", "no"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state != nil { + t.Fatal("state should be nil") + } + + // Verify the default paths dont exist since we had no state + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if !actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } +} + +// Saved backend state, unset config, legacy remote state +func TestMetaBackend_configuredUnsetWithLegacyCopyBackend(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-unset-with-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"yes", "yes", "no"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "backend" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default paths exist + if _, err := os.Stat(DefaultStateFilename); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if !actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify a local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// Saved backend state, unset config, legacy remote state +func TestMetaBackend_configuredUnsetWithLegacyCopyLegacy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-unset-with-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"no", "yes", "yes"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "legacy" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default paths exist + if _, err := os.Stat(DefaultStateFilename); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if !actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify a local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// Saved backend state, unset config, legacy remote state +func TestMetaBackend_configuredUnsetWithLegacyCopyBoth(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-unset-with-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Ask input + defer testInteractiveInput(t, []string{"yes", "yes", "yes", "yes"})() + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Init: true}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "legacy" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default paths exist + if _, err := os.Stat(DefaultStateFilename); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup exists + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !actual.Remote.Empty() { + t.Fatalf("bad: %#v", actual) + } + if !actual.Backend.Empty() { + t.Fatalf("bad: %#v", actual) + } + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify a local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// A plan that has no backend config +func TestMetaBackend_planLocal(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-plan-local"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Create the plan + plan := &terraform.Plan{ + Module: testModule(t, "backend-plan-local"), + State: nil, + } + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Plan: plan}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state != nil { + t.Fatalf("state should be nil: %#v", state) + } + + // Verify the default path + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exists + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + t.Fatalf("should not have backend configured") + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// A plan with a custom state save path +func TestMetaBackend_planLocalStatePath(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-plan-local"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Create our state + original := testState() + original.Lineage = "hello" + + // Create the plan + plan := &terraform.Plan{ + Module: testModule(t, "backend-plan-local"), + State: original, + } + + // Create an alternate output path + statePath := "foo.tfstate" + + // Setup the meta + m := testMetaBackend(t, nil) + m.stateOutPath = statePath + + // Get the backend + b, err := m.Backend(&BackendOpts{Plan: plan}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("state is nil") + } + if state.Lineage != "hello" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default path doesn't exist + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exists + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + t.Fatalf("should not have backend configured") + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify we have a backup + if _, err := os.Stat(statePath + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// A plan that has no backend config, matching local state +func TestMetaBackend_planLocalMatch(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-plan-local-match"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Create the plan + plan := &terraform.Plan{ + Module: testModule(t, "backend-plan-local-match"), + State: testStateRead(t, DefaultStateFilename), + } + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Plan: plan}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("should is nil") + } + if state.Lineage != "hello" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default path + if _, err := os.Stat(DefaultStateFilename); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup exists + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + t.Fatalf("should not have backend configured") + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open(DefaultStateFilename) + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil { + t.Fatalf("err: %s", err) + } +} + +// A plan that has no backend config, mismatched lineage +func TestMetaBackend_planLocalMismatchLineage(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-plan-local-mismatch-lineage"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Save the original + original := testStateRead(t, DefaultStateFilename) + + // Change the lineage + planState := testStateRead(t, DefaultStateFilename) + planState.Lineage = "bad" + + // Create the plan + plan := &terraform.Plan{ + Module: testModule(t, "backend-plan-local-mismatch-lineage"), + State: planState, + } + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + _, err := m.Backend(&BackendOpts{Plan: plan}) + if err == nil { + t.Fatal("should have error") + } + if !strings.Contains(err.Error(), "lineage") { + t.Fatalf("bad: %s", err) + } + + // Verify our local state didn't change + actual := testStateRead(t, DefaultStateFilename) + if !actual.Equal(original) { + t.Fatalf("bad: %#v", actual) + } + + // Verify a backup doesn't exists + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + t.Fatalf("should not have backend configured") + } +} + +// A plan that has no backend config, newer local +func TestMetaBackend_planLocalNewer(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-plan-local-newer"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Save the original + original := testStateRead(t, DefaultStateFilename) + + // Change the serial + planState := testStateRead(t, DefaultStateFilename) + planState.Serial = 7 + planState.RootModule().Dependencies = []string{"foo"} + + // Create the plan + plan := &terraform.Plan{ + Module: testModule(t, "backend-plan-local-newer"), + State: planState, + } + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + _, err := m.Backend(&BackendOpts{Plan: plan}) + if err == nil { + t.Fatal("should have error") + } + if !strings.Contains(err.Error(), "older") { + t.Fatalf("bad: %s", err) + } + + // Verify our local state didn't change + actual := testStateRead(t, DefaultStateFilename) + if !actual.Equal(original) { + t.Fatalf("bad: %#v", actual) + } + + // Verify a backup doesn't exists + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + t.Fatalf("should not have backend configured") + } +} + +// A plan that has a backend in an empty dir +func TestMetaBackend_planBackendEmptyDir(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-plan-backend-empty"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Get the state for the plan by getting the real state and + // adding the backend config to it. + original := testStateRead(t, filepath.Join( + testFixturePath("backend-plan-backend-empty-config"), + "local-state.tfstate")) + backendState := testStateRead(t, filepath.Join( + testFixturePath("backend-plan-backend-empty-config"), + DefaultDataDir, DefaultStateFilename)) + planState := original.DeepCopy() + planState.Backend = backendState.Backend + + // Create the plan + plan := &terraform.Plan{ + Module: testModule(t, "backend-plan-backend-empty-config"), + State: planState, + } + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Plan: plan}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("should is nil") + } + if state.Lineage != "hello" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default path + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup exists + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + t.Fatalf("should not have backend configured") + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no default path + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// A plan that has a backend with matching state +func TestMetaBackend_planBackendMatch(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-plan-backend-match"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Get the state for the plan by getting the real state and + // adding the backend config to it. + original := testStateRead(t, filepath.Join( + testFixturePath("backend-plan-backend-empty-config"), + "local-state.tfstate")) + backendState := testStateRead(t, filepath.Join( + testFixturePath("backend-plan-backend-empty-config"), + DefaultDataDir, DefaultStateFilename)) + planState := original.DeepCopy() + planState.Backend = backendState.Backend + + // Create the plan + plan := &terraform.Plan{ + Module: testModule(t, "backend-plan-backend-empty-config"), + State: planState, + } + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Plan: plan}) + if err != nil { + t.Fatalf("bad: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("should is nil") + } + if state.Lineage != "hello" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default path + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup exists + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + t.Fatalf("should not have backend configured") + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no default path + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +// A plan that has a backend with mismatching lineage +func TestMetaBackend_planBackendMismatchLineage(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-plan-backend-mismatch"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Get the state for the plan by getting the real state and + // adding the backend config to it. + original := testStateRead(t, filepath.Join( + testFixturePath("backend-plan-backend-empty-config"), + "local-state.tfstate")) + backendState := testStateRead(t, filepath.Join( + testFixturePath("backend-plan-backend-empty-config"), + DefaultDataDir, DefaultStateFilename)) + planState := original.DeepCopy() + planState.Backend = backendState.Backend + + // Get the real original + original = testStateRead(t, "local-state.tfstate") + + // Create the plan + plan := &terraform.Plan{ + Module: testModule(t, "backend-plan-backend-empty-config"), + State: planState, + } + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + _, err := m.Backend(&BackendOpts{Plan: plan}) + if err == nil { + t.Fatal("should have error") + } + if !strings.Contains(err.Error(), "lineage") { + t.Fatalf("bad: %s", err) + } + + // Verify our local state didn't change + actual := testStateRead(t, "local-state.tfstate") + if !actual.Equal(original) { + t.Fatalf("bad: %#v", actual) + } + + // Verify a backup doesn't exists + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + t.Fatalf("should not have backend configured") + } + + // Verify we have no default state + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } +} + +// A plan that has a legacy remote state +func TestMetaBackend_planLegacy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-plan-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Get the state for the plan by getting the real state and + // adding the backend config to it. + original := testStateRead(t, filepath.Join( + testFixturePath("backend-plan-legacy-data"), "local-state.tfstate")) + dataState := testStateRead(t, filepath.Join( + testFixturePath("backend-plan-legacy-data"), "state.tfstate")) + planState := original.DeepCopy() + planState.Remote = dataState.Remote + + // Create the plan + plan := &terraform.Plan{ + Module: testModule(t, "backend-plan-legacy-data"), + State: planState, + } + + // Setup the meta + m := testMetaBackend(t, nil) + + // Get the backend + b, err := m.Backend(&BackendOpts{Plan: plan}) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Check the state + s, err := b.State() + if err != nil { + t.Fatalf("bad: %s", err) + } + if err := s.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + state := s.State() + if state == nil { + t.Fatal("should is nil") + } + if state.Lineage != "hello" { + t.Fatalf("bad: %#v", state) + } + + // Verify the default path + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify a backup doesn't exist + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify we have no configured backend/legacy + path := filepath.Join(m.DataDir(), DefaultStateFilename) + if _, err := os.Stat(path); err == nil { + t.Fatalf("should not have backend configured") + } + + // Write some state + state = terraform.NewState() + state.Lineage = "changing" + s.WriteState(state) + if err := s.PersistState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Verify the state is where we expect + { + f, err := os.Open("local-state.tfstate") + if err != nil { + t.Fatalf("err: %s", err) + } + actual, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual.Lineage != state.Lineage { + t.Fatalf("bad: %#v", actual) + } + } + + // Verify no default path + if _, err := os.Stat(DefaultStateFilename); err == nil { + t.Fatalf("err: %s", err) + } + + // Verify no local backup + if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { + t.Fatalf("err: %s", err) + } +} + +func testMetaBackend(t *testing.T, args []string) *Meta { + var m Meta + m.Ui = new(cli.MockUi) + m.process(args, true) + f := m.flagSet("test") + if err := f.Parse(args); err != nil { + t.Fatalf("bad: %s", err) + } + + return &m +}