Skip to content

Fix bug where -backend-config could not override attributes that weren't at the top level in the backend schema #36919

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.13/BUG FIXES-20250423-164150.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'backends: Nested attrbiutes can now be overridden during `init` using the `-backend-config` flag'
time: 2025-04-23T16:41:50.904809+01:00
custom:
Issue: "36911"
52 changes: 46 additions & 6 deletions internal/backend/remote-state/inmem/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package inmem
import (
"errors"
"fmt"
"maps"
"os"
"sort"
"sync"
"time"
Expand Down Expand Up @@ -48,19 +50,57 @@ func Reset() {

// New creates a new backend for Inmem remote state.
func New() backend.Backend {
if os.Getenv("TF_INMEM_TEST") != "" {
// We use a different schema for testing. This isn't user facing unless they
// dig into the code.
fmt.Sprintln("TF_INMEM_TEST is set: Using test schema for the inmem backend")

return &Backend{
Base: backendbase.Base{
Schema: &configschema.Block{
Attributes: testSchemaAttrs(),
},
},
}
}

// Default schema that's user-facing
return &Backend{
Base: backendbase.Base{
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"lock_id": {
Type: cty.String,
Optional: true,
Description: "initializes the state in a locked configuration",
},
Attributes: defaultSchemaAttrs,
},
},
}
}

var defaultSchemaAttrs = map[string]*configschema.Attribute{
"lock_id": {
Type: cty.String,
Optional: true,
Description: "initializes the state in a locked configuration",
},
}

func testSchemaAttrs() map[string]*configschema.Attribute {
var newSchema = make(map[string]*configschema.Attribute)
maps.Copy(newSchema, defaultSchemaAttrs)

// Append test-specific parts of schema
newSchema["test_nesting_single"] = &configschema.Attribute{
Description: "An attribute that contains nested attributes, where nesting mode is NestingSingle",
NestedType: &configschema.Object{
Nesting: configschema.NestingSingle,
Attributes: map[string]*configschema.Attribute{
"child": {
Type: cty.String,
Optional: true,
Description: "A nested attribute inside the parent attribute `test_nesting_single`",
},
},
},
}
return newSchema
}

type Backend struct {
Expand Down
80 changes: 72 additions & 8 deletions internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -1031,23 +1031,87 @@ func (c *InitCommand) backendConfigOverrideBody(flags arguments.FlagNameValueSli
flushVals() // deal with any accumulated individual values first
mergeBody(newBody)
} else {
// The flag value is setting a single attribute's value
name := item.Value[:eq]
rawValue := item.Value[eq+1:]
attrS := schema.Attributes[name]
if attrS == nil {

splitName := strings.Split(name, ".")
isNested := len(splitName) > 1

var value cty.Value
var valueDiags tfdiags.Diagnostics
switch {
case !isNested:
// The flag item is overriding a top-level attribute
attrS := schema.Attributes[name]
if attrS == nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid backend configuration argument",
fmt.Sprintf("The backend configuration argument %q given on the command line is not expected for the selected backend type.", name),
))
continue
}

value, valueDiags = configValueFromCLI(item.String(), rawValue, attrS.Type)
diags = diags.Append(valueDiags)
if valueDiags.HasErrors() {
continue
}

// Synthetic values are collected as we parse each flag item
synthVals[name] = value
case isNested:
// The flag item is overriding a nested attribute
// e.g. assume_role.role_arn in the s3 backend
// We assume a max nesting-depth of 1 as s3 is the only
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we somehow bake this assumption in the code validation?

An input like "test_nesting_single.child.grand" was allowed to pass with no error, and the child value ended up being set.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've addressed this by swapping to a solution that allows any level of nesting, and that change also addresses issues related to missing parent fields. I figured this was a better approach, as the alternative was to add an error asking users to report issues with overriding fields at a greater level of nesting, and why kick that issue down the road?

// backend that contains nested fields.

parentName := splitName[0]
nestedName := splitName[1]
parentAttr := schema.Attributes[parentName]
nestedAttr := parentAttr.NestedType.Attributes[nestedName]
if nestedAttr == nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid backend configuration argument",
fmt.Sprintf("The backend configuration argument %q given on the command line is not expected for the selected backend type.", name),
))
continue
}

value, valueDiags = configValueFromCLI(item.String(), rawValue, nestedAttr.Type)
diags = diags.Append(valueDiags)
if valueDiags.HasErrors() {
continue
}

// Synthetic values are collected as we parse each flag item
// When doing this we need to account for attribute nesting
// and multiple nested fields being overridden.
synthParent, found := synthVals[parentName]
if !found {
synthVals[parentName] = cty.ObjectVal(map[string]cty.Value{
nestedName: value,
})
}
if found {
// add the new attribute override to any existing attributes
// also nested under the parent
nestedMap := synthParent.AsValueMap()
nestedMap[nestedName] = value
synthVals[parentName] = cty.ObjectVal(nestedMap)
}

default:
// Should not reach here
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid backend configuration argument",
fmt.Sprintf("The backend configuration argument %q given on the command line is not expected for the selected backend type.", name),
))
continue
}
value, valueDiags := configValueFromCLI(item.String(), rawValue, attrS.Type)
diags = diags.Append(valueDiags)
if valueDiags.HasErrors() {
continue
}
synthVals[name] = value
}
}

Expand Down
35 changes: 35 additions & 0 deletions internal/command/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,41 @@ func TestInit_backendConfigKV(t *testing.T) {
}
}

func TestInit_backendConfigKVNested(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
testCopyDir(t, testFixturePath("init-backend-config-kv-nested"), td)
defer testChdir(t, td)()
t.Setenv("TF_INMEM_TEST", "1") // Allows use of inmem backend with a more complex schema

ui := new(cli.MockUi)
view, done := testView(t)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
View: view,
},
}

// overridden field is nested:
// test_nesting_single = {
// child = "..."
// }
args := []string{
"-backend-config=test_nesting_single.child=foobar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", done(t).Stderr())
}

// Read our saved backend config and verify we have our settings
state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"lock_id":null,"test_nesting_single":{"child":"foobar"}}`; got != want {
t.Errorf("wrong config\ngot: %s\nwant: %s", got, want)
}
}

func TestInit_backendConfigKVReInit(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
backend "inmem" {
test_nesting_single = {
child = "" // to be overwritten in test
}
}
}