Merge pull request #28718 from hashicorp/jbardin/backend-migrate

Prevent automatic backend migration during `terraform init`
This commit is contained in:
James Bardin 2021-05-17 14:42:35 -04:00 committed by GitHub
commit f5e0d13079
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 87 additions and 40 deletions

View File

@ -45,6 +45,7 @@ func (c *InitCommand) Run(args []string) int {
cmdFlags.BoolVar(&flagGet, "get", true, "") cmdFlags.BoolVar(&flagGet, "get", true, "")
cmdFlags.BoolVar(&c.forceInitCopy, "force-copy", false, "suppress prompts about copying state data") cmdFlags.BoolVar(&c.forceInitCopy, "force-copy", false, "suppress prompts about copying state data")
cmdFlags.BoolVar(&c.reconfigure, "reconfigure", false, "reconfigure") cmdFlags.BoolVar(&c.reconfigure, "reconfigure", false, "reconfigure")
cmdFlags.BoolVar(&c.migrateState, "migrate-state", false, "migrate state")
cmdFlags.BoolVar(&flagUpgrade, "upgrade", false, "") cmdFlags.BoolVar(&flagUpgrade, "upgrade", false, "")
cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory") cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory")
cmdFlags.StringVar(&flagLockfile, "lockfile", "", "Set a dependency lockfile mode") cmdFlags.StringVar(&flagLockfile, "lockfile", "", "Set a dependency lockfile mode")
@ -53,6 +54,17 @@ func (c *InitCommand) Run(args []string) int {
return 1 return 1
} }
if c.migrateState && c.reconfigure {
c.Ui.Error("The -migrate-state and -reconfigure options are mutually-exclusive")
return 1
}
// Copying the state only happens during backend migration, so setting
// -force-copy implies -migrate-state
if c.forceInitCopy {
c.migrateState = true
}
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
if len(flagPluginPath) > 0 { if len(flagPluginPath) > 0 {
@ -926,6 +938,7 @@ func (c *InitCommand) AutocompleteFlags() complete.Flags {
"-no-color": complete.PredictNothing, "-no-color": complete.PredictNothing,
"-plugin-dir": complete.PredictDirs(""), "-plugin-dir": complete.PredictDirs(""),
"-reconfigure": complete.PredictNothing, "-reconfigure": complete.PredictNothing,
"-migrate-state": complete.PredictNothing,
"-upgrade": completePredictBoolean, "-upgrade": completePredictBoolean,
} }
} }
@ -980,6 +993,9 @@ Options:
-reconfigure Reconfigure the backend, ignoring any saved -reconfigure Reconfigure the backend, ignoring any saved
configuration. configuration.
-migrate-state Reconfigure the backend, and attempt to migrate any
existing state.
-upgrade=false If installing modules (-get) or plugins, ignore -upgrade=false If installing modules (-get) or plugins, ignore
previously-downloaded objects and install the previously-downloaded objects and install the
latest version allowed within configured constraints. latest version allowed within configured constraints.

View File

@ -412,7 +412,7 @@ func TestInit_backendConfigFile(t *testing.T) {
View: view, View: view,
}, },
} }
args := []string{"-backend-config="} args := []string{"-backend-config=", "-migrate-state"}
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
} }
@ -555,7 +555,7 @@ func TestInit_backendConfigFileChange(t *testing.T) {
}, },
} }
args := []string{"-backend-config", "input.config"} args := []string{"-backend-config", "input.config", "-migrate-state"}
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
} }
@ -644,7 +644,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) {
} }
// override the -backend-config options by settings // override the -backend-config options by settings
args = []string{"-input=false", "-backend-config", ""} args = []string{"-input=false", "-backend-config", "", "-migrate-state"}
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
} }
@ -906,7 +906,7 @@ func TestInit_inputFalse(t *testing.T) {
}, },
} }
args = []string{"-input=false", "-backend-config=path=bar"} args = []string{"-input=false", "-backend-config=path=bar", "-migrate-state"}
if code := c.Run(args); code == 0 { if code := c.Run(args); code == 0 {
t.Fatal("init should have failed", ui.OutputWriter) t.Fatal("init should have failed", ui.OutputWriter)
} }

View File

