From 28b6cc6c5dbafa3d58ddb03a9d670b6b9eef121e Mon Sep 17 00:00:00 2001 From: Bas Harenslak Date: Fri, 17 Jul 2026 10:41:07 +0200 Subject: [PATCH 1/6] Support multiple fallback paths in VaultBackend --- .../docs/secrets-backends/hashicorp-vault.rst | 18 +++ .../providers/hashicorp/secrets/vault.py | 116 ++++++++++---- .../unit/hashicorp/secrets/test_vault.py | 148 ++++++++++++++---- 3 files changed, 219 insertions(+), 63 deletions(-) diff --git a/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst b/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst index faf4db70f015a..eab5fea1bb0b1 100644 --- a/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst +++ b/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst @@ -69,6 +69,24 @@ For example, if you want to set parameter ``connections_path`` to ``"airflow-con backend = airflow.providers.hashicorp.secrets.vault.VaultBackend backend_kwargs = {"connections_path": "airflow-connections", "variables_path": null, "mount_point": "airflow", "url": "http://127.0.0.1:8200"} +Falling back across multiple secret paths +"""""""""""""""""""""""""""""""""""""""" + +``connections_path``, ``variables_path``, and ``config_path`` can take a list of paths. Each path is tried in order, and +the first path where a secret is found returns. This can be used as a fallback mechanism for e.g. teams, environments, +etc.: + +.. code-block:: ini + + [secrets] + backend = airflow.providers.hashicorp.secrets.vault.VaultBackend + backend_kwargs = {"connections_path": ["airflow-prod/connections", "airflow-common/connections"], "mount_point": "airflow", "url": "http://127.0.0.1:8200"} + +.. note:: + Historically, the ``global_secrets_path`` parameter partially supported the above. The ``global_secrets_path`` + parameter was deprecated in favour of a more generic approach. It is appended to the list of paths until it is + completely removed. + Storing and Retrieving Connections using connection URI representation """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" diff --git a/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py b/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py index 72ac30901b2e3..265c7c7e123f5 100644 --- a/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py +++ b/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py @@ -19,8 +19,11 @@ from __future__ import annotations +import warnings +from collections.abc import Sequence from typing import TYPE_CHECKING +from airflow.exceptions import AirflowProviderDeprecationWarning from airflow.providers.common.compat.sdk import conf from airflow.providers.hashicorp._internal_client.vault_client import _VaultClient from airflow.secrets import BaseSecretsBackend @@ -47,16 +50,20 @@ class VaultBackend(BaseSecretsBackend, LoggingMixin): would be accessible if you provide ``{"connections_path": "connections"}`` and request conn_id ``smtp_default``. - :param connections_path: Specifies the path of the secret to read to get Connections. + :param connections_path: Specifies the path(s) of the secret to read to get Connections. Accepts a + single path or a list of paths, tried in order until a secret is found. (default: 'connections'). If set to None (null), requests for connections will not be sent to Vault. - :param variables_path: Specifies the path of the secret to read to get Variable. + :param variables_path: Specifies the path(s) of the secret to read to get Variable. Accepts a + single path or a list of paths, tried in order until a secret is found. (default: 'variables'). If set to None (null), requests for variables will not be sent to Vault. - :param config_path: Specifies the path of the secret to read Airflow Configurations + :param config_path: Specifies the path(s) of the secret to read Airflow Configurations. Accepts a + single path or a list of paths, tried in order until a secret is found. (default: 'config'). If set to None (null), requests for configurations will not be sent to Vault. :param use_team_secrets_path: Flag to enable team scoped secret retrieval from {base_path}/{team_name}/{key} in multi team deployments. (default: true) - :param global_secrets_path: Path prefix to add to global scoped connections and variables in multi team deployments. - (default: No prefix) + :param global_secrets_path: (Deprecated) Path appended as an extra fallback entry to + ``connections_path``, ``variables_path``, and ``config_path``. (default: No prefix) Use a list of + paths in those parameters instead, e.g. ``["connections/team1", "connections/common"]``. :param url: Base URL for the Vault instance being addressed. :param auth_type: Authentication Type for Vault. Default is ``token``. Available values are: ('approle', 'aws_iam', 'azure', 'github', 'gcp', 'jwt', 'kubernetes', 'ldap', 'radius', 'token', 'userpass') @@ -101,9 +108,9 @@ class VaultBackend(BaseSecretsBackend, LoggingMixin): def __init__( self, - connections_path: str | None = "connections", - variables_path: str | None = "variables", - config_path: str | None = "config", + connections_path: str | Sequence[str] | None = "connections", + variables_path: str | Sequence[str] | None = "variables", + config_path: str | Sequence[str] | None = "config", use_team_secrets_path: bool = True, global_secrets_path: str | None = None, url: str | None = None, @@ -136,13 +143,23 @@ def __init__( **kwargs, ): super().__init__() - self.connections_path = connections_path.rstrip("/") if connections_path is not None else None - self.variables_path = variables_path.rstrip("/") if variables_path is not None else None - self.config_path = config_path.rstrip("/") if config_path is not None else None + self.connections_path = self._normalize_paths(connections_path) + self.variables_path = self._normalize_paths(variables_path) + self.config_path = self._normalize_paths(config_path) self.use_team_secrets_path = use_team_secrets_path - self.global_secrets_path = ( - global_secrets_path.rstrip("/") if global_secrets_path is not None else None - ) + if global_secrets_path is not None: + warnings.warn( + "The `global_secrets_path` parameter is deprecated and will be removed in a future " + "release. Provide a list of fallback paths to `connections_path`, `variables_path`, " + 'and `config_path` instead, e.g. `connections_path=["connections/foobar", "connections/common"]`.', + AirflowProviderDeprecationWarning, + stacklevel=2, + ) + global_secrets_path = global_secrets_path.rstrip("/") + self.connections_path = self._append_path(self.connections_path, global_secrets_path) + self.variables_path = self._append_path(self.variables_path, global_secrets_path) + self.config_path = self._append_path(self.config_path, global_secrets_path) + self.mount_point = mount_point self.kv_engine_version = kv_engine_version self.vault_client = _VaultClient( @@ -176,6 +193,31 @@ def __init__( **kwargs, ) + @staticmethod + def _normalize_paths(paths: str | Sequence[str] | None) -> list[str] | None: + """ + Normalize a single path or a sequence of paths into a list, stripping trailing slashes. + + :param paths: Paths to normalize. + :return: Normalized paths. + """ + if paths is None: + return None + if isinstance(paths, str): + paths = [paths] + return [path.rstrip("/") for path in paths] + + @staticmethod + def _append_path(paths: list[str] | None, path: str) -> list[str]: + """ + Append a path into a list (which could be None). + + :param paths: List of paths to append to. + :param path: Path to append. + :return: Resulting list of paths. + """ + return [*(paths or []), path] + def _parse_path(self, secret_path: str) -> tuple[str | None, str | None]: if not self.mount_point: split_secret_path = secret_path.split("/", 1) @@ -200,29 +242,31 @@ def _get_secret_with_base(self, base_path: str | None, key: str) -> dict | None: secret_path=(mount_point + "/" if mount_point else "") + secret_path ) - def _get_team_or_global_secret(self, base_path: str | None, team_name: str | None, key: str): + def _get_secret(self, base_paths: list[str] | None, team_name: str | None, key: str): """ - Get a secret from a team specific path or the global path. + Get a secret, trying each of ``base_paths`` in order until one yields a value. + + If multi team is enabled and a team name is given, check {base_path}/{team_name}/{key}. + Otherwise, check {base_path}/{key}. - If multi team is enabled, check {base_path}/{team_name}/{key}, then fallback to {base_path}/{global_path}/{key} or {base_path}/{key}. + :param base_paths: Base paths to try, in order. + :param team_name: Team name associated to the task trying to access the secret (if any). + :param key: Secret key. """ - if base_path is None: + if not base_paths: return None - if ( - conf.getboolean("core", "multi_team", fallback=False) - and self.use_team_secrets_path - and team_name is not None - ): - response = self._get_secret_with_base(self.build_path(base_path, team_name), key) + multi_team_enabled = conf.getboolean("core", "multi_team", fallback=False) + for base_path in base_paths: + if multi_team_enabled and self.use_team_secrets_path and team_name is not None: + path_ = self.build_path(base_path, team_name) + else: + path_ = base_path + + response = self._get_secret_with_base(path_, key) if response is not None: return response - # Fallback to global secret - if conf.getboolean("core", "multi_team", fallback=False) and self.global_secrets_path is not None: - path = self.build_path(base_path, self.global_secrets_path) - else: - path = base_path - return self._get_secret_with_base(path, key) + return None # Make sure connection is imported this way for type checking, otherwise when importing # the backend it will get a circular dependency and fail @@ -241,7 +285,7 @@ def get_connection(self, conn_id: str, team_name: str | None = None) -> Connecti # problems when instantiating the backend during configuration from airflow.models.connection import Connection - response = self._get_team_or_global_secret(self.connections_path, team_name, conn_id) + response = self._get_secret(self.connections_path, team_name, conn_id) if response is None: return None @@ -259,7 +303,7 @@ def get_variable(self, key: str, team_name: str | None = None) -> str | None: :param team_name: Team name associated to the task trying to access the variable (if any) :return: Variable Value retrieved from the vault """ - response = self._get_team_or_global_secret(self.variables_path, team_name, key) + response = self._get_secret(self.variables_path, team_name, key) if not response: return None @@ -273,10 +317,16 @@ def get_config(self, key: str) -> str | None: """ Get Airflow Configuration. + Tries each path in ``config_path`` in order and returns the value from the first one found. + :param key: Configuration Option Key :return: Configuration Option Value retrieved from the vault """ - response = self._get_secret_with_base(self.config_path, key) + response = None + for base_path in self.config_path or []: + response = self._get_secret_with_base(base_path, key) + if response is not None: + break if not response: return None try: diff --git a/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py b/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py index 8b6af85a725da..ea229c046b0b1 100644 --- a/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py +++ b/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py @@ -16,11 +16,13 @@ # under the License. from __future__ import annotations +import warnings from unittest import mock import pytest from hvac.exceptions import InvalidPath, VaultError +from airflow.exceptions import AirflowProviderDeprecationWarning from airflow.providers.hashicorp.secrets.vault import VaultBackend from tests_common.test_utils.config import conf_vars @@ -138,20 +140,6 @@ def test_get_connection(self, mock_hvac): "foo", id="team_conn_no_separation", ), - pytest.param( - ["secret_not_found", "connection_result"], - {"global_secrets_path": "global"}, - ["/foo/test_postgres", "/global/test_postgres"], - "foo", - id="fallback_global_conn", - ), - pytest.param( - ["connection_result"], - {"global_secrets_path": "global"}, - ["/global/test_postgres"], - None, - id="global_conn_no_team", - ), ], ) @conf_vars({("core", "multi_team"): "True"}) @@ -184,7 +172,7 @@ def test_get_connection_value_multi_team( mock_client.secrets.kv.v2.read_secret_version.assert_has_calls( [ mock.call( - path=test_client.connections_path + path, + path=test_client.connections_path[0] + path, mount_point="airflow", version=None, raise_on_deleted_version=True, @@ -194,6 +182,54 @@ def test_get_connection_value_multi_team( ) assert connection.get_uri() == "postgresql://airflow:airflow@host:5432/airflow?foo=bar&baz=taz" + @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac") + def test_get_connection_value_with_list_of_paths(self, mock_hvac, secret_not_found, connection_result): + """Test fetching a connection when two paths are supplied and a secret is found in the 2nd path.""" + mock_client = mock.MagicMock() + mock_hvac.Client.return_value = mock_client + mock_client.secrets.kv.v2.read_secret_version.side_effect = [secret_not_found, connection_result] + + kwargs = { + "connections_path": ["connections/team1", "connections/common"], + "mount_point": "airflow", + "auth_type": "token", + "url": "http://127.0.0.1:8200", + "token": "s.7AU0I51yv1Q1lxOIg1F3ZRAS", + } + + test_client = VaultBackend(**kwargs) + connection = test_client.get_connection(conn_id="test_postgres") + mock_client.secrets.kv.v2.read_secret_version.assert_has_calls( + [ + mock.call( + path=path, + mount_point="airflow", + version=None, + raise_on_deleted_version=True, + ) + for path in ("connections/team1/test_postgres", "connections/common/test_postgres") + ] + ) + assert connection.get_uri() == "postgresql://airflow:airflow@host:5432/airflow?foo=bar&baz=taz" + + @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac") + def test_get_connection_value_with_list_of_paths_not_found(self, mock_hvac, secret_not_found): + """Test fetching a connection when two paths are supplied and a secret is not found in either path.""" + mock_client = mock.MagicMock() + mock_hvac.Client.return_value = mock_client + mock_client.secrets.kv.v2.read_secret_version.side_effect = [secret_not_found, secret_not_found] + + kwargs = { + "connections_path": ["connections/team1", "connections/common"], + "mount_point": "airflow", + "auth_type": "token", + "url": "http://127.0.0.1:8200", + "token": "s.7AU0I51yv1Q1lxOIg1F3ZRAS", + } + + test_client = VaultBackend(**kwargs) + assert test_client.get_connection(conn_id="test_postgres") is None + @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac") def test_get_connection_without_predefined_mount_point(self, mock_hvac): mock_client = mock.MagicMock() @@ -294,20 +330,6 @@ def test_get_variable_value(self, mock_hvac): "foo", id="team_var_no_separation", ), - pytest.param( - ["secret_not_found", "variable_result"], - {"global_secrets_path": "global"}, - ["/foo/hello", "/global/hello"], - "foo", - id="fallback_global_var", - ), - pytest.param( - ["variable_result"], - {"global_secrets_path": "global"}, - ["/global/hello"], - None, - id="global_var_no_team", - ), ], ) @conf_vars({("core", "multi_team"): "True"}) @@ -340,7 +362,7 @@ def test_get_variable_value_multi_team( mock_client.secrets.kv.v2.read_secret_version.assert_has_calls( [ mock.call( - path=test_client.variables_path + path, + path=test_client.variables_path[0] + path, mount_point="airflow", version=None, raise_on_deleted_version=True, @@ -350,6 +372,36 @@ def test_get_variable_value_multi_team( ) assert returned_uri == "world" + @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac") + def test_get_variable_value_multipath(self, mock_hvac, secret_not_found, variable_result): + """Verify fetching a variable and falling back with multiple paths configured.""" + mock_client = mock.MagicMock() + mock_hvac.Client.return_value = mock_client + mock_client.secrets.kv.v2.read_secret_version.side_effect = [secret_not_found, variable_result] + + kwargs = { + "variables_path": ["variables/team1", "variables/common"], + "mount_point": "airflow", + "auth_type": "token", + "url": "http://127.0.0.1:8200", + "token": "s.7AU0I51yv1Q1lxOIg1F3ZRAS", + } + + test_client = VaultBackend(**kwargs) + returned_uri = test_client.get_variable("hello") + mock_client.secrets.kv.v2.read_secret_version.assert_has_calls( + [ + mock.call( + path=path, + mount_point="airflow", + version=None, + raise_on_deleted_version=True, + ) + for path in ("variables/team1/hello", "variables/common/hello") + ] + ) + assert returned_uri == "world" + @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac") def test_get_variable_value_without_predefined_mount_point(self, mock_hvac): mock_client = mock.MagicMock() @@ -640,6 +692,42 @@ def test_get_config_value(self, mock_hvac): returned_uri = test_client.get_config("sql_alchemy_conn") assert returned_uri == "sqlite:////Users/airflow/airflow/airflow.db" + @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac") + def test_get_config_value_multipath(self, mock_hvac, secret_not_found): + """Verify fetching a config value and falling back with multiple paths configured.""" + mock_client = mock.MagicMock() + mock_hvac.Client.return_value = mock_client + found_config = { + "data": { + "data": {"value": "sqlite:////Users/airflow/airflow/airflow.db"}, + "metadata": {"version": 1}, + }, + } + mock_client.secrets.kv.v2.read_secret_version.side_effect = [secret_not_found, found_config] + + kwargs = { + "config_path": ["config/team1", "config/common"], + "mount_point": "secret", + "auth_type": "token", + "url": "http://127.0.0.1:8200", + "token": "s.FnL7qg0YnHZDpf4zKKuFy0UK", + } + + test_client = VaultBackend(**kwargs) + returned_uri = test_client.get_config("sql_alchemy_conn") + mock_client.secrets.kv.v2.read_secret_version.assert_has_calls( + [ + mock.call( + path=path, + mount_point="secret", + version=None, + raise_on_deleted_version=True, + ) + for path in ("config/team1/sql_alchemy_conn", "config/common/sql_alchemy_conn") + ] + ) + assert returned_uri == "sqlite:////Users/airflow/airflow/airflow.db" + @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac") def test_get_config_value_without_predefined_mount_point(self, mock_hvac): mock_client = mock.MagicMock() From c1baf6940502a702cc85f39fabeb9c668b5e5adb Mon Sep 17 00:00:00 2001 From: Bas Harenslak Date: Fri, 17 Jul 2026 11:13:30 +0200 Subject: [PATCH 2/6] Fix postgres scheme now that TaskSDKConnection.from_json normalizes postgres scheme --- .../hashicorp/tests/unit/hashicorp/secrets/test_vault.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py b/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py index b9bf2ebaff664..509e0dd1e3b08 100644 --- a/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py +++ b/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py @@ -187,7 +187,7 @@ def test_get_connection_value_multi_team( assert json.loads(value)["conn_type"] == "postgresql" @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac") - def test_get_connection_value_with_list_of_paths(self, mock_hvac, secret_not_found, connection_result): + def test_get_connection_value_multipath(self, mock_hvac, secret_not_found, connection_result): """Test fetching a connection when two paths are supplied and a secret is found in the 2nd path.""" mock_client = mock.MagicMock() mock_hvac.Client.return_value = mock_client @@ -214,7 +214,7 @@ def test_get_connection_value_with_list_of_paths(self, mock_hvac, secret_not_fou for path in ("connections/team1/test_postgres", "connections/common/test_postgres") ] ) - assert connection.get_uri() == "postgresql://airflow:airflow@host:5432/airflow?foo=bar&baz=taz" + assert connection.get_uri() == "postgres://airflow:airflow@host:5432/airflow?foo=bar&baz=taz" @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac") def test_get_connection_value_with_list_of_paths_not_found(self, mock_hvac, secret_not_found): From 0c328c167318f33a2ebed95bcfe7d091b86c02a3 Mon Sep 17 00:00:00 2001 From: Bas Harenslak Date: Wed, 22 Jul 2026 13:43:49 +0200 Subject: [PATCH 3/6] Add test verifying if global_secrets_path is still leveraged as a fallback path --- .../unit/hashicorp/secrets/test_vault.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py b/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py index 509e0dd1e3b08..8ceb3d6d581f1 100644 --- a/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py +++ b/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py @@ -22,6 +22,7 @@ import pytest from hvac.exceptions import InvalidPath, VaultError +from airflow.exceptions import AirflowProviderDeprecationWarning from airflow.providers.hashicorp.secrets.vault import VaultBackend from tests_common.test_utils.config import conf_vars @@ -910,3 +911,37 @@ def test_config_path_none_value(self, mock_hvac): test_client = VaultBackend(**kwargs) assert test_client.get_config("test") is None mock_hvac.Client.assert_not_called() + + @mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac") + def test_global_secrets_path(self, mock_hvac, secret_not_found, connection_result): + """ + Verify if the (deprecated) global_secrets_path is appended to connections path. + """ + mock_client = mock.MagicMock() + mock_hvac.Client.return_value = mock_client + mock_client.secrets.kv.v2.read_secret_version.side_effect = [secret_not_found, connection_result] + + kwargs = { + "connections_path": "connections", + "global_secrets_path": "connections/common", + "mount_point": "airflow", + "auth_type": "token", + "url": "http://127.0.0.1:8200", + "token": "s.7AU0I51yv1Q1lxOIg1F3ZRAS", + } + + with pytest.warns(AirflowProviderDeprecationWarning): # Ensure global_secrets_path raises a deprecation warning + test_client = VaultBackend(**kwargs) + connection = test_client.get_connection(conn_id="test_postgres") + mock_client.secrets.kv.v2.read_secret_version.assert_has_calls( + [ + mock.call( + path=path, + mount_point="airflow", + version=None, + raise_on_deleted_version=True, + ) + for path in ("connections/test_postgres", "connections/common/test_postgres") + ] + ) + assert connection.get_uri() == "postgres://airflow:airflow@host:5432/airflow?foo=bar&baz=taz" From 370b9a0eb0e52422f7ee937d2f06e166cfe97a04 Mon Sep 17 00:00:00 2001 From: Bas Harenslak Date: Sat, 25 Jul 2026 08:58:15 +0200 Subject: [PATCH 4/6] Fix title underline --- providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst b/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst index eab5fea1bb0b1..336b116f85d0a 100644 --- a/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst +++ b/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst @@ -70,7 +70,7 @@ For example, if you want to set parameter ``connections_path`` to ``"airflow-con backend_kwargs = {"connections_path": "airflow-connections", "variables_path": null, "mount_point": "airflow", "url": "http://127.0.0.1:8200"} Falling back across multiple secret paths -"""""""""""""""""""""""""""""""""""""""" +""""""""""""""""""""""""""""""""""""""""" ``connections_path``, ``variables_path``, and ``config_path`` can take a list of paths. Each path is tried in order, and the first path where a secret is found returns. This can be used as a fallback mechanism for e.g. teams, environments, From 8bbb36e64bbb16a63eba2a6a470cc02acbcad1fd Mon Sep 17 00:00:00 2001 From: Bas Harenslak Date: Sat, 25 Jul 2026 09:01:58 +0200 Subject: [PATCH 5/6] Fix spelling --- providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst b/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst index 336b116f85d0a..5ac1e7bdfe4bb 100644 --- a/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst +++ b/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst @@ -84,7 +84,7 @@ etc.: .. note:: Historically, the ``global_secrets_path`` parameter partially supported the above. The ``global_secrets_path`` - parameter was deprecated in favour of a more generic approach. It is appended to the list of paths until it is + parameter was deprecated in favor of a more generic approach. It is appended to the list of paths until it is completely removed. Storing and Retrieving Connections using connection URI representation From 5a11d59517fe4bbe9d7c8b22a4b3474686d07149 Mon Sep 17 00:00:00 2001 From: Bas Harenslak Date: Sat, 25 Jul 2026 09:03:25 +0200 Subject: [PATCH 6/6] Move comment up to avoid weird ruff format --- providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py b/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py index 8ceb3d6d581f1..3b45df0e23e71 100644 --- a/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py +++ b/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py @@ -930,7 +930,8 @@ def test_global_secrets_path(self, mock_hvac, secret_not_found, connection_resul "token": "s.7AU0I51yv1Q1lxOIg1F3ZRAS", } - with pytest.warns(AirflowProviderDeprecationWarning): # Ensure global_secrets_path raises a deprecation warning + # Ensure global_secrets_path raises a deprecation warning + with pytest.warns(AirflowProviderDeprecationWarning): test_client = VaultBackend(**kwargs) connection = test_client.get_connection(conn_id="test_postgres") mock_client.secrets.kv.v2.read_secret_version.assert_has_calls(