@intentius/chant-lexicon-k8s 0.0.24 → 0.1.4

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.
@@ -1,43 +1,46 @@
1
1
  {
2
2
  "algorithm": "xxhash64",
3
3
  "artifacts": {
4
- "manifest.json": "65da11d30f270ee5",
5
- "meta.json": "1ce194f36f9b5f90",
6
- "types/index.d.ts": "beec4cc869064186",
4
+ "manifest.json": "ba34543f183fd269",
5
+ "meta.json": "939607658c0e8f4a",
6
+ "types/index.d.ts": "cbd8853f04c0849c",
7
+ "rules/hardcoded-namespace.ts": "54b216c71018e101",
7
8
  "rules/missing-resource-limits.ts": "a6f776d2ff477948",
8
9
  "rules/latest-image-tag.ts": "eb48e8d61e4ca84e",
9
- "rules/hardcoded-namespace.ts": "54b216c71018e101",
10
- "rules/wk8201.ts": "4dbbd20e21b5fa04",
11
- "rules/wk8204.ts": "9244d6fdd6d2f7d",
12
- "rules/wk8301.ts": "283265ab0c5b8511",
13
- "rules/wk8306.ts": "5338575bfb0f9251",
14
- "rules/wk8102.ts": "78d4aac387107b56",
15
- "rules/wk8305.ts": "5c4b9482a0f3b91d",
16
- "rules/wk8101.ts": "f8ffcf6e5c89076b",
17
- "rules/wk8302.ts": "a80d1eab37c0dbe4",
18
- "rules/wk8005.ts": "a9a1b93b80f3aa51",
19
10
  "rules/wk8209.ts": "820df53f304e0a59",
20
- "rules/wk8203.ts": "f5bf17539c0428c5",
21
- "rules/wk8104.ts": "94bd75c16b8d50b7",
22
- "rules/wk8303.ts": "c545464e2af8fbb9",
23
- "rules/wk8103.ts": "e5d6a506d966e312",
24
- "rules/wk8205.ts": "d000527c6371a05",
25
- "rules/wk8042.ts": "6064c84481ae8551",
26
- "rules/wk8006.ts": "6e04f754f79f076e",
27
- "rules/wk8041.ts": "4df512c93caaef50",
28
- "rules/wk8304.ts": "f51bf894c5a08dbe",
29
11
  "rules/wk8202.ts": "6bd950ae2128256c",
30
12
  "rules/wk8208.ts": "1133f9e53c174ae9",
13
+ "rules/k8s-helpers.ts": "4611ed839c0219b5",
14
+ "rules/wk8302.ts": "a80d1eab37c0dbe4",
15
+ "rules/wk8205.ts": "d000527c6371a05",
16
+ "rules/wk8006.ts": "6e04f754f79f076e",
17
+ "rules/wk8305.ts": "5c4b9482a0f3b91d",
18
+ "rules/wk8204.ts": "9244d6fdd6d2f7d",
19
+ "rules/wk8402.ts": "83b420543e246d20",
20
+ "rules/wk8401.ts": "62d5acc6a674b8ed",
21
+ "rules/wk8201.ts": "4dbbd20e21b5fa04",
22
+ "rules/wk8104.ts": "94bd75c16b8d50b7",
23
+ "rules/wk8101.ts": "f8ffcf6e5c89076b",
31
24
  "rules/wk8105.ts": "8dbcfe399f23656a",
32
- "rules/k8s-helpers.ts": "53a6d3bfbedb2852",
25
+ "rules/wk8041.ts": "4df512c93caaef50",
33
26
  "rules/wk8207.ts": "6f2bc621d530afa2",
27
+ "rules/wk8102.ts": "78d4aac387107b56",
28
+ "rules/wk8005.ts": "a9a1b93b80f3aa51",
29
+ "rules/wk8301.ts": "283265ab0c5b8511",
30
+ "rules/wk8304.ts": "f51bf894c5a08dbe",
31
+ "rules/wk8042.ts": "6064c84481ae8551",
32
+ "rules/wk8403.ts": "81397f7658d888a",
33
+ "rules/wk8103.ts": "e5d6a506d966e312",
34
+ "rules/wk8306.ts": "5338575bfb0f9251",
35
+ "rules/wk8203.ts": "f5bf17539c0428c5",
36
+ "rules/wk8303.ts": "c545464e2af8fbb9",
34
37
  "skills/chant-k8s.md": "bf3ac0c5bddd5d2a",
35
38
  "skills/chant-k8s-patterns.md": "c5151ed799145c4b",
36
39
  "skills/chant-k8s-deployment-strategies.md": "74f179e7cdb15ed5",
37
40
  "skills/chant-k8s-security.md": "f377edc5fe0a3587",
38
41
  "skills/chant-k8s-eks.md": "f79f31f058c7f2ed",
39
- "skills/chant-k8s-gke.md": "196b839fc8a6849c",
42
+ "skills/chant-k8s-gke.md": "2f65ca45aef40c22",
40
43
  "skills/chant-k8s-aks.md": "764fa4b1408b618d"
41
44
  },
42
- "composite": "91eed8e99d4982de"
45
+ "composite": "71a7bf45b20b1ebd"
43
46
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.0.24",
3
+ "version": "0.1.4",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
package/dist/meta.json CHANGED
@@ -46,6 +46,11 @@
46
46
  "kind": "property",
47
47
  "lexicon": "k8s"
48
48
  },
