diff --git a/.changes/v1.15/BUG FIXES-20260106-120000.yaml b/.changes/v1.15/BUG FIXES-20260106-120000.yaml new file mode 100644 index 000000000000..7158526dbed5 --- /dev/null +++ b/.changes/v1.15/BUG FIXES-20260106-120000.yaml @@ -0,0 +1,6 @@ +kind: BUG FIXES +body: 'backend/s3: `AWS_USE_FIPS_ENDPOINT` and `AWS_USE_DUALSTACK_ENDPOINT` environment variables are now properly parsed as booleans, allowing values like "false" to correctly disable FIPS/DualStack endpoints' +time: 2026-01-06T12:00:00.000000Z +custom: + Issue: "37601" + diff --git a/internal/backend/remote-state/s3/backend.go b/internal/backend/remote-state/s3/backend.go index be1af3f6fd19..ef1e5d40cb4f 100644 --- a/internal/backend/remote-state/s3/backend.go +++ b/internal/backend/remote-state/s3/backend.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "regexp" + "strconv" "strings" "time" "unicode" @@ -1331,14 +1332,16 @@ func boolAttrOk(obj cty.Value, name string) (bool, bool) { } } -// boolAttrDefaultEnvVarOk checks for a configured bool argument or a non-empty -// value in any of the provided environment variables. If any of the environment -// variables are non-empty, to boolean is considered true. +// boolAttrDefaultEnvVarOk checks for a configured bool argument or a boolean +// value in any of the provided environment variables. Environment variable +// values are parsed using strconv.ParseBool. Invalid values are ignored. func boolAttrDefaultEnvVarOk(obj cty.Value, name string, envvars ...string) (bool, bool) { if val := obj.GetAttr(name); val.IsNull() { for _, envvar := range envvars { if v := os.Getenv(envvar); v != "" { - return true, true + if b, err := strconv.ParseBool(v); err == nil { + return b, true + } } } return false, false diff --git a/internal/backend/remote-state/s3/backend_test.go b/internal/backend/remote-state/s3/backend_test.go index ace878f21933..3368d80dcf01 100644 --- a/internal/backend/remote-state/s3/backend_test.go +++ b/internal/backend/remote-state/s3/backend_test.go @@ -3963,3 +3963,85 @@ func objectLockPreCheck(t *testing.T) { t.Skip("s3 backend tests using object lock enabled buckets require setting TF_S3_OBJECT_LOCK_TEST") } } + +func TestBoolAttrDefaultEnvVarOk(t *testing.T) { + cases := map[string]struct { + envValue string + expectedBool bool + expectedOk bool + }{ + "true": { + envValue: "true", + expectedBool: true, + expectedOk: true, + }, + "TRUE": { + envValue: "TRUE", + expectedBool: true, + expectedOk: true, + }, + "True": { + envValue: "True", + expectedBool: true, + expectedOk: true, + }, + "1": { + envValue: "1", + expectedBool: true, + expectedOk: true, + }, + "false": { + envValue: "false", + expectedBool: false, + expectedOk: true, + }, + "FALSE": { + envValue: "FALSE", + expectedBool: false, + expectedOk: true, + }, + "False": { + envValue: "False", + expectedBool: false, + expectedOk: true, + }, + "0": { + envValue: "0", + expectedBool: false, + expectedOk: true, + }, + "invalid value": { + envValue: "invalid", + expectedBool: false, + expectedOk: false, + }, + "empty string": { + envValue: "", + expectedBool: false, + expectedOk: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + servicemocks.StashEnv(t) + + if tc.envValue != "" { + os.Setenv("TEST_BOOL_ENV_VAR", tc.envValue) + } + + obj := cty.ObjectVal(map[string]cty.Value{ + "test_bool": cty.NullVal(cty.Bool), + }) + + gotBool, gotOk := boolAttrDefaultEnvVarOk(obj, "test_bool", "TEST_BOOL_ENV_VAR") + + if gotBool != tc.expectedBool { + t.Errorf("expected bool %v, got %v", tc.expectedBool, gotBool) + } + if gotOk != tc.expectedOk { + t.Errorf("expected ok %v, got %v", tc.expectedOk, gotOk) + } + }) + } +}