Skip to content

Commit a09b62e

Browse files
authored
add support for different type of patch (#303)
* add support for different type of patch * fix isort issue
1 parent 4d909ab commit a09b62e

File tree

7 files changed

+208
-44
lines changed

7 files changed

+208
-44
lines changed

examples/patch.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import asyncio
2+
3+
from kubernetes_asyncio import client, config
4+
from kubernetes_asyncio.client.api_client import ApiClient
5+
6+
SERVICE_NAME = "example-service"
7+
SERVICE_NS = "default"
8+
SERVICE_SPEC = {
9+
"apiVersion": "v1",
10+
"kind": "Service",
11+
"metadata": {
12+
"labels": {"name": SERVICE_NAME},
13+
"name": SERVICE_NAME,
14+
"resourceversion": "v1",
15+
},
16+
"spec": {
17+
"ports": [{"name": "port-80", "port": 80, "protocol": "TCP", "targetPort": 80}],
18+
"selector": {"name": SERVICE_NAME},
19+
},
20+
}
21+
22+
23+
async def main():
24+
25+
await config.load_kube_config()
26+
27+
async with ApiClient() as api:
28+
29+
v1 = client.CoreV1Api(api)
30+
31+
print(f"Recreate {SERVICE_NAME}...")
32+
try:
33+
await v1.read_namespaced_service(SERVICE_NAME, SERVICE_NS)
34+
await v1.delete_namespaced_service(SERVICE_NAME, SERVICE_NS)
35+
except client.exceptions.ApiException as ex:
36+
if ex.status == 404:
37+
pass
38+
39+
await v1.create_namespaced_service(SERVICE_NS, SERVICE_SPEC)
40+
41+
print("Patch using JSON patch - replace port-80 with port-1000")
42+
patch = [
43+
{
44+
"op": "replace",
45+
"path": "/spec/ports/0",
46+
"value": {
47+
"name": "port-1000",
48+
"protocol": "TCP",
49+
"port": 1000,
50+
"targetPort": 1000,
51+
},
52+
}
53+
]
54+
await v1.patch_namespaced_service(
55+
SERVICE_NAME,
56+
SERVICE_NS,
57+
patch,
58+
# _content_type='application/json-patch+json' # (optional, default if patch is a list)
59+
)
60+
61+
print(
62+
"Patch using strategic merge patch - add port-2000, service will have two ports: port-1000 and port-2000"
63+
)
64+
patch = {
65+
"spec": {
66+
"ports": [
67+
{
68+
"name": "port-2000",
69+
"protocol": "TCP",
70+
"port": 2000,
71+
"targetPort": 2000,
72+
}
73+
]
74+
}
75+
}
76+
await v1.patch_namespaced_service(
77+
SERVICE_NAME,
78+
SERVICE_NS,
79+
patch,
80+
# _content_type='application/strategic-merge-patch+json' # (optional, default if patch is a dict)
81+
)
82+
83+
print(
84+
"Patch using merge patch - recreate list of ports, service will have only one port: port-3000"
85+
)
86+
patch = {
87+
"spec": {
88+
"ports": [
89+
{
90+
"name": "port-3000",
91+
"protocol": "TCP",
92+
"port": 3000,
93+
"targetPort": 3000,
94+
}
95+
]
96+
}
97+
}
98+
await v1.patch_namespaced_service(
99+
SERVICE_NAME,
100+
SERVICE_NS,
101+
patch,
102+
_content_type="application/merge-patch+json", # required to force merge patch
103+
)
104+
105+
106+
if __name__ == "__main__":
107+
loop = asyncio.get_event_loop()
108+
loop.run_until_complete(main())
109+
loop.close()

kubernetes_asyncio/client/api_client.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -535,10 +535,13 @@ def select_header_content_type(self, content_types, method=None, body=None):
535535

536536
content_types = [x.lower() for x in content_types]
537537

538-
if (method == 'PATCH' and
539-
'application/json-patch+json' in content_types and
540-
isinstance(body, list)):
541-
return 'application/json-patch+json'
538+
if method == 'PATCH':
539+
if ('application/json-patch+json' in content_types and
540+
isinstance(body, list)):
541+
return 'application/json-patch+json'
542+
if ('application/strategic-merge-patch+json' in content_types and
543+
isinstance(body, dict)):
544+
return 'application/strategic-merge-patch+json'
542545

543546
if 'application/json' in content_types or '*/*' in content_types:
544547
return 'application/json'

kubernetes_asyncio/client/rest.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,7 @@ async def request(self, method, url, query_params=None, headers=None,
142142

143143
# For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
144144
if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
145-
if (
146-
re.search("json", headers["Content-Type"], re.IGNORECASE)
147-
or headers["Content-Type"] == "application/apply-patch+yaml"
148-
):
149-
if headers['Content-Type'] == 'application/json-patch+json':
150-
if not isinstance(body, list):
151-
headers['Content-Type'] = 'application/strategic-merge-patch+json'
145+
if re.search('json', headers['Content-Type'], re.IGNORECASE):
152146
if body is not None:
153147
body = json.dumps(body)
154148
args["data"] = body

kubernetes_asyncio/e2e_test/test_client.py

+69-12
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,72 @@ async def test_service_apis(self):
120120
self.assertEqual(name, resp.metadata.name)
121121
self.assertTrue(resp.status)
122122

123-
service_manifest['spec']['ports'] = [
124-
{'name': 'new',
125-
'port': 8080,
126-
'protocol': 'TCP',
127-
'targetPort': 8080}
128-
]
123+
# strategic merge patch
129124
resp = await api.patch_namespaced_service(
130-
body=service_manifest,
131125
name=name,
132-
namespace='default'
126+
namespace="default",
127+
body={
128+
"spec": {
129+
"ports": [
130+
{
131+
"name": "new",
132+
"port": 8080,
133+
"protocol": "TCP",
134+
"targetPort": 8080,
135+
}
136+
]
137+
}
138+
},
139+
)
140+
self.assertEqual(len(resp.spec.ports), 2)
141+
self.assertTrue(resp.status)
142+
143+
# json merge patch
144+
resp = await api.patch_namespaced_service(
145+
name=name,
146+
namespace="default",
147+
body={
148+
"spec": {
149+
"ports": [
150+
{
151+
"name": "new2",
152+
"port": 8080,
153+
"protocol": "TCP",
154+
"targetPort": 8080,
155+
}
156+
]
157+
}
158+
},
159+
_content_type="application/merge-patch+json",
133160
)
134-
self.assertEqual(2, len(resp.spec.ports))
161+
self.assertEqual(len(resp.spec.ports), 1)
162+
self.assertEqual(resp.spec.ports[0].name, "new2")
135163
self.assertTrue(resp.status)
136164

137-
resp = await api.delete_namespaced_service(name=name, body={}, namespace='default')
165+
# json patch
166+
resp = await api.patch_namespaced_service(
167+
name=name,
168+
namespace="default",
169+
body=[
170+
{
171+
"op": "add",
172+
"path": "/spec/ports/0",
173+
"value": {
174+
"name": "new3",
175+
"protocol": "TCP",
176+
"port": 1000,
177+
"targetPort": 1000,
178+
},
179+
}
180+
],
181+
)
182+
self.assertEqual(len(resp.spec.ports), 2)
183+
self.assertEqual(resp.spec.ports[0].name, "new3")
184+
self.assertEqual(resp.spec.ports[1].name, "new2")
185+
self.assertTrue(resp.status)
186+
resp = await api.delete_namespaced_service(
187+
name=name, body={}, namespace="default"
188+
)
138189

139190
async def test_replication_controller_apis(self):
140191
client = api_client.ApiClient(configuration=self.config)
@@ -207,9 +258,15 @@ async def test_configmap_apis(self):
207258
name=name, namespace='default')
208259
self.assertEqual(name, resp.metadata.name)
209260

210-
test_configmap['data']['config.json'] = "{}"
261+
# strategic merge patch
211262
resp = await api.patch_namespaced_config_map(
212-
name=name, namespace='default', body=test_configmap)
263+
name=name, namespace='default', body={'data': {'key': 'value', 'frontend.cnf': 'patched'}})
264+
265+
resp = await api.read_namespaced_config_map(
266+
name=name, namespace='default')
267+
self.assertEqual(resp.data['config.json'], test_configmap['data']['config.json'])
268+
self.assertEqual(resp.data['frontend.cnf'], 'patched')
269+
self.assertEqual(resp.data['key'], 'value')
213270