49
+ "AutoscalerOptions": {
50
+ "resourceType": "K8s::Ray::RayCluster.autoscalerOptions",
51
+ "kind": "property",
52
+ "lexicon": "k8s"
53
+ },
49
54
  "BatchJob": {
50
55
  "resourceType": "K8s::Batch::Job",
51
56
  "kind": "resource",
@@ -439,6 +444,11 @@
439
444
  "apiVersion": "flowcontrol.apiserver.k8s.io/v1",
440
445
  "gvkKind": "FlowSchemaList"
441
446
  },
447
+ "GcsFaultToleranceOptions": {
448
+ "resourceType": "K8s::Ray::RayCluster.gcsFaultToleranceOptions",
449
+ "kind": "property",
450
+ "lexicon": "k8s"
451
+ },
442
452
  "HPA": {
443
453
  "resourceType": "K8s::Autoscaling::HorizontalPodAutoscaler",
444
454
  "kind": "resource",
@@ -456,6 +466,11 @@
456
466
  "kind": "property",
457
467
  "lexicon": "k8s"
458
468
  },
469
+ "HeadGroupSpec": {
470
+ "resourceType": "K8s::Ray::RayCluster.headGroupSpec",
471
+ "kind": "property",
472
+ "lexicon": "k8s"
473
+ },
459
474
  "HorizontalPodAutoscaler": {
460
475
  "resourceType": "K8s::Autoscaling::HorizontalPodAutoscaler",
461
476
  "kind": "resource",
@@ -942,6 +957,87 @@
942
957
  }
943
958
  }
944
959
  },
