diff --git a/examples/patch.py b/examples/patch.py new file mode 100644 index 000000000..c2d35014c --- /dev/null +++ b/examples/patch.py @@ -0,0 +1,110 @@ +import asyncio + +from kubernetes_asyncio import client, config +from kubernetes_asyncio.client.api_client import ApiClient + + +SERVICE_NAME = "example-service" +SERVICE_NS = "default" +SERVICE_SPEC = { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "labels": {"name": SERVICE_NAME}, + "name": SERVICE_NAME, + "resourceversion": "v1", + }, + "spec": { + "ports": [{"name": "port-80", "port": 80, "protocol": "TCP", "targetPort": 80}], + "selector": {"name": SERVICE_NAME}, + }, +} + + +async def main(): + + await config.load_kube_config() + + async with ApiClient() as api: + + v1 = client.CoreV1Api(api) + + print(f"Recreate {SERVICE_NAME}...") + try: + await v1.read_namespaced_service(SERVICE_NAME, SERVICE_NS) + await v1.delete_namespaced_service(SERVICE_NAME, SERVICE_NS) + except client.exceptions.ApiException as ex: + if ex.status == 404: + pass + + await v1.create_namespaced_service(SERVICE_NS, SERVICE_SPEC) + + print("Patch using JSON patch - replace port-80 with port-1000") + patch = [ + { + "op": "replace", + "path": "/spec/ports/0", + "value": { + "name": "port-1000", + "protocol": "TCP", + "port": 1000, + "targetPort": 1000, + }, + } + ] + await v1.patch_namespaced_service( + SERVICE_NAME, + SERVICE_NS, + patch, + # _content_type='application/json-patch+json' # (optional, default if patch is a list) + ) + + print( + "Patch using strategic merge patch - add port-2000, service will have two ports: port-1000 and port-2000" + ) + patch = { + "spec": { + "ports": [ + { + "name": "port-2000", + "protocol": "TCP", + "port": 2000, + "targetPort": 2000, + } + ] + } + } + await v1.patch_namespaced_service( + SERVICE_NAME, + SERVICE_NS, + patch, + # _content_type='application/strategic-merge-patch+json' # (optional, default if patch is a dict) + ) + + print( + "Patch using merge patch - recreate list of ports, service will have only one port: port-3000" + ) + patch = { + "spec": { + "ports": [ + { + "name": "port-3000", + "protocol": "TCP", + "port": 3000, + "targetPort": 3000, + } + ] + } + } + await v1.patch_namespaced_service( + SERVICE_NAME, + SERVICE_NS, + patch, + _content_type="application/merge-patch+json", # required to force merge patch + ) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/kubernetes_asyncio/client/api_client.py b/kubernetes_asyncio/client/api_client.py index e3684e5ad..0d543478e 100644 --- a/kubernetes_asyncio/client/api_client.py +++ b/kubernetes_asyncio/client/api_client.py @@ -535,10 +535,13 @@ def select_header_content_type(self, content_types, method=None, body=None): content_types = [x.lower() for x in content_types] - if (method == 'PATCH' and - 'application/json-patch+json' in content_types and - isinstance(body, list)): - return 'application/json-patch+json' + if method == 'PATCH': + 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)): + return 'application/strategic-merge-patch+json' if 'application/json' in content_types or '*/*' in content_types: return 'application/json' diff --git a/kubernetes_asyncio/client/rest.py b/kubernetes_asyncio/client/rest.py index 9da23a347..b0ec8441c 100644 --- a/kubernetes_asyncio/client/rest.py +++ b/kubernetes_asyncio/client/rest.py @@ -142,13 +142,7 @@ async def request(self, method, url, query_params=None, headers=None, # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: - if ( - re.search("json", headers["Content-Type"], re.IGNORECASE) - or headers["Content-Type"] == "application/apply-patch+yaml" - ): - if headers['Content-Type'] == 'application/json-patch+json': - if not isinstance(body, list): - headers['Content-Type'] = 'application/strategic-merge-patch+json' + if re.search('json', headers['Content-Type'], re.IGNORECASE): if body is not None: body = json.dumps(body) args["data"] = body diff --git a/kubernetes_asyncio/e2e_test/test_client.py b/kubernetes_asyncio/e2e_test/test_client.py index c18b40359..8fb22f38f 100644 --- a/kubernetes_asyncio/e2e_test/test_client.py +++ b/kubernetes_asyncio/e2e_test/test_client.py @@ -120,21 +120,72 @@ async def test_service_apis(self): self.assertEqual(name, resp.metadata.name) self.assertTrue(resp.status) - service_manifest['spec']['ports'] = [ - {'name': 'new', - 'port': 8080, - 'protocol': 'TCP', - 'targetPort': 8080} - ] + # strategic merge patch resp = await api.patch_namespaced_service( - body=service_manifest, name=name, - namespace='default' + namespace="default", + body={ + "spec": { + "ports": [ + { + "name": "new", + "port": 8080, + "protocol": "TCP", + "targetPort": 8080, + } + ] + } + }, + ) + self.assertEqual(len(resp.spec.ports), 2) + self.assertTrue(resp.status) + + # json merge patch + resp = await api.patch_namespaced_service( + name=name, + namespace="default", + body={ + "spec": { + "ports": [ + { + "name": "new2", + "port": 8080, + "protocol": "TCP", + "targetPort": 8080, + } + ] + } + }, + _content_type="application/merge-patch+json", ) - self.assertEqual(2, len(resp.spec.ports)) + self.assertEqual(len(resp.spec.ports), 1) + self.assertEqual(resp.spec.ports[0].name, "new2") self.assertTrue(resp.status) - resp = await api.delete_namespaced_service(name=name, body={}, namespace='default') + # json patch + resp = await api.patch_namespaced_service( + name=name, + namespace="default", + body=[ + { + "op": "add", + "path": "/spec/ports/0", + "value": { + "name": "new3", + "protocol": "TCP", + "port": 1000, + "targetPort": 1000, + }, + } + ], + ) + self.assertEqual(len(resp.spec.ports), 2) + self.assertEqual(resp.spec.ports[0].name, "new3") + self.assertEqual(resp.spec.ports[1].name, "new2") + self.assertTrue(resp.status) + resp = await api.delete_namespaced_service( + name=name, body={}, namespace="default" + ) async def test_replication_controller_apis(self): client = api_client.ApiClient(configuration=self.config) @@ -207,9 +258,15 @@ async def test_configmap_apis(self): name=name, namespace='default') self.assertEqual(name, resp.metadata.name) - test_configmap['data']['config.json'] = "{}" + # strategic merge patch resp = await api.patch_namespaced_config_map( - name=name, namespace='default', body=test_configmap) + name=name, namespace='default', body={'data': {'key': 'value', 'frontend.cnf': 'patched'}}) + + resp = await api.read_namespaced_config_map( + name=name, namespace='default') + self.assertEqual(resp.data['config.json'], test_configmap['data']['config.json']) + self.assertEqual(resp.data['frontend.cnf'], 'patched') + self.assertEqual(resp.data['key'], 'value') resp = await api.delete_namespaced_config_map( name=name, body={}, namespace='default') diff --git a/scripts/api_client_strategic_merge_patch.diff b/scripts/api_client_strategic_merge_patch.diff new file mode 100644 index 000000000..31d724cec --- /dev/null +++ b/scripts/api_client_strategic_merge_patch.diff @@ -0,0 +1,20 @@ +--- /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 @@ + + content_types = [x.lower() for x in content_types] + +- if (method == 'PATCH' and +- 'application/json-patch+json' in content_types and +- isinstance(body, list)): +- return 'application/json-patch+json' ++ if method == 'PATCH': ++ 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)): ++ return 'application/strategic-merge-patch+json' + + if 'application/json' in content_types or '*/*' in content_types: + return 'application/json' diff --git a/scripts/rest_client_patch.diff b/scripts/rest_client_patch.diff deleted file mode 100644 index 42df454a0..000000000 --- a/scripts/rest_client_patch.diff +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/kubernetes_asyncio/client/rest.py b/kubernetes_asyncio/client/rest.py -index e0a33ddb..37b3cf47 100644 ---- a/kubernetes_asyncio/client/rest.py -+++ b/kubernetes_asyncio/client/rest.py -@@ -130,7 +130,13 @@ class RESTClientObject(object): - - # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` - if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: -- if re.search('json', headers['Content-Type'], re.IGNORECASE): -+ if ( -+ re.search("json", headers["Content-Type"], re.IGNORECASE) -+ or headers["Content-Type"] == "application/apply-patch+yaml" -+ ): -+ if headers['Content-Type'] == 'application/json-patch+json': -+ if not isinstance(body, list): -+ headers['Content-Type'] = 'application/strategic-merge-patch+json' - if body is not None: - body = json.dumps(body) - args["data"] = body diff --git a/scripts/update-client.sh b/scripts/update-client.sh index 24720a23d..985265063 100755 --- a/scripts/update-client.sh +++ b/scripts/update-client.sh @@ -64,8 +64,8 @@ sed -i'' "s/^__version__ = .*/__version__ = \\\"${CLIENT_VERSION}\\\"/" "${CLIEN sed -i'' "s/^PACKAGE_NAME = .*/PACKAGE_NAME = \\\"${PACKAGE_NAME}\\\"/" "${SCRIPT_ROOT}/../setup.py" sed -i'' "s,^DEVELOPMENT_STATUS = .*,DEVELOPMENT_STATUS = \\\"${DEVELOPMENT_STATUS}\\\"," "${SCRIPT_ROOT}/../setup.py" -echo ">>> fix generated rest client for patching with strategic merge..." -patch "${CLIENT_ROOT}/client/rest.py" "${SCRIPT_ROOT}/rest_client_patch.diff" +echo ">>> fix generated api client for patching with strategic merge..." +patch "${CLIENT_ROOT}/client/api_client.py" "${SCRIPT_ROOT}/api_client_strategic_merge_patch.diff" echo ">>> fix generated rest client by increasing aiohttp read buffer to 2MiB..." patch "${CLIENT_ROOT}/client/rest.py" "${SCRIPT_ROOT}/rest_client_patch_read_bufsize.diff" echo ">>> fix generated rest client and configuration to support customer server hostname TLS verification..."