From 4d8fde3d6f2deba26305df46c3f4467786f5d885 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Tue, 7 Jan 2020 15:07:06 -0500 Subject: [PATCH] command: use backend config from state when backend=false is used. (#23802) * command: use backend config from state when backend=false is used. When a user runs `terraform init --backend=false`, terraform should inspect the state for a previously-configured backend, and use that backend, ignoring any backend config in the current configuration. If no backend is configured or there is no state, return a local backend. Fixes #16593 --- command/init.go | 9 +++ command/meta_backend.go | 69 +++++++++++++++++++ command/meta_backend_test.go | 27 +++++++- .../backend-from-state/terraform.tfstate | 10 +++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 command/testdata/backend-from-state/terraform.tfstate diff --git a/command/init.go b/command/init.go index 0501e6450c..3dd6f59c7d 100644 --- a/command/init.go +++ b/command/init.go @@ -295,6 +295,15 @@ func (c *InitCommand) Run(args []string) int { } back = be } + } else { + // load the previously-stored backend config + be, backendDiags := c.Meta.backendFromState() + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + back = be } if back == nil { diff --git a/command/meta_backend.go b/command/meta_backend.go index bf0dc657a7..650d4a26e0 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -562,6 +562,75 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } } +// backendFromState returns the initialized (not configured) backend directly +// from the state. This should be used only when a user runs `terraform init +// -backend=false`. This function returns a local backend if there is no state +// or no backend configured. +func (m *Meta) backendFromState() (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + // 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 { + diags = diags.Append(fmt.Errorf("Failed to load state: %s", err)) + return nil, diags + } + s := sMgr.State() + if s == nil { + // no state, so return a local backend + log.Printf("[TRACE] Meta.Backend: backend has not previously been initialized in this working directory") + return backendLocal.New(), diags + } + if s.Backend == nil { + // s.Backend is nil, so return a local backend + log.Printf("[TRACE] Meta.Backend: working directory was previously initialized but has no backend (is using legacy remote state?)") + return backendLocal.New(), diags + } + log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q backend", s.Backend.Type) + + //backend init function + if s.Backend.Type == "" { + return backendLocal.New(), diags + } + f := backendInit.Backend(s.Backend.Type) + if f == nil { + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type)) + return nil, diags + } + b := f() + + // The configuration saved in the working directory state file is used + // in this case, since it will contain any additional values that + // were provided via -backend-config arguments on terraform init. + schema := b.ConfigSchema() + configVal, err := s.Backend.Config(schema) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to decode current backend config", + fmt.Sprintf("The backend configuration created by the most recent run of \"terraform init\" could not be decoded: %s. The configuration may have been initialized by an earlier version that used an incompatible configuration structure. Run \"terraform init -reconfigure\" to force re-initialization of the backend.", err), + )) + return nil, diags + } + + // Validate the config and then configure the backend + newVal, validDiags := b.PrepareConfig(configVal) + diags = diags.Append(validDiags) + if validDiags.HasErrors() { + return nil, diags + } + + configDiags := b.Configure(newVal) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return nil, diags + } + + return b, diags +} + //------------------------------------------------------------------- // Backend Config Scenarios // diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index 92a3ee2586..0b099c3d37 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -22,6 +22,7 @@ import ( backendInit "github.com/hashicorp/terraform/backend/init" backendLocal "github.com/hashicorp/terraform/backend/local" + backendInmem "github.com/hashicorp/terraform/backend/remote-state/inmem" ) // Test empty directory with no config/state creates a local state. @@ -1771,7 +1772,7 @@ func TestMetaBackend_configureWithExtra(t *testing.T) { } } -// when confniguring a default local state, don't delete local state +// when configuring a default local state, don't delete local state func TestMetaBackend_localDoesNotDeleteLocal(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) @@ -1860,6 +1861,30 @@ func TestMetaBackend_configToExtra(t *testing.T) { } } +// no config; return inmem backend stored in state +func TestBackendFromState(t *testing.T) { + td := tempDir(t) + copy.CopyDir(testFixturePath("backend-from-state"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Setup the meta + m := testMetaBackend(t, nil) + // terraform caches a small "state" file that stores the backend config. + // This test must override m.dataDir so it loads the "terraform.tfstate" file in the + // test directory as the backend config cache + m.OverrideDataDir = td + + stateBackend, diags := m.backendFromState() + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + if _, ok := stateBackend.(*backendInmem.Backend); !ok { + t.Fatal("did not get expected inmem backend") + } +} + func testMetaBackend(t *testing.T, args []string) *Meta { var m Meta m.Ui = new(cli.MockUi) diff --git a/command/testdata/backend-from-state/terraform.tfstate b/command/testdata/backend-from-state/terraform.tfstate new file mode 100644 index 0000000000..091ecc19a6 --- /dev/null +++ b/command/testdata/backend-from-state/terraform.tfstate @@ -0,0 +1,10 @@ +{ + "version": 3, + "terraform_version": "0.12.0", + "serial": 7, + "lineage": "configured", + "backend": { + "type": "inmem", + "config": {} + } +}