Copilot commented on code in PR #65566:
URL: https://github.com/apache/airflow/pull/65566#discussion_r3113197078


##########
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/hooks/test_kubernetes.py:
##########
@@ -1935,3 +1952,96 @@ async def test_wait_until_container_started(
             ]
         )
         mock_sleep.assert_awaited_once_with(10)
+
+    # -------------------------------------------------------------------------
+    # Tests for the per-instance Configuration isolation fix
+    # -------------------------------------------------------------------------
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.cncf.kubernetes.hooks.kubernetes.async_config.load_kube_config_from_dict")
+    async def test_load_config_initialises_client_configuration(self, 
mock_load):
+        """_load_config allocates a fresh per-instance Configuration when none 
was supplied."""
+        mock_load.return_value = None
+        hook = AsyncKubernetesHook(conn_id=None, in_cluster=False, 
config_dict={"a": "b"})
+        assert hook.client_configuration is None
+
+        await hook._load_config()
+
+        assert isinstance(hook.client_configuration, 
async_client.Configuration)
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.cncf.kubernetes.hooks.kubernetes.async_config.load_incluster_config")
+    async def test_load_config_incluster_passes_client_configuration(self, 
mock_incluster):
+        """load_incluster_config is called with the per-instance 
client_configuration."""
+        hook = AsyncKubernetesHook(conn_id=None, in_cluster=True)
+
+        await hook._load_config()
+
+        
mock_incluster.assert_called_once_with(client_configuration=hook.client_configuration)
+        assert isinstance(hook.client_configuration, 
async_client.Configuration)
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.cncf.kubernetes.hooks.kubernetes.async_config.load_kube_config_from_dict")
+    async def test_load_config_config_dict_passes_client_configuration(self, 
mock_load):
+        """load_kube_config_from_dict is called with the per-instance 
client_configuration."""
+        mock_load.return_value = None
+        hook = AsyncKubernetesHook(conn_id=None, in_cluster=False, 
config_dict={"a": "b"})
+
+        await hook._load_config()
+
+        mock_load.assert_called_once()
+        _, kwargs = mock_load.call_args
+        assert "client_configuration" in kwargs
+        assert kwargs["client_configuration"] is hook.client_configuration
+        assert isinstance(hook.client_configuration, 
async_client.Configuration)
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.cncf.kubernetes.hooks.kubernetes.async_config.load_kube_config_from_dict")
+    async def test_get_conn_passes_client_configuration_to_async_client(self, 
mock_load):
+        """get_conn passes the per-instance configuration= to 
_TimeoutAsyncK8sApiClient."""
+        mock_load.return_value = None
+        hook = AsyncKubernetesHook(conn_id=None, in_cluster=False, 
config_dict={"a": "b"})
+
+        with mock.patch(HOOK_MODULE + "._TimeoutAsyncK8sApiClient") as 
mock_client_cls:
+            mock_client_cls.return_value.close = mock.AsyncMock()
+            async with hook.get_conn():
+                pass
+
+        
mock_client_cls.assert_called_once_with(configuration=hook.client_configuration)
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.cncf.kubernetes.hooks.kubernetes.async_config.load_kube_config_from_dict")
+    async def test_concurrent_hooks_have_isolated_configurations(self, 
mock_load):
+        """Two concurrent hooks for different clusters each use an independent 
Configuration.
+
+        This is the core regression test for the race condition: before the 
fix,
+        load_kube_config_from_dict was called with client_configuration=None, 
causing it
+        to overwrite the global kubernetes_asyncio Configuration via 
Configuration.set_default().
+        Concurrent triggerer coroutines for different clusters would 
interleave at the await
+        inside load_kube_config_from_dict and produce a mixed global state 
(e.g. cluster-a's
+        server URL paired with cluster-b's bearer token).
+        """
+
+        async def populate_config(config_dict, context, client_configuration):

Review Comment:
   The “concurrent hooks” regression test doesn’t currently force any 
interleaving: the `populate_config` side effect has no `await` points, so each 
`_load_config()` call can run to completion without yielding to the event loop. 
Adding at least one scheduling yield inside the side effect (or otherwise 
synchronizing both coroutines mid-call) would make this test actually exercise 
the race condition scenario it describes.
   ```suggestion
           async def populate_config(config_dict, context, 
client_configuration):
               # Simulate the library yielding while loading config so 
concurrent calls interleave.
               await asyncio.sleep(0)
   ```



##########
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/hooks/kubernetes.py:
##########
@@ -894,6 +900,9 @@ async def _load_config(self):
         and cached. For exec-based auth (EKS, GKE), the config is reloaded on 
every call so
         that short-lived tokens are always refreshed.
         """
+        if self.client_configuration is None:
+            self.client_configuration = async_client.Configuration()
+

Review Comment:
   `AsyncKubernetesHook` assigns `self.client_configuration` to an 
`async_client.Configuration`, but the inherited 
`KubernetesHook.client_configuration` parameter/attribute is typed as 
`kubernetes.client.Configuration`. Now that `get_conn()` forwards this object 
into `kubernetes_asyncio.client.ApiClient`, callers passing a sync 
`Configuration` via `client_configuration=` could hit runtime 
incompatibilities. Consider explicitly typing/overriding `client_configuration` 
on `AsyncKubernetesHook` (or using a separate attribute) and 
validating/converting the provided value to an `async_client.Configuration` to 
make the public API unambiguous.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to