@intentius/chant-lexicon-k8s 0.45.0 → 0.49.0

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 (160) hide show
  1. package/dist/components/kubectl-apply.d.ts +5 -0
  2. package/dist/components/kubectl-apply.d.ts.map +1 -1
  3. package/dist/composites/flux-app.d.ts +12 -0
  4. package/dist/composites/flux-app.d.ts.map +1 -1
  5. package/dist/composites/index.d.ts +6 -0
  6. package/dist/composites/index.d.ts.map +1 -1
  7. package/dist/composites/inference-service.d.ts +86 -0
  8. package/dist/composites/inference-service.d.ts.map +1 -0
  9. package/dist/composites/model.d.ts +80 -0
  10. package/dist/composites/model.d.ts.map +1 -0
  11. package/dist/composites/vllm-serving-runtime.d.ts +100 -0
  12. package/dist/composites/vllm-serving-runtime.d.ts.map +1 -0
  13. package/dist/crd/crd-sources.d.ts.map +1 -1
  14. package/dist/deep-observe-hooks.d.ts.map +1 -1
  15. package/dist/describe-resources.d.ts.map +1 -1
  16. package/dist/generated/index.d.ts +167 -40
  17. package/dist/generated/index.d.ts.map +1 -1
  18. package/dist/group-namespace.d.ts.map +1 -1
  19. package/dist/index.d.ts +3 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/integrity.json +13 -5
  22. package/dist/kube/apply.d.ts.map +1 -1
  23. package/dist/lint/audit-catalog.d.ts.map +1 -1
  24. package/dist/lint/post-synth/index.d.ts.map +1 -1
  25. package/dist/lint/post-synth/k8s-helpers.d.ts +28 -1
  26. package/dist/lint/post-synth/k8s-helpers.d.ts.map +1 -1
  27. package/dist/lint/post-synth/sops-helpers.d.ts +38 -0
  28. package/dist/lint/post-synth/sops-helpers.d.ts.map +1 -0
  29. package/dist/lint/post-synth/wk8404.d.ts +12 -0
  30. package/dist/lint/post-synth/wk8404.d.ts.map +1 -0
  31. package/dist/lint/post-synth/wk8405.d.ts +13 -0
  32. package/dist/lint/post-synth/wk8405.d.ts.map +1 -0
  33. package/dist/lint/post-synth/wk8406.d.ts +12 -0
  34. package/dist/lint/post-synth/wk8406.d.ts.map +1 -0
  35. package/dist/lint/post-synth/wk8407.d.ts +21 -0
  36. package/dist/lint/post-synth/wk8407.d.ts.map +1 -0
  37. package/dist/lint/post-synth/wk8503.d.ts +39 -0
  38. package/dist/lint/post-synth/wk8503.d.ts.map +1 -0
  39. package/dist/lint/post-synth/wk8504.d.ts +28 -0
  40. package/dist/lint/post-synth/wk8504.d.ts.map +1 -0
  41. package/dist/lint/post-synth/wk8505.d.ts +28 -0
  42. package/dist/lint/post-synth/wk8505.d.ts.map +1 -0
  43. package/dist/manifest.json +1 -1
  44. package/dist/meta.json +1039 -68
  45. package/dist/okf/index.md +26 -2
  46. package/dist/okf/rules/FLUX002.md +0 -1
  47. package/dist/okf/rules/WK8404.md +11 -0
  48. package/dist/okf/rules/WK8405.md +17 -0
  49. package/dist/okf/rules/WK8406.md +11 -0
  50. package/dist/okf/rules/WK8407.md +15 -0
  51. package/dist/okf/rules/WK8503.md +17 -0
  52. package/dist/okf/rules/WK8504.md +11 -0
  53. package/dist/okf/rules/WK8505.md +15 -0
  54. package/dist/okf/types/AWSClusterControllerIdentity.md +14 -0
  55. package/dist/okf/types/AWSManagedCluster.md +14 -0
  56. package/dist/okf/types/AWSManagedControlPlane.md +14 -0
  57. package/dist/okf/types/AWSManagedMachinePool.md +14 -0
  58. package/dist/okf/types/CAPICluster.md +14 -0
  59. package/dist/okf/types/Certificate.md +1 -0
  60. package/dist/okf/types/ClusterResourceSet.md +14 -0
  61. package/dist/okf/types/ClusterServingRuntime.md +14 -0
  62. package/dist/okf/types/{Cluster.md → CnpgCluster.md} +2 -2
  63. package/dist/okf/types/DBInstance.md +14 -0
  64. package/dist/okf/types/{Bucket.md → FluxBucket.md} +2 -6
  65. package/dist/okf/types/HelmChartProxy.md +14 -0
  66. package/dist/okf/types/IamRole.md +14 -0
  67. package/dist/okf/types/InferenceService.md +19 -0
  68. package/dist/okf/types/InfisicalDynamicSecret.md +4 -0
  69. package/dist/okf/types/InfisicalSecret.md +4 -0
  70. package/dist/okf/types/Kustomization.md +1 -0
  71. package/dist/okf/types/MachinePool.md +14 -0
  72. package/dist/okf/types/PDB.md +1 -0
  73. package/dist/okf/types/PodIdentityAssociation.md +14 -0
  74. package/dist/okf/types/Policy.md +14 -0
  75. package/dist/okf/types/S3Bucket.md +14 -0
  76. package/dist/okf/types/ServingRuntime.md +14 -0
  77. package/dist/okf/types/StatefulSet.md +1 -0
  78. package/dist/okf/types/User.md +14 -0
  79. package/dist/op/activities/ensure-secret.d.ts +41 -0
  80. package/dist/op/activities/ensure-secret.d.ts.map +1 -0
  81. package/dist/op/activities/index.d.ts +2 -0
  82. package/dist/op/activities/index.d.ts.map +1 -1
  83. package/dist/op/activities/kubectl.d.ts +9 -0
  84. package/dist/op/activities/kubectl.d.ts.map +1 -1
  85. package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
  86. package/dist/plugin.d.ts.map +1 -1
  87. package/dist/rules/k8s-helpers.ts +55 -1
  88. package/dist/rules/sops-helpers.ts +76 -0
  89. package/dist/rules/wk8404.ts +55 -0
  90. package/dist/rules/wk8405.ts +88 -0
  91. package/dist/rules/wk8406.ts +60 -0
  92. package/dist/rules/wk8407.ts +71 -0
  93. package/dist/rules/wk8503.ts +220 -0
  94. package/dist/rules/wk8504.ts +51 -0
  95. package/dist/rules/wk8505.ts +63 -0
  96. package/dist/secret-labels.d.ts +31 -0
  97. package/dist/secret-labels.d.ts.map +1 -0
  98. package/dist/secret-store.d.ts +73 -0
  99. package/dist/secret-store.d.ts.map +1 -0
  100. package/dist/serializer.d.ts.map +1 -1
  101. package/dist/sops/encrypted-secret-file.d.ts +52 -0
  102. package/dist/sops/encrypted-secret-file.d.ts.map +1 -0
  103. package/dist/sops/entity.d.ts +31 -0
  104. package/dist/sops/entity.d.ts.map +1 -0
  105. package/dist/teardown.d.ts +46 -0
  106. package/dist/teardown.d.ts.map +1 -0
  107. package/dist/types/index.d.ts +261 -9
  108. package/package.json +4 -4
  109. package/src/components/kubectl-apply.ts +5 -0
  110. package/src/composites/composites.test.ts +273 -0
  111. package/src/composites/flux-app.ts +23 -0
  112. package/src/composites/index.ts +11 -0
  113. package/src/composites/inference-service.ts +140 -0
  114. package/src/composites/model.ts +150 -0
  115. package/src/composites/vllm-serving-runtime.test.ts +167 -0
  116. package/src/composites/vllm-serving-runtime.ts +196 -0
  117. package/src/crd/cnpg.test.ts +8 -3
  118. package/src/crd/crd-sources.ts +183 -0
  119. package/src/crd/parser.test.ts +14 -0
  120. package/src/deep-observe-hooks.ts +24 -0
  121. package/src/deep-observe.test.ts +89 -0
  122. package/src/describe-resources.ts +6 -1
  123. package/src/generated/index.d.ts +261 -9
  124. package/src/generated/index.ts +167 -40
  125. package/src/generated/lexicon-k8s.json +1039 -68
  126. package/src/generated/operations.json +272 -0
  127. package/src/group-namespace.test.ts +31 -0
  128. package/src/group-namespace.ts +13 -0
  129. package/src/index.ts +6 -0
  130. package/src/kube/apply.ts +5 -0
  131. package/src/lifecycle-integration.test.ts +42 -0
  132. package/src/lint/audit-catalog.ts +7 -0
  133. package/src/lint/post-synth/index.ts +14 -0
  134. package/src/lint/post-synth/k8s-helpers.ts +55 -1
  135. package/src/lint/post-synth/post-synth.test.ts +826 -0
  136. package/src/lint/post-synth/sops-helpers.ts +76 -0
  137. package/src/lint/post-synth/wk8404.ts +55 -0
  138. package/src/lint/post-synth/wk8405.ts +88 -0
  139. package/src/lint/post-synth/wk8406.ts +60 -0
  140. package/src/lint/post-synth/wk8407.ts +71 -0
  141. package/src/lint/post-synth/wk8503.ts +220 -0
  142. package/src/lint/post-synth/wk8504.ts +51 -0
  143. package/src/lint/post-synth/wk8505.ts +63 -0
  144. package/src/op/activities/ensure-secret.ts +71 -0
  145. package/src/op/activities/index.ts +7 -0
  146. package/src/op/activities/kubectl.test.ts +32 -0
  147. package/src/op/activities/kubectl.ts +28 -5
  148. package/src/op/activities/wait-for-ready.test.ts +34 -0
  149. package/src/op/activities/wait-for-ready.ts +26 -0
  150. package/src/plugin.ts +34 -2
  151. package/src/secret-labels.ts +36 -0
  152. package/src/secret-store.test.ts +184 -0
  153. package/src/secret-store.ts +165 -0
  154. package/src/serializer.ts +33 -2
  155. package/src/sops/encrypted-secret-file.test.ts +333 -0
  156. package/src/sops/encrypted-secret-file.ts +264 -0
  157. package/src/sops/entity.ts +53 -0
  158. package/src/teardown.test.ts +240 -0
  159. package/src/teardown.ts +239 -0
  160. package/src/testdata/sops/db-credentials.sops.yaml +23 -0
