diff --git a/command/init.go b/command/init.go index f9b39fbda7..6cf0c42edb 100644 --- a/command/init.go +++ b/command/init.go @@ -45,6 +45,7 @@ func (c *InitCommand) Run(args []string) int { cmdFlags.BoolVar(&flagGet, "get", true, "") cmdFlags.BoolVar(&c.forceInitCopy, "force-copy", false, "suppress prompts about copying state data") cmdFlags.BoolVar(&c.reconfigure, "reconfigure", false, "reconfigure") + cmdFlags.BoolVar(&c.migrateState, "migrate-state", false, "migrate state") cmdFlags.BoolVar(&flagUpgrade, "upgrade", false, "") cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory") cmdFlags.StringVar(&flagLockfile, "lockfile", "", "Set a dependency lockfile mode") @@ -53,6 +54,12 @@ func (c *InitCommand) Run(args []string) int { 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 if len(flagPluginPath) > 0 { diff --git a/command/init_test.go b/command/init_test.go index 99fdbe9649..c16ea2ec57 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -412,7 +412,7 @@ func TestInit_backendConfigFile(t *testing.T) { View: view, }, } - args := []string{"-backend-config="} + args := []string{"-backend-config=", "-migrate-state"} if code := c.Run(args); code != 0 { 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 { 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 - args = []string{"-input=false", "-backend-config", ""} + args = []string{"-input=false", "-backend-config", "", "-migrate-state"} if code := c.Run(args); code != 0 { 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 { t.Fatal("init should have failed", ui.OutputWriter) } diff --git a/command/meta.go b/command/meta.go index 1cad1bffe4..4bebceb4e2 100644 --- a/command/meta.go +++ b/command/meta.go @@ -204,6 +204,9 @@ type Meta struct { // // 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 // of warnings in the output when they are not accompanied by errors. statePath string @@ -214,6 +217,7 @@ type Meta struct { stateLockTimeout time.Duration forceInitCopy bool reconfigure bool + migrateState bool compactWarnings bool // Used with the import command to allow import of state when no matching config exists. diff --git a/command/meta_backend.go b/command/meta_backend.go index 3aed843264..cce4c35682 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -6,7 +6,6 @@ package command import ( "context" "encoding/json" - "errors" "fmt" "log" "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) case c == nil && !s.Backend.Empty(): 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 { - initReason := fmt.Sprintf( - "Unsetting the previously set backend %q", - s.Backend.Type) - m.backendInitRequired(initReason) - diags = diags.Append(errBackendInitRequired) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Backend initialization required, please run \"terraform init\"", + fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason), + )) + return nil, diags + } + + if !m.migrateState { + diags = diags.Append(migrateOrReconfigDiag) return nil, diags } @@ -529,11 +535,12 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di case c != nil && s.Backend.Empty(): log.Printf("[TRACE] Meta.Backend: moving from default local state only to %q backend", c.Type) if !opts.Init { - initReason := fmt.Sprintf( - "Initial configuration of the requested backend %q", - c.Type) - m.backendInitRequired(initReason) - diags = diags.Append(errBackendInitRequired) + initReason := fmt.Sprintf("Initial configuration of the requested backend %q", c.Type) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Backend initialization required, please run \"terraform init\"", + fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason), + )) 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) + 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 { - initReason := fmt.Sprintf( - "Backend configuration changed for %q", - c.Type) - m.backendInitRequired(initReason) - diags = diags.Append(errBackendInitRequired) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Backend initialization required, please run \"terraform init\"", + fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason), + )) + return nil, diags + } + + if !m.migrateState { + diags = diags.Append(migrateOrReconfigDiag) return nil, diags } @@ -1138,11 +1155,6 @@ func (m *Meta) remoteBackendVersionCheck(b backend.Backend, workspace string) tf // 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 = ` Error reading local state: %s @@ -1205,8 +1217,7 @@ and try again. ` const errBackendInit = ` -[reset][bold][yellow]Backend reinitialization required. Please run "terraform init".[reset] -[yellow]Reason: %s +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 @@ -1214,8 +1225,9 @@ Terraform configuration you're using is using a custom configuration for the Terraform backend. Changes to backend configurations require reinitialization. This allows -Terraform to set up the new configuration, copy existing state, etc. This is -only done during "terraform init". Please run that command now then try again. +Terraform to set up the new configuration, copy existing state, etc. Please run +"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 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 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".`) diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index cbd642ed74..1749b28e96 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -314,6 +314,10 @@ func TestMetaBackend_configureNewWithState(t *testing.T) { // Setup the meta 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 b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { @@ -1884,5 +1888,8 @@ func testMetaBackend(t *testing.T, args []string) *Meta { t.Fatalf("unexpected error: %s", err) } + // metaBackend tests are verifying migrate actions + m.migrateState = true + return &m } diff --git a/command/state_rm_test.go b/command/state_rm_test.go index e88cfff8b1..7cac37e47f 100644 --- a/command/state_rm_test.go +++ b/command/state_rm_test.go @@ -400,7 +400,7 @@ func TestStateRm_needsInit(t *testing.T) { 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()) } }