214271
resp = await api.delete_namespaced_config_map(
215272
name=name, body={}, namespace='default')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--- /tmp/api_client.py 2024-02-25 20:40:28.143350042 +0100
2+
+++ kubernetes_asyncio/client/api_client.py 2024-02-25 20:40:32.954201652 +0100
3+
@@ -535,10 +535,13 @@
4+
5+
content_types = [x.lower() for x in content_types]
6+
7+
- if (method == 'PATCH' and
8+
- 'application/json-patch+json' in content_types and
9+
- isinstance(body, list)):
10+
- return 'application/json-patch+json'
11+
+ if method == 'PATCH':
12+
+ if ('application/json-patch+json' in content_types and
13+
+ isinstance(body, list)):
14+
+ return 'application/json-patch+json'
15+
+ if ('application/strategic-merge-patch+json' in content_types and
16+
+ isinstance(body, dict)):
17+
+ return 'application/strategic-merge-patch+json'
18+
19+
if 'application/json' in content_types or '*/*' in content_types:
20+
return 'application/json'

scripts/rest_client_patch.diff

-19
This file was deleted.

scripts/update-client.sh

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ sed -i'' "s/^__version__ = .*/__version__ = \\\"${CLIENT_VERSION}\\\"/" "${CLIEN
6464
sed -i'' "s/^PACKAGE_NAME = .*/PACKAGE_NAME = \\\"${PACKAGE_NAME}\\\"/" "${SCRIPT_ROOT}/../setup.py"
6565
sed -i'' "s,^DEVELOPMENT_STATUS = .*,DEVELOPMENT_STATUS = \\\"${DEVELOPMENT_STATUS}\\\"," "${SCRIPT_ROOT}/../setup.py"
6666

67-
echo ">>> fix generated rest client for patching with strategic merge..."
68-
patch "${CLIENT_ROOT}/client/rest.py" "${SCRIPT_ROOT}/rest_client_patch.diff"
67+
echo ">>> fix generated api client for patching with strategic merge..."
68+
patch "${CLIENT_ROOT}/client/api_client.py" "${SCRIPT_ROOT}/api_client_strategic_merge_patch.diff"
6969
echo ">>> fix generated rest client by increasing aiohttp read buffer to 2MiB..."
7070
patch "${CLIENT_ROOT}/client/rest.py" "${SCRIPT_ROOT}/rest_client_patch_read_bufsize.diff"
7171
echo ">>> fix generated rest client and configuration to support customer server hostname TLS verification..."

0 commit comments

Comments
 (0)