@intentius/chant-lexicon-k8s 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/integrity.json +42 -39
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +121 -0
  4. package/dist/rules/k8s-helpers.ts +39 -0
  5. package/dist/rules/wk8401.ts +98 -0
  6. package/dist/rules/wk8402.ts +43 -0
  7. package/dist/rules/wk8403.ts +60 -0
  8. package/dist/types/index.d.ts +30 -0
  9. package/package.json +11 -7
  10. package/src/actions/actions.test.ts +1 -1
  11. package/src/codegen/generate-cli.ts +1 -1
  12. package/src/codegen/generate.ts +22 -0
  13. package/src/codegen/naming.test.ts +1 -1
  14. package/src/codegen/package.ts +2 -5
  15. package/src/codegen/snapshot.test.ts +1 -1
  16. package/src/codegen/typecheck.test.ts +1 -1
  17. package/src/composites/cockroachdb-region-stack.ts +553 -0
  18. package/src/composites/composites.test.ts +4 -4
  19. package/src/composites/index.ts +8 -0
  20. package/src/composites/ray-cluster.ts +590 -0
  21. package/src/composites/ray-job.ts +235 -0
  22. package/src/composites/ray-service.ts +271 -0
  23. package/src/coverage.test.ts +1 -1
  24. package/src/crd/crd-sources.ts +29 -0
  25. package/src/crd/loader.ts +13 -21
  26. package/src/crd/parser.test.ts +1 -1
  27. package/src/crd/parser.ts +17 -12
  28. package/src/default-labels.test.ts +1 -1
  29. package/src/generated/index.d.ts +30 -0
  30. package/src/generated/index.ts +13 -0
  31. package/src/generated/lexicon-k8s.json +121 -0
  32. package/src/import/generator.test.ts +1 -1
  33. package/src/import/parser.test.ts +1 -1
  34. package/src/import/roundtrip.test.ts +1 -1
  35. package/src/index.ts +4 -0
  36. package/src/lint/post-synth/k8s-helpers.test.ts +1 -1
  37. package/src/lint/post-synth/k8s-helpers.ts +39 -0
  38. package/src/lint/post-synth/post-synth.test.ts +149 -1
  39. package/src/lint/post-synth/wk8401.ts +98 -0
  40. package/src/lint/post-synth/wk8402.ts +43 -0
  41. package/src/lint/post-synth/wk8403.ts +60 -0
  42. package/src/lint/rules/rules.test.ts +1 -1
  43. package/src/lsp/completions.test.ts +1 -1
  44. package/src/lsp/hover.test.ts +1 -1
  45. package/src/package-cli.ts +1 -1
  46. package/src/plugin.test.ts +3 -3
  47. package/src/plugin.ts +7 -9
  48. package/src/serializer.test.ts +1 -1
  49. package/src/serializer.ts +2 -0
  50. package/src/skills/chant-k8s-ray.md +252 -0
  51. package/src/spec/fetch.test.ts +1 -1
  52. package/src/spec/parse.test.ts +1 -1
  53. package/src/validate-cli.ts +1 -1
  54. package/src/validate.test.ts +1 -1