960
+ "RayCluster": {
961
+ "resourceType": "K8s::Ray::RayCluster",
962
+ "kind": "resource",
963
+ "lexicon": "k8s",
964
+ "apiVersion": "ray.io/v1",
965
+ "gvkKind": "RayCluster"
966
+ },
967
+ "RayClusterConfig": {
968
+ "resourceType": "K8s::Ray::RayService.rayClusterConfig",
969
+ "kind": "property",
970
+ "lexicon": "k8s"
971
+ },
972
+ "RayClusterSpec": {
973
+ "resourceType": "K8s::Ray::RayJob.rayClusterSpec",
974
+ "kind": "property",
975
+ "lexicon": "k8s"
976
+ },
977
+ "RayCluster_AutoscalerOptions": {
978
+ "resourceType": "K8s::Ray::RayCluster.autoscalerOptions",
979
+ "kind": "property",
980
+ "lexicon": "k8s"
981
+ },
982
+ "RayCluster_GcsFaultToleranceOptions": {
983
+ "resourceType": "K8s::Ray::RayCluster.gcsFaultToleranceOptions",
984
+ "kind": "property",
985
+ "lexicon": "k8s"
986
+ },
987
+ "RayCluster_HeadGroupSpec": {
988
+ "resourceType": "K8s::Ray::RayCluster.headGroupSpec",
989
+ "kind": "property",
990
+ "lexicon": "k8s"
991
+ },
992
+ "RayCluster_WorkerGroupSpec": {
993
+ "resourceType": "K8s::Ray::RayCluster.workerGroupSpecs",
994
+ "kind": "property",
995
+ "lexicon": "k8s"
996
+ },
997
+ "RayJob": {
998
+ "resourceType": "K8s::Ray::RayJob",
999
+ "kind": "resource",
1000
+ "lexicon": "k8s",
1001
+ "apiVersion": "ray.io/v1",
1002
+ "gvkKind": "RayJob"
1003
+ },
1004
+ "RayJob_RayClusterSpec": {
1005
+ "resourceType": "K8s::Ray::RayJob.rayClusterSpec",
1006
+ "kind": "property",
1007
+ "lexicon": "k8s"
1008
+ },
1009
+ "RayJob_SubmitterConfig": {
1010
+ "resourceType": "K8s::Ray::RayJob.submitterConfig",
1011
+ "kind": "property",
1012
+ "lexicon": "k8s"
1013
+ },
1014
+ "RayJob_SubmitterPodTemplate": {
1015
+ "resourceType": "K8s::Ray::RayJob.submitterPodTemplate",
1016
+ "kind": "property",
1017
+ "lexicon": "k8s"
1018
+ },
1019
+ "RayService": {
1020
+ "resourceType": "K8s::Ray::RayService",
1021
+ "kind": "resource",
1022
+ "lexicon": "k8s",
1023
+ "apiVersion": "ray.io/v1",
1024
+ "gvkKind": "RayService"
1025
+ },
1026
+ "RayService_RayClusterConfig": {
1027
+ "resourceType": "K8s::Ray::RayService.rayClusterConfig",
1028
+ "kind": "property",
1029
+ "lexicon": "k8s"
1030
+ },
1031
+ "RayService_ServeService": {
1032
+ "resourceType": "K8s::Ray::RayService.serveService",
1033
+ "kind": "property",
1034
+ "lexicon": "k8s"
1035
+ },
1036
+ "RayService_UpgradeStrategy": {
1037
+ "resourceType": "K8s::Ray::RayService.upgradeStrategy",
1038
+ "kind": "property",
1039
+ "lexicon": "k8s"
1040
+ },
945
1041
  "ReplicaSet": {
946
1042
  "resourceType": "K8s::Apps::ReplicaSet",
947
1043
  "kind": "resource",
@@ -1164,6 +1260,11 @@
1164
1260
  "apiVersion": "authorization.k8s.io/v1",
1165
1261
  "gvkKind": "SelfSubjectRulesReview"
1166
1262
  },
1263
+ "ServeService": {
1264
+ "resourceType": "K8s::Ray::RayService.serveService",
1265
+ "kind": "property",
1266
+ "lexicon": "k8s"
1267
+ },
1167
1268
  "Service": {
1168
1269
  "resourceType": "K8s::Core::Service",
1169
1270
  "kind": "resource",
@@ -1281,6 +1382,16 @@
1281
1382
  "apiVersion": "authorization.k8s.io/v1",
1282
1383
  "gvkKind": "SubjectAccessReview"
1283
1384
  },
1385
+ "SubmitterConfig": {
1386
+ "resourceType": "K8s::Ray::RayJob.submitterConfig",
1387
+ "kind": "property",
1388
+ "lexicon": "k8s"
1389
+ },
1390
+ "SubmitterPodTemplate": {
1391
+ "resourceType": "K8s::Ray::RayJob.submitterPodTemplate",
1392
+ "kind": "property",
1393
+ "lexicon": "k8s"
1394
+ },
1284
1395
  "TCPSocketAction": {
1285
1396
  "resourceType": "K8s::Core::TCPSocketAction",
1286
1397
  "kind": "property",
@@ -1323,6 +1434,11 @@
1323
1434
  }
