command/init: Support static eval for backend config migration check

The "backendConfigNeedsMigration" helper evaluates the backend
configuration inline to compare it with the object previously saved in the
.terraform/terraform.tfstate file.

However, this wasn't updated to use the new "static eval" functionality
and so was treating any references to variables or function calls as
invalid, causing a spurious "backend configuration changed" error when
re-initializing the working directory with identical backend configuration
settings.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins 2024-10-08 16:38:56 -07:00
parent de69070b02
commit 8b0b5b271b
5 changed files with 146 additions and 3 deletions

View File

@ -20,7 +20,6 @@ import (
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
@ -1363,8 +1362,7 @@ 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
schema := b.ConfigSchema()
decSpec := schema.NoneRequired().DecoderSpec()
givenVal, diags := hcldec.Decode(c.Config, decSpec, nil)
givenVal, diags := c.Decode(schema)
if diags.HasErrors() {
log.Printf("[TRACE] backendConfigNeedsMigration: failed to decode given config; migration codepath must handle problem: %s", diags.Error())
return true // let the migration codepath deal with these errors

View File

@ -14,6 +14,8 @@ import (
"strings"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
@ -655,6 +657,104 @@ func TestMetaBackend_configuredUnchanged(t *testing.T) {
}
}
// Saved backend state matching config when the configuration uses static eval references
// and there's an argument overridden on the commandl ine.
func TestMetaBackend_configuredUnchangedWithStaticEvalVars(t *testing.T) {
// This test is covering the fix for the following issue:
// https://github.com/opentofu/opentofu/issues/2024
//
// To match that issue's reproduction case the following must both be true:
// - The configuration written in the fixture's .tf file must include either a
// reference to a named value or a function call. Currently we use a reference
// to a variable.
// - There must be at least one -backend-config argument on the command line,
// which causes us to go into the trickier comparison codepath that has to
// re-evaluate _just_ the configuration to distinguish from the combined
// configuration plus command-line overrides. Without this the configuration
// doesn't get re-evaluated and so the expressions used to construct it are
// irrelevant.
//
// Although not strictly required for reproduction at the time of writing this
// test, the local-state.tfstate file in the fixture also includes an output
// value to ensure that it can't be classified as an "empty state" and thus
// have migration skipped, even if the rules for activating that fast path
// change in future.
defer testChdir(t, testFixturePath("backend-unchanged-vars"))()
// Setup the meta
m := testMetaBackend(t, nil)
// testMetaBackend normally sets migrateState on, because most of the tests
// _want_ to perform migration, but for this one we're behaving as if the
// user hasn't set the -migrate-state option and thus it should be an error
// if state migration is required.
m.migrateState = false
// Get the backend
b, diags := m.Backend(
&BackendOpts{
Init: true,
// ConfigOverride is the internal representation of the -backend-config
// command line options. In the normal codepath this gets built into
// a synthetic hcl.Body so it can be merged with the real hcl.Body
// for evaluation. For testing purposes here we're constructing the
// synthetic body using the hcltest package instead, but the effect
// is the same.
ConfigOverride: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"workspace_dir": {
Name: "workspace_dir",
// We're using the "default" workspace in this test and so the workspace_dir
// isn't actually significant -- we're setting it only to enter the full-evaluation
// codepath. The only thing that matters is that the value here matches the
// argument value stored in the .terraform/terraform.tfstate file in the
// test fixture, meaning that state migration is not required because the
// configuration is unchanged.
Expr: hcltest.MockExprLiteral(cty.StringVal("doesnt-actually-matter-what-this-is")),
},
},
}),
},
encryption.StateEncryptionDisabled(),
)
if diags.HasErrors() {
// The original problem reported in https://github.com/opentofu/opentofu/issues/2024
// would return an error here: "Backend configuration has changed".
t.Fatal(diags.Err())
}
// The remaining checks are not directly related to the issue that this test
// is covering, but are included for completeness to check that this situation
// also follows the usual invariants for a failed backend init.
// Check the state
s, err := b.StateMgr(backend.DefaultStateName)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if err := s.RefreshState(); err != nil {
t.Fatalf("unexpected error: %s", err)
}
state := s.State()
if state == nil {
t.Fatal("nil state")
}
if testStateMgrCurrentLineage(s) != "configuredUnchanged" {
t.Fatalf("bad: %#v", state)
}
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatal("file should not exist")
}
}
// Changing a configured backend
func TestMetaBackend_configuredChange(t *testing.T) {
// Create a temporary working directory that is empty

View File

@ -0,0 +1,23 @@
{
"version": 3,
"serial": 1,
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
"backend": {
"type": "local",
"config": {
"path": "local-state.tfstate",
"workspace_dir": "doesnt-actually-matter-what-this-is"
},
"hash": 4282859327
},
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}

View File

@ -0,0 +1,12 @@
{
"version": 4,
"terraform_version": "0.14.0",
"serial": 7,
"lineage": "configuredUnchanged",
"outputs": {
"foo": {
"type": "string",
"value": "This is only here so that the state snapshot isn't 'empty' and so can't enter a no-migration-needed fast path."
}
}
}

View File

@ -0,0 +1,10 @@
variable "state_filename" {
type = string
default = "local-state.tfstate"
}
terraform {
backend "local" {
path = var.state_filename
}
}