@@ -0,0 +1,167 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { emitYAML } from "@intentius/chant/yaml";
3
+ import { VllmServingRuntime } from "./vllm-serving-runtime";
4
+
5
+ /** Helper to access props on a Declarable member. */
6
+ function p(member: unknown): Record<string, unknown> {
7
+ return (member as any).props;
8
+ }
9
+
10
+ describe("VllmServingRuntime", () => {
11
+ test("emits a namespaced ServingRuntime by default", () => {
12
+ const result = VllmServingRuntime({
13
+ name: "vllm-runtime",
14
+ namespace: "models",
15
+ image: "vllm/vllm-openai:v0.7.0",
16
+ });
17
+ const props = p(result.servingRuntime);
18
+ expect(props.metadata).toMatchObject({ name: "vllm-runtime", namespace: "models" });
19
+ });
20
+
21
+ test("emits a ClusterServingRuntime when clusterScoped is true", () => {
22
+ const result = VllmServingRuntime({
23
+ name: "vllm-runtime",
24
+ clusterScoped: true,
25
+ image: "vllm/vllm-openai:v0.7.0",
26
+ });
27
+ const props = p(result.servingRuntime);
28
+ expect(props.metadata).toMatchObject({ name: "vllm-runtime" });
29
+ expect((props.metadata as any).namespace).toBeUndefined();
30
+ });
31
+
32
+ test("throws when namespace is missing and clusterScoped is false", () => {
33
+ expect(() =>
34
+ VllmServingRuntime({
35
+ name: "vllm-runtime",
36
+ image: "vllm/vllm-openai:v0.7.0",
37
+ } as any),
38
+ ).toThrow(/namespace is required/);
39
+ });
40
+
41
+ test("registers the vllm supportedModelFormats entry", () => {
42
+ const result = VllmServingRuntime({
43
+ name: "vllm-runtime",
44
+ namespace: "models",
45
+ image: "vllm/vllm-openai:v0.7.0",
46
+ });
47
+ const spec = p(result.servingRuntime).spec as any;
48
+ expect(spec.supportedModelFormats).toEqual([{ name: "vllm", autoSelect: true }]);
49
+ });
50
+
51
+ test("supports overriding the model format", () => {
52
+ const result = VllmServingRuntime({
53
+ name: "vllm-runtime",
54
+ namespace: "models",
55
+ image: "vllm/vllm-openai:v0.7.0",
56
+ modelFormat: { name: "vllm", version: "1", priority: 1 },
57
+ });
58
+ const spec = p(result.servingRuntime).spec as any;
59
+ expect(spec.supportedModelFormats).toEqual([{ name: "vllm", version: "1", priority: 1 }]);
60
+ });
61
+
62
+ // ── Golden test: vLLM args are spec-true (real `vllm serve` flag names) ──
63
+
64
+ test("wires typed vLLM props to real container args, in flag order", () => {
65
+ const result = VllmServingRuntime({
66
+ name: "vllm-runtime",
67
+ namespace: "models",
68
+ image: "vllm/vllm-openai:v0.7.0",
69
+ tensorParallelSize: 2,
70
+ maxModelLen: 8192,
71
+ dtype: "bfloat16",
72
+ quantization: "awq",
73
+ gpuMemoryUtilization: 0.9,
74
+ maxNumSeqs: 256,
75
+ });
76
+ const spec = p(result.servingRuntime).spec as any;
77
+ const container = spec.containers[0];
78
+ expect(container.name).toBe("kserve-container");
79
+ expect(container.command).toEqual(["vllm", "serve", "/mnt/models"]);
80
+ expect(container.args).toEqual([
81
+ "--tensor-parallel-size", "2",
82
+ "--max-model-len", "8192",
83
+ "--dtype", "bfloat16",
84
+ "--quantization", "awq",
85
+ "--gpu-memory-utilization", "0.9",
86
+ "--max-num-seqs", "256",
87
+ ]);
88
+ });
89
+
90
+ test("containerArgs escape hatch appends after typed flags", () => {
91
+ const result = VllmServingRuntime({
92
+ name: "vllm-runtime",
93
+ namespace: "models",
94
+ image: "vllm/vllm-openai:v0.7.0",
95
+ tensorParallelSize: 1,
96
+ containerArgs: ["--enable-prefix-caching", "--disable-log-requests"],
97
+ });
98
+ const spec = p(result.servingRuntime).spec as any;
99
+ expect(spec.containers[0].args).toEqual([
100
+ "--tensor-parallel-size", "1",
101
+ "--enable-prefix-caching",
102
+ "--disable-log-requests",
103
+ ]);
104
+ });
105
+
106
+ test("omits unset vLLM flags rather than emitting empty/undefined args", () => {
107
+ const result = VllmServingRuntime({
108
+ name: "vllm-runtime",
109
+ namespace: "models",
110
+ image: "vllm/vllm-openai:v0.7.0",
111
+ });
112
+ const spec = p(result.servingRuntime).spec as any;
113
+ expect(spec.containers[0].args).toEqual([]);
114
+ });
115
+
116
+ test("GPU count maps to nvidia.com/gpu in resource requests and limits", () => {
117
+ const result = VllmServingRuntime({
118
+ name: "vllm-runtime",
119
+ namespace: "models",
120
+ image: "vllm/vllm-openai:v0.7.0",
121
+ resources: { cpu: "8", memory: "32Gi", gpu: 2 },
122
+ });
123
+ const spec = p(result.servingRuntime).spec as any;
124
+ const resources = spec.containers[0].resources;
125
+ expect(resources).toEqual({
126
+ requests: { cpu: "8", memory: "32Gi", "nvidia.com/gpu": "2" },
127
+ limits: { cpu: "8", memory: "32Gi", "nvidia.com/gpu": "2" },
128
+ });
129
+ });
130
+
131
+ test("resources is omitted entirely when not provided", () => {
132
+ const result = VllmServingRuntime({
133
+ name: "vllm-runtime",
134
+ namespace: "models",
135
+ image: "vllm/vllm-openai:v0.7.0",
136
+ });
137
+ const spec = p(result.servingRuntime).spec as any;
138
+ expect(spec.containers[0].resources).toBeUndefined();
139
+ });
140
+
141
+ test("serializes to valid YAML", () => {
142
+ const result = VllmServingRuntime({
143
+ name: "vllm-runtime",
144
+ namespace: "models",
145
+ image: "vllm/vllm-openai:v0.7.0",
146
+ tensorParallelSize: 2,
147
+ resources: { cpu: "8", memory: "32Gi", gpu: 2 },
148
+ });
149
+ const yaml = emitYAML(p(result.servingRuntime), 0);
150
+ expect(yaml).toContain("vllm-runtime");
151
+ expect(yaml).toContain("tensor-parallel-size");
152
+ expect(yaml).not.toContain("[object Object]");
153
+ });
154
+
155
+ test("defaults.servingRuntime passthrough merges into the resource", () => {
156
+ const result = VllmServingRuntime({
157
+ name: "vllm-runtime",
158
+ namespace: "models",
159
+ image: "vllm/vllm-openai:v0.7.0",
160
+ defaults: {
161
+ servingRuntime: { spec: { replicas: 3 } },
162
+ },
163
+ });
164
+ const spec = p(result.servingRuntime).spec as any;
165
+ expect(spec.replicas).toBe(3);
166
+ });
167
+ });
@@ -0,0 +1,196 @@
1
+ /**
2
+ * VllmServingRuntime composite — KServe ServingRuntime/ClusterServingRuntime
3
+ * running vLLM.
4
+ *
5
+ * Design rule (chant #982, #984): spec-true, not chant-flavored. Props mirror
6
+ * vLLM's real `vllm serve` CLI args — no generic runtime abstraction. KServe's
7
+ * ServingRuntime CRD is the pluggability seam; this composite types vLLM
8
+ * faithfully on top of it, it doesn't invent a wrapper around it.
9
+ *
10
+ * The model itself is not a prop here — an InferenceService (chant #985)
11
+ * references this runtime and supplies storageUri, which KServe mounts at
12
+ * /mnt/models inside the pod. That's the path this composite passes to
13
+ * `vllm serve`.
14
+ */
15
+
16
+ import { Composite, mergeDefaults } from "@intentius/chant";
17
+ import {
18
+ ServingRuntime,
19
+ ClusterServingRuntime,
20
+ } from "../generated";
21
+
22
+ /** Container resource spec. GPU count maps to nvidia.com/gpu requests/limits. */
23
+ export interface VllmResourceSpec {
24
+ /** CPU request/limit (e.g. "4", "500m"). */
25
+ cpu?: string;
26
+ /** Memory request/limit (e.g. "16Gi"). */
27
+ memory?: string;
28
+ /** GPU count — adds nvidia.com/gpu to resource requests and limits. */
29
+ gpu?: number;
30
+ }
31
+
32
+ export interface VllmServingRuntimeProps {
33
+ /** ServingRuntime/ClusterServingRuntime name. */
34
+ name: string;
35
+ /** Namespace — required unless clusterScoped is true. */
36
+ namespace?: string;
37
+ /**
38
+ * Emit a cluster-scoped ClusterServingRuntime instead of a namespaced
39
+ * ServingRuntime. Default false.
40
+ */
41
+ clusterScoped?: boolean;
42
+ /** vLLM container image (e.g. "vllm/vllm-openai:v0.7.0"). */
43
+ image: string;
44
+
45
+ // ── vLLM args (spec-true to `vllm serve --help`) ──────────────────────
46
+ /** --tensor-parallel-size */
47
+ tensorParallelSize?: number;
48
+ /** --max-model-len */
49
+ maxModelLen?: number;
50
+ /** --dtype */
51
+ dtype?: "auto" | "half" | "float16" | "bfloat16" | "float" | "float32";
52
+ /** --quantization (e.g. "awq", "gptq", "fp8") */
53
+ quantization?: string;
54
+ /** --gpu-memory-utilization (0.0-1.0) */
55
+ gpuMemoryUtilization?: number;
56
+ /** --max-num-seqs */
57
+ maxNumSeqs?: number;
58
+
59
+ /** Container resources. gpu maps to nvidia.com/gpu. */
60
+ resources?: VllmResourceSpec;
61
+ /**
62
+ * Additional raw args appended after the typed vLLM flags — escape hatch
63
+ * for anything not yet modeled as a typed prop.
64
+ */
65
+ containerArgs?: string[];
66
+ /**
67
+ * supportedModelFormats entry registered on the runtime. Default
68
+ * `{ name: "vllm", autoSelect: true }`.
69
+ */
70
+ modelFormat?: { name: string; version?: string; autoSelect?: boolean; priority?: number };
71
+ /** Additional labels applied to the resource. */
72
+ labels?: Record<string, string>;
73
+ /** Per-member defaults for fine-grained overrides via mergeDefaults. */
74
+ defaults?: {
75
+ servingRuntime?: Partial<Record<string, unknown>>;
76
+ };
77
+ }
78
+
79
+ export interface VllmServingRuntimeResult {
80
+ servingRuntime: InstanceType<typeof ServingRuntime> | InstanceType<typeof ClusterServingRuntime>;
81
+ }
82
+
83
+ /** Build the container args list, in `vllm serve --help` flag order. */
84
+ function buildVllmArgs(props: VllmServingRuntimeProps): string[] {
85
+ const {
86
+ tensorParallelSize,
87
+ maxModelLen,
88
+ dtype,
89
+ quantization,
90
+ gpuMemoryUtilization,
91
+ maxNumSeqs,
92
+ containerArgs = [],
93
+ } = props;
94
+
95
+ const args: string[] = [];
96
+ if (tensorParallelSize !== undefined) args.push("--tensor-parallel-size", String(tensorParallelSize));
97
+ if (maxModelLen !== undefined) args.push("--max-model-len", String(maxModelLen));
98
+ if (dtype !== undefined) args.push("--dtype", dtype);
99
+ if (quantization !== undefined) args.push("--quantization", quantization);
100
+ if (gpuMemoryUtilization !== undefined) args.push("--gpu-memory-utilization", String(gpuMemoryUtilization));
101
+ if (maxNumSeqs !== undefined) args.push("--max-num-seqs", String(maxNumSeqs));
102
+ args.push(...containerArgs);
103
+ return args;
104
+ }
105
+
106
+ /** Build container resource requests/limits. GPU count maps to nvidia.com/gpu. */
107
+ function buildResources(spec?: VllmResourceSpec): Record<string, unknown> | undefined {
108
+ if (!spec) return undefined;
109
+ const base: Record<string, unknown> = {};
110
+ if (spec.cpu) base.cpu = spec.cpu;
111
+ if (spec.memory) base.memory = spec.memory;
112
+ if (spec.gpu) base["nvidia.com/gpu"] = String(spec.gpu);
113
+ if (Object.keys(base).length === 0) return undefined;
114
+ return { requests: { ...base }, limits: { ...base } };
115
+ }
116
+
117
+ /**
118
+ * Create a VllmServingRuntime composite — a KServe ServingRuntime (or, with
119
+ * `clusterScoped: true`, a ClusterServingRuntime) that runs vLLM's OpenAI-
120
+ * compatible server, with vLLM's own flags surfaced as typed props.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * import { VllmServingRuntime } from "@intentius/chant-lexicon-k8s";
125
+ *
126
+ * const runtime = VllmServingRuntime({
127
+ * name: "vllm-runtime",
128
+ * namespace: "models",
129
+ * image: "vllm/vllm-openai:v0.7.0",
130
+ * tensorParallelSize: 2,
131
+ * maxModelLen: 8192,
132
+ * dtype: "bfloat16",
133
+ * gpuMemoryUtilization: 0.9,
134
+ * resources: { cpu: "8", memory: "32Gi", gpu: 2 },
135
+ * });
136
+ * ```
137
+ */
138
+ export const VllmServingRuntime = Composite((props: VllmServingRuntimeProps) => {
139
+ const {
140
+ name,
141
+ namespace,
142
+ clusterScoped = false,
143
+ image,
144
+ modelFormat = { name: "vllm", autoSelect: true },
145
+ labels: extraLabels = {},
146
+ defaults: defs,
147
+ } = props;
148
+
149
+ if (!clusterScoped && !namespace) {
150
+ throw new Error(
151
+ `VllmServingRuntime("${name}"): namespace is required unless clusterScoped is true.`,
152
+ );
153
+ }
154
+
155
+ const commonLabels: Record<string, string> = {
156
+ "app.kubernetes.io/name": name,
157
+ "app.kubernetes.io/managed-by": "chant",
158
+ "app.kubernetes.io/component": "vllm-serving-runtime",
159
+ ...extraLabels,
160
+ };
161
+
162
+ const resources = buildResources(props.resources);
163
+
164
+ const spec: Record<string, unknown> = {
165
+ supportedModelFormats: [modelFormat],
166
+ containers: [
167
+ {
168
+ name: "kserve-container",
169
+ image,
170
+ command: ["vllm", "serve", "/mnt/models"],
171
+ args: buildVllmArgs(props),
172
+ ports: [{ containerPort: 8000, protocol: "TCP", name: "http1" }],
173
+ ...(resources && { resources }),
174
+ },
175
+ ],
176
+ };
177
+
178
+ const servingRuntime = clusterScoped
179
+ ? new ClusterServingRuntime(mergeDefaults({
180
+ metadata: {
181
+ name,
182
+ labels: commonLabels,
183
+ },
184
+ spec,
185
+ }, defs?.servingRuntime))
186
+ : new ServingRuntime(mergeDefaults({
187
+ metadata: {
188
+ name,
189
+ namespace,
190
+ labels: commonLabels,
191
+ },
192
+ spec,
193
+ }, defs?.servingRuntime));
194
+
195
+ return { servingRuntime };
196
+ }, "VllmServingRuntime");
@@ -9,7 +9,12 @@
9
9
  */
