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):

Reply via email to