This is an automated email from the ASF dual-hosted git repository.

amoghrajesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 68ee32a61f4 Authenticate only once per task process to external 
secrets backends (#71756)
68ee32a61f4 is described below

commit 68ee32a61f41ae65122d2804100f581ccd5d088e
Author: Amogh Desai <[email protected]>
AuthorDate: Wed Aug 26 10:32:31 2026 +0530

    Authenticate only once per task process to external secrets backends 
(#71756)
---
 task-sdk/src/airflow/sdk/configuration.py          | 30 +++++++++++------
 task-sdk/tests/conftest.py                         | 10 ++++++
 .../tests/task_sdk/execution_time/test_secrets.py  | 38 ++++++++++++++++++++++
 3 files changed, 68 insertions(+), 10 deletions(-)

diff --git a/task-sdk/src/airflow/sdk/configuration.py 
b/task-sdk/src/airflow/sdk/configuration.py
index 395ecb5b491..e6bcf0ca429 100644
--- a/task-sdk/src/airflow/sdk/configuration.py
+++ b/task-sdk/src/airflow/sdk/configuration.py
@@ -304,22 +304,32 @@ def initialize_secrets_backends(
     return backend_list
 
 
+_secrets_backend_cache: dict[tuple[str, ...], list] = {}
+
+
+def clear_secrets_backends_cache() -> None:
+    """Drop the memoised backends so the next load rebuilds them from the 
current config."""
+    _secrets_backend_cache.clear()
+
+
 def ensure_secrets_loaded(
     default_backends: list[str] = _SERVER_DEFAULT_SECRETS_SEARCH_PATH,
 ) -> list:
     """
-    Ensure that all secrets backends are loaded.
+    Return the secrets backends for the given search path, building them once 
per process.
 
-    If the secrets_backend_list contains only 2 default backends, reload it.
+    A backend holds an authenticated client, so rebuilding one per lookup 
makes every secret
+    fetch authenticate again against the remote store. Nothing is memoised 
until a custom
+    backend is configured, so one appearing after the first lookup is still 
picked up.
     """
-    # Check if the secrets_backend_list contains only 2 default backends.
-
-    # Check if we are loading the backends for worker too by checking if the 
default_backends is equal
-    # to _SERVER_DEFAULT_SECRETS_SEARCH_PATH.
-    secrets_backend_list = initialize_secrets_backends()
-    if len(secrets_backend_list) == 2 or default_backends != 
_SERVER_DEFAULT_SECRETS_SEARCH_PATH:
-        return initialize_secrets_backends(default_backends=default_backends)
-    return secrets_backend_list
+    key = tuple(default_backends)
+    if key not in _secrets_backend_cache:
+        backends = 
initialize_secrets_backends(default_backends=default_backends)
+        # Equal lengths mean nothing was prepended, so no custom backend is 
configured yet.
+        if len(backends) == len(default_backends):
+            return backends
+        _secrets_backend_cache[key] = backends
+    return _secrets_backend_cache[key]
 
 
 def initialize_config() -> AirflowSDKConfigParser:
diff --git a/task-sdk/tests/conftest.py b/task-sdk/tests/conftest.py
index eed48a1554c..a7d14ef1b54 100644
--- a/task-sdk/tests/conftest.py
+++ b/task-sdk/tests/conftest.py
@@ -174,6 +174,16 @@ def _disable_ol_plugin():
     airflow.sdk.plugins_manager._get_plugins = old
 
 
[email protected](autouse=True)
+def _clear_secrets_backends_cache():
+    """Keep memoised backends from leaking config between tests."""
+    from airflow.sdk.configuration import clear_secrets_backends_cache
+
+    clear_secrets_backends_cache()
+    yield
+    clear_secrets_backends_cache()
+
+
 @pytest.fixture(autouse=True)
 def _cleanup_async_resources(request):
     """
diff --git a/task-sdk/tests/task_sdk/execution_time/test_secrets.py 
b/task-sdk/tests/task_sdk/execution_time/test_secrets.py
index b97a85f3693..2fe41e328ce 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_secrets.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_secrets.py
@@ -20,8 +20,13 @@ from __future__ import annotations
 import pytest
 
 from airflow.sdk.api.datamodels._generated import ConnectionResponse
+from airflow.sdk.configuration import clear_secrets_backends_cache, 
ensure_secrets_loaded
 from airflow.sdk.exceptions import AirflowSecretsBackendAccessDenied, ErrorType
 from airflow.sdk.execution_time.comms import ConnectionResult, ErrorResponse, 
VariableResult
+from airflow.sdk.execution_time.secrets import (
+    _SERVER_DEFAULT_SECRETS_SEARCH_PATH,
+    DEFAULT_SECRETS_SEARCH_PATH_WORKERS,
+)
 from airflow.sdk.execution_time.secrets.execution_api import 
ExecutionAPISecretsBackend
 
 
@@ -302,3 +307,36 @@ class TestContextDetection:
         assert "EnvironmentVariablesBackend" in backend_classes
         assert "MetastoreBackend" not in backend_classes
         assert "ExecutionAPISecretsBackend" not in backend_classes
+
+
+class TestSecretsBackendMemoisation:
+    BACKEND = 
"airflow.secrets.environment_variables.EnvironmentVariablesBackend"
+
+    def test_nothing_is_memoised_until_a_custom_backend_is_configured(self):
+        first = 
ensure_secrets_loaded(default_backends=_SERVER_DEFAULT_SECRETS_SEARCH_PATH)
+
+        assert 
ensure_secrets_loaded(default_backends=_SERVER_DEFAULT_SECRETS_SEARCH_PATH) is 
not first
+
+    def test_backends_are_built_once_with_a_custom_backend(self, monkeypatch):
+        monkeypatch.setenv("AIRFLOW__SECRETS__BACKEND", self.BACKEND)
+
+        first = 
ensure_secrets_loaded(default_backends=_SERVER_DEFAULT_SECRETS_SEARCH_PATH)
+
+        assert 
ensure_secrets_loaded(default_backends=_SERVER_DEFAULT_SECRETS_SEARCH_PATH) is 
first
+
+    def test_worker_chain_is_memoised_separately_from_the_server_chain(self, 
monkeypatch):
+        monkeypatch.setenv("AIRFLOW__SECRETS__BACKEND", self.BACKEND)
+
+        server = 
ensure_secrets_loaded(default_backends=_SERVER_DEFAULT_SECRETS_SEARCH_PATH)
+        worker = 
ensure_secrets_loaded(default_backends=DEFAULT_SECRETS_SEARCH_PATH_WORKERS)
+
+        assert server is not worker
+        assert 
ensure_secrets_loaded(default_backends=DEFAULT_SECRETS_SEARCH_PATH_WORKERS) is 
worker
+
+    def test_clearing_the_cache_rebuilds_backends(self, monkeypatch):
+        monkeypatch.setenv("AIRFLOW__SECRETS__BACKEND", self.BACKEND)
+
+        first = 
ensure_secrets_loaded(default_backends=_SERVER_DEFAULT_SECRETS_SEARCH_PATH)
+        clear_secrets_backends_cache()
+
+        assert 
ensure_secrets_loaded(default_backends=_SERVER_DEFAULT_SECRETS_SEARCH_PATH) is 
not first

Reply via email to