Skip to content

Commit 7142df6

Browse files
lilyz-aiclaude
andcommitted
docs: add e2e test guide and update temporal endpoint proposal
- temporal-endpoint-type-proposal.md: update status to Implemented, document actual design decisions (annotations, no readiness probe, no forwarder sidecar, fixed replicas MVP), resolve open questions - temporal-endpoint-e2e-test.md: step-by-step test procedure for ml-serving-new including image build, configmap patch, DB migration, endpoint creation, and K8s Deployment verification - patch_temporal_configmap.py: runnable script to patch production service-template ConfigMap with temporal templates during e2e testing - e2e test results from 2026-04-25: all checks passed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0efddf2 commit 7142df6

3 files changed

Lines changed: 748 additions & 53 deletions

File tree

docs/patch_temporal_configmap.py

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Patch the model-engine-service-template-config ConfigMap on ml-serving-new to add
4+
temporal CPU and GPU deployment templates.
5+
6+
Usage:
7+
python3 docs/patch_temporal_configmap.py
8+
9+
Requires: kubectl context pointing at ml-serving-new.
10+
"""
11+
12+
import json
13+
import subprocess
14+
15+
TEMPORAL_CPU_TEMPLATE = """\
16+
apiVersion: apps/v1
17+
kind: Deployment
18+
metadata:
19+
name: ${RESOURCE_NAME}
20+
namespace: ${NAMESPACE}
21+
labels:
22+
user_id: ${OWNER}
23+
team: ${TEAM}
24+
product: ${PRODUCT}
25+
created_by: ${CREATED_BY}
26+
owner: ${OWNER}
27+
env: prod
28+
managed-by: model-engine
29+
use_scale_launch_endpoint_network_policy: "true"
30+
tags.datadoghq.com/env: prod
31+
tags.datadoghq.com/version: ${GIT_TAG}
32+
tags.datadoghq.com/service: ${ENDPOINT_NAME}
33+
endpoint_id: ${ENDPOINT_ID}
34+
endpoint_name: ${ENDPOINT_NAME}
35+
annotations:
36+
temporal.scaleml.io/taskQueue: "${TEMPORAL_TASK_QUEUE}"
37+
temporal.scaleml.io/minWorkers: "${MIN_WORKERS}"
38+
temporal.scaleml.io/maxWorkers: "${MAX_WORKERS}"
39+
temporal.scaleml.io/perWorker: "${PER_WORKER}"
40+
spec:
41+
strategy:
42+
type: RollingUpdate
43+
rollingUpdate:
44+
maxSurge: 1
45+
maxUnavailable: 0
46+
replicas: ${MAX_WORKERS}
47+
selector:
48+
matchLabels:
49+
app: ${RESOURCE_NAME}
50+
version: v1
51+
template:
52+
metadata:
53+
labels:
54+
app: ${RESOURCE_NAME}
55+
user_id: ${OWNER}
56+
team: ${TEAM}
57+
product: ${PRODUCT}
58+
created_by: ${CREATED_BY}
59+
owner: ${OWNER}
60+
env: prod
61+
managed-by: model-engine
62+
use_scale_launch_endpoint_network_policy: "true"
63+
tags.datadoghq.com/env: prod
64+
tags.datadoghq.com/version: ${GIT_TAG}
65+
tags.datadoghq.com/service: ${ENDPOINT_NAME}
66+
endpoint_id: ${ENDPOINT_ID}
67+
endpoint_name: ${ENDPOINT_NAME}
68+
sidecar.istio.io/inject: "false"
69+
version: v1
70+
annotations:
71+
ad.datadoghq.com/main.logs: '[{"service": "${ENDPOINT_NAME}", "source": "python"}]'
72+
kubernetes.io/change-cause: "${CHANGE_CAUSE_MESSAGE}"
73+
spec:
74+
affinity:
75+
podAffinity:
76+
preferredDuringSchedulingIgnoredDuringExecution:
77+
- weight: 1
78+
podAffinityTerm:
79+
labelSelector:
80+
matchExpressions:
81+
- key: app
82+
operator: In
83+
values:
84+
- ${RESOURCE_NAME}
85+
topologyKey: kubernetes.io/hostname
86+
- weight: 100
87+
podAffinityTerm:
88+
labelSelector:
89+
matchExpressions:
90+
- key: ${IMAGE_HASH}
91+
operator: In
92+
values:
93+
- "True"
94+
topologyKey: kubernetes.io/hostname
95+
terminationGracePeriodSeconds: 1800
96+
serviceAccount: ml-worker
97+
nodeSelector:
98+
node-lifecycle: normal
99+
priorityClassName: ${PRIORITY}
100+
containers:
101+
- name: main
102+
image: ${IMAGE}
103+
imagePullPolicy: IfNotPresent
104+
command: ${COMMAND}
105+
env: ${MAIN_ENV}
106+
resources:
107+
requests:
108+
cpu: ${CPUS}
109+
memory: ${MEMORY}
110+
${STORAGE_DICT}
111+
limits:
112+
cpu: ${CPUS}
113+
memory: ${MEMORY}
114+
${STORAGE_DICT}
115+
volumeMounts:
116+
- name: user-config
117+
mountPath: /app/user_config
118+
subPath: raw_data
119+
- name: endpoint-config
120+
mountPath: /app/endpoint_config
121+
subPath: raw_data
122+
- name: infra-service-config-volume
123+
mountPath: ${INFRA_SERVICE_CONFIG_VOLUME_MOUNT_PATH}
124+
securityContext:
125+
fsGroup: 65534
126+
volumes:
127+
- name: user-config
128+
configMap:
129+
name: ${RESOURCE_NAME}
130+
- name: endpoint-config
131+
configMap:
132+
name: ${RESOURCE_NAME}-endpoint-config
133+
- name: infra-service-config-volume
134+
configMap:
135+
name: model-engine-service-config
136+
items:
137+
- key: infra_service_config
138+
path: config.yaml"""
139+
140+
TEMPORAL_GPU_TEMPLATE = """\
141+
apiVersion: apps/v1
142+
kind: Deployment
143+
metadata:
144+
name: ${RESOURCE_NAME}
145+
namespace: ${NAMESPACE}
146+
labels:
147+
user_id: ${OWNER}
148+
team: ${TEAM}
149+
product: ${PRODUCT}
150+
created_by: ${CREATED_BY}
151+
owner: ${OWNER}
152+
env: prod
153+
managed-by: model-engine
154+
use_scale_launch_endpoint_network_policy: "true"
155+
tags.datadoghq.com/env: prod
156+
tags.datadoghq.com/version: ${GIT_TAG}
157+
tags.datadoghq.com/service: ${ENDPOINT_NAME}
158+
endpoint_id: ${ENDPOINT_ID}
159+
endpoint_name: ${ENDPOINT_NAME}
160+
annotations:
161+
temporal.scaleml.io/taskQueue: "${TEMPORAL_TASK_QUEUE}"
162+
temporal.scaleml.io/minWorkers: "${MIN_WORKERS}"
163+
temporal.scaleml.io/maxWorkers: "${MAX_WORKERS}"
164+
temporal.scaleml.io/perWorker: "${PER_WORKER}"
165+
spec:
166+
strategy:
167+
type: RollingUpdate
168+
rollingUpdate:
169+
maxSurge: 1
170+
maxUnavailable: 0
171+
replicas: ${MAX_WORKERS}
172+
selector:
173+
matchLabels:
174+
app: ${RESOURCE_NAME}
175+
version: v1
176+
template:
177+
metadata:
178+
labels:
179+
app: ${RESOURCE_NAME}
180+
user_id: ${OWNER}
181+
team: ${TEAM}
182+
product: ${PRODUCT}
183+
created_by: ${CREATED_BY}
184+
owner: ${OWNER}
185+
env: prod
186+
managed-by: model-engine
187+
use_scale_launch_endpoint_network_policy: "true"
188+
tags.datadoghq.com/env: prod
189+
tags.datadoghq.com/version: ${GIT_TAG}
190+
tags.datadoghq.com/service: ${ENDPOINT_NAME}
191+
endpoint_id: ${ENDPOINT_ID}
192+
endpoint_name: ${ENDPOINT_NAME}
193+
sidecar.istio.io/inject: "false"
194+
version: v1
195+
annotations:
196+
ad.datadoghq.com/main.logs: '[{"service": "${ENDPOINT_NAME}", "source": "python"}]'
197+
kubernetes.io/change-cause: "${CHANGE_CAUSE_MESSAGE}"
198+
spec:
199+
affinity:
200+
podAffinity:
201+
preferredDuringSchedulingIgnoredDuringExecution:
202+
- weight: 1
203+
podAffinityTerm:
204+
labelSelector:
205+
matchExpressions:
206+
- key: app
207+
operator: In
208+
values:
209+
- ${RESOURCE_NAME}
210+
topologyKey: kubernetes.io/hostname
211+
- weight: 100
212+
podAffinityTerm:
213+
labelSelector:
214+
matchExpressions:
215+
- key: ${IMAGE_HASH}
216+
operator: In
217+
values:
218+
- "True"
219+
topologyKey: kubernetes.io/hostname
220+
terminationGracePeriodSeconds: 1800
221+
serviceAccount: ml-worker
222+
nodeSelector:
223+
node-lifecycle: normal
224+
k8s.amazonaws.com/accelerator: ${GPU_TYPE}
225+
tolerations:
226+
- key: "nvidia.com/gpu"
227+
operator: "Exists"
228+
effect: "NoSchedule"
229+
priorityClassName: ${PRIORITY}
230+
containers:
231+
- name: main
232+
image: ${IMAGE}
233+
imagePullPolicy: IfNotPresent
234+
command: ${COMMAND}
235+
env: ${MAIN_ENV}
236+
resources:
237+
requests:
238+
nvidia.com/gpu: ${GPUS}
239+
cpu: ${CPUS}
240+
memory: ${MEMORY}
241+
${STORAGE_DICT}
242+
limits:
243+
nvidia.com/gpu: ${GPUS}
244+
cpu: ${CPUS}
245+
memory: ${MEMORY}
246+
${STORAGE_DICT}
247+
volumeMounts:
248+
- mountPath: /dev/shm
249+
name: dshm
250+
- name: user-config
251+
mountPath: /app/user_config
252+
subPath: raw_data
253+
- name: endpoint-config
254+
mountPath: /app/endpoint_config
255+
subPath: raw_data
256+
- name: infra-service-config-volume
257+
mountPath: ${INFRA_SERVICE_CONFIG_VOLUME_MOUNT_PATH}
258+
securityContext:
259+
fsGroup: 65534
260+
volumes:
261+
- name: dshm
262+
emptyDir:
263+
medium: Memory
264+
- name: user-config
265+
configMap:
266+
name: ${RESOURCE_NAME}
267+
- name: endpoint-config
268+
configMap:
269+
name: ${RESOURCE_NAME}-endpoint-config
270+
- name: infra-service-config-volume
271+
configMap:
272+
name: model-engine-service-config
273+
items:
274+
- key: infra_service_config
275+
path: config.yaml"""
276+
277+
278+
def main():
279+
result = subprocess.run(
280+
[
281+
"kubectl",
282+
"get",
283+
"configmap",
284+
"model-engine-service-template-config",
285+
"-n",
286+
"default",
287+
"-o",
288+
"json",
289+
],
290+
capture_output=True,
291+
text=True,
292+
check=True,
293+
)
294+
cm = json.loads(result.stdout)
295+
cm["data"]["deployment-runnable-image-temporal-cpu.yaml"] = TEMPORAL_CPU_TEMPLATE
296+
cm["data"]["deployment-runnable-image-temporal-gpu.yaml"] = TEMPORAL_GPU_TEMPLATE
297+
298+
result = subprocess.run(
299+
["kubectl", "apply", "-f", "-", "-n", "default"],
300+
input=json.dumps(cm),
301+
capture_output=True,
302+
text=True,
303+
)
304+
print(result.stdout or result.stderr)
305+
306+
keys = [k for k in cm["data"] if "temporal" in k]
307+
print(f"Temporal template keys added: {keys}")
308+
309+
310+
if __name__ == "__main__":
311+
main()

0 commit comments

Comments
 (0)