no longer automatically attempt state migration

Add `init -migrate-state` flag to indicate automatic state migration is
desired. This flag will be implied by the `-force-copy` flag, since that
would indicate state migration is expected.

If `init` encounters a change to the stored backend configuration, it
will now always return an error when neither `-reconfigure` or
`-migrate-state` is supplied.

Turn the most common legacy output strings into diagnostics, removing
the "see above text" error output.
This commit is contained in:
James Bardin 2021-05-14 17:36:54 -04:00
parent 73d07e28c0
commit edc2695d18
6 changed files with 67 additions and 30 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,12 @@ func (c *InitCommand) Run(args []string) int {
return 1 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 {

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
} }
@ -1138,11 +1155,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 +1217,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 +1225,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 +1266,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())
} }
} }