Repository: tomplus/kubernetes_asyncio
Pull request #269 (2023-08-19 16:13:46)

Load certificates from plugin exec
This mostly imports what was done in the sync client in https://github.com/kubernetes-client/python/commit/b85aff2b3e6c950cb9128d281cd6f7394563e202

This may solve #231: if the configuration from #213 was trying to load a configuration file containing these client certificates instead of a bearer token, the log message would display and the connection to Kubernetes would fail.

tomplus: > This may solve #231

it's very likely, thanks for pointing this and your contribution :+1:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tomplus/kubernetes_asyncio/pull/269?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tomasz+Prus) Report
Patch coverage: **`81.81%`** and no project coverage change.
> Comparison is base [(`c73467e`)](https://app.codecov.io/gh/tomplus/kubernetes_asyncio/commit/c73467e6c2d7197662ef8e1ca837bc4212ce71d4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tomasz+Prus) 24.68% compared to head [(`76efe37`)](https://app.codecov.io/gh/tomplus/kubernetes_asyncio/pull/269?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tomasz+Prus) 24.69%.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master     #269   +/-   ##
=======================================
  Coverage   24.68%   24.69%           
=======================================
  Files         736      736           
  Lines       99048    99062   +14     
=======================================
+ Hits        24452    24465   +13     
- Misses      74596    74597    +1     
```


| [Files Changed](https://app.codecov.io/gh/tomplus/kubernetes_asyncio/pull/269?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tomasz+Prus) | Coverage Δ | |
|---|---|---|
| [kubernetes\_asyncio/config/kube\_config.py](https://app.codecov.io/gh/tomplus/kubernetes_asyncio/pull/269?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tomasz+Prus#diff-a3ViZXJuZXRlc19hc3luY2lvL2NvbmZpZy9rdWJlX2NvbmZpZy5weQ==) | `92.02% <73.33%> (-0.12%)` | :arrow_down: |
| [kubernetes\_asyncio/config/kube\_config\_test.py](https://app.codecov.io/gh/tomplus/kubernetes_asyncio/pull/269?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tomasz+Prus#diff-a3ViZXJuZXRlc19hc3luY2lvL2NvbmZpZy9rdWJlX2NvbmZpZ190ZXN0LnB5) | `95.00% <100.00%> (+0.08%)` | :arrow_up: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tomplus/kubernetes_asyncio/pull/269?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tomasz+Prus).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tomasz+Prus).


Patch:
diff --git a/kubernetes_asyncio/config/kube_config.py b/kubernetes_asyncio/config/kube_config.py
index 4c3b7994..2cb32142 100644
--- a/kubernetes_asyncio/config/kube_config.py
+++ b/kubernetes_asyncio/config/kube_config.py
@@ -314,13 +314,34 @@ class KubeConfigLoader(object):
         try:
             if hasattr(self, 'exec_plugin_expiry') and not _is_expired(self.exec_plugin_expiry):
                 return True
+            base_path = self._get_base_path(self._cluster.path)
             status = await ExecProvider(self._user['exec']).run()
-            if 'token' not in status:
-                logger.error('exec: missing token field in plugin output')
-                return None
-            self.token = "Bearer %s" % status['token']
-            if 'expirationTimestamp' in status:
-                self.exec_plugin_expiry = parse_rfc3339(status['expirationTimestamp'])
+            if 'token' in status:
+                self.token = "Bearer %s" % status['token']
+                if 'expirationTimestamp' in status:
+                    self.exec_plugin_expiry = parse_rfc3339(status['expirationTimestamp'])
+            elif 'clientCertificateData' in status:
+                # https://kubernetes.io/docs/reference/access-authn-authz/authentication/#input-and-output-formats
+                # Plugin has provided certificates instead of a token.
+                if 'clientKeyData' not in status:
+                    logger.error('exec: missing clientKeyData field in '
+                                 'plugin output')
+                    return None
+                self.cert_file = FileOrData(
+                    status, None,
+                    data_key_name='clientCertificateData',
+                    file_base_path=base_path,
+                    base64_file_content=False,
+                    temp_file_path=self._temp_file_path).as_file()
+                self.key_file = FileOrData(
+                    status, None,
+                    data_key_name='clientKeyData',
+                    file_base_path=base_path,
+                    base64_file_content=False,
+                    temp_file_path=self._temp_file_path).as_file()
+            else:
+                logger.error('exec: missing token or clientCertificateData '
+                             'field in plugin output')
             return True
         except Exception as e:
             logger.error(str(e))
@@ -358,14 +379,18 @@ class KubeConfigLoader(object):
                     self._cluster, 'certificate-authority',
                     file_base_path=base_path,
                     temp_file_path=self._temp_file_path).as_file()
-                self.cert_file = FileOrData(
-                    self._user, 'client-certificate',
-                    file_base_path=base_path,
-                    temp_file_path=self._temp_file_path).as_file()
-                self.key_file = FileOrData(
-                    self._user, 'client-key',
-                    file_base_path=base_path,
-                    temp_file_path=self._temp_file_path).as_file()
+                if 'cert_file' not in self.__dict__:
+                    # cert_file could have been provided by
+                    # _load_from_exec_plugin; only load from the _user
+                    # section if we need it.
+                    self.cert_file = FileOrData(
+                        self._user, 'client-certificate',
+                        file_base_path=base_path,
+                        temp_file_path=self._temp_file_path).as_file()
+                    self.key_file = FileOrData(
+                        self._user, 'client-key',
+                        file_base_path=base_path,
+                        temp_file_path=self._temp_file_path).as_file()
         if 'insecure-skip-tls-verify' in self._cluster:
             self.verify_ssl = not self._cluster['insecure-skip-tls-verify']
 


Test patch:
diff --git a/kubernetes_asyncio/config/kube_config_test.py b/kubernetes_asyncio/config/kube_config_test.py
index ac0c741b..9c489fc2 100644
--- a/kubernetes_asyncio/config/kube_config_test.py
+++ b/kubernetes_asyncio/config/kube_config_test.py
@@ -436,6 +436,13 @@ class TestKubeConfigLoader(BaseTestCase):
                     "user": "exec_cred_user"
                 }
             },
+            {
+                "name": "exec_cred_user_certificate",
+                "context": {
+                    "cluster": "ssl",
+                    "user": "exec_cred_user_certificate"
+                }
+            },
         ],
         "clusters": [
             {
@@ -462,7 +469,9 @@ class TestKubeConfigLoader(BaseTestCase):
                 "name": "ssl",
                 "cluster": {
                     "server": TEST_SSL_HOST,
-                    "certificate-authority-data": TEST_CERTIFICATE_AUTH_BASE64,
+                    "certificate-authority-data":
+                        TEST_CERTIFICATE_AUTH_BASE64,
+                    "insecure-skip-tls-verify": False,
                 }
             },
             {
@@ -601,6 +610,16 @@ class TestKubeConfigLoader(BaseTestCase):
                     }
                 }
             },
+            {
+                "name": "exec_cred_user_certificate",
+                "user": {
+                    "exec": {
+                        "apiVersion": "client.authentication.k8s.io/v1beta1",
+                        "command": "custom-certificate-authenticator",
+                        "args": []
+                    }
+                }
+            },
         ]
     }
 
@@ -757,6 +776,24 @@ class TestKubeConfigLoader(BaseTestCase):
             active_context="exec_cred_user").load_and_set(actual)
         self.assertEqual(expected, actual)
 
+    @patch('kubernetes_asyncio.config.kube_config.ExecProvider.run')
+    async def test_user_exec_auth_certificates(self, mock):
+        mock.return_value = {
+            "clientCertificateData": TEST_CLIENT_CERT,
+            "clientKeyData": TEST_CLIENT_KEY,
+        }
+        expected = FakeConfig(
+            host=TEST_SSL_HOST,
+            cert_file=self._create_temp_file(TEST_CLIENT_CERT),
+            key_file=self._create_temp_file(TEST_CLIENT_KEY),
+            ssl_ca_cert=self._create_temp_file(TEST_CERTIFICATE_AUTH),
+            verify_ssl=True)
+        actual = FakeConfig()
+        await KubeConfigLoader(
+            config_dict=self.TEST_KUBE_CONFIG,
+            active_context="exec_cred_user_certificate").load_and_set(actual)
+        self.assertEqual(expected, actual)
+
     async def test_user_pass(self):
         expected = FakeConfig(host=TEST_HOST, token=TEST_BASIC_TOKEN)
         actual = FakeConfig()
@@ -787,7 +824,8 @@ class TestKubeConfigLoader(BaseTestCase):
             token=BEARER_TOKEN_FORMAT % TEST_DATA_BASE64,
             cert_file=self._create_temp_file(TEST_CLIENT_CERT),
             key_file=self._create_temp_file(TEST_CLIENT_KEY),
-            ssl_ca_cert=self._create_temp_file(TEST_CERTIFICATE_AUTH)
+            ssl_ca_cert=self._create_temp_file(TEST_CERTIFICATE_AUTH),
+            verify_ssl=True,
         )
         actual = FakeConfig()
         await KubeConfigLoader(
@@ -900,7 +938,8 @@ class TestKubeConfigLoader(BaseTestCase):
             token=BEARER_TOKEN_FORMAT % TEST_DATA_BASE64,
             cert_file=self._create_temp_file(TEST_CLIENT_CERT),
             key_file=self._create_temp_file(TEST_CLIENT_KEY),
-            ssl_ca_cert=self._create_temp_file(TEST_CERTIFICATE_AUTH)
+            ssl_ca_cert=self._create_temp_file(TEST_CERTIFICATE_AUTH),
+            verify_ssl=True,
         )
         actual = FakeConfig()