Repository: tomplus/kubernetes_asyncio (language: python)

Issue:
Since 30.1.0, receive 400 error patching a secret
Since upgrading from 29.0.0 to 30.1.0 or later, we receive a 400 Bad Request when trying to patch a secret.

```python
import asyncio

from kubernetes_asyncio import client
from kubernetes_asyncio.config import load_kube_config


async def patch_secret(secret_name: str):
    await load_kube_config()
    async with client.ApiClient() as api_client:
        corev1 = client.CoreV1Api(api_client)

        secret = client.V1Secret(
            kind='Secret',
            string_data={
                'some': 'data'
            },
            metadata=client.V1ObjectMeta(
                name=secret_name
            ),
            type='Opaque'
        )

        await corev1.create_namespaced_secret(
            namespace="pw-bug-fixes",
            body=secret
        )
        try:
            await corev1.patch_namespaced_secret(
                namespace="pw-bug-fixes",
                name=secret_name,
                body=secret
            )
        finally:
            await corev1.delete_namespaced_secret(
                namespace="pw-bug-fixes",
                name=secret_name,
            )

if __name__ == "__main__":
    asyncio.run(patch_secret('some-secret'))
```

```
Traceback (most recent call last):
  File "/home/plaid/patch_secret_bug.py", line 29, in patch_secret
    await corev1.patch_namespaced_secret(
  File "/home/plaid/py311/lib/python3.11/site-packages/kubernetes_asyncio/client/api_client.py", line 192, in __call_api
    raise e
  File "/home/plaid/py311/lib/python3.11/site-packages/kubernetes_asyncio/client/api_client.py", line 185, in __call_api
    response_data = await self.request(
                    ^^^^^^^^^^^^^^^^^^^
  File "/home/plaid/py311/lib/python3.11/site-packages/kubernetes_asyncio/client/rest.py", line 256, in PATCH
    return (await self.request("PATCH", url,
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/plaid/py311/lib/python3.11/site-packages/kubernetes_asyncio/client/rest.py", line 192, in request
    raise ApiException(http_resp=r)
kubernetes_asyncio.client.exceptions.ApiException: (400)
Reason: Bad Request
HTTP response headers: <CIMultiDictProxy('Audit-Id': '5ea7bf85-2b49-491e-98f1-323f102e72fc', 'Cache-Control': 'no-cache, private', 'Content-Type': 'application/json', 'X-Kubernetes-Pf-Flowschema-Uid': '684e80ca-63dd-4bbf-86a8-050948d97fd3', 'X-Kubernetes-Pf-Prioritylevel-Uid': 'aed42fd9-bb8a-4011-b997-c97e950c24f8', 'Date': 'Fri, 23 Aug 2024 10:38:19 GMT', 'Content-Length': '211')>
HTTP response body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"error decoding patch: json: cannot unmarshal object into Go value of type []handlers.jsonPatchOp","reason":"BadRequest","code":400}
```


Discussion / hints:
Thanks for reporting this issue.

It's been introduced in 31.0.0 (by PR #303).

I'll fix it but for now you can use workarounds:

1. converting a body to a dict

```
await corev1.patch_namespaced_secret(
                namespace="pw-bug-fixes",
                name=secret_name,
                body=secret.to_dict()
            )
```

2. forcing a valid content-type:

```
await corev1.patch_namespaced_secret(
                namespace="pw-bug-fixes",
                name=secret_name,
                body=secret,
                _content_type='application/strategic-merge-patch+json'
            )
```

Fix patch:
diff --git a/kubernetes_asyncio/client/api_client.py b/kubernetes_asyncio/client/api_client.py
index 9353ecc0..28bae63e 100644
--- a/kubernetes_asyncio/client/api_client.py
+++ b/kubernetes_asyncio/client/api_client.py
@@ -540,7 +540,7 @@ def select_header_content_type(self, content_types, method=None, body=None):
                     isinstance(body, list)):
                 return 'application/json-patch+json'
             if ('application/strategic-merge-patch+json' in content_types and
-                    isinstance(body, dict)):
+                    (isinstance(body, dict) or hasattr(body, "to_dict"))):
                 return 'application/strategic-merge-patch+json'
 
         if 'application/json' in content_types or '*/*' in content_types:
diff --git a/scripts/api_client_strategic_merge_patch.diff b/scripts/api_client_strategic_merge_patch.diff
index 31d724ce..f286b88d 100644
--- a/scripts/api_client_strategic_merge_patch.diff
+++ b/scripts/api_client_strategic_merge_patch.diff
@@ -13,7 +13,7 @@
 +                    isinstance(body, list)):
 +                return 'application/json-patch+json'
 +            if ('application/strategic-merge-patch+json' in content_types and
-+                    isinstance(body, dict)):
++                    (isinstance(body, dict) or hasattr(body, "to_dict"))):
 +                return 'application/strategic-merge-patch+json'
  
          if 'application/json' in content_types or '*/*' in content_types:


Test patch:
diff --git a/kubernetes_asyncio/e2e_test/test_apply_patch.py b/kubernetes_asyncio/e2e_test/test_apply_patch.py
index 2c9deac3..bf8bfc43 100644
--- a/kubernetes_asyncio/e2e_test/test_apply_patch.py
+++ b/kubernetes_asyncio/e2e_test/test_apply_patch.py
@@ -50,8 +50,18 @@ async def test_apply_patch(self):
         )
         self.assertEqual(name, resp.metadata.name)
 
-        resp = await api.read_namespaced_config_map(name=name, namespace="default")
-        self.assertEqual(name, resp.metadata.name)
+        cm = await api.read_namespaced_config_map(name=name, namespace="default")
+        self.assertEqual(name, cm.metadata.name)
+
+        # strategic merge patch for object
+        cm.data["new"] = "value"
+        resp = await api.patch_namespaced_config_map(
+            field_manager="test",
+            body=cm,
+            name=name,
+            namespace="default",
+        )
+        self.assertEqual(resp.data, {'hello': 'world!', 'new': 'value'})
 
         resp = await api.delete_namespaced_config_map(
             name=name, body={}, namespace="default"