This is an automated email from the ASF dual-hosted git repository.
jedcunningham pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-3-test by this push:
new 4d23d2968a8 [v3-3-test] Authenticate only once per task process to
external secrets backends (#71756) (#72237)
4d23d2968a8 is described below
commit 4d23d2968a8c7fa4b6e6389b9fd986c8b02e69a6
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Aug 28 16:27:43 2026 -0600
[v3-3-test] Authenticate only once per task process to external secrets
backends (#71756) (#72237)
(cherry picked from commit 68ee32a61f41ae65122d2804100f581ccd5d088e)
Co-authored-by: Amogh Desai <[email protected]>
---
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