Skip to content

Commit

Permalink
add support for different type of patch
Browse files Browse the repository at this point in the history
  • Loading branch information
tomplus committed Feb 25, 2024
1 parent 4d909ab commit 5f12a06
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 44 deletions.
110 changes: 110 additions & 0 deletions examples/patch.py
Original file line number Diff line number Diff line change
@@ -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()
11 changes: 7 additions & 4 deletions kubernetes_asyncio/client/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
8 changes: 1 addition & 7 deletions kubernetes_asyncio/client/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 69 additions & 12 deletions kubernetes_asyncio/e2e_test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down
20 changes: 20 additions & 0 deletions scripts/api_client_strategic_merge_patch.diff
Original file line number Diff line number Diff line change
@@ -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'
19 changes: 0 additions & 19 deletions scripts/rest_client_patch.diff

This file was deleted.

4 changes: 2 additions & 2 deletions scripts/update-client.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down

0 comments on commit 5f12a06

Please sign in to comment.