@@ -0,0 +1,590 @@
1
+ /**
2
+ * RayCluster composite — KubeRay RayCluster CR + surrounding K8s infra.
3
+ *
4
+ * Encodes production defaults for running Ray on Kubernetes:
5
+ * - NetworkPolicy with podSelector rules (avoids GKE pod CIDR mismatch)
6
+ * - PodDisruptionBudget on the head (minAvailable: 1)
7
+ * - preStop: ray stop hooks + terminationGracePeriodSeconds: 120 on all pods
8
+ * - ServiceAccount for the head + optional autoscaler ClusterRole/CRB
9
+ * - Optional shared ReadWriteMany PVC for training data (Filestore/EFS)
10
+ * - Optional GCS spillover env var for object store spill-to-GCS
11
+ */
12
+
13
+ import { Composite, mergeDefaults } from "@intentius/chant";
14
+ import {
15
+ ServiceAccount,
16
+ ClusterRole,
17
+ ClusterRoleBinding,
18
+ NetworkPolicy,
19
+ PodDisruptionBudget,
20
+ PersistentVolumeClaim,
21
+ Service,
22
+ RayCluster as RayClusterResource,
23
+ } from "../generated";
24
+
25
+ // ── Shared types (re-exported for RayJob and RayService) ────────────────────
26
+
27
+ /** Container resource spec for Ray pods. */
28
+ export interface ResourceSpec {
29
+ /** CPU request/limit (e.g. "2", "500m"). */
30
+ cpu: string;
31
+ /** Memory request/limit (e.g. "4Gi", "512Mi"). */
32
+ memory: string;
33
+ /** GPU count — adds nvidia.com/gpu to resource requests and limits. */
34
+ gpu?: number;
35
+ }
36
+
37
+ /** Head node configuration. */
38
+ export interface HeadGroupSpec {
39
+ resources: ResourceSpec;
40
+ /**
41
+ * Shared memory size mounted at /dev/shm (default: "2Gi").
42
+ * Prevents OOM when PyTorch workers share tensors via /dev/shm.
43
+ */
44
+ shmSize?: string;
45
+ /**
46
+ * When true, sets num-cpus=0 so the Ray scheduler never places user tasks on
47
+ * the head node. Recommended for production — keeps the head free for GCS,
48
+ * dashboard, and autoscaler. Default: false.
49
+ */
50
+ reserveHead?: boolean;
51
+ /** Additional Ray start params merged into defaults. */
52
+ rayStartParams?: Record<string, string>;
53
+ /** Additional environment variables for the head container. */
54
+ env?: Array<{ name: string; value: string }>;
55
+ }
56
+
57
+ /** Worker group configuration. */
58
+ export interface WorkerGroupSpec {
59
+ /** Worker group name — used as a selector label (e.g. "cpu", "gpu"). */
60
+ groupName: string;
61
+ /** Initial replica count. */
62
+ replicas: number;
63
+ /** Minimum replicas for autoscaling (default: replicas). */
64
+ minReplicas?: number;
65
+ /** Maximum replicas for autoscaling (default: replicas). */
66
+ maxReplicas?: number;
67
+ resources: ResourceSpec;
68
+ /**
69
+ * Idle timeout before the Ray autoscaler terminates idle workers (default: 60s).
70
+ * Use 300s for GPU groups to amortize initialization overhead.
71
+ */
72
+ idleTimeoutSeconds?: number;
73
+ /** Add nvidia.com/gpu NoSchedule toleration — required for GPU tainted node pools. */
74
+ gpuTolerations?: boolean;
75
+ /** Additional Ray start params. num-cpus is derived from resources.cpu automatically. */
76
+ rayStartParams?: Record<string, string>;
77
+ /** Additional environment variables for worker containers. */
78
+ env?: Array<{ name: string; value: string }>;
79
+ }
80
+
81
+ /** Cluster spec shared by RayCluster, RayJob, and RayService. */
82
+ export interface RayClusterSpec {
83
+ /**
84
+ * Container image for head and all worker groups.
85
+ * Use a pre-built image stored in Artifact Registry / ECR for production —
86
+ * pip installs via runtimeEnv add minutes to cold start at scale.
87
+ */
88
+ image: string;
89
+ head: HeadGroupSpec;
90
+ workerGroups: WorkerGroupSpec[];
91
+ }
92
+
93
+ // ── RayCluster composite props ───────────────────────────────────────────────
94
+
95
+ export interface RayClusterProps {
96
+ name: string;
97
+ namespace: string;
98
+ cluster: RayClusterSpec;
99
+ /**
100
+ * Shared ReadWriteMany storage for training data.
101
+ * Mounts the same PVC into head and all worker pods.
102
+ * Use Filestore ENTERPRISE (GKE) or EFS (AWS) for production SLA.
103
+ */
104
+ sharedStorage?: {
105
+ storageClass: string;
106
+ size: string;
107
+ /** Mount path in all containers (default: "/mnt/ray-data"). */
108
+ mountPath?: string;
109
+ };
110
+ /**
111
+ * Enable Ray in-tree autoscaler.
112
+ * Creates a ClusterRole/CRB granting pod CRUD to the head ServiceAccount.
113
+ */
114
+ enableAutoscaler?: boolean;
115
+ /**
116
+ * GCS bucket name for object store spill-to-GCS.
117
+ * Injects RAY_object_spilling_config into the head container.
118
+ * Without this, large object graphs (model weights) cause OOM on the head.
119
+ */
120
+ spilloverBucket?: string;
121
+ /**
122
+ * Emit a LoadBalancer Service exposing the Ray dashboard on port 8265.
123
+ * Default false — use `kubectl port-forward svc/<name>-head-svc 8265:8265`.
124
+ */
125
+ exposeDashboard?: boolean;
126
+ /** Additional labels applied to all resources. */
127
+ labels?: Record<string, string>;
128
+ /** Per-member defaults for fine-grained overrides via mergeDefaults. */
129
+ defaults?: {
130
+ serviceAccount?: Partial<Record<string, unknown>>;
131
+ clusterRole?: Partial<Record<string, unknown>>;
132
+ clusterRoleBinding?: Partial<Record<string, unknown>>;
133
+ networkPolicy?: Partial<Record<string, unknown>>;
134
+ pdb?: Partial<Record<string, unknown>>;
135
+ pvc?: Partial<Record<string, unknown>>;
136
+ dashboardService?: Partial<Record<string, unknown>>;
137
+ rayCluster?: Partial<Record<string, unknown>>;
138
+ };
139
+ }
140
+
141
+ export interface RayClusterResult {
142
+ serviceAccount: InstanceType<typeof ServiceAccount>;
143
+ clusterRole?: InstanceType<typeof ClusterRole>;
144
+ clusterRoleBinding?: InstanceType<typeof ClusterRoleBinding>;
145
+ networkPolicy: InstanceType<typeof NetworkPolicy>;
146
+ pdb: InstanceType<typeof PodDisruptionBudget>;
147
+ pvc?: InstanceType<typeof PersistentVolumeClaim>;
148
+ /** Only present when exposeDashboard is true. */
149
+ dashboardService?: InstanceType<typeof Service>;
150
+ rayCluster: InstanceType<typeof RayClusterResource>;
151
+ }
152
+
153
+ // ── Internal helpers ─────────────────────────────────────────────────────────
154
+
155
+ /** Parse a CPU string to a numeric count for --num-cpus. "2" → "2", "500m" → "0.5". */
156
+ function cpuToNumCpus(cpu: string): string {
157
+ if (cpu.endsWith("m")) {
158
+ const millis = parseInt(cpu.slice(0, -1), 10);
159
+ return String(millis / 1000);
160
+ }
161
+ return cpu;
162
+ }
163
+
164
+ /** Build resource requests/limits. GPU count maps to nvidia.com/gpu. */
165
+ function buildResources(spec: ResourceSpec): Record<string, unknown> {
166
+ const base: Record<string, unknown> = { cpu: spec.cpu, memory: spec.memory };
167
+ if (spec.gpu) base["nvidia.com/gpu"] = String(spec.gpu);
168
+ return { requests: { ...base }, limits: { ...base } };
169
+ }
170
+
171
+ /** Shared volume list for a pod (dshm + optional PVC). */
172
+ function buildPodVolumes(
173
+ shmSize: string,
174
+ pvcName?: string,
175
+ ): Array<Record<string, unknown>> {
176
+ const vols: Array<Record<string, unknown>> = [
177
+ { name: "dshm", emptyDir: { medium: "Memory", sizeLimit: shmSize } },
178
+ ];
179
+ if (pvcName) {
180
+ vols.push({ name: "shared-data", persistentVolumeClaim: { claimName: pvcName } });
181
+ }
182
+ return vols;
183
+ }
184
+
185
+ /** Shared volume mounts for a container (dshm + optional shared-data). */
186
+ function buildVolumeMounts(mountPath?: string): Array<Record<string, unknown>> {
187
+ const mounts: Array<Record<string, unknown>> = [
188
+ { name: "dshm", mountPath: "/dev/shm" },
189
+ ];
190
+ if (mountPath) {
191
+ mounts.push({ name: "shared-data", mountPath });
192
+ }
193
+ return mounts;
194
+ }
195
+
196
+ /**
197
+ * Extract the Ray version from a container image reference.
198
+ * Used to populate spec.rayVersion on KubeRay CRs so the autoscaler
199
+ * pulls an image that matches the running Ray version.
200
+ *
201
+ * "rayproject/ray:2.40.0-py310-cpu" → "2.40.0"
202
+ * "rayproject/ray:latest" → undefined
203
+ */
204
+ function extractRayVersion(image: string): string | undefined {
205
+ const tag = image.includes(":") ? image.split(":").pop()! : image;
206
+ const m = /^([0-9]+\.[0-9]+\.[0-9]+)/.exec(tag);
207
+ return m ? m[1] : undefined;
208
+ }
209
+
210
+ // ── Shared cluster spec builder (used by RayJob and RayService too) ──────────
211
+
212
+ /**
213
+ * Build headGroupSpec and workerGroupSpecs for a KubeRay CR.
214
+ * Exported for use in RayJob and RayService composites.
215
+ */
216
+ export function buildRayClusterParts(
217
+ cluster: RayClusterSpec,
218
+ saName: string,
219
+ spilloverBucket: string | undefined,
220
+ pvcName: string | undefined,
221
+ mountPath: string | undefined,
222
+ ): { headGroupSpec: Record<string, unknown>; workerGroupSpecs: Array<Record<string, unknown>>; rayVersion: string | undefined } {
223
+ const { image, head, workerGroups } = cluster;
224
+ const rayVersion = extractRayVersion(image);
225
+ const shmSize = head.shmSize ?? "2Gi";
226
+ const resolvedMountPath = pvcName ? (mountPath ?? "/mnt/ray-data") : undefined;
227
+
228
+ // ── Head container ─────────────────────────────────────────────────────
229
+ const headEnv: Array<Record<string, unknown>> = [...(head.env ?? [])];
230
+ if (spilloverBucket) {
231
+ headEnv.push({
232
+ name: "RAY_object_spilling_config",
233
+ value: JSON.stringify({
234
+ type: "smart_open",
235
+ params: { uri: `gs://${spilloverBucket}/spill` },
236
+ }),
237
+ });
238
+ }
239
+
240
+ const headContainer: Record<string, unknown> = {
241
+ name: "ray-head",
242
+ image,
243
+ resources: buildResources(head.resources),
244
+ ports: [
245
+ { containerPort: 6379, name: "gcs-server" },
246
+ { containerPort: 8265, name: "dashboard" },
247
+ { containerPort: 10001, name: "client" },
248
+ { containerPort: 8080, name: "metrics" },
249
+ ],
250
+ ...(headEnv.length > 0 && { env: headEnv }),
251
+ volumeMounts: buildVolumeMounts(resolvedMountPath),
252
+ lifecycle: { preStop: { exec: { command: ["ray", "stop"] } } },
253
+ };
254
+
255
+ const headGroupSpec: Record<string, unknown> = {
256
+ rayStartParams: {
257
+ ...(head.reserveHead && { "num-cpus": "0" }),
258
+ ...(head.rayStartParams ?? {}),
259
+ },
260
+ template: {
261
+ spec: {
262
+ serviceAccountName: saName,
263
+ terminationGracePeriodSeconds: 120,
264
+ containers: [headContainer],
265
+ volumes: buildPodVolumes(shmSize, pvcName),
266
+ },
267
+ },
268
+ };
269
+
270
+ // ── Worker groups ───────────────────────────────────────────────────────
271
+ const workerGroupSpecs = workerGroups.map((group) => {
272
+ // Always inject the GCS reconnect timeout — default 60s terminates workers
273
+ // when the head is briefly unavailable (eviction, rolling restart). 600s gives
274
+ // workers 10 minutes to wait for head recovery before self-terminating.
275
+ const workerEnv: Array<Record<string, unknown>> = [
276
+ { name: "RAY_gcs_rpc_server_reconnect_timeout_s", value: "600" },
277
+ ...(group.env ?? []),
278
+ ];
279
+
280
+ const workerContainer: Record<string, unknown> = {
281
+ name: "ray-worker",
282
+ image,
283
+ resources: buildResources(group.resources),
284
+ env: workerEnv,
285
+ volumeMounts: buildVolumeMounts(resolvedMountPath),
286
+ lifecycle: { preStop: { exec: { command: ["ray", "stop"] } } },
287
+ };
288
+
289
+ const rayStartParams: Record<string, string> = {
290
+ "num-cpus": cpuToNumCpus(group.resources.cpu),
291
+ ...(group.resources.gpu ? { "num-gpus": String(group.resources.gpu) } : {}),
292
+ ...(group.rayStartParams ?? {}),
293
+ };
294
+
295
+ const podSpec: Record<string, unknown> = {
296
+ terminationGracePeriodSeconds: 120,
297
+ containers: [workerContainer],
298
+ volumes: buildPodVolumes("2Gi", pvcName),
299
+ };
300
+
301
+ if (group.gpuTolerations) {
302
+ podSpec.tolerations = [
303
+ { key: "nvidia.com/gpu", operator: "Exists", effect: "NoSchedule" },
304
+ ];
305
+ }
306
+
307
+ return {
308
+ groupName: group.groupName,
309
+ replicas: group.replicas,
310
+ minReplicas: group.minReplicas ?? group.replicas,
311
+ maxReplicas: group.maxReplicas ?? group.replicas,
312
+ idleTimeoutSeconds: group.idleTimeoutSeconds ?? 60,
313
+ rayStartParams,
314
+ template: { spec: podSpec },
315
+ };
316
+ });
317
+
318
+ return { headGroupSpec, workerGroupSpecs, rayVersion };
319
+ }
320
+
321
+ // ── NetworkPolicy builder ─────────────────────────────────────────────────────
322
+
323
+ /**
324
+ * Build a NetworkPolicy for a Ray cluster.
325
+ * Uses podSelector-only rules for intra-cluster traffic to avoid GKE pod CIDR
326
+ * mismatch (GKE secondary pod CIDRs differ from declared subnet CIDRs).
327
+ * GCS egress uses ipBlock excluding RFC1918 to allow Google APIs while
328
+ * blocking internal lateral movement.
329
+ */
330
+ export function buildRayNetworkPolicy(
331
+ name: string,
332
+ namespace: string,
333
+ commonLabels: Record<string, string>,
334
+ exposeDashboard: boolean,
335
+ defOverride: Partial<Record<string, unknown>> | undefined,
336
+ ): InstanceType<typeof NetworkPolicy> {
337
+ const clusterSelector = { matchLabels: { "ray.io/cluster-name": name } };
338
+
339
+ const rayPorts = [
340
+ { port: 6379, protocol: "TCP" },
341
+ { port: 10001, protocol: "TCP" },
342
+ { port: 10002, protocol: "TCP" },
343
+ { port: 8080, protocol: "TCP" },
344
+ // Ephemeral gRPC ports for worker-to-worker communication (requires K8s 1.25+)
345
+ { port: 32768, endPort: 60999, protocol: "TCP" },
346
+ ];
347
+
348
+ const ingressRules: Array<Record<string, unknown>> = [
349
+ {
350
+ from: [{ podSelector: clusterSelector }],
351
+ ports: [...rayPorts, { port: 8265, protocol: "TCP" }],
352
+ },
353
+ ];
354
+
355
+ // When the dashboard is externally exposed, allow ingress from outside the cluster
356
+ if (exposeDashboard) {
357
+ ingressRules.push({ ports: [{ port: 8265, protocol: "TCP" }] });
358
+ }
359
+
360
+ const egressRules: Array<Record<string, unknown>> = [
361
+ // Intra-cluster Ray traffic
362
+ {
363
+ to: [{ podSelector: clusterSelector }],
364
+ ports: [...rayPorts, { port: 8265, protocol: "TCP" }],
365
+ },
366
+ // DNS — required for head service name resolution
367
+ {
368
+ ports: [
369
+ { port: 53, protocol: "UDP" },
370
+ { port: 53, protocol: "TCP" },
371
+ ],
372
+ },
373
+ // GCS / Artifact Registry HTTPS — ipBlock excludes RFC1918 to prevent
374
+ // lateral movement while allowing Google API endpoints
375
+ {
376
+ to: [
377
+ {
378
+ ipBlock: {
379
+ cidr: "0.0.0.0/0",
380
+ except: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"],
381
+ },
382
+ },
383
+ ],
384
+ ports: [{ port: 443, protocol: "TCP" }],
385
+ },
386
+ ];
387
+
388
+ return new NetworkPolicy(mergeDefaults({
389
+ metadata: {
390
+ name,
391
+ namespace,
392
+ labels: { ...commonLabels, "app.kubernetes.io/component": "network-policy" },
393
+ },
394
+ spec: {
395
+ podSelector: clusterSelector,
396
+ policyTypes: ["Ingress", "Egress"],
397
+ ingress: ingressRules,
398
+ egress: egressRules,
399
+ },
400
+ }, defOverride));
401
+ }
402
+
403
+ // ── Composite ────────────────────────────────────────────────────────────────
404
+
405
+ /**
406
+ * Create a RayCluster composite — returns a KubeRay RayCluster CR and the
407
+ * surrounding K8s resources needed for a production Ray cluster.
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * import { RayCluster } from "@intentius/chant-lexicon-k8s";
412
+ *
413
+ * const ray = RayCluster({
414
+ * name: "ray",
415
+ * namespace: "ray-system",
416
+ * cluster: {
417
+ * image: "us-docker.pkg.dev/my-project/ray-images/ray:2.40.0",
418
+ * head: { resources: { cpu: "2", memory: "8Gi" } },
419
+ * workerGroups: [
420
+ * { groupName: "cpu", replicas: 2, minReplicas: 1, maxReplicas: 8,
421
+ * resources: { cpu: "2", memory: "4Gi" } },
422
+ * ],
423
+ * },
424
+ * enableAutoscaler: true,
425
+ * spilloverBucket: "my-ray-spill",
426
+ * });
427
+ * ```
428
+ */
429
+ export const RayCluster = Composite<RayClusterProps>((props) => {
430
+ const {
431
+ name,
432
+ namespace,
433
+ cluster,
434
+ sharedStorage,
435
+ enableAutoscaler = false,
436
+ spilloverBucket,
437
+ exposeDashboard = false,
438
+ labels: extraLabels = {},
439
+ defaults: defs,
440
+ } = props;
441
+
442
+ const saName = `${name}-head`;
443
+ const pvcName = sharedStorage ? `${name}-shared` : undefined;
444
+ const mountPath = sharedStorage?.mountPath;
445
+
446
+ const commonLabels: Record<string, string> = {
447
+ "app.kubernetes.io/name": name,
448
+ "app.kubernetes.io/managed-by": "chant",
449
+ "app.kubernetes.io/component": "ray",
450
+ ...extraLabels,
451
+ };
452
+
453
+ // -- ServiceAccount --
454
+
455
+ const serviceAccount = new ServiceAccount(mergeDefaults({
456
+ metadata: {
457
+ name: saName,
458
+ namespace,
459
+ labels: { ...commonLabels, "app.kubernetes.io/component": "service-account" },
460
+ },
461
+ }, defs?.serviceAccount));
462
+
463
+ // -- Autoscaler RBAC (optional) --
464
+
465
+ const clusterRole = enableAutoscaler ? new ClusterRole(mergeDefaults({
466
+ metadata: {
467
+ name: `${name}-autoscaler`,
468
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
469
+ },
470
+ rules: [
471
+ {
472
+ apiGroups: [""],
473
+ resources: ["pods"],
474
+ verbs: ["get", "list", "watch", "create", "delete", "patch"],
475
+ },
476
+ {
477
+ apiGroups: [""],
478
+ resources: ["pods/status"],
479
+ verbs: ["get"],
480
+ },
481
+ {
482
+ apiGroups: ["ray.io"],
483
+ resources: ["rayclusters"],
484
+ verbs: ["get", "list", "patch"],
485
+ },
486
+ ],
487
+ }, defs?.clusterRole)) : undefined;
488
+
489
+ const clusterRoleBinding = enableAutoscaler ? new ClusterRoleBinding(mergeDefaults({
490
+ metadata: {
491
+ name: `${name}-autoscaler`,
492
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
493
+ },
494
+ roleRef: {
495
+ apiGroup: "rbac.authorization.k8s.io",
496
+ kind: "ClusterRole",
497
+ name: `${name}-autoscaler`,
498
+ },
499
+ subjects: [{ kind: "ServiceAccount", name: saName, namespace }],
500
+ }, defs?.clusterRoleBinding)) : undefined;
501
+
502
+ // -- NetworkPolicy --
503
+
504
+ const networkPolicy = buildRayNetworkPolicy(
505
+ name, namespace, commonLabels, exposeDashboard, defs?.networkPolicy,
506
+ );
507
+
508
+ // -- PodDisruptionBudget (head) --
509
+
510
+ const pdb = new PodDisruptionBudget(mergeDefaults({
511
+ metadata: {
512
+ name: `${name}-head`,
513
+ namespace,
514
+ labels: { ...commonLabels, "app.kubernetes.io/component": "disruption-budget" },
515
+ },
516
+ spec: {
517
+ minAvailable: 1,
518
+ selector: {
519
+ matchLabels: {
520
+ "ray.io/cluster-name": name,
521
+ "ray.io/node-type": "head",
522
+ },
523
+ },
524
+ },
525
+ }, defs?.pdb));
526
+
527
+ // -- Shared PVC (optional) --
528
+
529
+ const pvc = sharedStorage ? new PersistentVolumeClaim(mergeDefaults({
530
+ metadata: {
531
+ name: pvcName!,
532
+ namespace,
533
+ labels: { ...commonLabels, "app.kubernetes.io/component": "storage" },
534
+ },
535
+ spec: {
536
+ accessModes: ["ReadWriteMany"],
537
+ storageClassName: sharedStorage.storageClass,
538
+ resources: { requests: { storage: sharedStorage.size } },
539
+ },
540
+ }, defs?.pvc)) : undefined;
541
+
542
+ // -- Dashboard LoadBalancer Service (optional) --
543
+
544
+ const dashboardService = exposeDashboard ? new Service(mergeDefaults({
545
+ metadata: {
546
+ name: `${name}-dashboard`,
547
+ namespace,
548
+ labels: { ...commonLabels, "app.kubernetes.io/component": "dashboard" },
549
+ },
550
+ spec: {
551
+ type: "LoadBalancer",
552
+ selector: {
553
+ "ray.io/cluster-name": name,
554
+ "ray.io/node-type": "head",
555
+ },
556
+ ports: [{ port: 8265, targetPort: 8265, protocol: "TCP", name: "dashboard" }],
557
+ },
558
+ }, defs?.dashboardService)) : undefined;
559
+
560
+ // -- RayCluster CR --
561
+
562
+ const { headGroupSpec, workerGroupSpecs, rayVersion } = buildRayClusterParts(
563
+ cluster, saName, spilloverBucket, pvcName, mountPath,
564
+ );
565
+
566
+ const rayCluster = new RayClusterResource(mergeDefaults({
567
+ metadata: {
568
+ name,
569
+ namespace,
570
+ labels: commonLabels,
571
+ },
572
+ spec: {
573
+ ...(rayVersion && { rayVersion }),
574
+ ...(enableAutoscaler && { enableInTreeAutoscaling: true }),
575
+ headGroupSpec,
576
+ workerGroupSpecs,
577
+ },
578
+ }, defs?.rayCluster));
579
+
580
+ return {
581
+ serviceAccount,
582
+ ...(clusterRole && { clusterRole }),
583
+ ...(clusterRoleBinding && { clusterRoleBinding }),
584
+ networkPolicy,
585
+ pdb,
586
+ ...(pvc && { pvc }),
587
+ ...(dashboardService && { dashboardService }),
588
+ rayCluster,
589
+ };
590
+ }, "RayCluster");