From 043429a16cf480cdac15c7ca3835bb9f34dc2a68 Mon Sep 17 00:00:00 2001 From: Jacob Henner Date: Wed, 18 Dec 2024 03:55:23 +0000 Subject: [PATCH 1/2] Fallback to merge patch Add fallback to merge patch if strategic merge patch is unavailable, as is the case for the custom objects API. --- examples/patch.py | 3 ++- kubernetes_asyncio/client/api_client.py | 10 ++++++--- scripts/api_client_strategic_merge_patch.diff | 22 +++++++++++-------- scripts/update-client.sh | 2 +- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/examples/patch.py b/examples/patch.py index ba28e231..1182644f 100644 --- a/examples/patch.py +++ b/examples/patch.py @@ -99,7 +99,8 @@ async def main(): SERVICE_NAME, SERVICE_NS, patch, - _content_type="application/merge-patch+json", # required to force merge patch + # required to force merge patch when strategic merge patch would otherwise be used + _content_type="application/merge-patch+json", ) diff --git a/kubernetes_asyncio/client/api_client.py b/kubernetes_asyncio/client/api_client.py index dfa99b7e..3edca404 100644 --- a/kubernetes_asyncio/client/api_client.py +++ b/kubernetes_asyncio/client/api_client.py @@ -539,9 +539,13 @@ def select_header_content_type(self, content_types, method=None, body=None): if ('application/json-patch+json' in content_types and isinstance(body, list)): return 'application/json-patch+json' - if ('application/strategic-merge-patch+json' in content_types and - (isinstance(body, dict) or hasattr(body, "to_dict"))): - return 'application/strategic-merge-patch+json' + if isinstance(body, dict) or hasattr(body, "to_dict"): + if 'application/strategic-merge-patch+json' in content_types: + return 'application/strategic-merge-patch+json' + elif 'application/merge-patch+json' in content_types: + # Intended for cases where strategic merge patch is not + # supported, like when patching custom objects. + return 'application/merge-patch+json' if 'application/json' in content_types or '*/*' in content_types: return 'application/json' diff --git a/scripts/api_client_strategic_merge_patch.diff b/scripts/api_client_strategic_merge_patch.diff index f286b88d..047e5548 100644 --- a/scripts/api_client_strategic_merge_patch.diff +++ b/scripts/api_client_strategic_merge_patch.diff @@ -1,9 +1,9 @@ ---- /tmp/api_client.py 2024-02-25 20:40:28.143350042 +0100 -+++ kubernetes_asyncio/client/api_client.py 2024-02-25 20:40:32.954201652 +0100 -@@ -535,10 +535,13 @@ - +--- /tmp/api_client.py 2024-12-18 03:36:59.552742383 +0000 ++++ kubernetes_asyncio/client/api_client.py 2024-12-18 03:36:11.062928089 +0000 +@@ -535,10 +535,17 @@ + content_types = [x.lower() for x in content_types] - + - if (method == 'PATCH' and - 'application/json-patch+json' in content_types and - isinstance(body, list)): @@ -12,9 +12,13 @@ + if ('application/json-patch+json' in content_types and + isinstance(body, list)): + return 'application/json-patch+json' -+ if ('application/strategic-merge-patch+json' in content_types and -+ (isinstance(body, dict) or hasattr(body, "to_dict"))): -+ return 'application/strategic-merge-patch+json' - ++ if isinstance(body, dict) or hasattr(body, "to_dict"): ++ if 'application/strategic-merge-patch+json' in content_types: ++ return 'application/strategic-merge-patch+json' ++ elif 'application/merge-patch+json' in content_types: ++ # Intended for cases where strategic merge patch is not ++ # supported, like when patching custom objects. ++ return 'application/merge-patch+json' + if 'application/json' in content_types or '*/*' in content_types: return 'application/json' diff --git a/scripts/update-client.sh b/scripts/update-client.sh index a9e68d48..dd8ee9f5 100755 --- a/scripts/update-client.sh +++ b/scripts/update-client.sh @@ -35,7 +35,7 @@ pushd "${CLIENT_ROOT}" > /dev/null CLIENT_ROOT=`pwd` popd > /dev/null -TEMP_FOLDER=$(mktemp -d) +TEMP_FOLDER=$(mktemp -d) trap "rm -rf ${TEMP_FOLDER}" EXIT SIGINT SETTING_FILE="${TEMP_FOLDER}/settings" From d886878f15c08564825fbfac1301dd342e82a050 Mon Sep 17 00:00:00 2001 From: Jacob Henner Date: Sun, 5 Jan 2025 04:25:56 +0000 Subject: [PATCH 2/2] Add e2e custom object test --- kubernetes_asyncio/e2e_test/test_client.py | 90 +++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/kubernetes_asyncio/e2e_test/test_client.py b/kubernetes_asyncio/e2e_test/test_client.py index 8fb22f38..ecf3910a 100644 --- a/kubernetes_asyncio/e2e_test/test_client.py +++ b/kubernetes_asyncio/e2e_test/test_client.py @@ -17,7 +17,9 @@ from unittest import IsolatedAsyncioTestCase from kubernetes_asyncio.client import api_client -from kubernetes_asyncio.client.api import core_v1_api +from kubernetes_asyncio.client.api import ( + apiextensions_v1_api, core_v1_api, custom_objects_api, +) from kubernetes_asyncio.e2e_test import base from kubernetes_asyncio.stream import WsApiClient @@ -187,6 +189,92 @@ async def test_service_apis(self): name=name, body={}, namespace="default" ) + async def test_custom_objects_api(self): + client = api_client.ApiClient(configuration=self.config) + + apiextensions_api_client = apiextensions_v1_api.ApiextensionsV1Api(client) + custom_objects_api_client = custom_objects_api.CustomObjectsApi(client) + + name = 'clusterchangemes.apps.example.com' + crd_manifest = { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": name, + }, + "spec": { + "group": "apps.example.com", + "names": { + "kind": "ClusterChangeMe", + "listKind": "ClusterChangeMeList", + "plural": "clusterchangemes", + "singular": "clusterchangeme", + }, + "scope": "Cluster", + "versions": [ + { + "name": "v1", + "served": True, + "storage": True, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "size": {"type": "integer"} + }, + } + }, + } + }, + } + ], + }, + } + custom_object_manifest = { + 'apiVersion': 'apps.example.com/v1', + 'kind': 'ClusterChangeMe', + 'metadata': { + 'name': "changeme-name", + }, + 'spec': {} + } + + await apiextensions_api_client.create_custom_resource_definition( + crd_manifest + ) + + await apiextensions_api_client.read_custom_resource_definition( + crd_manifest["metadata"]["name"] + ) + + await custom_objects_api_client.create_cluster_custom_object( + crd_manifest["spec"]["group"], + crd_manifest["spec"]["versions"][0]["name"], + crd_manifest["spec"]["names"]["plural"], + custom_object_manifest + ) + + # json merge patch (implied) + resp = await custom_objects_api_client.patch_cluster_custom_object( + group=crd_manifest["spec"]["group"], + version=crd_manifest["spec"]["versions"][0]["name"], + plural=crd_manifest["spec"]["names"]["plural"], + name=custom_object_manifest["metadata"]["name"], + body={ + "spec": { + "size": 0 + } + }, + ) + self.assertEqual(resp["spec"]["size"], 0) + + await apiextensions_api_client.delete_custom_resource_definition( + crd_manifest["metadata"]["name"] + ) + async def test_replication_controller_apis(self): client = api_client.ApiClient(configuration=self.config) api = core_v1_api.CoreV1Api(client)