mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 07:33:32 -06:00
Fix regression of backend reinit detection when backend schema has required arguments (#2119)
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
parent
79a2bb3c47
commit
6707ef6ca3
@ -54,6 +54,10 @@ func Init(services *disco.Disco) {
|
|||||||
backendsLock.Lock()
|
backendsLock.Lock()
|
||||||
defer backendsLock.Unlock()
|
defer backendsLock.Unlock()
|
||||||
|
|
||||||
|
// NOTE: Underscore-prefixed named are reserved for unit testing use via
|
||||||
|
// the RegisterTemp function. Do not add any underscore-prefixed names
|
||||||
|
// to the following table.
|
||||||
|
|
||||||
backends = map[string]backend.InitFn{
|
backends = map[string]backend.InitFn{
|
||||||
"local": func(enc encryption.StateEncryption) backend.Backend { return backendLocal.New(enc) },
|
"local": func(enc encryption.StateEncryption) backend.Backend { return backendLocal.New(enc) },
|
||||||
"remote": func(enc encryption.StateEncryption) backend.Backend { return backendRemote.New(services, enc) },
|
"remote": func(enc encryption.StateEncryption) backend.Backend { return backendRemote.New(services, enc) },
|
||||||
@ -100,6 +104,10 @@ func Backend(name string) backend.InitFn {
|
|||||||
// This method sets this backend globally and care should be taken to do
|
// This method sets this backend globally and care should be taken to do
|
||||||
// this only before OpenTofu is executing to prevent odd behavior of backends
|
// this only before OpenTofu is executing to prevent odd behavior of backends
|
||||||
// changing mid-execution.
|
// changing mid-execution.
|
||||||
|
//
|
||||||
|
// NOTE: Underscore-prefixed named are reserved for unit testing use via
|
||||||
|
// the RegisterTemp function. Do not add any underscore-prefixed names
|
||||||
|
// using this function.
|
||||||
func Set(name string, f backend.InitFn) {
|
func Set(name string, f backend.InitFn) {
|
||||||
backendsLock.Lock()
|
backendsLock.Lock()
|
||||||
defer backendsLock.Unlock()
|
defer backendsLock.Unlock()
|
||||||
|
204
internal/backend/init/testing.go
Normal file
204
internal/backend/init/testing.go
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// Copyright (c) The OpenTofu Authors
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
// Copyright (c) 2023 HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package init
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/opentofu/opentofu/internal/backend"
|
||||||
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||||
|
"github.com/opentofu/opentofu/internal/states/statemgr"
|
||||||
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterTemp adds a new entry to the table of backends and returns a
|
||||||
|
// function that will deregister it once called.
|
||||||
|
//
|
||||||
|
// This is essentially a workaround for the fact that the OpenTofu CLI
|
||||||
|
// layer expects backends to always come from a centrally-maintained table
|
||||||
|
// and doesn't have any way to directly pass an anonymous backend
|
||||||
|
// implementation.
|
||||||
|
//
|
||||||
|
// The given name MUST start with an underscore, to ensure that it cannot
|
||||||
|
// collide with any "real" backend. If the given name does not start with
|
||||||
|
// an underscore then this function will panic. If we introduce plugin-based
|
||||||
|
// backends in future then we might consider reserving part of the plugin
|
||||||
|
// address namespace to represent temporary backends for test purposes only,
|
||||||
|
// which would then replace this special underscore prefix as the differentiator.
|
||||||
|
//
|
||||||
|
// This is intended for unit tests that use [MockBackend], or a derivative
|
||||||
|
// thereof. A typical usage pattern from a unit test would be:
|
||||||
|
//
|
||||||
|
// // ("backendInit" represents _this_ package)
|
||||||
|
// t.Cleanup(backendInit.RegisterTemp("_test", func (enc encryption.StateEncryption) Backend {
|
||||||
|
// return &backendInit.MockBackend{
|
||||||
|
// // (and then whichever mock settings your test case needs)
|
||||||
|
// }
|
||||||
|
// }))
|
||||||
|
//
|
||||||
|
// Because this function modifies global state observable throughout the
|
||||||
|
// program, any test using this function MUST NOT use t.Parallel. If a
|
||||||
|
// single test needs to register multiple temporary backends for some reason
|
||||||
|
// then it must select a different name for each one.
|
||||||
|
func RegisterTemp(name string, f backend.InitFn) func() {
|
||||||
|
// FIXME: It would be better to add a map of backends to command.Meta's existing
|
||||||
|
// "testingOverrides" field, but at the time of writing direct calls to this
|
||||||
|
// package's func Backend are made from too many different places in the
|
||||||
|
// codebase to retrofit "testing overrides" without considerable risk to
|
||||||
|
// already-working code, so for now we compromise and just offer this helper
|
||||||
|
// function to hopefully help tests properly manage their temporary addition
|
||||||
|
// to the global backend table.
|
||||||
|
|
||||||
|
if !strings.HasPrefix(name, "_") {
|
||||||
|
panic("temporary backend name must begin with underscore")
|
||||||
|
}
|
||||||
|
backendsLock.Lock()
|
||||||
|
defer backendsLock.Unlock()
|
||||||
|
|
||||||
|
if _, exists := backends[name]; exists {
|
||||||
|
// If we get in here then it suggests one of the following mistakes in
|
||||||
|
// the calling code:
|
||||||
|
// - Using RegisterTemp in a test case that uses t.Parallel.
|
||||||
|
// - Forgetting to call the cleanup function in some other earlier test that
|
||||||
|
// happened to choose the same temporary name.
|
||||||
|
// - Registering more than one temporary backend in a single test without
|
||||||
|
// assigning each one a unique name.
|
||||||
|
panic(fmt.Sprintf("there is already a temporary backend named %q", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The given init function is temporarily added to the global table, so that
|
||||||
|
// the CLI package can find it using the given (underscore-prefixed) name.
|
||||||
|
backends[name] = f
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
backendsLock.Lock()
|
||||||
|
delete(backends, name)
|
||||||
|
backendsLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockBackend is an implementation of [Backend] that largely just routes incoming
|
||||||
|
// calls to a set of closures provided by a caller.
|
||||||
|
//
|
||||||
|
// This is included for testing purposes only. Use [RegisterTemp] to
|
||||||
|
// temporarily add a MockBackend instance to the table of available backends
|
||||||
|
// from a unit test function. Do not include MockBackend instances in the
|
||||||
|
// initial static backend table.
|
||||||
|
//
|
||||||
|
// The mock automatically tracks the most recent call to each method for ease
|
||||||
|
// of writing assertions in simple cases. If you need more complex tracking such
|
||||||
|
// as a log of all calls then you can implement that inside your provided callback
|
||||||
|
// functions.
|
||||||
|
//
|
||||||
|
// This implementation intentionally covers only the basic [Backend] interface,
|
||||||
|
// and not any extension interfaces like [CLI] and [Enhanced]. Consider embedding
|
||||||
|
// this into another type if you need to mock extension interfaces too, since
|
||||||
|
// OpenTofu backend init uses type assertions to check for extension interfaces
|
||||||
|
// and so having this type implement them would prevent its use in testing
|
||||||
|
// situations that occur with non-extended backend implementations.
|
||||||
|
type MockBackend struct {
|
||||||
|
// If you add support for new methods here in future, please preserve the
|
||||||
|
// alphabetical order by function name and the other naming suffix conventions
|
||||||
|
// for each field.
|
||||||
|
|
||||||
|
ConfigSchemaFn func() *configschema.Block
|
||||||
|
ConfigSchemaCalled bool
|
||||||
|
|
||||||
|
ConfigureFn func(configObj cty.Value) tfdiags.Diagnostics
|
||||||
|
ConfigureCalled bool
|
||||||
|
ConfigureConfigObj cty.Value
|
||||||
|
|
||||||
|
DeleteWorkspaceFn func(name string, force bool) error
|
||||||
|
DeleteWorkspaceCalled bool
|
||||||
|
DeleteWorkspaceName string
|
||||||
|
DeleteWorkspaceForce bool
|
||||||
|
|
||||||
|
PrepareConfigFn func(configObj cty.Value) (cty.Value, tfdiags.Diagnostics)
|
||||||
|
PrepareConfigCalled bool
|
||||||
|
PrepareConfigConfigObj cty.Value
|
||||||
|
|
||||||
|
StateMgrFn func(workspace string) (statemgr.Full, error)
|
||||||
|
StateMgrCalled bool
|
||||||
|
StateMgrWorkspace string
|
||||||
|
|
||||||
|
WorkspacesFn func() ([]string, error)
|
||||||
|
WorkspacesCalled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ backend.Backend = (*MockBackend)(nil)
|
||||||
|
|
||||||
|
// ConfigSchema implements Backend.
|
||||||
|
func (m *MockBackend) ConfigSchema() *configschema.Block {
|
||||||
|
m.ConfigSchemaCalled = true
|
||||||
|
|
||||||
|
if m.ConfigSchemaFn == nil {
|
||||||
|
// Default behavior: return an empty schema
|
||||||
|
return &configschema.Block{}
|
||||||
|
}
|
||||||
|
return m.ConfigSchemaFn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure implements Backend.
|
||||||
|
func (m *MockBackend) Configure(configObj cty.Value) tfdiags.Diagnostics {
|
||||||
|
m.ConfigureCalled = true
|
||||||
|
m.ConfigureConfigObj = configObj
|
||||||
|
|
||||||
|
if m.ConfigureFn == nil {
|
||||||
|
// Default behavior: do nothing at all, and report success
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m.ConfigureFn(configObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWorkspace implements Backend.
|
||||||
|
func (m *MockBackend) DeleteWorkspace(name string, force bool) error {
|
||||||
|
m.DeleteWorkspaceCalled = true
|
||||||
|
m.DeleteWorkspaceName = name
|
||||||
|
m.DeleteWorkspaceForce = force
|
||||||
|
|
||||||
|
if m.DeleteWorkspaceFn == nil {
|
||||||
|
// Default behavior: do nothing at all, and report success
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m.DeleteWorkspaceFn(name, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareConfig implements Backend.
|
||||||
|
func (m *MockBackend) PrepareConfig(configObj cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||||
|
m.PrepareConfigCalled = true
|
||||||
|
m.PrepareConfigConfigObj = configObj
|
||||||
|
|
||||||
|
if m.PrepareConfigFn == nil {
|
||||||
|
// Default behavior: just echo back the given config object and indicate success
|
||||||
|
return configObj, nil
|
||||||
|
}
|
||||||
|
return m.PrepareConfigFn(configObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateMgr implements Backend.
|
||||||
|
func (m *MockBackend) StateMgr(workspace string) (statemgr.Full, error) {
|
||||||
|
m.StateMgrCalled = true
|
||||||
|
m.StateMgrWorkspace = workspace
|
||||||
|
|
||||||
|
if m.StateMgrFn == nil {
|
||||||
|
// Default behavior: fail as if there is no workspace of the given name
|
||||||
|
return nil, fmt.Errorf("no workspace named %q", workspace)
|
||||||
|
}
|
||||||
|
return m.StateMgrFn(workspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workspaces implements Backend.
|
||||||
|
func (m *MockBackend) Workspaces() ([]string, error) {
|
||||||
|
m.WorkspacesCalled = true
|
||||||
|
|
||||||
|
if m.WorkspacesFn == nil {
|
||||||
|
// Default behavior: report no workspaces at all.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return m.WorkspacesFn()
|
||||||
|
}
|
@ -1361,7 +1361,12 @@ func (m *Meta) backendConfigNeedsMigration(c *configs.Backend, s *legacy.Backend
|
|||||||
}
|
}
|
||||||
b := f(nil) // We don't need encryption here as it's only used for config/schema
|
b := f(nil) // We don't need encryption here as it's only used for config/schema
|
||||||
|
|
||||||
schema := b.ConfigSchema()
|
// We use "NoneRequired" here because we're only evaluating the body written directly
|
||||||
|
// in the root module configuration, and we're intentionally not including any
|
||||||
|
// additional arguments passed on the command line (using -backend-config), so
|
||||||
|
// some of the required arguments might be satisfied from outside of the body we're
|
||||||
|
// evaluating here.
|
||||||
|
schema := b.ConfigSchema().NoneRequired()
|
||||||
givenVal, diags := c.Decode(schema)
|
givenVal, diags := c.Decode(schema)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
log.Printf("[TRACE] backendConfigNeedsMigration: failed to decode given config; migration codepath must handle problem: %s", diags.Error())
|
log.Printf("[TRACE] backendConfigNeedsMigration: failed to decode given config; migration codepath must handle problem: %s", diags.Error())
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/opentofu/opentofu/internal/addrs"
|
"github.com/opentofu/opentofu/internal/addrs"
|
||||||
"github.com/opentofu/opentofu/internal/backend"
|
"github.com/opentofu/opentofu/internal/backend"
|
||||||
"github.com/opentofu/opentofu/internal/configs"
|
"github.com/opentofu/opentofu/internal/configs"
|
||||||
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||||
"github.com/opentofu/opentofu/internal/copy"
|
"github.com/opentofu/opentofu/internal/copy"
|
||||||
"github.com/opentofu/opentofu/internal/encryption"
|
"github.com/opentofu/opentofu/internal/encryption"
|
||||||
"github.com/opentofu/opentofu/internal/plans"
|
"github.com/opentofu/opentofu/internal/plans"
|
||||||
@ -682,6 +683,53 @@ func TestMetaBackend_configuredUnchangedWithStaticEvalVars(t *testing.T) {
|
|||||||
|
|
||||||
defer testChdir(t, testFixturePath("backend-unchanged-vars"))()
|
defer testChdir(t, testFixturePath("backend-unchanged-vars"))()
|
||||||
|
|
||||||
|
// We'll use a mock backend here because we need to control the schema to
|
||||||
|
// make sure that we always have a required field for the ConfigOverride
|
||||||
|
// argument to populate. This is covering the regression caused by the first
|
||||||
|
// fix to the original bug, discussed here:
|
||||||
|
// https://github.com/opentofu/opentofu/issues/2118
|
||||||
|
t.Cleanup(
|
||||||
|
backendInit.RegisterTemp("_test_local", func(enc encryption.StateEncryption) backend.Backend {
|
||||||
|
return &backendInit.MockBackend{
|
||||||
|
ConfigSchemaFn: func() *configschema.Block {
|
||||||
|
// The following is based on a subset of the normal "local"
|
||||||
|
// backend at the time of writing this test, but subsetted
|
||||||
|
// to only what we need and with all of the arguments
|
||||||
|
// marked as required (even though the real backend doesn't)
|
||||||
|
// so we can make sure that we handle required arguments
|
||||||
|
// properly.
|
||||||
|
return &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"path": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
// We'll set this one in the root module, using early eval.
|
||||||
|
},
|
||||||
|
"workspace_dir": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
// We'll treat this one as if it were set with the -backend-config option to "tofu init"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WorkspacesFn: func() ([]string, error) {
|
||||||
|
return []string{"default"}, nil
|
||||||
|
},
|
||||||
|
StateMgrFn: func(workspace string) (statemgr.Full, error) {
|
||||||
|
// The migration-detection code actually fetches the state to
|
||||||
|
// decide if it's "empty" so it can avoid proposing to migrate
|
||||||
|
// an empty state, and so unfortunately we do need to have
|
||||||
|
// a relatively-realistic implementation of this. We'll
|
||||||
|
// just use the same filesystem-based implementation that
|
||||||
|
// the real local backend would use, but fixed to use our
|
||||||
|
// local-state.tfstate file from the test fixture.
|
||||||
|
return statemgr.NewFilesystem("local-state.tfstate", enc), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// Setup the meta
|
// Setup the meta
|
||||||
m := testMetaBackend(t, nil)
|
m := testMetaBackend(t, nil)
|
||||||
// testMetaBackend normally sets migrateState on, because most of the tests
|
// testMetaBackend normally sets migrateState on, because most of the tests
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
"serial": 1,
|
"serial": 1,
|
||||||
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
"backend": {
|
"backend": {
|
||||||
"type": "local",
|
"type": "_test_local",
|
||||||
"config": {
|
"config": {
|
||||||
"path": "local-state.tfstate",
|
"path": "local-state.tfstate",
|
||||||
"workspace_dir": "doesnt-actually-matter-what-this-is"
|
"workspace_dir": "doesnt-actually-matter-what-this-is"
|
||||||
},
|
},
|
||||||
"hash": 4282859327
|
"hash": 117044127
|
||||||
},
|
},
|
||||||
"modules": [
|
"modules": [
|
||||||
{
|
{
|
||||||
|
@ -4,7 +4,7 @@ variable "state_filename" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
terraform {
|
terraform {
|
||||||
backend "local" {
|
backend "_test_local" {
|
||||||
path = var.state_filename
|
path = var.state_filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user