10
10
 
11
11
  import { describe, test, expect } from "vitest";
12
- import { Cluster, ScheduledBackup, ObjectStore } from "../generated";
12
+ // chant #10 — CAPI's own `Cluster` kind (K8s::CAPI::Cluster) collides with
13
+ // CNPG's `Cluster` (K8s::Cnpg::Cluster) on the bare class name, so codegen's
14
+ // disambiguation now exports this one as `CnpgCluster`. The generated
15
+ // resource type name (K8s::Cnpg::Cluster) and the serialized `kind: Cluster`
16
+ // are unaffected — only the TS identifier changed.
17
+ import { CnpgCluster, ScheduledBackup, ObjectStore } from "../generated";
13
18
  import { k8sSerializer } from "../serializer";
14
19
  import { parseYAML } from "@intentius/chant/yaml";
15
20
 
@@ -20,7 +25,7 @@ function synth(logicalName: string, resource: unknown): any {
20
25
  }
21
26
 
22
27
  describe("CNPG Cluster", () => {
23
- const cluster = new Cluster({
28
+ const cluster = new CnpgCluster({
24
29
  metadata: { name: "fountain-pg", namespace: "fountain", labels: { app: "fountain" } },
25
30
  spec: {
26
31
  instances: 1,
@@ -166,7 +171,7 @@ describe("barman-cloud ObjectStore", () => {
166
171
  // not exist.
167
172
  const clusterDoc = synth(
168
173
  "fountainPg",
169
- new Cluster({
174
+ new CnpgCluster({
170
175
  metadata: { name: "fountain-pg", namespace: "fountain" },
171
176
  spec: {
172
177
  instances: 1,
@@ -311,6 +311,172 @@ const K3S_API_VERSION = "v0.1.4"; // vendored by k3s v1.36.3+k3s1
311
311
  const K3S_HELM_CONTROLLER_CRD_BASE = `https://raw.githubusercontent.com/k3s-io/helm-controller/${K3S_HELM_CONTROLLER_VERSION}/pkg/crds/yaml/generated`;
312
312
  const K3S_API_CRD_BASE = `https://raw.githubusercontent.com/k3s-io/api/${K3S_API_VERSION}/pkg/crds/yaml/generated`;
313
313
 
314
+ /**
315
+ * KServe CRDs — serving.kserve.io
316
+ *
317
+ * The model-serving control plane chant #982 builds vLLM composites on top
318
+ * of. Produces (the `serving.kserve.io` group maps to the `KServe` namespace
319
+ * — see GROUP_NAMESPACE_OVERRIDES in group-namespace.ts; the first-segment
320
+ * rule would otherwise give `K8s::Serving::*`):
321
+ * K8s::KServe::InferenceService → serving.kserve.io/v1beta1, kind: InferenceService
322
+ * K8s::KServe::ServingRuntime → serving.kserve.io/v1alpha1, kind: ServingRuntime
323
+ * K8s::KServe::ClusterServingRuntime → serving.kserve.io/v1alpha1, kind: ClusterServingRuntime
324
+ *
325
+ * Sourced from `config/crd/full`, not `config/crd/minimal` — `full` is what
326
+ * the release's own kustomization.yaml (and `kubectl apply -k config/crd`)
327
+ * actually installs; `minimal` is a stripped, non-validating variant with no
328
+ * OpenAPI schema to speak of. `full`'s InferenceService schema is the entire
329
+ * spec-true predictor/transformer/explainer union (~1MB of YAML) — see
330
+ * schemaPatches in codegen/patches.ts if generation trips on its recursion
331
+ * depth.
332
+ *
333
+ * Pinned to the last patch of the 0.15 line rather than latest (0.20.0,
334
+ * weeks old at pin time) — a release that's had over a year to settle,
335
+ * matching the CNPG precedent above of tracking what a real operator runs
336
+ * rather than the newest tag.
337
+ *
338
+ * Operator install: kubectl apply --server-side -k
339
+ * "github.com/kserve/kserve/config/default?ref=v0.15.2"
340
+ */
341
+ const KSERVE_VERSION = "v0.15.2";
342
+ const KSERVE_CRD_BASE = `https://raw.githubusercontent.com/kserve/kserve/${KSERVE_VERSION}/config/crd/full`;
343
+
344
+ /**
345
+ * Cluster API (CAPI) core CRDs — cluster.x-k8s.io/v1beta2
346
+ *
347
+ * The provider-agnostic cluster-lifecycle surface CAPA, CAAPH, and every
348
+ * other CAPI provider build on. Only the two kinds a project actually
349
+ * declares are taken; `ClusterClass`, `MachineDeployment`, `MachineSet`,
350
+ * `MachineHealthCheck`, `MachineDrainRule`, the `ipam.cluster.x-k8s.io` pair,
351
+ * and `ExtensionConfig` are deliberately left out (the scale note at the top
352
+ * of this file: add the kinds a project uses). `ClusterResourceSet`, CAPI
353
+ * core's other addons.cluster.x-k8s.io kind, is a separate source below —
354
+ * see the CAAPH block for why it's grouped there. Produces (the group takes
355
+ * the `CAPI` override — see GROUP_NAMESPACE_OVERRIDES in group-namespace.ts,
356
+ * which avoids the `K8s::Cluster::Cluster` stutter):
357
+ * K8s::CAPI::Cluster → cluster.x-k8s.io/v1beta2, kind: Cluster
358
+ * K8s::CAPI::MachinePool → cluster.x-k8s.io/v1beta2, kind: MachinePool
359
+ *
360
+ * Controller install: kubectl apply -f
361
+ * https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.14.0/cluster-api-components.yaml
362
+ */
363
+ const CAPI_VERSION = "v1.14.0";
364
+ const CAPI_CRD_BASE = `https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/${CAPI_VERSION}/core/config/crd/bases`;
365
+
366
+ /**
367
+ * Cluster API Provider AWS (CAPA) CRDs — infrastructure.cluster.x-k8s.io + controlplane.cluster.x-k8s.io
368
+ *
369
+ * The AWS infrastructure and EKS control-plane providers for CAPI. Produces
370
+ * (first-segment rule, no override — the `AWS` kind prefix already keeps
371
+ * these from reading like anything else in the file):
372
+ * K8s::Infrastructure::AWSManagedCluster → infrastructure.cluster.x-k8s.io/v1beta2
373
+ * K8s::Infrastructure::AWSManagedMachinePool → infrastructure.cluster.x-k8s.io/v1beta2
374
+ * K8s::Infrastructure::AWSClusterControllerIdentity → infrastructure.cluster.x-k8s.io/v1beta2
375
+ * K8s::Controlplane::AWSManagedControlPlane → controlplane.cluster.x-k8s.io/v1beta2
376
+ *
377
+ * Contradicts chant #10's own text: the issue describes AWSManagedControlPlane
378
+ * as an `infrastructure.cluster.x-k8s.io` kind alongside the other three. The
379
+ * shipped CRD (`config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml`
380
+ * in the v2.13.0 tag) puts it under `controlplane.cluster.x-k8s.io` instead —
381
+ * CAPA's EKS control-plane provider is a `controlplane.*` provider like
382
+ * kubeadm's, not an infra one. The parser reads `spec.group` from the fetched
383
+ * CRD document itself (`parseCRDSpec` in `crd/parser.ts`), so the source list
384
+ * below only needs the right URL; the resulting type name follows upstream's
385
+ * actual group, not the issue text's.
386
+ *
387
+ * Operator install: with clusterctl,
388
+ * clusterctl init --infrastructure aws:v2.13.0
389
+ */
390
+ const CAPA_VERSION = "v2.13.0";
391
+ const CAPA_CRD_BASE = `https://raw.githubusercontent.com/kubernetes-sigs/cluster-api-provider-aws/${CAPA_VERSION}/config/crd/bases`;
392
+
393
+ /**
394
+ * Cluster API Addon Provider for Helm (CAAPH) CRD, plus CAPI core's own
395
+ * addon-binding kind — both addons.cluster.x-k8s.io/v1alpha1|v1beta2
396
+ *
397
+ * `HelmChartProxy` is CAAPH's kind — installs a Helm chart across every
398
+ * cluster a label selector matches. `ClusterResourceSet` ships in CAPI
399
+ * core's own `core/` component (same CAPI_VERSION/CAPI_CRD_BASE pin as the
400
+ * `Cluster`/`MachinePool` source above), not a separate provider — it binds
401
+ * a set of raw manifests (ConfigMaps/Secrets) to matching clusters. They're
402
+ * grouped in one source block because both are what the RegionCluster
403
+ * composite's flux-addon wiring needs (chant#11) and both share the
404
+ * `addons.cluster.x-k8s.io` group, so both land under `K8s::Addons::*`:
405
+ * K8s::Addons::HelmChartProxy → addons.cluster.x-k8s.io/v1alpha1, kind: HelmChartProxy
406
+ * K8s::Addons::ClusterResourceSet → addons.cluster.x-k8s.io/v1beta2, kind: ClusterResourceSet
407
+ *
408
+ * `HelmReleaseProxy` (CAAPH's per-cluster status-tracking CR, never authored
409
+ * directly) is deliberately left out.
410
+ *
411
+ * Operator install: with clusterctl,
412
+ * clusterctl init --addon helm:v0.6.4
413
+ */
414
+ const CAAPH_VERSION = "v0.6.4";
415
+ const CAAPH_CRD_BASE = `https://raw.githubusercontent.com/kubernetes-sigs/cluster-api-addon-provider-helm/${CAAPH_VERSION}/config/crd/bases`;
416
+
417
+ /**
418
+ * AWS Controllers for Kubernetes (ACK) — S3 controller CRD — s3.services.k8s.aws/v1alpha1
419
+ *
420
+ * Each ACK service controller is its own release train with its own CRDs, so
421
+ * (per the docs' "add the specific kinds a project uses" guidance, and
422
+ * matching how the ACK IAM group below stays split from EKS/RDS/S3) it is
423
+ * its own scoped source rather than one entry per AWS "provider". Produces:
424
+ * K8s::S3::Bucket → s3.services.k8s.aws/v1alpha1, kind: Bucket
425
+ *
426
+ * Controller install: helm install ack-s3-controller
427
+ * oci://public.ecr.aws/aws-controllers-k8s/s3-chart --version 1.10.0
428
+ */
429
+ const ACK_S3_VERSION = "v1.10.0";
430
+ const ACK_S3_CRD_BASE = `https://raw.githubusercontent.com/aws-controllers-k8s/s3-controller/${ACK_S3_VERSION}/config/crd/bases`;
431
+
432
+ /**
433
+ * ACK RDS controller CRD — rds.services.k8s.aws/v1alpha1
434
+ *
435
+ * Only `DBInstance` — the standalone-instance kind a project provisions
436
+ * directly. `DBCluster` and its endpoint/parameter-group/snapshot satellites
437
+ * are left out (no known consumer; add them when one shows up). Produces:
438
+ * K8s::Rds::DBInstance → rds.services.k8s.aws/v1alpha1, kind: DBInstance
439
+ *
440
+ * Controller install: helm install ack-rds-controller
441
+ * oci://public.ecr.aws/aws-controllers-k8s/rds-chart --version 1.11.1
442
+ */
443
+ const ACK_RDS_VERSION = "v1.11.1";
444
+ const ACK_RDS_CRD_BASE = `https://raw.githubusercontent.com/aws-controllers-k8s/rds-controller/${ACK_RDS_VERSION}/config/crd/bases`;
445
+
446
+ /**
447
+ * ACK IAM controller CRDs — iam.services.k8s.aws/v1alpha1
448
+ *
449
+ * `Role`, `User`, `Policy` — the three IAM primitives a cluster workload
450
+ * needing AWS access declares (a Role's trust policy, a User's access keys,
451
+ * a Policy document attached to either). `Group`, `InstanceProfile`,
452
+ * `OpenIDConnectProvider`, and `ServiceLinkedRole` are left out. Produces:
453
+ * K8s::Iam::Role → iam.services.k8s.aws/v1alpha1, kind: Role
454
+ * K8s::Iam::User → iam.services.k8s.aws/v1alpha1, kind: User
455
+ * K8s::Iam::Policy → iam.services.k8s.aws/v1alpha1, kind: Policy
456
+ *
457
+ * Controller install: helm install ack-iam-controller
458
+ * oci://public.ecr.aws/aws-controllers-k8s/iam-chart --version 1.8.1
459
+ */
460
+ const ACK_IAM_VERSION = "v1.8.1";
461
+ const ACK_IAM_CRD_BASE = `https://raw.githubusercontent.com/aws-controllers-k8s/iam-controller/${ACK_IAM_VERSION}/config/crd/bases`;
462
+
463
+ /**
464
+ * ACK EKS controller CRD — eks.services.k8s.aws/v1alpha1
465
+ *
466
+ * Only `PodIdentityAssociation` — EKS Pod Identity is the modern replacement
467
+ * for IRSA, and the ACK-managed way to bind a Kubernetes ServiceAccount to
468
+ * an IAM role without a webhook. `Cluster`, `Addon`, `Nodegroup`,
469
+ * `FargateProfile`, `AccessEntry`, `IdentityProviderConfig`, and
470
+ * `Capability` are left out (this k8s lexicon doesn't provision the EKS
471
+ * cluster itself). Produces:
472
+ * K8s::Eks::PodIdentityAssociation → eks.services.k8s.aws/v1alpha1, kind: PodIdentityAssociation
473
+ *
474
+ * Controller install: helm install ack-eks-controller
475
+ * oci://public.ecr.aws/aws-controllers-k8s/eks-chart --version 1.20.0
476
+ */
477
+ const ACK_EKS_VERSION = "v1.20.0";
478
+ const ACK_EKS_CRD_BASE = `https://raw.githubusercontent.com/aws-controllers-k8s/eks-controller/${ACK_EKS_VERSION}/config/crd/bases`;
479
+
314
480
  export const CRD_SOURCES: CRDSource[] = [
315
481
  { type: "url", url: `${KUBERAY_CRD_BASE}/ray.io_rayclusters.yaml` },
316
482
  { type: "url", url: `${KUBERAY_CRD_BASE}/ray.io_rayjobs.yaml` },
@@ -378,4 +544,21 @@ export const CRD_SOURCES: CRDSource[] = [
378
544
  version: KUBEMICROVM_VERSION,
379
545
  kinds: ["MicroVM", "MicroVMImage", "MicroVMNetwork", "MicroVMClass", "MicroVMReplicaSet"],
380
546
  },
547
+ { type: "url", url: `${KSERVE_CRD_BASE}/serving.kserve.io_inferenceservices.yaml` },
548
+ { type: "url", url: `${KSERVE_CRD_BASE}/serving.kserve.io_servingruntimes.yaml` },
549
+ { type: "url", url: `${KSERVE_CRD_BASE}/serving.kserve.io_clusterservingruntimes.yaml` },
550
+ { type: "url", url: `${CAPI_CRD_BASE}/cluster.x-k8s.io_clusters.yaml` },
551
+ { type: "url", url: `${CAPI_CRD_BASE}/cluster.x-k8s.io_machinepools.yaml` },
552
+ { type: "url", url: `${CAPA_CRD_BASE}/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml` },
553
+ { type: "url", url: `${CAPA_CRD_BASE}/infrastructure.cluster.x-k8s.io_awsmanagedclusters.yaml` },
554
+ { type: "url", url: `${CAPA_CRD_BASE}/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml` },
555
+ { type: "url", url: `${CAPA_CRD_BASE}/infrastructure.cluster.x-k8s.io_awsclustercontrolleridentities.yaml` },
556
+ { type: "url", url: `${CAAPH_CRD_BASE}/addons.cluster.x-k8s.io_helmchartproxies.yaml` },
557
+ { type: "url", url: `${CAPI_CRD_BASE}/addons.cluster.x-k8s.io_clusterresourcesets.yaml` },
558
+ { type: "url", url: `${ACK_S3_CRD_BASE}/s3.services.k8s.aws_buckets.yaml` },
559
+ { type: "url", url: `${ACK_RDS_CRD_BASE}/rds.services.k8s.aws_dbinstances.yaml` },
560
+ { type: "url", url: `${ACK_IAM_CRD_BASE}/iam.services.k8s.aws_roles.yaml` },
561
+ { type: "url", url: `${ACK_IAM_CRD_BASE}/iam.services.k8s.aws_users.yaml` },
562
+ { type: "url", url: `${ACK_IAM_CRD_BASE}/iam.services.k8s.aws_policies.yaml` },
563
+ { type: "url", url: `${ACK_EKS_CRD_BASE}/eks.services.k8s.aws_podidentityassociations.yaml` },
381
564
  ];
@@ -286,6 +286,20 @@ describe("parseCRDSpec", () => {
286
286
  expect(results[0].resource.typeName).toBe("K8s::Argo::Application");
287
287
  });
288
288
 
289
+ test("serving.kserve.io group maps to the KServe namespace (override)", () => {
290
+ const spec = {
291
+ group: "serving.kserve.io",
292
+ names: { kind: "InferenceService", plural: "inferenceservices" },
293
+ scope: "Namespaced" as const,
294
+ versions: [
295
+ { name: "v1beta1", served: true, storage: true },
296
+ ],
297
+ };
298
+
299
+ const results = parseCRDSpec(spec);
300
+ expect(results[0].resource.typeName).toBe("K8s::KServe::InferenceService");
301
+ });
302
+
289
303
  test("all Flux toolkit + operator groups collapse to the Flux namespace", () => {
290
304
  const cases: Array<[string, string]> = [
291
305
  ["source.toolkit.fluxcd.io", "GitRepository"],
@@ -55,6 +55,7 @@ import {
55
55
  k8sListMapOrderKey,
56
56
  } from "@intentius/chant/managed-fields";
57
57
  import { LABEL_OWNERSHIP_KEYS, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
58
+ import { GENERATED_ONCE_LABEL_KEY } from "./secret-labels";
58
59
 
59
60
  /**
60
61
  * Kubernetes-defaulted fields, per entity type, as index-erased property
@@ -115,6 +116,10 @@ export const K8S_SERVICE_DEFAULTS: Record<string, Record<string, unknown>> = {
115
116
  "spec.ipFamilyPolicy": "SingleStack",
116
117
  "spec.ports[].protocol": "TCP",
117
118
  },
119
+ "K8s::Core::Secret": {
120
+ // The API server defaults an untyped Secret to Opaque (#1830).
121
+ "type": "Opaque",
122
+ },
118
123
  };
119
124
 
120
125
  /**
@@ -142,6 +147,9 @@ export const K8S_SERVER_ASSIGNED_PATTERNS: Record<string, ReadonlySet<string>> =
142
147
  const K8S_OWNERSHIP_LABEL_PATTERNS: ReadonlySet<string> = new Set([
143
148
  `metadata.labels.${LABEL_OWNERSHIP_KEYS.stack}`,
144
149
  `metadata.labels.${LABEL_OWNERSHIP_KEYS.env}`,
150
+ // The generated-once marker (#1830) is stamped by the secret store at
151
+ // mint time, never by a serializer — chant's own signature, not drift.
152
+ `metadata.labels.${GENERATED_ONCE_LABEL_KEY}`,
145
153
  ]);
146
154
 
147
155
  const K8S_MANAGED_BY_LABEL_PATTERN = `metadata.labels.${LABEL_OWNERSHIP_KEYS.managedBy}`;
@@ -306,7 +314,23 @@ function isRecordLike(value: unknown): value is Record<string, unknown> {
306
314
  return typeof value === "object" && value !== null && !Array.isArray(value);
307
315
  }
308
316
 
317
+ /**
318
+ * The paths on a `K8s::Core::Secret` whose values are secret material —
319
+ * `data.<key>` (live and declared base64) and `stringData.<key>` (declared
320
+ * plaintext). Masked on BOTH sides by core's normalization pass, so presence
321
+ * and key names still classify as drift while no value (or value-derived
322
+ * comparison) ever reaches a diff row — #1365 decision 6, the contract #1830's
323
+ * generated-once secrets rely on. Key-name masking alone cannot do this:
324
+ * Secret keys are arbitrary (`app.conf`), not name-shaped.
325
+ */
326
+ const K8S_SECRET_ENTITY_TYPE = "K8s::Core::Secret";
327
+ const K8S_SECRET_VALUE_PATTERN = /^(?:data|stringData)\./;
328
+
309
329
  export const k8sDeepNormalizationHooks: DeepNormalizationHooks = {
330
+ mask(node: DeepNode): boolean {
331
+ return node.entityType === K8S_SECRET_ENTITY_TYPE && K8S_SECRET_VALUE_PATTERN.test(node.pattern);
332
+ },
333
+
310
334
  prune(node: DeepNode): boolean {
311
335
  if (K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS.has(node.pattern)) return true;
312
336