1324
1435
  }
1325
1436
  },
1437
+ "UpgradeStrategy": {
1438
+ "resourceType": "K8s::Ray::RayService.upgradeStrategy",
1439
+ "kind": "property",
1440
+ "lexicon": "k8s"
1441
+ },
1326
1442
  "ValidatingAdmissionPolicy": {
1327
1443
  "resourceType": "K8s::Admissionregistration::ValidatingAdmissionPolicy",
1328
1444
  "kind": "resource",
@@ -1409,5 +1525,10 @@
1409
1525
  "lexicon": "k8s",
1410
1526
  "apiVersion": "v1",
1411
1527
  "gvkKind": "WatchEvent"
1528
+ },
1529
+ "WorkerGroupSpec": {
1530
+ "resourceType": "K8s::Ray::RayCluster.workerGroupSpecs",
1531
+ "kind": "property",
1532
+ "lexicon": "k8s"
1412
1533
  }
1413
1534
  }
@@ -147,3 +147,42 @@ export const WORKLOAD_KINDS = new Set([
147
147
  "Job",
148
148
  "CronJob",
149
149
  ]);
150
+
151
+ /**
152
+ * Parse a Kubernetes memory string to bytes.
153
+ *
154
+ * Handles binary suffixes (Ki, Mi, Gi, Ti) and SI suffixes (K, M, G, T).
155
+ * Returns NaN for unrecognised strings — callers should skip checks on NaN.
156
+ *
157
+ * @example
158
+ * parseMemoryBytes("4Gi") // 4294967296
159
+ * parseMemoryBytes("512Mi") // 536870912
160
+ * parseMemoryBytes("2048") // 2048
161
+ */
162
+ export function parseMemoryBytes(s: string): number {
163
+ const m = /^([0-9]+(?:\.[0-9]+)?)(Ki|Mi|Gi|Ti|K|M|G|T)?$/.exec(s.trim());
164
+ if (!m) return NaN;
165
+ const n = parseFloat(m[1]);
166
+ const multipliers: Record<string, number> = {
167
+ Ki: 1024, Mi: 1024 ** 2, Gi: 1024 ** 3, Ti: 1024 ** 4,
168
+ K: 1000, M: 1000 ** 2, G: 1000 ** 3, T: 1000 ** 4,
169
+ };
170
+ return n * (multipliers[m[2] ?? ""] ?? 1);
171
+ }
172
+
173
+ /**
174
+ * Extract the Ray version from an image tag.
175
+ *
176
+ * Looks for a semver-style version prefix in the image tag:
177
+ * "rayproject/ray:2.40.0" → "2.40.0"
178
+ * "rayproject/ray:2.40.0-py310" → "2.40.0"
179
+ * "us-docker.pkg.dev/.../ray:2.9.3-gpu" → "2.9.3"
180
+ * "rayproject/ray:latest" → undefined
181
+ *
182
+ * Returns undefined when no version can be found.
183
+ */
184
+ export function extractRayVersion(image: string): string | undefined {
185
+ const tag = image.includes(":") ? image.split(":").pop()! : image;
186
+ const m = /^([0-9]+\.[0-9]+\.[0-9]+)/.exec(tag);
187
+ return m ? m[1] : undefined;
188
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * WK8401: shmSize exceeds container memory limit
3
+ *
4
+ * A RayCluster pod uses an emptyDir volume with medium: Memory for /dev/shm.
5
+ * Kubernetes counts that memory against the container's memory limit — if the
6
+ * sizeLimit exceeds the container's memory limit the pod will never schedule
7
+ * (Kubelet rejects it with "Invalid value … must be less than or equal to memory limit").
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { getPrimaryOutput, parseK8sManifests, parseMemoryBytes } from "./k8s-helpers";
12
+
13
+ export const wk8401: PostSynthCheck = {
14
+ id: "WK8401",
15
+ description: "shmSize must not exceed the container memory limit — pod will not schedule if it does",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [, output] of ctx.outputs) {
21
+ const yaml = getPrimaryOutput(output);
22
+ const manifests = parseK8sManifests(yaml);
23
+
24
+ for (const manifest of manifests) {
25
+ if (manifest.kind !== "RayCluster") continue;
26
+
27
+ const clusterName = manifest.metadata?.name ?? "RayCluster";
28
+ const spec = manifest.spec as Record<string, unknown> | undefined;
29
+ if (!spec) continue;
30
+
31
+ const groups: Array<{ label: string; templateSpec: Record<string, unknown> }> = [];
32
+
33
+ // Head group
34
+ const headGroupSpec = spec.headGroupSpec as Record<string, unknown> | undefined;
35
+ if (headGroupSpec) {
36
+ const tmpl = headGroupSpec.template as Record<string, unknown> | undefined;
37
+ const podSpec = tmpl?.spec as Record<string, unknown> | undefined;
38
+ if (podSpec) groups.push({ label: "head", templateSpec: podSpec });
39
+ }
40
+
41
+ // Worker groups
42
+ const workerGroupSpecs = spec.workerGroupSpecs as Array<Record<string, unknown>> | undefined;
43
+ if (Array.isArray(workerGroupSpecs)) {
44
+ for (const wg of workerGroupSpecs) {
45
+ const name = (wg.groupName as string | undefined) ?? "worker";
46
+ const tmpl = wg.template as Record<string, unknown> | undefined;
47
+ const podSpec = tmpl?.spec as Record<string, unknown> | undefined;
48
+ if (podSpec) groups.push({ label: `worker "${name}"`, templateSpec: podSpec });
49
+ }
50
+ }
51
+
52
+ for (const { label, templateSpec } of groups) {
53
+ // Find the emptyDir Memory volume (dshm)
54
+ const volumes = templateSpec.volumes as Array<Record<string, unknown>> | undefined;
55
+ if (!Array.isArray(volumes)) continue;
56
+
57
+ for (const vol of volumes) {
58
+ const emptyDir = vol.emptyDir as Record<string, unknown> | undefined;
59
+ if (!emptyDir || emptyDir.medium !== "Memory") continue;
60
+
61
+ const sizeLimit = emptyDir.sizeLimit as string | undefined;
62
+ if (!sizeLimit) continue;
63
+
64
+ const shmBytes = parseMemoryBytes(sizeLimit);
65
+ if (isNaN(shmBytes)) continue;
66
+
67
+ // Find memory limit from the first container that mounts this volume
68
+ const containers = templateSpec.containers as Array<Record<string, unknown>> | undefined;
69
+ if (!Array.isArray(containers)) continue;
70
+
71
+ for (const container of containers) {
72
+ const resources = container.resources as Record<string, unknown> | undefined;
73
+ const limits = resources?.limits as Record<string, unknown> | undefined;
74
+ const memLimit = limits?.memory as string | undefined;
75
+ if (!memLimit) continue;
76
+
77
+ const memBytes = parseMemoryBytes(memLimit);
78
+ if (isNaN(memBytes)) continue;
79
+
80
+ if (shmBytes > memBytes) {
81
+ diagnostics.push({
82
+ checkId: "WK8401",
83
+ severity: "error",
84
+ message: `RayCluster "${clusterName}" ${label}: shmSize "${sizeLimit}" exceeds memory limit "${memLimit}" — pod will not schedule`,
85
+ entity: clusterName,
86
+ lexicon: "k8s",
87
+ });
88
+ }
89
+ break; // Only check the first container per group
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ return diagnostics;
97
+ },
98
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * WK8402: RayCluster missing spec.rayVersion
3
+ *
4
+ * KubeRay uses spec.rayVersion to select the Ray autoscaler sidecar image.
5
+ * Without it, KubeRay defaults to the "latest" tag — autoscaler and Ray head
6
+ * may run mismatched versions, leading to silent protocol failures.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
11
+
12
+ export const wk8402: PostSynthCheck = {
13
+ id: "WK8402",
14
+ description: "RayCluster should set spec.rayVersion so KubeRay selects the correct autoscaler image",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [, output] of ctx.outputs) {
20
+ const yaml = getPrimaryOutput(output);
21
+ const manifests = parseK8sManifests(yaml);
22
+
23
+ for (const manifest of manifests) {
24
+ if (manifest.kind !== "RayCluster") continue;
25
+
26
+ const name = manifest.metadata?.name ?? "RayCluster";
27
+ const rayVersion = (manifest.spec as Record<string, unknown> | undefined)?.rayVersion;
28
+
29
+ if (!rayVersion) {
30
+ diagnostics.push({
31
+ checkId: "WK8402",
32
+ severity: "warning",
33
+ message: `RayCluster "${name}" is missing spec.rayVersion — KubeRay autoscaler will pull the "latest" image tag`,
34
+ entity: name,
35
+ lexicon: "k8s",
36
+ });
37
+ }
38
+ }
39
+ }
40
+
41
+ return diagnostics;
42
+ },
43
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * WK8403: spec.rayVersion does not match head image tag
3
+ *
4
+ * When spec.rayVersion is set but doesn't match the version in the head
5
+ * container image tag, the KubeRay autoscaler sidecar will run a different
6
+ * Ray version than the cluster. This can cause gRPC compatibility failures.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, parseK8sManifests, extractRayVersion } from "./k8s-helpers";
11
+
12
+ export const wk8403: PostSynthCheck = {
13
+ id: "WK8403",
14
+ description: "spec.rayVersion should match the Ray version in the head container image tag",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [, output] of ctx.outputs) {
20
+ const yaml = getPrimaryOutput(output);
21
+ const manifests = parseK8sManifests(yaml);
22
+
23
+ for (const manifest of manifests) {
24
+ if (manifest.kind !== "RayCluster") continue;
25
+
26
+ const clusterName = manifest.metadata?.name ?? "RayCluster";
27
+ const spec = manifest.spec as Record<string, unknown> | undefined;
28
+ if (!spec) continue;
29
+
30
+ const rayVersion = spec.rayVersion as string | undefined;
31
+ if (!rayVersion) continue; // WK8402 covers the missing case
32
+
33
+ // Extract version from head container image
34
+ const headGroupSpec = spec.headGroupSpec as Record<string, unknown> | undefined;
35
+ const tmpl = headGroupSpec?.template as Record<string, unknown> | undefined;
36
+ const podSpec = tmpl?.spec as Record<string, unknown> | undefined;
37
+ const containers = podSpec?.containers as Array<Record<string, unknown>> | undefined;
38
+ if (!Array.isArray(containers) || containers.length === 0) continue;
39
+
40
+ const image = containers[0].image as string | undefined;
41
+ if (!image) continue;
42
+
43
+ const imageVersion = extractRayVersion(image);
44
+ if (!imageVersion) continue; // Can't determine version from tag
45
+
46
+ if (imageVersion !== rayVersion) {
47
+ diagnostics.push({
48
+ checkId: "WK8403",
49
+ severity: "warning",
50
+ message: `RayCluster "${clusterName}": spec.rayVersion "${rayVersion}" does not match head image tag "${imageVersion}" — autoscaler may run a mismatched Ray version`,
51
+ entity: clusterName,
52
+ lexicon: "k8s",
53
+ });
54
+ }
55
+ }
56
+ }
57
+
58
+ return diagnostics;
59
+ },
60
+ };
@@ -206,7 +206,7 @@ import { ConfigConnectorContext } from "@intentius/chant-lexicon-k8s";
206
206
  const { context } = ConfigConnectorContext({
207
207
  googleServiceAccountEmail: "cc-sa@my-project.iam.gserviceaccount.com",
208
208
  namespace: "config-connector",
209
- stateIntoSpec: "absent",
209
+ stateIntoSpec: "Absent",
210
210
  });
211
211
  ```
212
212
 
@@ -1244,6 +1244,36 @@ export declare class PVC {
1244
1244
  readonly uid: string;
1245
1245
  }
1246
1246
 
1247
+ export declare class RayCluster {
1248
+ constructor(props: {
1249
+ metadata?: Record<string, unknown>;
1250
+ spec?: Record<string, unknown>;
1251
+ });
1252
+ readonly name: string;
1253
+ readonly namespace: string;
1254
+ readonly uid: string;
1255
+ }
1256
+
1257
+ export declare class RayJob {
1258
+ constructor(props: {
1259
+ metadata?: Record<string, unknown>;
1260
+ spec?: Record<string, unknown>;
1261
+ });
1262
+ readonly name: string;
1263
+ readonly namespace: string;
1264
+ readonly uid: string;
1265
+ }
1266
+
1267
+ export declare class RayService {
1268
+ constructor(props: {
1269
+ metadata?: Record<string, unknown>;
1270
+ spec?: Record<string, unknown>;
1271
+ });
1272
+ readonly name: string;
1273
+ readonly namespace: string;
1274
+ readonly uid: string;
1275
+ }
1276
+
1247
1277
  export declare class ReplicaSet {
1248
1278
  constructor(props: {
1249
1279
  /** If the Labels of a ReplicaSet are empty, they are defaulted to be the same as the Pod(s) that the ReplicaSet manages. Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-k8s",
3
- "version": "0.0.24",
3
+ "version": "0.1.4",
4
4
  "description": "Kubernetes lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -42,10 +42,13 @@
42
42
  "docs": "bun run src/codegen/docs-cli.ts",
43
43
  "prepack": "bun run generate && bun run bundle && bun run validate"
44
44
  },
45
- "dependencies": {
46
- "@intentius/chant": "0.0.22"
47
- },
48
45
  "devDependencies": {
46
+ "@intentius/chant": "0.1.4",
47
+ "@types/js-yaml": "^4.0.9",
48
+ "js-yaml": "^4.1.1",
49
49
  "typescript": "^5.9.3"
50
+ },
51
+ "peerDependencies": {
52
+ "@intentius/chant": "^0.1.0"
50
53
  }
51
54
  }
@@ -13,6 +13,8 @@ import {
13
13
  } from "@intentius/chant/codegen/generate";
14
14
  import { fetchSchemas } from "../spec/fetch";
15
15
  import { parseK8sSwagger, k8sShortName, type K8sParseResult } from "../spec/parse";
16
+ import { loadMultipleCRDs } from "../crd/loader";
17
+ import { CRD_SOURCES } from "../crd/crd-sources";
16
18
  import { NamingStrategy, propertyTypeName, extractDefName } from "./naming";
17
19
  import { generateLexiconJSON } from "./generate-lexicon";
18
20
  import { generateTypeScriptDeclarations } from "./generate-typescript";
@@ -54,6 +56,26 @@ export async function generate(opts: K8sGenerateOptions = {}): Promise<GenerateR
54
56
 
55
57
  createNaming: (results) => new NamingStrategy(results),
56
58
 
59
+ augmentSchemas: async (schemas, _opts, log) => {
60
+ // Load third-party CRDs and return as extraResults so they are
61
+ // included in the generated types alongside the core K8s resources.
62
+ const crdResults: K8sParseResult[] = [];
63
+ const warnings: Array<{ file: string; error: string }> = [];
64
+ for (const source of CRD_SOURCES) {
65
+ try {
66
+ const parsed = await loadMultipleCRDs([source]);
67
+ crdResults.push(...parsed);
68
+ log(`Loaded ${parsed.length} CRD type(s) from ${source.url ?? source.path ?? "cluster"}`);
69
+ } catch (err) {
70
+ const msg = err instanceof Error ? err.message : String(err);
71
+ warnings.push({ file: source.url ?? source.path ?? "cluster", error: msg });
72
+ log(`Warning: failed to load CRD: ${msg}`);
73
+ }
74
+ }
75
+ log(`Total CRD types loaded: ${crdResults.length}`);
76
+ return { schemas, extraResults: crdResults, warnings };
77
+ },
78
+
57
79
  augmentResults: (results, _opts, log) => {
58
80
  // Add the remaining results from the single-schema parse
59
81
  if (pendingResults.length > 0) {
@@ -66,6 +66,16 @@ export interface CockroachDbClusterProps {
66
66
  * E.g. pod "cockroachdb-0" in east advertises "cockroachdb-0.east.crdb.internal".
67
67
  */
68
68
  advertiseHostDomain?: string;
69
+ /**
70
+ * Mount the client certs secret (`${name}-client-certs`) into pods at
71
+ * `/cockroach/cockroach-client-certs` (default: false).
72
+ *
73
+ * Enable for multi-region deployments where `cockroach init`, `cockroach sql`,
74
+ * and backup schedule setup run inside a pod via `kubectl exec`. The client cert
75
+ * is separate from the node cert and is NOT included in the node certs secret.
76
+ * Without this flag you must inject client certs manually (e.g. via /tmp).
77
+ */
78
+ mountClientCerts?: boolean;
69
79
  /** Additional labels to apply to all resources. */
70
80
  labels?: Record<string, string>;
71
81
  /** Per-member defaults for fine-grained overrides. */
@@ -142,12 +152,14 @@ export const CockroachDbCluster = Composite<CockroachDbClusterProps>((props) =>
142
152
  skipCertGen = false,
143
153
  extraCertNodeAddresses = [],
144
154
  advertiseHostDomain,
155
+ mountClientCerts = false,
145
156
  labels: extraLabels = {},
146
157
  defaults: defs,
147
158
  } = props;
148
159
 
149
160
  const saName = name;
150
161
  const certsDir = "/cockroach/cockroach-certs";
162
+ const clientCertsDir = "/cockroach/cockroach-client-certs";
151
163
  const dataDir = "/cockroach/cockroach-data";
152
164
 
153
165
  const commonLabels: Record<string, string> = {
@@ -302,6 +314,10 @@ export const CockroachDbCluster = Composite<CockroachDbClusterProps>((props) =>
302
314
  if (secure) {
303
315
  volumes.push({ name: "certs", secret: { secretName: `${name}-node-certs`, defaultMode: 0o400 } });
304
316
  volumeMounts.push({ name: "certs", mountPath: certsDir });
317
+ if (mountClientCerts) {
318
+ volumes.push({ name: "client-certs", secret: { secretName: `${name}-client-certs`, defaultMode: 0o400 } });
319
+ volumeMounts.push({ name: "client-certs", mountPath: clientCertsDir });
320
+ }
305
321
  }
306
322
 
307
323
  const container: Record<string, unknown> = {
@@ -2752,14 +2752,14 @@ describe("ConfigConnectorContext", () => {
2752
2752
  expect(spec.googleServiceAccount).toBe("cnrm@my-project.iam.gserviceaccount.com");
2753
2753
  });
2754
2754
 
2755
- test("default stateIntoSpec is absent", () => {
2755
+ test("default stateIntoSpec is Absent", () => {
2756
2756
  const result = ConfigConnectorContext(minProps);
2757
- expect((p(result.context) as any).spec.stateIntoSpec).toBe("absent");
2757
+ expect((p(result.context) as any).spec.stateIntoSpec).toBe("Absent");
2758
2758
  });
2759
2759
 
2760
2760
  test("custom stateIntoSpec", () => {
2761
- const result = ConfigConnectorContext({ ...minProps, stateIntoSpec: "merge" });
2762
- expect((p(result.context) as any).spec.stateIntoSpec).toBe("merge");
2761
+ const result = ConfigConnectorContext({ ...minProps, stateIntoSpec: "Merge" });
2762
+ expect((p(result.context) as any).spec.stateIntoSpec).toBe("Merge");
2763
2763
  });
2764
2764
 
2765
2765
  test("default namespace is default", () => {