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" diff --git a/internal/backend/backendbase/base_test.go b/internal/backend/backendbase/base_test.go index 0882489df2c3..e892cc79b17c 100644 --- a/internal/backend/backendbase/base_test.go +++ b/internal/backend/backendbase/base_test.go @@ -239,3 +239,54 @@ 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", + EnvVars: []string{"FOO"}, + }, + }, + } + + 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(""), + })) + 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 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 { + 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. 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),