@intentius/chant-lexicon-k8s 0.124.31 → 0.124.32

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 (270) hide show
  1. package/dist/actions/apps.js +18 -0
  2. package/dist/actions/batch.js +6 -0
  3. package/dist/actions/core.js +48 -0
  4. package/dist/actions/index.js +13 -0
  5. package/dist/actions/networking.js +11 -0
  6. package/dist/actions/rbac.js +8 -0
  7. package/dist/api/classify.js +123 -0
  8. package/dist/api/connect.js +62 -0
  9. package/dist/api/fake-cluster.js +175 -0
  10. package/dist/api/operation-surface.d.ts.map +1 -1
  11. package/dist/api/operation-surface.js +109 -0
  12. package/dist/api/owner-chain.js +100 -0
  13. package/dist/api/sweep-types.js +35 -0
  14. package/dist/behaviour-kinds.js +82 -0
  15. package/dist/codegen/docs-cli.js +2 -0
  16. package/dist/codegen/docs.js +146 -0
  17. package/dist/codegen/generate-cli.js +30 -0
  18. package/dist/codegen/generate-lexicon.js +74 -0
  19. package/dist/codegen/generate-list-map-keys.js +88 -0
  20. package/dist/codegen/generate-operations.js +53 -0
  21. package/dist/codegen/generate-typescript.js +124 -0
  22. package/dist/codegen/generate.js +173 -0
  23. package/dist/codegen/naming.js +174 -0
  24. package/dist/codegen/package.js +36 -0
  25. package/dist/codegen/patches.js +88 -0
  26. package/dist/codegen/typecheck.js +4 -0
  27. package/dist/codegen/versions.js +41 -0
  28. package/dist/components/argo-app.js +51 -0
  29. package/dist/components/builders.js +22 -0
  30. package/dist/components/capability-plugin.js +31 -0
  31. package/dist/components/flux-reconcile.js +61 -0
  32. package/dist/components/index.d.ts +1 -1
  33. package/dist/components/index.d.ts.map +1 -1
  34. package/dist/components/index.js +18 -0
  35. package/dist/components/kubectl-apply.d.ts +83 -1
  36. package/dist/components/kubectl-apply.d.ts.map +1 -1
  37. package/dist/components/kubectl-apply.js +274 -0
  38. package/dist/components/kustomize-apply.js +25 -0
  39. package/dist/composites/adot-collector.js +184 -0
  40. package/dist/composites/agic-ingress.js +85 -0
  41. package/dist/composites/aks-external-dns-agent.js +146 -0
  42. package/dist/composites/alb-ingress.js +86 -0
  43. package/dist/composites/argo-app.js +199 -0
  44. package/dist/composites/autoscaled-service.js +174 -0
  45. package/dist/composites/azure-disk-storage-class.js +47 -0
  46. package/dist/composites/azure-file-storage-class.js +48 -0
  47. package/dist/composites/azure-monitor-collector.js +176 -0
  48. package/dist/composites/batch-job.js +139 -0
  49. package/dist/composites/catalog.js +4110 -0
  50. package/dist/composites/cockroachdb-cluster.js +371 -0
  51. package/dist/composites/cockroachdb-region-stack.js +369 -0
  52. package/dist/composites/config-connector-context.js +40 -0
  53. package/dist/composites/configured-app.js +135 -0
  54. package/dist/composites/cron-schedule.js +38 -0
  55. package/dist/composites/cron-workload.js +109 -0
  56. package/dist/composites/ebs-storage-class.js +54 -0
  57. package/dist/composites/efs-storage-class.js +46 -0
  58. package/dist/composites/external-dns-agent.js +122 -0
  59. package/dist/composites/filestore-storage-class.js +49 -0
  60. package/dist/composites/fluent-bit-agent.js +157 -0
  61. package/dist/composites/flux-app.js +156 -0
  62. package/dist/composites/gce-ingress.js +83 -0
  63. package/dist/composites/gce-pd-storage-class.js +49 -0
  64. package/dist/composites/gke-external-dns-agent.js +129 -0
  65. package/dist/composites/gke-fluent-bit-agent.js +165 -0
  66. package/dist/composites/gke-gateway.js +91 -0
  67. package/dist/composites/gke-otel-collector.js +94 -0
  68. package/dist/composites/index.js +51 -0
  69. package/dist/composites/inference-service.js +76 -0
  70. package/dist/composites/irsa-service-account.js +77 -0
  71. package/dist/composites/member-defaults.js +14 -0
  72. package/dist/composites/metrics-server.js +181 -0
  73. package/dist/composites/model.js +92 -0
  74. package/dist/composites/monitored-service.js +152 -0
  75. package/dist/composites/namespace-env.js +114 -0
  76. package/dist/composites/network-isolated-app.js +124 -0
  77. package/dist/composites/node-agent.js +149 -0
  78. package/dist/composites/operator-stack.js +268 -0
  79. package/dist/composites/otel-collector-agent.js +142 -0
  80. package/dist/composites/otel-collector-gateway.js +216 -0
  81. package/dist/composites/otel-collector-node.js +176 -0
  82. package/dist/composites/otel-collector-rbac.js +115 -0
  83. package/dist/composites/otel-collector-shape.js +128 -0
  84. package/dist/composites/otel-collector.js +115 -0
  85. package/dist/composites/otel-operator-collector.js +123 -0
  86. package/dist/composites/ray-cluster.js +378 -0
  87. package/dist/composites/ray-job.js +136 -0
  88. package/dist/composites/ray-service.js +178 -0
  89. package/dist/composites/secure-ingress.js +100 -0
  90. package/dist/composites/security-context.js +1 -0
  91. package/dist/composites/sidecar-app.js +105 -0
  92. package/dist/composites/stateful-app.js +111 -0
  93. package/dist/composites/vllm-serving-runtime.js +114 -0
  94. package/dist/composites/web-app.js +157 -0
  95. package/dist/composites/worker-pool.js +167 -0
  96. package/dist/composites/workload-identity-sa.js +81 -0
  97. package/dist/composites/workload-identity-service-account.js +77 -0
  98. package/dist/config-schema.js +72 -0
  99. package/dist/config.js +59 -0
  100. package/dist/coverage.js +27 -0
  101. package/dist/crd/crd-sources.js +615 -0
  102. package/dist/crd/loader.js +181 -0
  103. package/dist/crd/oci-chart.js +144 -0
  104. package/dist/crd/parser.js +382 -0
  105. package/dist/crd/project-codegen.js +273 -0
  106. package/dist/crd/types.js +7 -0
  107. package/dist/deep-observe-hooks.js +306 -0
  108. package/dist/deep-observe.js +234 -0
  109. package/dist/default-labels.js +87 -0
  110. package/dist/describe-resources.js +699 -0
  111. package/dist/describe-stack-status.js +99 -0
  112. package/dist/detect.js +11 -0
  113. package/dist/effect-receipt-row.js +205 -0
  114. package/dist/export-resources.js +39 -0
  115. package/dist/generated/index.js +1092 -0
  116. package/dist/generated/runtime.js +4 -0
  117. package/dist/group-namespace.js +96 -0
  118. package/dist/import/embedded.js +197 -0
  119. package/dist/import/generator.js +288 -0
  120. package/dist/import/live-export.js +85 -0
  121. package/dist/import/parser.js +204 -0
  122. package/dist/import/testdata/embedded/fixtures.js +60 -0
  123. package/dist/include/entity.js +78 -0
  124. package/dist/include/resolve.js +170 -0
  125. package/dist/index.js +59 -0
  126. package/dist/integrity.json +3 -3
  127. package/dist/kube/apply.js +90 -0
  128. package/dist/kube/connect.js +46 -0
  129. package/dist/kube/delete.js +138 -0
  130. package/dist/kube/describe.js +169 -0
  131. package/dist/kube/events.js +132 -0
  132. package/dist/kube/flags.js +91 -0
  133. package/dist/kube/get.js +159 -0
  134. package/dist/kube/group.js +123 -0
  135. package/dist/kube/logs.js +82 -0
  136. package/dist/kube/project.js +94 -0
  137. package/dist/kube/render.js +227 -0
  138. package/dist/kube/source.js +122 -0
  139. package/dist/kube/target.js +63 -0
  140. package/dist/kube/testing.js +28 -0
  141. package/dist/kube/top.js +99 -0
  142. package/dist/kube/verdict.js +72 -0
  143. package/dist/kube/wait.js +129 -0
  144. package/dist/kustomize/render.js +73 -0
  145. package/dist/kustomize/rendered-entity.js +67 -0
  146. package/dist/kustomize/root.js +68 -0
  147. package/dist/lint/audit-catalog.js +61 -0
  148. package/dist/lint/audit-lineage.js +118 -0
  149. package/dist/lint/post-synth/argo-helpers.js +36 -0
  150. package/dist/lint/post-synth/argo002.js +31 -0
  151. package/dist/lint/post-synth/argo003.js +53 -0
  152. package/dist/lint/post-synth/argo005.js +54 -0
  153. package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
  154. package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
  155. package/dist/lint/post-synth/flux002.js +56 -0
  156. package/dist/lint/post-synth/flux003.js +50 -0
  157. package/dist/lint/post-synth/index.js +104 -0
  158. package/dist/lint/post-synth/k8s-helpers.js +186 -0
  159. package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
  160. package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
  161. package/dist/lint/post-synth/sops-helpers.js +43 -0
  162. package/dist/lint/post-synth/wk8005.js +44 -0
  163. package/dist/lint/post-synth/wk8006.js +56 -0
  164. package/dist/lint/post-synth/wk8041.js +60 -0
  165. package/dist/lint/post-synth/wk8042.js +36 -0
  166. package/dist/lint/post-synth/wk8101.js +52 -0
  167. package/dist/lint/post-synth/wk8102.js +30 -0
  168. package/dist/lint/post-synth/wk8103.js +33 -0
  169. package/dist/lint/post-synth/wk8104.js +58 -0
  170. package/dist/lint/post-synth/wk8105.js +33 -0
  171. package/dist/lint/post-synth/wk8201.js +44 -0
  172. package/dist/lint/post-synth/wk8202.js +34 -0
  173. package/dist/lint/post-synth/wk8203.js +34 -0
  174. package/dist/lint/post-synth/wk8204.js +71 -0
  175. package/dist/lint/post-synth/wk8205.js +38 -0
  176. package/dist/lint/post-synth/wk8207.js +33 -0
  177. package/dist/lint/post-synth/wk8208.js +33 -0
  178. package/dist/lint/post-synth/wk8209.js +33 -0
  179. package/dist/lint/post-synth/wk8301.js +49 -0
  180. package/dist/lint/post-synth/wk8302.js +34 -0
  181. package/dist/lint/post-synth/wk8303.js +68 -0
  182. package/dist/lint/post-synth/wk8304.js +58 -0
  183. package/dist/lint/post-synth/wk8305.js +96 -0
  184. package/dist/lint/post-synth/wk8306.js +38 -0
  185. package/dist/lint/post-synth/wk8401.js +86 -0
  186. package/dist/lint/post-synth/wk8402.js +31 -0
  187. package/dist/lint/post-synth/wk8403.js +49 -0
  188. package/dist/lint/post-synth/wk8404.js +36 -0
  189. package/dist/lint/post-synth/wk8405.js +65 -0
  190. package/dist/lint/post-synth/wk8406.js +50 -0
  191. package/dist/lint/post-synth/wk8407.js +58 -0
  192. package/dist/lint/post-synth/wk8501.js +36 -0
  193. package/dist/lint/post-synth/wk8502.js +35 -0
  194. package/dist/lint/post-synth/wk8503.js +164 -0
  195. package/dist/lint/post-synth/wk8504.js +20 -0
  196. package/dist/lint/post-synth/wk8505.js +31 -0
  197. package/dist/lint/post-synth/wk8601.js +40 -0
  198. package/dist/lint/post-synth/wk8602.js +120 -0
  199. package/dist/lint/post-synth/wk8603.js +63 -0
  200. package/dist/lint/post-synth/wk8604.js +31 -0
  201. package/dist/lint/post-synth/wk8605.js +124 -0
  202. package/dist/lint/post-synth/wk8606.js +22 -0
  203. package/dist/lint/post-synth/wk8701.js +31 -0
  204. package/dist/lint/post-synth/wk8702.js +37 -0
  205. package/dist/lint/post-synth/wk8703.js +31 -0
  206. package/dist/lint/post-synth/wk8704.js +135 -0
  207. package/dist/lint/post-synth/wk8705.js +38 -0
  208. package/dist/lint/rules/argo-appset-single-project.js +56 -0
  209. package/dist/lint/rules/argo-ast.js +94 -0
  210. package/dist/lint/rules/argo-automated-prune.js +59 -0
  211. package/dist/lint/rules/flux-source-ref-pin.js +44 -0
  212. package/dist/lint/rules/hardcoded-namespace.js +57 -0
  213. package/dist/lint/rules/latest-image-tag.js +103 -0
  214. package/dist/lint/rules/missing-resource-limits.js +85 -0
  215. package/dist/lsp/completions.d.ts.map +1 -1
  216. package/dist/lsp/completions.js +18 -0
  217. package/dist/lsp/hover.d.ts.map +1 -1
  218. package/dist/lsp/hover.js +57 -0
  219. package/dist/manifest-entity.js +71 -0
  220. package/dist/manifest.json +1 -1
  221. package/dist/micro-time.js +49 -0
  222. package/dist/op/activities/apply-diff.d.ts +31 -0
  223. package/dist/op/activities/apply-diff.d.ts.map +1 -0
  224. package/dist/op/activities/apply-diff.js +79 -0
  225. package/dist/op/activities/argo.js +98 -0
  226. package/dist/op/activities/ensure-secret.js +38 -0
  227. package/dist/op/activities/index.js +51 -0
  228. package/dist/op/activities/kubectl.d.ts +64 -0
  229. package/dist/op/activities/kubectl.d.ts.map +1 -1
  230. package/dist/op/activities/kubectl.js +517 -0
  231. package/dist/op/activities/wait-for-ready.js +229 -0
  232. package/dist/op/builders.js +68 -0
  233. package/dist/package-cli.js +21 -0
  234. package/dist/patch/json-patch.js +187 -0
  235. package/dist/patch/rendered-patch.js +94 -0
  236. package/dist/plugin.js +741 -0
  237. package/dist/project-kinds.js +56 -0
  238. package/dist/receipt-store.js +216 -0
  239. package/dist/rules/crd-schema-helpers.ts +2 -1
  240. package/dist/secret-labels.js +33 -0
  241. package/dist/secret-store.js +116 -0
  242. package/dist/serializer.d.ts.map +1 -1
  243. package/dist/serializer.js +578 -0
  244. package/dist/sops/encrypted-secret-file.js +194 -0
  245. package/dist/sops/entity.js +32 -0
  246. package/dist/spec/fetch.js +58 -0
  247. package/dist/spec/parse.js +622 -0
  248. package/dist/subscribe-changes.js +168 -0
  249. package/dist/teardown.js +197 -0
  250. package/dist/telemetry-release.js +86 -0
  251. package/dist/validate-cli.js +17 -0
  252. package/dist/validate.js +47 -0
  253. package/dist/variables.js +34 -0
  254. package/dist/whoami.js +109 -0
  255. package/package.json +12 -12
  256. package/src/api/operation-surface.ts +2 -1
  257. package/src/codegen/package.ts +2 -2
  258. package/src/components/index.ts +3 -0
  259. package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
  260. package/src/components/kubectl-apply.test.ts +262 -0
  261. package/src/components/kubectl-apply.ts +341 -12
  262. package/src/deep-observe-hooks.ts +2 -2
  263. package/src/import/roundtrip-build.test.ts +46 -0
  264. package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
  265. package/src/lsp/completions.ts +2 -1
  266. package/src/lsp/hover.ts +2 -1
  267. package/src/op/activities/apply-diff.ts +87 -0
  268. package/src/op/activities/kubectl-plan.test.ts +224 -0
  269. package/src/op/activities/kubectl.ts +204 -16
  270. package/src/serializer.ts +2 -1
