From 594d24b617d7d98576823201f5eab5b73d1bea54 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Nov 2025 12:46:14 +0000 Subject: [PATCH 1/4] fix: Update `backendbase` to use empty strings from configuration, instead of ignoring them and looking for ENVs or fallback values. Add test coverage. --- internal/backend/backendbase/base_test.go | 44 +++++++++++++++++++++++ internal/backend/backendbase/sdklike.go | 5 +++ 2 files changed, 49 insertions(+) diff --git a/internal/backend/backendbase/base_test.go b/internal/backend/backendbase/base_test.go index 0882489df2c3..9488f6dff89d 100644 --- a/internal/backend/backendbase/base_test.go +++ b/internal/backend/backendbase/base_test.go @@ -239,3 +239,47 @@ func TestBase_nullCrash(t *testing.T) { } }) } + +func TestBase_emptyStringsUsed(t *testing.T) { + // This test ensures that empty strings in config are used and aren't ignored + // in favor of SDKLikeDefaults + + b := Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Required: true, + }, + }, + }, + SDKLikeDefaults: SDKLikeDefaults{ + "foo": { + Fallback: "fallback", + }, + }, + } + + t.Run("empty string is used", func(t *testing.T) { + // We pass an explicit empty string as the value of foo + val, gotDiags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal(""), + })) + if gotDiags.HasErrors() { + t.Fatalf("unexpected error diagnostics\n%s", gotDiags.Err()) + } + attrs := val.AsValueMap() + if v, ok := attrs["foo"]; ok { + if v.IsNull() { + t.Fatal("value shouldn't be null") + } + if v.AsString() != "" { + // If the empty string from config is ignored, the value here + // will be the "fallback" string defined in SDKLikeDefaults above. + t.Fatalf("value should be an empty string, but got: %q", v.AsString()) + } + } else { + t.Fatal("cannot find attribute foo") + } + }) +} diff --git a/internal/backend/backendbase/sdklike.go b/internal/backend/backendbase/sdklike.go index 9d53cbf6b543..ec35b9d1ac5c 100644 --- a/internal/backend/backendbase/sdklike.go +++ b/internal/backend/backendbase/sdklike.go @@ -199,6 +199,11 @@ func (d SDKLikeDefaults) ApplyTo(base cty.Value) (cty.Value, error) { retAttrs[attrName] = givenVal continue } + if !givenVal.IsNull() && (givenVal.Type() == cty.String) && (givenVal.AsString() == "") { + // Don't use defaults if a string attribute is explicitly set to an empty string in the configuration. + retAttrs[attrName] = givenVal + continue + } // The legacy SDK shims convert all values into strings (for flatmap) // and then do their work in terms of that, so we'll follow suit here. From 91f0204fd19761b47e67714695bc31265b8e0649 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Nov 2025 13:12:58 +0000 Subject: [PATCH 2/4] Add change file --- .changes/v1.15/BUG FIXES-20251105-131122.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/v1.15/BUG FIXES-20251105-131122.yaml diff --git a/.changes/v1.15/BUG FIXES-20251105-131122.yaml b/.changes/v1.15/BUG FIXES-20251105-131122.yaml new file mode 100644 index 000000000000..caa0b010589c --- /dev/null +++ b/.changes/v1.15/BUG FIXES-20251105-131122.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: 'backend: Fixed a bug where backends began to ignore empty string values set in the backend configuration.' +time: 2025-11-05T13:11:22.098037Z +custom: + Issue: "37846" From efd2f4a6113ac0bf4db97c9a0ba7a4504c19c0ce Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Nov 2025 13:25:03 +0000 Subject: [PATCH 3/4] test: Update test to cover both default values and use of ENVs Previously the test assumed if the default wasn't used then no ENV would be used either. --- internal/backend/backendbase/base_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/backend/backendbase/base_test.go b/internal/backend/backendbase/base_test.go index 9488f6dff89d..e892cc79b17c 100644 --- a/internal/backend/backendbase/base_test.go +++ b/internal/backend/backendbase/base_test.go @@ -256,11 +256,16 @@ func TestBase_emptyStringsUsed(t *testing.T) { SDKLikeDefaults: SDKLikeDefaults{ "foo": { Fallback: "fallback", + EnvVars: []string{"FOO"}, }, }, } - t.Run("empty string is used", func(t *testing.T) { + t.Run("empty string from config is used despite fallback default value and environment variable value set", func(t *testing.T) { + // There is a fallback value supplied by the environment + envValue := "value from ENV" + t.Setenv("FOO", envValue) + // We pass an explicit empty string as the value of foo val, gotDiags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal(""), @@ -275,7 +280,9 @@ func TestBase_emptyStringsUsed(t *testing.T) { } if v.AsString() != "" { // If the empty string from config is ignored, the value here - // will be the "fallback" string defined in SDKLikeDefaults above. + // will be either thing defined in SDKLikeDefaults above: + // 1) the "fallback" string. + // 2) the value of FOO environment variable. t.Fatalf("value should be an empty string, but got: %q", v.AsString()) } } else { From 76248611ffa0288796965c86a6b5ba9ac7591496 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Nov 2025 13:40:19 +0000 Subject: [PATCH 4/4] test: Update `TestSDKLikeApplyEnvDefaults` to reflect that empty strings are used as-is --- internal/backend/backendbase/sdklike_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/backend/backendbase/sdklike_test.go b/internal/backend/backendbase/sdklike_test.go index bca3b8a4ebf8..37ba45314400 100644 --- a/internal/backend/backendbase/sdklike_test.go +++ b/internal/backend/backendbase/sdklike_test.go @@ -264,9 +264,9 @@ func TestSDKLikeApplyEnvDefaults(t *testing.T) { "string_set_fallback": cty.StringVal("set in config"), "string_set_env": cty.StringVal("set in config"), "string_fallback_null": cty.StringVal("boop from fallback"), - "string_fallback_empty": cty.StringVal("boop from fallback"), + "string_fallback_empty": cty.StringVal(""), // config value is used "string_env_null": cty.StringVal("beep from environment"), - "string_env_empty": cty.StringVal("beep from environment"), + "string_env_empty": cty.StringVal(""), // config value is used "string_env_unsetfirst": cty.StringVal("beep from environment"), "string_env_unsetsecond": cty.StringVal("beep from environment"), "string_nothing_null": cty.NullVal(cty.String),