This is an automated email from the ASF dual-hosted git repository.
potiuk 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 63d9d874a66 fix(providers/cncf-kubernetes): apply verify_ssl=False to
returned ApiClient in KubernetesHook (#63478)
63d9d874a66 is described below
commit 63d9d874a669688f64d5a56c82e5d014afea26ce
Author: Antonio Mello <[email protected]>
AuthorDate: Sat Mar 14 11:00:17 2026 -0300
fix(providers/cncf-kubernetes): apply verify_ssl=False to returned
ApiClient in KubernetesHook (#63478)
* Fix KubernetesHook verify_ssl flag not applied to returned ApiClient
When disable_verify_ssl=True, _disable_verify_ssl() sets the global
Configuration default via Configuration.set_default(). However,
config.load_kube_config() called immediately after with
client_configuration=None creates a fresh Configuration instance,
loads the kubeconfig into it (with verify_ssl=True), and overwrites
the global default via Configuration.set_default() again. The
_TimeoutK8sApiClient() created without arguments then calls
Configuration.get_default_copy() and gets verify_ssl=True.
Fix by ensuring that when disable_verify_ssl=True, self.client_configuration
is initialized from the updated default (with verify_ssl=False) before
calling load_kube_config(), then re-applying verify_ssl=False after the
load (since load_and_set() may overwrite it), and finally passing the
configuration explicitly to _TimeoutK8sApiClient.
Closes #56432
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: narrow client_configuration type for mypy union-attr check
Replace `assert` with conditional check to satisfy both mypy
(union-attr on Optional type) and ruff (S101 no-assert rule).
Fixes #56432
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: centralize disable_verify_ssl in _TimeoutK8sApiClient
constructor
Move verify_ssl handling into _TimeoutK8sApiClient.__init__ so callers
pass disable_verify_ssl as a flag instead of repeating the config
adjustment at every call site. Reduces risk of future inconsistency.
Addresses review feedback from jscheffl.
Fixes #56432
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../providers/cncf/kubernetes/hooks/kubernetes.py | 53 ++++++++++++++++++----
.../unit/cncf/kubernetes/hooks/test_kubernetes.py | 45 +++++++++++++++++-
2 files changed, 89 insertions(+), 9 deletions(-)
diff --git
a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/hooks/kubernetes.py
b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/hooks/kubernetes.py
index 81f09b0bee4..5aa20cf5cba 100644
---
a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/hooks/kubernetes.py
+++
b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/hooks/kubernetes.py
@@ -81,7 +81,25 @@ def _get_request_timeout(timeout_seconds: int | None) ->
float:
class _TimeoutK8sApiClient(client.ApiClient):
- """Wrapper around kubernetes sync ApiClient to set default timeout."""
+ """
+ Wrapper around kubernetes sync ApiClient to set default timeout.
+
+ When *disable_verify_ssl* is True the TLS certificate check is turned off
+ on the *client_configuration* that is passed (or on a fresh default copy)
+ so that callers do not need to repeat this logic at every call-site.
+ """
+
+ def __init__(
+ self,
+ configuration: client.Configuration | None = None,
+ *,
+ disable_verify_ssl: bool = False,
+ ) -> None:
+ if disable_verify_ssl:
+ if configuration is None:
+ configuration = client.Configuration.get_default_copy()
+ configuration.verify_ssl = False
+ super().__init__(configuration=configuration)
def call_api(self, *args, **kwargs):
timeout_seconds = kwargs.get("timeout_seconds") # get server-side
timeout
@@ -302,7 +320,10 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
self.log.debug("loading kube_config from: in_cluster
configuration")
self._is_in_cluster = True
config.load_incluster_config()
- return _TimeoutK8sApiClient()
+ return _TimeoutK8sApiClient(
+ configuration=self.client_configuration,
+ disable_verify_ssl=disable_verify_ssl is True,
+ )
if kubeconfig_path is not None:
self.log.debug("loading kube_config from: %s", kubeconfig_path)
@@ -312,7 +333,10 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
client_configuration=self.client_configuration,
context=cluster_context,
)
- return _TimeoutK8sApiClient()
+ return _TimeoutK8sApiClient(
+ configuration=self.client_configuration,
+ disable_verify_ssl=disable_verify_ssl is True,
+ )
if kubeconfig is not None:
with tempfile.NamedTemporaryFile() as temp_config:
@@ -327,7 +351,10 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
client_configuration=self.client_configuration,
context=cluster_context,
)
- return _TimeoutK8sApiClient()
+ return _TimeoutK8sApiClient(
+ configuration=self.client_configuration,
+ disable_verify_ssl=disable_verify_ssl is True,
+ )
if self.config_dict:
self.log.debug(LOADING_KUBE_CONFIG_FILE_RESOURCE.format("config
dictionary"))
@@ -337,11 +364,18 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
client_configuration=self.client_configuration,
context=cluster_context,
)
- return _TimeoutK8sApiClient()
+ return _TimeoutK8sApiClient(
+ configuration=self.client_configuration,
+ disable_verify_ssl=disable_verify_ssl is True,
+ )
- return self._get_default_client(cluster_context=cluster_context)
+ return self._get_default_client(
+ cluster_context=cluster_context,
disable_verify_ssl=disable_verify_ssl
+ )
- def _get_default_client(self, *, cluster_context: str | None = None) ->
client.ApiClient:
+ def _get_default_client(
+ self, *, cluster_context: str | None = None, disable_verify_ssl: bool
| None = None
+ ) -> client.ApiClient:
# if we get here, then no configuration has been supplied
# we should try in_cluster since that's most likely
# but failing that just load assuming a kubeconfig file
@@ -356,7 +390,10 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
client_configuration=self.client_configuration,
context=cluster_context,
)
- return _TimeoutK8sApiClient()
+ return _TimeoutK8sApiClient(
+ configuration=self.client_configuration,
+ disable_verify_ssl=disable_verify_ssl is True,
+ )
@property
def is_in_cluster(self) -> bool:
diff --git
a/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/hooks/test_kubernetes.py
b/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/hooks/test_kubernetes.py
index 0f51a6b6170..49b70c02793 100644
---
a/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/hooks/test_kubernetes.py
+++
b/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/hooks/test_kubernetes.py
@@ -329,6 +329,49 @@ class TestKubernetesHook:
assert mock_disable.called is disable_called
assert isinstance(api_conn, kubernetes.client.api_client.ApiClient)
+ @pytest.mark.parametrize(
+ "config_source",
+ [
+ pytest.param("kube_config_path", id="kube_config_path"),
+ pytest.param("kube_config", id="kube_config"),
+ pytest.param("config_dict", id="config_dict"),
+ pytest.param("default", id="default_client"),
+ ],
+ )
+ @patch("kubernetes.config.incluster_config.InClusterConfigLoader",
new=MagicMock())
+ @patch("kubernetes.config.kube_config.KubeConfigLoader", new=MagicMock())
+ @patch("kubernetes.config.kube_config.KubeConfigMerger", new=MagicMock())
+ def test_disable_verify_ssl_applies_to_client_configuration(self,
config_source):
+ """
+ Verifies that when disable_verify_ssl=True, the returned ApiClient has
verify_ssl=False.
+
+ Previously, _disable_verify_ssl() only mutated the global default
configuration via
+ Configuration.set_default(), but config.load_kube_config() would
subsequently overwrite
+ that default with verify_ssl=True from the kubeconfig file. This test
ensures that
+ verify_ssl=False is correctly propagated to the returned ApiClient's
configuration.
+ """
+ if config_source == "kube_config_path":
+ kubernetes_hook = KubernetesHook(
+ conn_id="kube_config_path",
+ disable_verify_ssl=True,
+ )
+ elif config_source == "kube_config":
+ kubernetes_hook = KubernetesHook(
+ conn_id="kube_config",
+ disable_verify_ssl=True,
+ )
+ elif config_source == "config_dict":
+ kubernetes_hook = KubernetesHook(
+ config_dict={"apiVersion": "v1", "kind": "Config"},
+ disable_verify_ssl=True,
+ )
+ else:
+ kubernetes_hook = KubernetesHook(disable_verify_ssl=True)
+
+ api_conn = kubernetes_hook.get_conn()
+ assert isinstance(api_conn, kubernetes.client.api_client.ApiClient)
+ assert api_conn.configuration.verify_ssl is False
+
@pytest.mark.parametrize(
("disable_tcp_keepalive", "conn_id", "expected"),
(
@@ -514,7 +557,7 @@ class TestKubernetesHook:
with mock.patch.dict("os.environ",
AIRFLOW_CONN_KUBERNETES_DEFAULT=conn_uri):
kubernetes_hook = KubernetesHook(conn_id="kubernetes_default")
kubernetes_hook.get_conn()
- mock_get_client.assert_called_with(cluster_context="test")
+ mock_get_client.assert_called_with(cluster_context="test",
disable_verify_ssl=None)
assert kubernetes_hook.get_namespace() == "test"
def test_missing_default_connection_is_ok(self, remove_default_conn,
sdk_connection_not_found):