@@ -0,0 +1,68 @@
1
+ /**
2
+ * WK8303: PodDisruptionBudget Recommended for HA Deployments
3
+ *
4
+ * Deployments with 2 or more replicas should have a corresponding
5
+ * PodDisruptionBudget (PDB) to ensure minimum availability during
6
+ * voluntary disruptions like node drains and cluster upgrades.
7
+ */
8
+ import { docsToManifests } from "./k8s-helpers.js";
9
+ export const wk8303 = {
10
+ id: "WK8303",
11
+ description: "PDB recommended for HA Deployments — ensures availability during voluntary disruptions",
12
+ check(ctx) {
13
+ const diagnostics = [];
14
+ const manifests = docsToManifests(ctx);
15
+ // Collect all PDB selectors to match against Deployments
16
+ const pdbSelectors = collectPdbSelectors(manifests);
17
+ for (const manifest of manifests) {
18
+ if (manifest.kind !== "Deployment")
19
+ continue;
20
+ const spec = manifest.spec;
21
+ if (!spec)
22
+ continue;
23
+ const replicas = spec.replicas;
24
+ // Only check HA deployments (replicas >= 2)
25
+ if (typeof replicas !== "number" || replicas < 2)
26
+ continue;
27
+ const resourceName = manifest.metadata?.name ?? "Deployment";
28
+ // Check if any PDB targets this Deployment's labels
29
+ const selector = spec.selector;
30
+ const matchLabels = selector?.matchLabels;
31
+ if (!matchLabels || !hasCoveringPdb(matchLabels, pdbSelectors)) {
32
+ diagnostics.push({
33
+ checkId: "WK8303",
34
+ severity: "info",
35
+ message: `Deployment "${resourceName}" has ${replicas} replicas but no PodDisruptionBudget — add a PDB to ensure availability during voluntary disruptions`,
36
+ entity: resourceName,
37
+ lexicon: "k8s",
38
+ });
39
+ }
40
+ }
41
+ return diagnostics;
42
+ },
43
+ };
44
+ /**
45
+ * Collect matchLabels from all PodDisruptionBudget resources.
46
+ */
47
+ function collectPdbSelectors(manifests) {
48
+ const selectors = [];
49
+ for (const manifest of manifests) {
50
+ if (manifest.kind !== "PodDisruptionBudget")
51
+ continue;
52
+ const spec = manifest.spec;
53
+ if (!spec)
54
+ continue;
55
+ const selector = spec.selector;
56
+ const matchLabels = selector?.matchLabels;
57
+ if (matchLabels && typeof matchLabels === "object") {
58
+ selectors.push(matchLabels);
59
+ }
60
+ }
61
+ return selectors;
62
+ }
63
+ /**
64
+ * Check if any PDB selector covers (is a subset of) the given labels.
65
+ */
66
+ function hasCoveringPdb(deploymentLabels, pdbSelectors) {
67
+ return pdbSelectors.some((pdbLabels) => Object.entries(pdbLabels).every(([key, value]) => deploymentLabels[key] === value));
68
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * WK8304: SSL Redirect Without Certificate
3
+ *
4
+ * Flags Ingress resources that have `alb.ingress.kubernetes.io/ssl-redirect`
5
+ * annotation set but are missing `alb.ingress.kubernetes.io/certificate-arn`
6
+ * or don't have HTTPS in their listen-ports.
7
+ */
8
+ import { docsToManifests } from "./k8s-helpers.js";
9
+ export const wk8304 = {
10
+ id: "WK8304",
11
+ description: "SSL redirect without certificate — ssl-redirect annotation requires a valid certificate-arn and HTTPS listen-ports",
12
+ check(ctx) {
13
+ const diagnostics = [];
14
+ for (const manifest of docsToManifests(ctx)) {
15
+ if (manifest.kind !== "Ingress")
16
+ continue;
17
+ const annotations = manifest.metadata?.annotations;
18
+ if (!annotations)
19
+ continue;
20
+ const sslRedirect = annotations["alb.ingress.kubernetes.io/ssl-redirect"];
21
+ if (!sslRedirect)
22
+ continue;
23
+ const resourceName = manifest.metadata?.name ?? "Ingress";
24
+ const certArn = annotations["alb.ingress.kubernetes.io/certificate-arn"];
25
+ if (!certArn) {
26
+ diagnostics.push({
27
+ checkId: "WK8304",
28
+ severity: "warning",
29
+ message: `Ingress "${resourceName}" has ssl-redirect annotation but no certificate-arn — HTTPS redirect will fail without a TLS certificate`,
30
+ entity: resourceName,
31
+ lexicon: "k8s",
32
+ });
33
+ continue;
34
+ }
35
+ // Check listen-ports includes HTTPS
36
+ const listenPorts = annotations["alb.ingress.kubernetes.io/listen-ports"];
37
+ if (listenPorts) {
38
+ try {
39
+ const ports = JSON.parse(listenPorts);
40
+ const hasHttps = ports.some((p) => "HTTPS" in p);
41
+ if (!hasHttps) {
42
+ diagnostics.push({
43
+ checkId: "WK8304",
44
+ severity: "warning",
45
+ message: `Ingress "${resourceName}" has ssl-redirect but listen-ports does not include HTTPS`,
46
+ entity: resourceName,
47
+ lexicon: "k8s",
48
+ });
49
+ }
50
+ }
51
+ catch {
52
+ // Can't parse listen-ports — skip this check
53
+ }
54
+ }
55
+ }
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * WK8305: Ingress Port Not Matching Service
3
+ *
4
+ * Flags Ingress backends whose `service.port.number` does not match
5
+ * any declared port on the referenced Service in the manifest set.
6
+ */
7
+ import { docsToManifests } from "./k8s-helpers.js";
8
+ export const wk8305 = {
9
+ id: "WK8305",
10
+ description: "Ingress port not matching Service — backend port must match a declared Service port",
11
+ check(ctx) {
12
+ const diagnostics = [];
13
+ const manifests = docsToManifests(ctx);
14
+ // Build a map of Service name+namespace → set of port numbers
15
+ const servicePorts = collectServicePorts(manifests);
16
+ for (const manifest of manifests) {
17
+ if (manifest.kind !== "Ingress")
18
+ continue;
19
+ const ingressName = manifest.metadata?.name ?? "Ingress";
20
+ const ingressNamespace = manifest.metadata?.namespace ?? "default";
21
+ const spec = manifest.spec;
22
+ if (!spec)
23
+ continue;
24
+ const rules = spec.rules;
25
+ if (!rules)
26
+ continue;
27
+ for (const rule of rules) {
28
+ const http = rule.http;
29
+ if (!http)
30
+ continue;
31
+ const paths = http.paths;
32
+ if (!paths)
33
+ continue;
34
+ for (const pathEntry of paths) {
35
+ const backend = pathEntry.backend;
36
+ if (!backend)
37
+ continue;
38
+ const service = backend.service;
39
+ if (!service)
40
+ continue;
41
+ const svcName = service.name;
42
+ const port = service.port;
43
+ const portNumber = port?.number;
44
+ if (!svcName || portNumber === undefined)
45
+ continue;
46
+ // Look up the Service in the manifest set
47
+ const key = `${ingressNamespace}/${svcName}`;
48
+ const knownPorts = servicePorts.get(key);
49
+ // Skip if the Service is not in the manifest set (external service)
50
+ if (!knownPorts)
51
+ continue;
52
+ if (!knownPorts.has(portNumber)) {
53
+ diagnostics.push({
54
+ checkId: "WK8305",
55
+ severity: "warning",
56
+ message: `Ingress "${ingressName}" references Service "${svcName}" port ${portNumber}, but the Service only declares ports [${[...knownPorts].join(", ")}]`,
57
+ entity: ingressName,
58
+ lexicon: "k8s",
59
+ });
60
+ }
61
+ }
62
+ }
63
+ }
64
+ return diagnostics;
65
+ },
66
+ };
67
+ /**
68
+ * Collect port numbers from all Service manifests, keyed by namespace/name.
69
+ */
70
+ function collectServicePorts(manifests) {
71
+ const result = new Map();
72
+ for (const manifest of manifests) {
73
+ if (manifest.kind !== "Service")
74
+ continue;
75
+ const name = manifest.metadata?.name;
76
+ const namespace = manifest.metadata?.namespace ?? "default";
77
+ if (!name)
78
+ continue;
79
+ const key = `${namespace}/${name}`;
80
+ const ports = new Set();
81
+ const spec = manifest.spec;
82
+ if (spec) {
83
+ const specPorts = spec.ports;
84
+ if (specPorts) {
85
+ for (const p of specPorts) {
86
+ const port = p.port;
87
+ if (port !== undefined) {
88
+ ports.add(port);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ result.set(key, ports);
94
+ }
95
+ return result;
96
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * WK8306: Container Command Starts With Flag
3
+ *
4
+ * If `command[0]` starts with `-` or `--`, it's almost certainly a mistake —
5
+ * the first element should be the binary/entrypoint, flags belong in `args`.
6
+ * This causes OCI runtime errors because the container runtime tries to
7
+ * execute the flag as a binary.
8
+ */
9
+ import { docsToManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers.js";
10
+ export const wk8306 = {
11
+ id: "WK8306",
12
+ description: "Container command starts with flag — first element should be a binary, not a flag",
13
+ check(ctx) {
14
+ const diagnostics = [];
15
+ for (const manifest of docsToManifests(ctx)) {
16
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind))
17
+ continue;
18
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
19
+ const containers = extractContainers(manifest);
20
+ for (const container of containers) {
21
+ const command = container.command;
22
+ if (!Array.isArray(command) || command.length === 0)
23
+ continue;
24
+ const firstArg = String(command[0]);
25
+ if (firstArg.startsWith("-")) {
26
+ diagnostics.push({
27
+ checkId: "WK8306",
28
+ severity: "error",
29
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has command[0]="${firstArg}" which starts with a flag — the first element should be the binary, flags belong in args`,
30
+ entity: resourceName,
31
+ lexicon: "k8s",
32
+ });
33
+ }
34
+ }
35
+ }
36
+ return diagnostics;
37
+ },
38
+ };
@@ -0,0 +1,86 @@
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
+ import { docsToManifests, parseMemoryBytes } from "./k8s-helpers.js";
10
+ export const wk8401 = {
11
+ id: "WK8401",
12
+ description: "shmSize must not exceed the container memory limit — pod will not schedule if it does",
13
+ check(ctx) {
14
+ const diagnostics = [];
15
+ for (const manifest of docsToManifests(ctx)) {
16
+ if (manifest.kind !== "RayCluster")
17
+ continue;
18
+ const clusterName = manifest.metadata?.name ?? "RayCluster";
19
+ const spec = manifest.spec;
20
+ if (!spec)
21
+ continue;
22
+ const groups = [];
23
+ // Head group
24
+ const headGroupSpec = spec.headGroupSpec;
25
+ if (headGroupSpec) {
26
+ const tmpl = headGroupSpec.template;
27
+ const podSpec = tmpl?.spec;
28
+ if (podSpec)
29
+ groups.push({ label: "head", templateSpec: podSpec });
30
+ }
31
+ // Worker groups
32
+ const workerGroupSpecs = spec.workerGroupSpecs;
33
+ if (Array.isArray(workerGroupSpecs)) {
34
+ for (const wg of workerGroupSpecs) {
35
+ const name = wg.groupName ?? "worker";
36
+ const tmpl = wg.template;
37
+ const podSpec = tmpl?.spec;
38
+ if (podSpec)
39
+ groups.push({ label: `worker "${name}"`, templateSpec: podSpec });
40
+ }
41
+ }
42
+ for (const { label, templateSpec } of groups) {
43
+ // Find the emptyDir Memory volume (dshm)
44
+ const volumes = templateSpec.volumes;
45
+ if (!Array.isArray(volumes))
46
+ continue;
47
+ for (const vol of volumes) {
48
+ const emptyDir = vol.emptyDir;
49
+ if (!emptyDir || emptyDir.medium !== "Memory")
50
+ continue;
51
+ const sizeLimit = emptyDir.sizeLimit;
52
+ if (!sizeLimit)
53
+ continue;
54
+ const shmBytes = parseMemoryBytes(sizeLimit);
55
+ if (isNaN(shmBytes))
56
+ continue;
57
+ // Find memory limit from the first container that mounts this volume
58
+ const containers = templateSpec.containers;
59
+ if (!Array.isArray(containers))
60
+ continue;
61
+ for (const container of containers) {
62
+ const resources = container.resources;
63
+ const limits = resources?.limits;
64
+ const memLimit = limits?.memory;
65
+ if (!memLimit)
66
+ continue;
67
+ const memBytes = parseMemoryBytes(memLimit);
68
+ if (isNaN(memBytes))
69
+ continue;
70
+ if (shmBytes > memBytes) {
71
+ diagnostics.push({
72
+ checkId: "WK8401",
73
+ severity: "error",
74
+ message: `RayCluster "${clusterName}" ${label}: shmSize "${sizeLimit}" exceeds memory limit "${memLimit}" — pod will not schedule`,
75
+ entity: clusterName,
76
+ lexicon: "k8s",
77
+ });
78
+ }
79
+ break; // Only check the first container per group
80
+ }
81
+ }
82
+ }
83
+ }
84
+ return diagnostics;
85
+ },
86
+ };
@@ -0,0 +1,31 @@
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
+ import { docsToManifests } from "./k8s-helpers.js";
9
+ export const wk8402 = {
10
+ id: "WK8402",
11
+ description: "RayCluster should set spec.rayVersion so KubeRay selects the correct autoscaler image",
12
+ check(ctx) {
13
+ const diagnostics = [];
14
+ for (const manifest of docsToManifests(ctx)) {
15
+ if (manifest.kind !== "RayCluster")
16
+ continue;
17
+ const name = manifest.metadata?.name ?? "RayCluster";
18
+ const rayVersion = manifest.spec?.rayVersion;
19
+ if (!rayVersion) {
20
+ diagnostics.push({
21
+ checkId: "WK8402",
22
+ severity: "warning",
23
+ message: `RayCluster "${name}" is missing spec.rayVersion — KubeRay autoscaler will pull the "latest" image tag`,
24
+ entity: name,
25
+ lexicon: "k8s",
26
+ });
27
+ }
28
+ }
29
+ return diagnostics;
30
+ },
31
+ };
@@ -0,0 +1,49 @@
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
+ import { docsToManifests, extractRayVersion } from "./k8s-helpers.js";
9
+ export const wk8403 = {
10
+ id: "WK8403",
11
+ description: "spec.rayVersion should match the Ray version in the head container image tag",
12
+ check(ctx) {
13
+ const diagnostics = [];
14
+ for (const manifest of docsToManifests(ctx)) {
15
+ if (manifest.kind !== "RayCluster")
16
+ continue;
17
+ const clusterName = manifest.metadata?.name ?? "RayCluster";
18
+ const spec = manifest.spec;
19
+ if (!spec)
20
+ continue;
21
+ const rayVersion = spec.rayVersion;
22
+ if (!rayVersion)
23
+ continue; // WK8402 covers the missing case
24
+ // Extract version from head container image
25
+ const headGroupSpec = spec.headGroupSpec;
26
+ const tmpl = headGroupSpec?.template;
27
+ const podSpec = tmpl?.spec;
28
+ const containers = podSpec?.containers;
29
+ if (!Array.isArray(containers) || containers.length === 0)
30
+ continue;
31
+ const image = containers[0].image;
32
+ if (!image)
33
+ continue;
34
+ const imageVersion = extractRayVersion(image);
35
+ if (!imageVersion)
36
+ continue; // Can't determine version from tag
37
+ if (imageVersion !== rayVersion) {
38
+ diagnostics.push({
39
+ checkId: "WK8403",
40
+ severity: "warning",
41
+ message: `RayCluster "${clusterName}": spec.rayVersion "${rayVersion}" does not match head image tag "${imageVersion}" — autoscaler may run a mismatched Ray version`,
42
+ entity: clusterName,
43
+ lexicon: "k8s",
44
+ });
45
+ }
46
+ }
47
+ return diagnostics;
48
+ },
49
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * WK8404: GPU request without a matching toleration
3
+ *
4
+ * A pod that requests `nvidia.com/gpu` but carries no toleration for the
5
+ * `nvidia.com/gpu` taint won't schedule onto a tainted GPU node pool (see
6
+ * `GpuNodePool`, #987, which taints its nodes `nvidia.com/gpu=present` by
7
+ * default). The pod sits `Pending` with `0/N nodes are available` until a
8
+ * toleration is added.
9
+ */
10
+ import { docsToManifests, extractContainers, extractPodSpec, requestsGpu, toleratesGpu, GPU_POD_KINDS, } from "./k8s-helpers.js";
11
+ export const wk8404 = {
12
+ id: "WK8404",
13
+ description: "GPU request without a matching nvidia.com/gpu toleration",
14
+ check(ctx) {
15
+ const diagnostics = [];
16
+ for (const manifest of docsToManifests(ctx)) {
17
+ if (!manifest.kind || !GPU_POD_KINDS.has(manifest.kind))
18
+ continue;
19
+ const containers = extractContainers(manifest);
20
+ if (!requestsGpu(containers))
21
+ continue;
22
+ const podSpec = extractPodSpec(manifest);
23
+ if (toleratesGpu(podSpec?.tolerations))
24
+ continue;
25
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
26
+ diagnostics.push({
27
+ checkId: "WK8404",
28
+ severity: "error",
29
+ message: `${manifest.kind} "${resourceName}" requests nvidia.com/gpu but has no toleration for the nvidia.com/gpu taint — it will not schedule onto a tainted GPU node pool`,
30
+ entity: resourceName,
31
+ lexicon: "k8s",
32
+ });
33
+ }
34
+ return diagnostics;
35
+ },
36
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * WK8405: Serving workload without a PodDisruptionBudget
3
+ *
4
+ * An InferenceService (or a Deployment carrying a serving
5
+ * `app.kubernetes.io/component` label — e.g. `inference-service` or
6
+ * `vllm-serving-runtime`) with no PodDisruptionBudget selecting it can lose
7
+ * every replica at once during a voluntary disruption (node drain, cluster
8
+ * upgrade), taking the model endpoint down. Serving workloads should ship
9
+ * with a PDB.
10
+ */
11
+ import { docsToManifests } from "./k8s-helpers.js";
12
+ /** `app.kubernetes.io/component` values this lexicon's composites use for serving workloads. */
13
+ const SERVING_COMPONENTS = new Set(["inference-service", "vllm-serving-runtime", "model-serving"]);
14
+ function isServingWorkload(manifest) {
15
+ if (manifest.kind === "InferenceService")
16
+ return true;
17
+ if (manifest.kind !== "Deployment" && manifest.kind !== "StatefulSet")
18
+ return false;
19
+ const component = manifest.metadata?.labels?.["app.kubernetes.io/component"];
20
+ return component !== undefined && SERVING_COMPONENTS.has(component);
21
+ }
22
+ export const wk8405 = {
23
+ id: "WK8405",
24
+ description: "Serving workload (InferenceService / serving Deployment) has no PodDisruptionBudget",
25
+ check(ctx) {
26
+ const diagnostics = [];
27
+ const manifests = docsToManifests(ctx);
28
+ const pdbSelectors = collectPdbSelectors(manifests);
29
+ for (const manifest of manifests) {
30
+ if (!isServingWorkload(manifest))
31
+ continue;
32
+ const labels = manifest.metadata?.labels;
33
+ const resourceName = manifest.metadata?.name ?? manifest.kind ?? "workload";
34
+ if (!labels || !hasCoveringPdb(labels, pdbSelectors)) {
35
+ diagnostics.push({
36
+ checkId: "WK8405",
37
+ severity: "info",
38
+ message: `${manifest.kind} "${resourceName}" is a serving workload with no PodDisruptionBudget — add a PDB so a node drain or cluster upgrade cannot take down every replica at once`,
39
+ entity: resourceName,
40
+ lexicon: "k8s",
41
+ });
42
+ }
43
+ }
44
+ return diagnostics;
45
+ },
46
+ };
47
+ function collectPdbSelectors(manifests) {
48
+ const selectors = [];
49
+ for (const manifest of manifests) {
50
+ if (manifest.kind !== "PodDisruptionBudget")
51
+ continue;
52
+ const spec = manifest.spec;
53
+ if (!spec)
54
+ continue;
55
+ const selector = spec.selector;
56
+ const matchLabels = selector?.matchLabels;
57
+ if (matchLabels && typeof matchLabels === "object") {
58
+ selectors.push(matchLabels);
59
+ }
60
+ }
61
+ return selectors;
62
+ }
63
+ function hasCoveringPdb(workloadLabels, pdbSelectors) {
64
+ return pdbSelectors.some((pdbLabels) => Object.entries(pdbLabels).every(([key, value]) => workloadLabels[key] === value));
65
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * WK8406: No resource limits on a GPU pod
3
+ *
4
+ * A container that requests `nvidia.com/gpu` but sets no cpu/memory limits
5
+ * can starve other pods on the same (expensive, scarce) GPU node of CPU and
6
+ * memory headroom. Unlike WK8201's general resource-limits guidance, this
7
+ * check is scoped to GPU-requesting containers specifically, since GPU node
8
+ * capacity is costlier and more contended than a general-purpose pool.
9
+ */
10
+ import { docsToManifests, extractContainers, GPU_POD_KINDS, NVIDIA_GPU_RESOURCE } from "./k8s-helpers.js";
11
+ export const wk8406 = {
12
+ id: "WK8406",
13
+ description: "GPU-requesting container has no cpu/memory resource limits",
14
+ check(ctx) {
15
+ const diagnostics = [];
16
+ for (const manifest of docsToManifests(ctx)) {
17
+ if (!manifest.kind || !GPU_POD_KINDS.has(manifest.kind))
18
+ continue;
19
+ const containers = extractContainers(manifest);
20
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
21
+ for (const container of containers) {
22
+ const requests = container.resources?.requests;
23
+ const limits = container.resources?.limits;
24
+ const requestsGpu = Boolean(requests?.[NVIDIA_GPU_RESOURCE]) || Boolean(limits?.[NVIDIA_GPU_RESOURCE]);
25
+ if (!requestsGpu)
26
+ continue;
27
+ const missing = [];
28
+ if (!limits || typeof limits !== "object") {
29
+ missing.push("cpu", "memory");
30
+ }
31
+ else {
32
+ if (!limits.cpu)
33
+ missing.push("cpu");
34
+ if (!limits.memory)
35
+ missing.push("memory");
36
+ }
37
+ if (missing.length > 0) {
38
+ diagnostics.push({
39
+ checkId: "WK8406",
40
+ severity: "warning",
41
+ message: `GPU container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" is missing resource limits for ${missing.join(", ")} — a GPU pod without cpu/memory limits can starve its (scarce, expensive) node`,
42
+ entity: resourceName,
43
+ lexicon: "k8s",
44
+ });
45
+ }
46
+ }
47
+ }
48
+ return diagnostics;
49
+ },
50
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * WK8407: Unpinned model version
3
+ *
4
+ * KServe's InferenceService predictor pins a model by `storageUri`. The
5
+ * `Model` composite (#986) always appends `version` as the trailing path
6
+ * segment (`scheme://id/version`), so a resolved `Model` reference is
7
+ * always pinned — but `InferenceService.model` also accepts a raw
8
+ * `storageUri` string (#985), which can bypass that guarantee (e.g.
9
+ * `"gs://bucket/model"` with no version segment, or a floating tag like
10
+ * `"latest"`/`"main"`). An unpinned reference means a redeploy of the same
11
+ * InferenceService can silently pick up different weights.
12
+ */
13
+ import { docsToManifests } from "./k8s-helpers.js";
14
+ /** Trailing path segments that mean "no explicit version" even though present. */
15
+ const FLOATING_TAGS = new Set(["latest", "main", "master", "head"]);
16
+ /**
17
+ * True when `storageUri` has an explicit, non-floating version segment:
18
+ * `scheme://path/version` with at least two path segments after the
19
+ * scheme, and the last segment isn't a known floating tag.
20
+ */
21
+ export function isPinnedStorageUri(storageUri) {
22
+ const m = /^[a-z][a-z0-9+.-]*:\/\/(.+)$/i.exec(storageUri.trim());
23
+ if (!m)
24
+ return false;
25
+ const segments = m[1].split("/").filter((s) => s.length > 0);
26
+ if (segments.length < 2)
27
+ return false;
28
+ const last = segments[segments.length - 1].toLowerCase();
29
+ return !FLOATING_TAGS.has(last);
30
+ }
31
+ export const wk8407 = {
32
+ id: "WK8407",
33
+ description: "InferenceService model storageUri has no explicit version segment",
34
+ check(ctx) {
35
+ const diagnostics = [];
36
+ for (const manifest of docsToManifests(ctx)) {
37
+ if (manifest.kind !== "InferenceService")
38
+ continue;
39
+ const spec = manifest.spec;
40
+ const predictor = spec?.predictor;
41
+ const model = predictor?.model;
42
+ const storageUri = model?.storageUri;
43
+ if (!storageUri)
44
+ continue;
45
+ if (!isPinnedStorageUri(storageUri)) {
46
+ const resourceName = manifest.metadata?.name ?? "InferenceService";
47
+ diagnostics.push({
48
+ checkId: "WK8407",
49
+ severity: "warning",
50
+ message: `InferenceService "${resourceName}": model storageUri "${storageUri}" has no explicit version segment — pin it (e.g. via the Model composite's required \`version\`) so a redeploy can't silently pick up different weights`,
51
+ entity: resourceName,
52
+ lexicon: "k8s",
53
+ });
54
+ }
55
+ }
56
+ return diagnostics;
57
+ },
58
+ };