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
This commit is contained in:
Kristin Laemmert 2020-01-07 15:07:06 -05:00 committed by GitHub
parent 5d3ca8aaf1
commit 4d8fde3d6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 114 additions and 1 deletions

View File

@ -295,6 +295,15 @@ func (c *InitCommand) Run(args []string) int {
} }
back = be 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 { if back == nil {

View File

@ -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 // Backend Config Scenarios
// //

View File

@ -22,6 +22,7 @@ import (
backendInit "github.com/hashicorp/terraform/backend/init" backendInit "github.com/hashicorp/terraform/backend/init"
backendLocal "github.com/hashicorp/terraform/backend/local" 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. // 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) { func TestMetaBackend_localDoesNotDeleteLocal(t *testing.T) {
// Create a temporary working directory that is empty // Create a temporary working directory that is empty
td := tempDir(t) 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 { func testMetaBackend(t *testing.T, args []string) *Meta {
var m Meta var m Meta
m.Ui = new(cli.MockUi) m.Ui = new(cli.MockUi)

View File

@ -0,0 +1,10 @@
{
"version": 3,
"terraform_version": "0.12.0",
"serial": 7,
"lineage": "configured",
"backend": {
"type": "inmem",
"config": {}
}
}