@ -204,6 +204,9 @@ type Meta struct {
// //
// reconfigure forces init to ignore any stored configuration. // reconfigure forces init to ignore any stored configuration.
// //
// migrateState confirms the user wishes to migrate from the prior backend
// configuration to a new configuration.
//
// compactWarnings (-compact-warnings) selects a more compact presentation // compactWarnings (-compact-warnings) selects a more compact presentation
// of warnings in the output when they are not accompanied by errors. // of warnings in the output when they are not accompanied by errors.
statePath string statePath string
@ -214,6 +217,7 @@ type Meta struct {
stateLockTimeout time.Duration stateLockTimeout time.Duration
forceInitCopy bool forceInitCopy bool
reconfigure bool reconfigure bool
migrateState bool
compactWarnings bool compactWarnings bool
// Used with the import command to allow import of state when no matching config exists. // Used with the import command to allow import of state when no matching config exists.

View File

@ -6,7 +6,6 @@ package command
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log" "log"
"path/filepath" "path/filepath"
@ -514,12 +513,19 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
// We're unsetting a backend (moving from backend => local) // We're unsetting a backend (moving from backend => local)
case c == nil && !s.Backend.Empty(): case c == nil && !s.Backend.Empty():
log.Printf("[TRACE] Meta.Backend: previously-initialized %q backend is no longer present in config", s.Backend.Type) log.Printf("[TRACE] Meta.Backend: previously-initialized %q backend is no longer present in config", s.Backend.Type)
initReason := fmt.Sprintf("Unsetting the previously set backend %q", s.Backend.Type)
if !opts.Init { if !opts.Init {
initReason := fmt.Sprintf( diags = diags.Append(tfdiags.Sourceless(
"Unsetting the previously set backend %q", tfdiags.Error,
s.Backend.Type) "Backend initialization required, please run \"terraform init\"",
m.backendInitRequired(initReason) fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason),
diags = diags.Append(errBackendInitRequired) ))
return nil, diags
}
if !m.migrateState {
diags = diags.Append(migrateOrReconfigDiag)
return nil, diags return nil, diags
} }
@ -529,11 +535,12 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
case c != nil && s.Backend.Empty(): case c != nil && s.Backend.Empty():
log.Printf("[TRACE] Meta.Backend: moving from default local state only to %q backend", c.Type) log.Printf("[TRACE] Meta.Backend: moving from default local state only to %q backend", c.Type)
if !opts.Init { if !opts.Init {
initReason := fmt.Sprintf( initReason := fmt.Sprintf("Initial configuration of the requested backend %q", c.Type)
"Initial configuration of the requested backend %q", diags = diags.Append(tfdiags.Sourceless(
c.Type) tfdiags.Error,
m.backendInitRequired(initReason) "Backend initialization required, please run \"terraform init\"",
diags = diags.Append(errBackendInitRequired) fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason),
))
return nil, diags return nil, diags
} }
@ -558,12 +565,22 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
} }
log.Printf("[TRACE] Meta.Backend: backend configuration has changed (from type %q to type %q)", s.Backend.Type, c.Type) log.Printf("[TRACE] Meta.Backend: backend configuration has changed (from type %q to type %q)", s.Backend.Type, c.Type)
initReason := fmt.Sprintf("Backend configuration changed for %q", c.Type)
if s.Backend.Type != c.Type {
initReason = fmt.Sprintf("Backend configuration changed from %q to %q", s.Backend.Type, c.Type)
}
if !opts.Init { if !opts.Init {
initReason := fmt.Sprintf( diags = diags.Append(tfdiags.Sourceless(
"Backend configuration changed for %q", tfdiags.Error,
c.Type) "Backend initialization required, please run \"terraform init\"",
m.backendInitRequired(initReason) fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason),
diags = diags.Append(errBackendInitRequired) ))
return nil, diags
}
if !m.migrateState {
diags = diags.Append(migrateOrReconfigDiag)
return nil, diags return nil, diags
} }
@ -1097,11 +1114,6 @@ func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.V
return b, configVal, diags return b, configVal, diags
} }
func (m *Meta) backendInitRequired(reason string) {
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
"[reset]"+strings.TrimSpace(errBackendInit)+"\n", reason)))
}
// Helper method to ignore remote backend version conflicts. Only call this // Helper method to ignore remote backend version conflicts. Only call this
// for commands which cannot accidentally upgrade remote state files. // for commands which cannot accidentally upgrade remote state files.
func (m *Meta) ignoreRemoteBackendVersionConflict(b backend.Backend) { func (m *Meta) ignoreRemoteBackendVersionConflict(b backend.Backend) {
@ -1138,11 +1150,6 @@ func (m *Meta) remoteBackendVersionCheck(b backend.Backend, workspace string) tf
// Output constants and initialization code // Output constants and initialization code
//------------------------------------------------------------------- //-------------------------------------------------------------------
// errBackendInitRequired is the final error message shown when reinit
// is required for some reason. The error message includes the reason.
var errBackendInitRequired = errors.New(
"Initialization required. Please see the error message above.")
const errBackendLocalRead = ` const errBackendLocalRead = `
Error reading local state: %s Error reading local state: %s
@ -1205,8 +1212,7 @@ and try again.
` `
const errBackendInit = ` const errBackendInit = `
[reset][bold][yellow]Backend reinitialization required. Please run "terraform init".[reset] Reason: %s
[yellow]Reason: %s
The "backend" is the interface that Terraform uses to store state, The "backend" is the interface that Terraform uses to store state,
perform operations, etc. If this message is showing up, it means that the perform operations, etc. If this message is showing up, it means that the
@ -1214,8 +1220,9 @@ Terraform configuration you're using is using a custom configuration for
the Terraform backend. the Terraform backend.
Changes to backend configurations require reinitialization. This allows Changes to backend configurations require reinitialization. This allows
Terraform to set up the new configuration, copy existing state, etc. This is Terraform to set up the new configuration, copy existing state, etc. Please run
only done during "terraform init". Please run that command now then try again. "terraform init" with either the "-reconfigure" or "-migrate-state" flags to
use the current configuration.
If the change reason above is incorrect, please verify your configuration 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 hasn't changed and try again. At this point, no changes to your existing
@ -1254,3 +1261,10 @@ const successBackendSet = `
Successfully configured the backend %q! Terraform will automatically Successfully configured the backend %q! Terraform will automatically
use this backend unless the backend configuration changes. use this backend unless the backend configuration changes.
` `
var migrateOrReconfigDiag = tfdiags.Sourceless(
tfdiags.Error,
"Backend configuration changed",
"A change in the backend configuration has been detected, which may require migrating existing state.\n\n"+
"If you wish to attempt automatic migration of the state, use \"terraform init -migrate-state\".\n"+
`If you wish to store the current configuration with no changes to the state, use "terraform init -reconfigure".`)

View File

@ -314,6 +314,10 @@ func TestMetaBackend_configureNewWithState(t *testing.T) {
// Setup the meta // Setup the meta
m := testMetaBackend(t, nil) m := testMetaBackend(t, nil)
// This combination should not require the extra -migrate-state flag, since
// there is no existing backend config
m.migrateState = false
// Get the backend // Get the backend
b, diags := m.Backend(&BackendOpts{Init: true}) b, diags := m.Backend(&BackendOpts{Init: true})
if diags.HasErrors() { if diags.HasErrors() {
@ -1884,5 +1888,8 @@ func testMetaBackend(t *testing.T, args []string) *Meta {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
// metaBackend tests are verifying migrate actions
m.migrateState = true
return &m return &m
} }

View File

@ -400,7 +400,7 @@ func TestStateRm_needsInit(t *testing.T) {
t.Fatalf("expected error output, got:\n%s", ui.OutputWriter.String()) t.Fatalf("expected error output, got:\n%s", ui.OutputWriter.String())
} }
if !strings.Contains(ui.ErrorWriter.String(), "Initialization") { if !strings.Contains(ui.ErrorWriter.String(), "Backend initialization") {
t.Fatalf("expected initialization error, got:\n%s", ui.ErrorWriter.String()) t.Fatalf("expected initialization error, got:\n%s", ui.ErrorWriter.String())
} }
} }

View File

@ -79,11 +79,17 @@ During init, the root configuration directory is consulted for
is initialized using the given configuration settings. is initialized using the given configuration settings.
Re-running init with an already-initialized backend will update the working Re-running init with an already-initialized backend will update the working
directory to use the new backend settings. Depending on what changed, this directory to use the new backend settings. Either `-reconfigure` or
may result in interactive prompts to confirm migration of workspace states. `-migrate-state` must be supplied to update the backend configuration.
The `-force-copy` option suppresses these prompts and answers "yes" to the
migration questions. The `-reconfigure` option disregards any existing The `-migrate-state` option will attempt to copy existing state to the new
configuration, preventing migration of any existing state. backend, and depending on what changed, may result in interactive prompts to
confirm migration of workspace states. The `-force-copy` option suppresses
these prompts and answers "yes" to the migration questions. This implies
`-migrate-state`.
The `-reconfigure` option disregards any existing configuration, preventing
migration of any existing state.
To skip backend configuration, use `-backend=false`. Note that some other init To skip backend configuration, use `-backend=false`. Note that some other init
steps require an initialized backend, so it is recommended to use this flag only steps require an initialized backend, so it is recommended to use this flag only