Repository: PrefectHQ/prefect

Issue:
Special-case secrets from environment
Right now, setting
```
PREFECT__CONTEXT__SECRETS__MY_API_KEY=hello
```
creates a local secret called `my_api_key` (lowercase), and the only way to create a local secret called `MY_API_KEY` is by setting the value directly in your user config.  Ideally we should allow both secret setting mechanisms to work smoothly.


Discussion:

Resolving patch:
diff --git a/src/prefect/client/secrets.py b/src/prefect/client/secrets.py
--- a/src/prefect/client/secrets.py
+++ b/src/prefect/client/secrets.py
@@ -31,6 +31,12 @@
 prefect.context.secrets["MY_KEY"] = "MY_VALUE"
 ```
 
+or specify the secret via environment variable:
+
+```bash
+export PREFECT__CONTEXT__SECRETS__MY_KEY="MY_VALUE"
+```
+
 ::: tip
 When settings secrets via `.toml` config files, you can use the [TOML Keys](https://github.com/toml-lang/toml#keys) docs for data structure specifications. Running `prefect` commands with invalid `.toml` config files will lead to tracebacks that contain references to: `..../toml/decoder.py`.
 :::
diff --git a/src/prefect/configuration.py b/src/prefect/configuration.py
--- a/src/prefect/configuration.py
+++ b/src/prefect/configuration.py
@@ -222,9 +222,17 @@ def interpolate_config(config: dict, env_var_prefix: str = None) -> Config:
                 value = cast(str, env_var_value.encode().decode("unicode_escape"))
 
                 # place the env var in the flat config as a compound key
-                config_option = collections.CompoundKey(
-                    env_var_option.lower().split("__")
-                )
+                if env_var_option.upper().startswith("CONTEXT__SECRETS"):
+                    formatted_option = env_var_option.split("__")
+                    formatted_option[:-1] = [
+                        val.lower() for val in formatted_option[:-1]
+                    ]
+                    config_option = collections.CompoundKey(formatted_option)
+                else:
+                    config_option = collections.CompoundKey(
+                        env_var_option.lower().split("__")
+                    )
+
                 flat_config[config_option] = string_to_type(
                     cast(str, interpolate_env_vars(value))
                 )


Test patch:
diff --git a/tests/client/test_secrets.py b/tests/client/test_secrets.py
--- a/tests/client/test_secrets.py
+++ b/tests/client/test_secrets.py
@@ -36,6 +36,12 @@ def test_secret_value_pulled_from_context():
 
 
 def test_secret_value_depends_on_use_local_secrets(monkeypatch):
+    response = {"errors": "Malformed Authorization header"}
+    post = MagicMock(return_value=MagicMock(json=MagicMock(return_value=response)))
+    session = MagicMock()
+    session.return_value.post = post
+    monkeypatch.setattr("requests.Session", session)
+
     secret = Secret(name="test")
     with set_temporary_config(
         {"cloud.use_local_secrets": False, "cloud.auth_token": None}
diff --git a/tests/test_configuration.py b/tests/test_configuration.py
--- a/tests/test_configuration.py
+++ b/tests/test_configuration.py
@@ -2,6 +2,7 @@
 import os
 import shlex
 import subprocess
+import sys
 import tempfile
 import uuid
 
@@ -367,3 +368,37 @@ def test_env_vars_are_interpolated_as_lower_case(self, monkeypatch):
         assert "KeY" in config.SeCtIoN
         assert config.SeCtIoN.KeY == 1
         assert config.section.key == 2
+
+    @pytest.mark.skipif(
+        sys.platform == "win32",
+        reason="Windows converts env vars to uppercase automatically.",
+    )
+    def test_env_vars_for_secrets_alone_are_not_lower_cased(self, monkeypatch):
+
+        monkeypatch.setenv("PREFECT_TEST__CONTEXT__SECRETS__mY_spECIal_kEY", "42")
+        monkeypatch.setenv("PREFECT_TEST__CONTEXT__strange_VAlUE", "false")
+        monkeypatch.setenv("PREFECT_TEST__CONTEXT__FLOW_RUN_ID", "12345")
+
+        with tempfile.TemporaryDirectory() as test_config_dir:
+            test_config_loc = os.path.join(test_config_dir, "test_config.toml")
+            with open(test_config_loc, "wb") as test_config:
+                test_config.write(
+                    b"""
+                    [context]
+                    spECIAL_TOP_key = "foo"
+
+                    [context.secrets]
+                    KeY = 1
+                    """
+                )
+
+            config = configuration.load_configuration(
+                test_config_loc, env_var_prefix="PREFECT_TEST"
+            )
+
+        assert "secrets" in config.context
+        assert config.context.secrets.mY_spECIal_kEY == 42
+        assert config.context.secrets.KeY == 1
+        assert config.context.spECIAL_TOP_key == "foo"
+        assert config.context.flow_run_id == 12345
+        assert config.context.strange_value is False