@intentius/chant-lexicon-k8s 0.46.0 → 0.50.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 (248) 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 +8 -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/operator-stack.d.ts +183 -0
  12. package/dist/composites/operator-stack.d.ts.map +1 -0
  13. package/dist/composites/vllm-serving-runtime.d.ts +100 -0
  14. package/dist/composites/vllm-serving-runtime.d.ts.map +1 -0
  15. package/dist/crd/crd-sources.d.ts.map +1 -1
  16. package/dist/deep-observe-hooks.d.ts.map +1 -1
  17. package/dist/generated/index.d.ts +167 -40
  18. package/dist/generated/index.d.ts.map +1 -1
  19. package/dist/group-namespace.d.ts.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/integrity.json +41 -33
  23. package/dist/kube/apply.d.ts.map +1 -1
  24. package/dist/lint/audit-catalog.d.ts.map +1 -1
  25. package/dist/lint/post-synth/argo-helpers.d.ts.map +1 -1
  26. package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
  27. package/dist/lint/post-synth/index.d.ts.map +1 -1
  28. package/dist/lint/post-synth/k8s-helpers.d.ts +56 -6
  29. package/dist/lint/post-synth/k8s-helpers.d.ts.map +1 -1
  30. package/dist/lint/post-synth/sops-helpers.d.ts +38 -0
  31. package/dist/lint/post-synth/sops-helpers.d.ts.map +1 -0
  32. package/dist/lint/post-synth/wk8005.d.ts.map +1 -1
  33. package/dist/lint/post-synth/wk8006.d.ts.map +1 -1
  34. package/dist/lint/post-synth/wk8041.d.ts.map +1 -1
  35. package/dist/lint/post-synth/wk8042.d.ts.map +1 -1
  36. package/dist/lint/post-synth/wk8101.d.ts.map +1 -1
  37. package/dist/lint/post-synth/wk8102.d.ts.map +1 -1
  38. package/dist/lint/post-synth/wk8103.d.ts.map +1 -1
  39. package/dist/lint/post-synth/wk8104.d.ts.map +1 -1
  40. package/dist/lint/post-synth/wk8105.d.ts.map +1 -1
  41. package/dist/lint/post-synth/wk8201.d.ts.map +1 -1
  42. package/dist/lint/post-synth/wk8202.d.ts.map +1 -1
  43. package/dist/lint/post-synth/wk8203.d.ts.map +1 -1
  44. package/dist/lint/post-synth/wk8204.d.ts.map +1 -1
  45. package/dist/lint/post-synth/wk8205.d.ts.map +1 -1
  46. package/dist/lint/post-synth/wk8207.d.ts.map +1 -1
  47. package/dist/lint/post-synth/wk8208.d.ts.map +1 -1
  48. package/dist/lint/post-synth/wk8209.d.ts.map +1 -1
  49. package/dist/lint/post-synth/wk8301.d.ts.map +1 -1
  50. package/dist/lint/post-synth/wk8302.d.ts.map +1 -1
  51. package/dist/lint/post-synth/wk8303.d.ts.map +1 -1
  52. package/dist/lint/post-synth/wk8304.d.ts.map +1 -1
  53. package/dist/lint/post-synth/wk8305.d.ts.map +1 -1
  54. package/dist/lint/post-synth/wk8306.d.ts.map +1 -1
  55. package/dist/lint/post-synth/wk8401.d.ts.map +1 -1
  56. package/dist/lint/post-synth/wk8402.d.ts.map +1 -1
  57. package/dist/lint/post-synth/wk8403.d.ts.map +1 -1
  58. package/dist/lint/post-synth/wk8404.d.ts +12 -0
  59. package/dist/lint/post-synth/wk8404.d.ts.map +1 -0
  60. package/dist/lint/post-synth/wk8405.d.ts +13 -0
  61. package/dist/lint/post-synth/wk8405.d.ts.map +1 -0
  62. package/dist/lint/post-synth/wk8406.d.ts +12 -0
  63. package/dist/lint/post-synth/wk8406.d.ts.map +1 -0
  64. package/dist/lint/post-synth/wk8407.d.ts +21 -0
  65. package/dist/lint/post-synth/wk8407.d.ts.map +1 -0
  66. package/dist/lint/post-synth/wk8503.d.ts +39 -0
  67. package/dist/lint/post-synth/wk8503.d.ts.map +1 -0
  68. package/dist/lint/post-synth/wk8504.d.ts +28 -0
  69. package/dist/lint/post-synth/wk8504.d.ts.map +1 -0
  70. package/dist/lint/post-synth/wk8505.d.ts +28 -0
  71. package/dist/lint/post-synth/wk8505.d.ts.map +1 -0
  72. package/dist/manifest.json +1 -1
  73. package/dist/meta.json +1039 -68
  74. package/dist/okf/index.md +26 -2
  75. package/dist/okf/rules/FLUX002.md +0 -1
  76. package/dist/okf/rules/WK8404.md +11 -0
  77. package/dist/okf/rules/WK8405.md +17 -0
  78. package/dist/okf/rules/WK8406.md +11 -0
  79. package/dist/okf/rules/WK8407.md +15 -0
  80. package/dist/okf/rules/WK8503.md +17 -0
  81. package/dist/okf/rules/WK8504.md +11 -0
  82. package/dist/okf/rules/WK8505.md +15 -0
  83. package/dist/okf/types/AWSClusterControllerIdentity.md +14 -0
  84. package/dist/okf/types/AWSManagedCluster.md +14 -0
  85. package/dist/okf/types/AWSManagedControlPlane.md +14 -0
  86. package/dist/okf/types/AWSManagedMachinePool.md +14 -0
  87. package/dist/okf/types/CAPICluster.md +14 -0
  88. package/dist/okf/types/Certificate.md +1 -0
  89. package/dist/okf/types/ClusterResourceSet.md +14 -0
  90. package/dist/okf/types/ClusterServingRuntime.md +14 -0
  91. package/dist/okf/types/{Cluster.md → CnpgCluster.md} +2 -2
  92. package/dist/okf/types/DBInstance.md +14 -0
  93. package/dist/okf/types/{Bucket.md → FluxBucket.md} +2 -6
  94. package/dist/okf/types/HelmChartProxy.md +14 -0
  95. package/dist/okf/types/IamRole.md +14 -0
  96. package/dist/okf/types/InferenceService.md +19 -0
  97. package/dist/okf/types/InfisicalDynamicSecret.md +4 -0
  98. package/dist/okf/types/InfisicalSecret.md +4 -0
  99. package/dist/okf/types/Kustomization.md +1 -0
  100. package/dist/okf/types/MachinePool.md +14 -0
  101. package/dist/okf/types/PDB.md +1 -0
  102. package/dist/okf/types/PodIdentityAssociation.md +14 -0
  103. package/dist/okf/types/Policy.md +14 -0
  104. package/dist/okf/types/S3Bucket.md +14 -0
  105. package/dist/okf/types/ServingRuntime.md +14 -0
  106. package/dist/okf/types/StatefulSet.md +1 -0
  107. package/dist/okf/types/User.md +14 -0
  108. package/dist/op/activities/ensure-secret.d.ts +41 -0
  109. package/dist/op/activities/ensure-secret.d.ts.map +1 -0
  110. package/dist/op/activities/index.d.ts +2 -0
  111. package/dist/op/activities/index.d.ts.map +1 -1
  112. package/dist/op/activities/kubectl.d.ts +9 -0
  113. package/dist/op/activities/kubectl.d.ts.map +1 -1
  114. package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
  115. package/dist/op/builders.d.ts +69 -0
  116. package/dist/op/builders.d.ts.map +1 -0
  117. package/dist/plugin.d.ts.map +1 -1
  118. package/dist/rules/argo-helpers.ts +2 -6
  119. package/dist/rules/crd-schema-helpers.ts +4 -6
  120. package/dist/rules/k8s-helpers.ts +87 -24
  121. package/dist/rules/sops-helpers.ts +76 -0
  122. package/dist/rules/wk8005.ts +27 -32
  123. package/dist/rules/wk8006.ts +33 -38
  124. package/dist/rules/wk8041.ts +19 -24
  125. package/dist/rules/wk8042.ts +17 -22
  126. package/dist/rules/wk8101.ts +29 -34
  127. package/dist/rules/wk8102.ts +13 -18
  128. package/dist/rules/wk8103.ts +17 -22
  129. package/dist/rules/wk8104.ts +30 -35
  130. package/dist/rules/wk8105.ts +16 -21
  131. package/dist/rules/wk8201.ts +26 -31
  132. package/dist/rules/wk8202.ts +17 -22
  133. package/dist/rules/wk8203.ts +17 -22
  134. package/dist/rules/wk8204.ts +46 -51
  135. package/dist/rules/wk8205.ts +27 -32
  136. package/dist/rules/wk8207.ts +14 -19
  137. package/dist/rules/wk8208.ts +14 -19
  138. package/dist/rules/wk8209.ts +14 -19
  139. package/dist/rules/wk8301.ts +25 -30
  140. package/dist/rules/wk8302.ts +19 -24
  141. package/dist/rules/wk8303.ts +23 -26
  142. package/dist/rules/wk8304.ts +35 -40
  143. package/dist/rules/wk8305.ts +39 -42
  144. package/dist/rules/wk8306.ts +20 -25
  145. package/dist/rules/wk8401.ts +59 -64
  146. package/dist/rules/wk8402.ts +13 -18
  147. package/dist/rules/wk8403.ts +32 -37
  148. package/dist/rules/wk8404.ts +49 -0
  149. package/dist/rules/wk8405.ts +85 -0
  150. package/dist/rules/wk8406.ts +55 -0
  151. package/dist/rules/wk8407.ts +66 -0
  152. package/dist/rules/wk8503.ts +220 -0
  153. package/dist/rules/wk8504.ts +51 -0
  154. package/dist/rules/wk8505.ts +63 -0
  155. package/dist/secret-labels.d.ts +31 -0
  156. package/dist/secret-labels.d.ts.map +1 -0
  157. package/dist/secret-store.d.ts +73 -0
  158. package/dist/secret-store.d.ts.map +1 -0
  159. package/dist/serializer.d.ts.map +1 -1
  160. package/dist/sops/encrypted-secret-file.d.ts +52 -0
  161. package/dist/sops/encrypted-secret-file.d.ts.map +1 -0
  162. package/dist/sops/entity.d.ts +31 -0
  163. package/dist/sops/entity.d.ts.map +1 -0
  164. package/dist/teardown.d.ts.map +1 -1
  165. package/dist/types/index.d.ts +261 -9
  166. package/package.json +4 -4
  167. package/src/components/kubectl-apply.ts +5 -0
  168. package/src/composites/composites.test.ts +556 -0
  169. package/src/composites/flux-app.ts +23 -0
  170. package/src/composites/index.ts +15 -0
  171. package/src/composites/inference-service.ts +140 -0
  172. package/src/composites/model.ts +150 -0
  173. package/src/composites/operator-stack.ts +385 -0
  174. package/src/composites/vllm-serving-runtime.test.ts +167 -0
  175. package/src/composites/vllm-serving-runtime.ts +196 -0
  176. package/src/crd/cnpg.test.ts +8 -3
  177. package/src/crd/crd-sources.ts +183 -0
  178. package/src/crd/parser.test.ts +14 -0
  179. package/src/deep-observe-hooks.ts +24 -0
  180. package/src/deep-observe.test.ts +89 -0
  181. package/src/generated/index.d.ts +261 -9
  182. package/src/generated/index.ts +167 -40
  183. package/src/generated/lexicon-k8s.json +1039 -68
  184. package/src/generated/operations.json +272 -0
  185. package/src/group-namespace.test.ts +31 -0
  186. package/src/group-namespace.ts +13 -0
  187. package/src/index.ts +17 -0
  188. package/src/kube/apply.ts +5 -0
  189. package/src/lint/audit-catalog.ts +7 -0
  190. package/src/lint/post-synth/argo-helpers.ts +2 -6
  191. package/src/lint/post-synth/crd-schema-helpers.ts +4 -6
  192. package/src/lint/post-synth/index.ts +14 -0
  193. package/src/lint/post-synth/k8s-helpers.test.ts +26 -9
  194. package/src/lint/post-synth/k8s-helpers.ts +87 -24
  195. package/src/lint/post-synth/post-synth.test.ts +826 -0
  196. package/src/lint/post-synth/sops-helpers.ts +76 -0
  197. package/src/lint/post-synth/wk8005.ts +27 -32
  198. package/src/lint/post-synth/wk8006.ts +33 -38
  199. package/src/lint/post-synth/wk8041.ts +19 -24
  200. package/src/lint/post-synth/wk8042.ts +17 -22
  201. package/src/lint/post-synth/wk8101.ts +29 -34
  202. package/src/lint/post-synth/wk8102.ts +13 -18
  203. package/src/lint/post-synth/wk8103.ts +17 -22
  204. package/src/lint/post-synth/wk8104.ts +30 -35
  205. package/src/lint/post-synth/wk8105.ts +16 -21
  206. package/src/lint/post-synth/wk8201.ts +26 -31
  207. package/src/lint/post-synth/wk8202.ts +17 -22
  208. package/src/lint/post-synth/wk8203.ts +17 -22
  209. package/src/lint/post-synth/wk8204.ts +46 -51
  210. package/src/lint/post-synth/wk8205.ts +27 -32
  211. package/src/lint/post-synth/wk8207.ts +14 -19
  212. package/src/lint/post-synth/wk8208.ts +14 -19
  213. package/src/lint/post-synth/wk8209.ts +14 -19
  214. package/src/lint/post-synth/wk8301.ts +25 -30
  215. package/src/lint/post-synth/wk8302.ts +19 -24
  216. package/src/lint/post-synth/wk8303.ts +23 -26
  217. package/src/lint/post-synth/wk8304.ts +35 -40
  218. package/src/lint/post-synth/wk8305.ts +39 -42
  219. package/src/lint/post-synth/wk8306.ts +20 -25
  220. package/src/lint/post-synth/wk8401.ts +59 -64
  221. package/src/lint/post-synth/wk8402.ts +13 -18
  222. package/src/lint/post-synth/wk8403.ts +32 -37
  223. package/src/lint/post-synth/wk8404.ts +49 -0
  224. package/src/lint/post-synth/wk8405.ts +85 -0
  225. package/src/lint/post-synth/wk8406.ts +55 -0
  226. package/src/lint/post-synth/wk8407.ts +66 -0
  227. package/src/lint/post-synth/wk8503.ts +220 -0
  228. package/src/lint/post-synth/wk8504.ts +51 -0
  229. package/src/lint/post-synth/wk8505.ts +63 -0
  230. package/src/op/activities/ensure-secret.ts +71 -0
  231. package/src/op/activities/index.ts +7 -0
  232. package/src/op/activities/kubectl.test.ts +32 -0
  233. package/src/op/activities/kubectl.ts +28 -5
  234. package/src/op/activities/wait-for-ready.test.ts +34 -0
  235. package/src/op/activities/wait-for-ready.ts +26 -0
  236. package/src/op/builders.test.ts +82 -0
  237. package/src/op/builders.ts +95 -0
  238. package/src/plugin.ts +21 -2
  239. package/src/secret-labels.ts +36 -0
  240. package/src/secret-store.test.ts +184 -0
  241. package/src/secret-store.ts +165 -0
  242. package/src/serializer.ts +41 -2
  243. package/src/sops/encrypted-secret-file.test.ts +333 -0
  244. package/src/sops/encrypted-secret-file.ts +264 -0
  245. package/src/sops/entity.ts +53 -0
  246. package/src/teardown.test.ts +36 -0
  247. package/src/teardown.ts +14 -0
  248. package/src/testdata/sops/db-credentials.sops.yaml +23 -0
@@ -28,6 +28,10 @@ import { wk8306 } from "./wk8306";
28
28
  import { wk8401 } from "./wk8401";
29
29
  import { wk8402 } from "./wk8402";
30
30
  import { wk8403 } from "./wk8403";
31
+ import { wk8404 } from "./wk8404";
32
+ import { wk8405 } from "./wk8405";
33
+ import { wk8406 } from "./wk8406";
34
+ import { wk8407 } from "./wk8407";
31
35
  import { argo002 } from "./argo002";
32
36
  import { argo003 } from "./argo003";
33
37
  import { argo005 } from "./argo005";
@@ -35,6 +39,15 @@ import { flux002 } from "./flux002";
35
39
  import { flux003 } from "./flux003";
36
40
  import { wk8501 } from "./wk8501";
37
41
  import { wk8502 } from "./wk8502";
42
+ import { wk8503 } from "./wk8503";
43
+ import { wk8504 } from "./wk8504";
44
+ import { wk8505 } from "./wk8505";
45
+ import { declareSecret, type SecretDeclarationInput } from "@intentius/chant/secret-provenance";
46
+ import type { Declarable } from "@intentius/chant/declarable";
47
+ import type { SerializerResult } from "@intentius/chant/serializer";
48
+ import { readFileSync } from "node:fs";
49
+ import { dirname, join } from "node:path";
50
+ import { fileURLToPath } from "node:url";
38
51
  import { getCrdSchemaRegistry, setCrdSchemaRegistry, validateSpec } from "./crd-schema-helpers";
39
52
 
40
53
  function makeCtx(yaml: string): PostSynthContext {
@@ -1550,6 +1563,235 @@ describe("WK8403: spec.rayVersion does not match image tag", () => {
1550
1563
  });
1551
1564
  });
1552
1565
 
1566
+ // ── WK8404: GPU request without a matching toleration ─────────────────────────
1567
+
1568
+ function servingRuntime(opts: {
1569
+ resources?: Record<string, unknown>;
1570
+ tolerations?: unknown[];
1571
+ kind?: string;
1572
+ }) {
1573
+ return {
1574
+ apiVersion: "serving.kserve.io/v1alpha1",
1575
+ kind: opts.kind ?? "ServingRuntime",
1576
+ metadata: { name: "vllm-runtime", namespace: "models" },
1577
+ spec: {
1578
+ containers: [
1579
+ {
1580
+ name: "kserve-container",
1581
+ image: "vllm/vllm-openai:v0.7.0",
1582
+ ...(opts.resources && { resources: opts.resources }),
1583
+ },
1584
+ ],
1585
+ ...(opts.tolerations && { tolerations: opts.tolerations }),
1586
+ },
1587
+ };
1588
+ }
1589
+
1590
+ describe("WK8404: GPU request without a matching toleration", () => {
1591
+ test("flags a GPU request with no toleration", () => {
1592
+ const ctx = makeCtx(JSON.stringify(servingRuntime({
1593
+ resources: { requests: { "nvidia.com/gpu": "1" }, limits: { "nvidia.com/gpu": "1" } },
1594
+ })));
1595
+ const diags = wk8404.check(ctx);
1596
+ expect(diags.length).toBe(1);
1597
+ expect(diags[0].checkId).toBe("WK8404");
1598
+ expect(diags[0].severity).toBe("error");
1599
+ expect(diags[0].message).toContain("nvidia.com/gpu");
1600
+ });
1601
+
1602
+ test("passes with an explicit nvidia.com/gpu toleration", () => {
1603
+ const ctx = makeCtx(JSON.stringify(servingRuntime({
1604
+ resources: { requests: { "nvidia.com/gpu": "1" }, limits: { "nvidia.com/gpu": "1" } },
1605
+ tolerations: [{ key: "nvidia.com/gpu", operator: "Exists", effect: "NoSchedule" }],
1606
+ })));
1607
+ expect(wk8404.check(ctx).length).toBe(0);
1608
+ });
1609
+
1610
+ test("passes with a wildcard Exists toleration (no key)", () => {
1611
+ const ctx = makeCtx(JSON.stringify(servingRuntime({
1612
+ resources: { requests: { "nvidia.com/gpu": "1" } },
1613
+ tolerations: [{ operator: "Exists" }],
1614
+ })));
1615
+ expect(wk8404.check(ctx).length).toBe(0);
1616
+ });
1617
+
1618
+ test("skips containers that don't request a GPU", () => {
1619
+ const ctx = makeCtx(JSON.stringify(servingRuntime({
1620
+ resources: { requests: { cpu: "2" }, limits: { cpu: "2" } },
1621
+ })));
1622
+ expect(wk8404.check(ctx).length).toBe(0);
1623
+ });
1624
+
1625
+ test("also flags a plain Deployment requesting a GPU with no toleration", () => {
1626
+ const ctx = makeCtx(JSON.stringify({
1627
+ apiVersion: "apps/v1",
1628
+ kind: "Deployment",
1629
+ metadata: { name: "gpu-app" },
1630
+ spec: {
1631
+ template: {
1632
+ spec: {
1633
+ containers: [
1634
+ { name: "app", image: "app:1.0", resources: { requests: { "nvidia.com/gpu": "2" }, limits: { "nvidia.com/gpu": "2" } } },
1635
+ ],
1636
+ },
1637
+ },
1638
+ },
1639
+ }));
1640
+ const diags = wk8404.check(ctx);
1641
+ expect(diags.length).toBe(1);
1642
+ expect(diags[0].entity).toBe("gpu-app");
1643
+ });
1644
+ });
1645
+
1646
+ // ── WK8405: Serving workload without a PodDisruptionBudget ─────────────────────
1647
+
1648
+ function inferenceService(opts: { name?: string; labels?: Record<string, string> }) {
1649
+ return {
1650
+ apiVersion: "serving.kserve.io/v1beta1",
1651
+ kind: "InferenceService",
1652
+ metadata: {
1653
+ name: opts.name ?? "llama-3-8b",
1654
+ namespace: "serving",
1655
+ labels: { "app.kubernetes.io/component": "inference-service", ...opts.labels },
1656
+ },
1657
+ spec: {
1658
+ predictor: {
1659
+ model: { runtime: "vllm-runtime", storageUri: "gs://my-models/llama-3-8b/v1" },
1660
+ },
1661
+ },
1662
+ };
1663
+ }
1664
+
1665
+ function pdb(matchLabels: Record<string, string>) {
1666
+ return {
1667
+ apiVersion: "policy/v1",
1668
+ kind: "PodDisruptionBudget",
1669
+ metadata: { name: "pdb" },
1670
+ spec: { minAvailable: 1, selector: { matchLabels } },
1671
+ };
1672
+ }
1673
+
1674
+ describe("WK8405: Serving workload without a PDB", () => {
1675
+ test("flags an InferenceService with no covering PDB", () => {
1676
+ const ctx = makeCtx(JSON.stringify(inferenceService({})));
1677
+ const diags = wk8405.check(ctx);
1678
+ expect(diags.length).toBe(1);
1679
+ expect(diags[0].checkId).toBe("WK8405");
1680
+ expect(diags[0].severity).toBe("info");
1681
+ });
1682
+
1683
+ test("passes when a PDB selector covers the InferenceService's labels", () => {
1684
+ const ctx = manifestsCtx(
1685
+ inferenceService({}),
1686
+ pdb({ "app.kubernetes.io/component": "inference-service" }),
1687
+ );
1688
+ expect(wk8405.check(ctx).length).toBe(0);
1689
+ });
1690
+
1691
+ test("flags a Deployment labeled as a serving component with no PDB", () => {
1692
+ const ctx = makeCtx(JSON.stringify({
1693
+ apiVersion: "apps/v1",
1694
+ kind: "Deployment",
1695
+ metadata: { name: "serving-dep", labels: { "app.kubernetes.io/component": "vllm-serving-runtime" } },
1696
+ spec: { replicas: 1, template: { spec: { containers: [{ name: "app", image: "app:1.0" }] } } },
1697
+ }));
1698
+ const diags = wk8405.check(ctx);
1699
+ expect(diags.length).toBe(1);
1700
+ expect(diags[0].entity).toBe("serving-dep");
1701
+ });
1702
+
1703
+ test("ignores a plain Deployment with no serving component label", () => {
1704
+ const ctx = makeCtx(JSON.stringify({
1705
+ apiVersion: "apps/v1",
1706
+ kind: "Deployment",
1707
+ metadata: { name: "other-dep", labels: { "app.kubernetes.io/component": "web" } },
1708
+ spec: { replicas: 1, template: { spec: { containers: [{ name: "app", image: "app:1.0" }] } } },
1709
+ }));
1710
+ expect(wk8405.check(ctx).length).toBe(0);
1711
+ });
1712
+ });
1713
+
1714
+ // ── WK8406: No resource limits on a GPU pod ─────────────────────────────────
1715
+
1716
+ describe("WK8406: GPU container missing resource limits", () => {
1717
+ test("flags a GPU-requesting container with no limits", () => {
1718
+ const ctx = makeCtx(JSON.stringify(servingRuntime({
1719
+ resources: { requests: { "nvidia.com/gpu": "1" } },
1720
+ })));
1721
+ const diags = wk8406.check(ctx);
1722
+ expect(diags.length).toBe(1);
1723
+ expect(diags[0].checkId).toBe("WK8406");
1724
+ expect(diags[0].severity).toBe("warning");
1725
+ expect(diags[0].message).toContain("cpu, memory");
1726
+ });
1727
+
1728
+ test("flags a GPU-requesting container missing only memory limit", () => {
1729
+ const ctx = makeCtx(JSON.stringify(servingRuntime({
1730
+ resources: { requests: { "nvidia.com/gpu": "1" }, limits: { cpu: "4", "nvidia.com/gpu": "1" } },
1731
+ })));
1732
+ const diags = wk8406.check(ctx);
1733
+ expect(diags.length).toBe(1);
1734
+ expect(diags[0].message).toContain("memory");
1735
+ expect(diags[0].message).not.toContain("cpu,");
1736
+ });
1737
+
1738
+ test("passes with full cpu/memory limits on the GPU container", () => {
1739
+ const ctx = makeCtx(JSON.stringify(servingRuntime({
1740
+ resources: { requests: { "nvidia.com/gpu": "1" }, limits: { cpu: "4", memory: "16Gi", "nvidia.com/gpu": "1" } },
1741
+ })));
1742
+ expect(wk8406.check(ctx).length).toBe(0);
1743
+ });
1744
+
1745
+ test("ignores containers with no GPU request even without limits", () => {
1746
+ const ctx = makeCtx(JSON.stringify(servingRuntime({})));
1747
+ expect(wk8406.check(ctx).length).toBe(0);
1748
+ });
1749
+ });
1750
+
1751
+ // ── WK8407: Unpinned model version ──────────────────────────────────────────
1752
+
1753
+ describe("WK8407: Unpinned model version", () => {
1754
+ test("flags a storageUri with no version segment", () => {
1755
+ const manifest = inferenceService({});
1756
+ manifest.spec.predictor.model.storageUri = "gs://my-models-bucket";
1757
+ const diags = wk8407.check(makeCtx(JSON.stringify(manifest)));
1758
+ expect(diags.length).toBe(1);
1759
+ expect(diags[0].checkId).toBe("WK8407");
1760
+ expect(diags[0].severity).toBe("warning");
1761
+ });
1762
+
1763
+ test("flags a floating tag (latest) as unpinned", () => {
1764
+ const manifest = inferenceService({});
1765
+ manifest.spec.predictor.model.storageUri = "gs://my-models-bucket/llama-3-8b/latest";
1766
+ const diags = wk8407.check(makeCtx(JSON.stringify(manifest)));
1767
+ expect(diags.length).toBe(1);
1768
+ });
1769
+
1770
+ test("passes a pinned storageUri (scheme://id/version)", () => {
1771
+ const manifest = inferenceService({});
1772
+ manifest.spec.predictor.model.storageUri = "gs://my-models/llama-3-8b/2024-07-01";
1773
+ const diags = wk8407.check(makeCtx(JSON.stringify(manifest)));
1774
+ expect(diags.length).toBe(0);
1775
+ });
1776
+
1777
+ test("passes a pinned storageUri with a semver version", () => {
1778
+ const manifest = inferenceService({});
1779
+ manifest.spec.predictor.model.storageUri = "hf://meta-llama/Llama-3-8B/v1.0.0";
1780
+ const diags = wk8407.check(makeCtx(JSON.stringify(manifest)));
1781
+ expect(diags.length).toBe(0);
1782
+ });
1783
+
1784
+ test("ignores non-InferenceService manifests", () => {
1785
+ const ctx = makeCtx(JSON.stringify({
1786
+ apiVersion: "apps/v1",
1787
+ kind: "Deployment",
1788
+ metadata: { name: "app" },
1789
+ spec: { template: { spec: { containers: [{ name: "app", image: "app:1.0" }] } } },
1790
+ }));
1791
+ expect(wk8407.check(ctx).length).toBe(0);
1792
+ });
1793
+ });
1794
+
1553
1795
  // ── ARGO002: Application.spec.project references a declared AppProject ─────────
1554
1796
 
1555
1797
  function argoApp(name: string, spec: Record<string, unknown>) {
@@ -1809,6 +2051,28 @@ describe("WK8501/WK8502: custom resource spec validated against the shipped CRD
1809
2051
  };
1810
2052
  }
1811
2053
 
2054
+ // chant #13 — CAPI's own Cluster (K8s::CAPI::Cluster) and ACK's S3 Bucket
2055
+ // (K8s::S3::Bucket), the CAPI/CAPA and ACK kinds this suite's WK8501/WK8502
2056
+ // coverage exercises. Same shape as `microVm` above: a bare manifest, spec
2057
+ // supplied per test.
2058
+ function capiCluster(spec: Record<string, unknown>) {
2059
+ return {
2060
+ apiVersion: "cluster.x-k8s.io/v1beta2",
2061
+ kind: "Cluster",
2062
+ metadata: { name: "prod-cluster", namespace: "capi-system" },
2063
+ spec,
2064
+ };
2065
+ }
2066
+
2067
+ function ackBucket(spec: Record<string, unknown>) {
2068
+ return {
2069
+ apiVersion: "s3.services.k8s.aws/v1alpha1",
2070
+ kind: "Bucket",
2071
+ metadata: { name: "build-artifacts", namespace: "aws-system" },
2072
+ spec,
2073
+ };
2074
+ }
2075
+
1812
2076
  test("metadata", () => {
1813
2077
  expect(wk8501.id).toBe("WK8501");
1814
2078
  expect(wk8502.id).toBe("WK8502");
@@ -1895,6 +2159,59 @@ describe("WK8501/WK8502: custom resource spec validated against the shipped CRD
1895
2159
  ]);
1896
2160
  });
1897
2161
 
2162
+ // chant #13 — coverage proving the epic's actual point: the CAPI/CAPA and
2163
+ // ACK sources added in #10/#11/#12 carry the same WK8501/WK8502 schema
2164
+ // guarantee as every other CRD source, not just types with no lint behind
2165
+ // them.
2166
+
2167
+ test.skipIf(!hasLexicon)("WK8501 flags a misspelled CAPI Cluster field and suggests the real one", () => {
2168
+ const ctx = manifestsCtx(capiCluster({ paused: true, clusterNetwok: { serviceDomain: "cluster.local" } }));
2169
+ const diags = wk8501.check(ctx);
2170
+ expect(diags.length).toBe(1);
2171
+ expect(diags[0].severity).toBe("error");
2172
+ expect(diags[0].entity).toBe("prod-cluster");
2173
+ expect(diags[0].message).toContain('unknown field "spec.clusterNetwok"');
2174
+ expect(diags[0].message).toContain('did you mean "clusterNetwork"');
2175
+ // WK8502 has nothing to say about a field the schema does not know.
2176
+ expect(wk8502.check(ctx).length).toBe(0);
2177
+ });
2178
+
2179
+ test.skipIf(!hasLexicon)("WK8502 flags a wrong-typed scalar and an out-of-enum value on CAPI Cluster", () => {
2180
+ const ctx = manifestsCtx(capiCluster({
2181
+ paused: "yes",
2182
+ availabilityGates: [{ conditionType: "Ready", polarity: "Postive" }],
2183
+ }));
2184
+ const diags = wk8502.check(ctx);
2185
+ const messages = diags.map((d) => d.message);
2186
+ expect(messages).toEqual([
2187
+ expect.stringContaining('"spec.paused" expects a boolean, got string "yes"'),
2188
+ expect.stringContaining('"spec.availabilityGates[0].polarity" must be one of "Positive", "Negative", got string "Postive"'),
2189
+ ]);
2190
+ expect(diags.every((d) => d.checkId === "WK8502" && d.severity === "error")).toBe(true);
2191
+ expect(wk8501.check(ctx).length).toBe(0);
2192
+ });
2193
+
2194
+ test.skipIf(!hasLexicon)("WK8501 flags a misspelled ACK S3 Bucket field and suggests the real one", () => {
2195
+ const ctx = manifestsCtx(ackBucket({ nmae: "build-artifacts", objectLockEnabledForBucket: true }));
2196
+ const diags = wk8501.check(ctx);
2197
+ expect(diags.length).toBe(1);
2198
+ expect(diags[0].severity).toBe("error");
2199
+ expect(diags[0].entity).toBe("build-artifacts");
2200
+ expect(diags[0].message).toContain('unknown field "spec.nmae"');
2201
+ expect(diags[0].message).toContain('did you mean "name"');
2202
+ expect(wk8502.check(ctx).length).toBe(0);
2203
+ });
2204
+
2205
+ test.skipIf(!hasLexicon)("WK8502 flags a wrong-typed scalar on ACK S3 Bucket", () => {
2206
+ const ctx = manifestsCtx(ackBucket({ name: "build-artifacts", objectLockEnabledForBucket: "true" }));
2207
+ const diags = wk8502.check(ctx);
2208
+ expect(diags.length).toBe(1);
2209
+ expect(diags[0].checkId).toBe("WK8502");
2210
+ expect(diags[0].severity).toBe("error");
2211
+ expect(diags[0].message).toContain('"spec.objectLockEnabledForBucket" expects a boolean, got string "true"');
2212
+ expect(wk8501.check(ctx).length).toBe(0);
2213
+ });
2214
+
1898
2215
  test("built-in kinds and unknown apiVersion/kind pairs are never checked", () => {
1899
2216
  const ctx = manifestsCtx(
1900
2217
  { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "web" }, spec: { replicas: "2", bogus: true } },
@@ -1947,3 +2264,512 @@ describe("WK8501/WK8502: custom resource spec validated against the shipped CRD
1947
2264
  ]);
1948
2265
  });
1949
2266
  });
2267
+
2268
+ // ── WK8503: consumed-but-unproduced Secret ──────────────────────────
2269
+
2270
+ describe("WK8503: workload consumes a Secret nothing in the output produces", () => {
2271
+ function appConsuming(name: string) {
2272
+ return {
2273
+ apiVersion: "apps/v1",
2274
+ kind: "Deployment",
2275
+ metadata: { name: "app", namespace: "web" },
2276
+ spec: {
2277
+ template: {
2278
+ spec: {
2279
+ containers: [
2280
+ { name: "app", image: "app:1.0", envFrom: [{ secretRef: { name } }] },
2281
+ ],
2282
+ },
2283
+ },
2284
+ },
2285
+ };
2286
+ }
2287
+
2288
+ /** Like manifestsCtx, but with SecretProvenance declarations discovered as entities. */
2289
+ function ctxWithDeclarations(decls: SecretDeclarationInput[], ...objs: unknown[]): PostSynthContext {
2290
+ const ctx = manifestsCtx(...objs);
2291
+ decls.forEach((d, i) => ctx.entities.set(`secretDecl${i}`, declareSecret(d as never)));
2292
+ return ctx;
2293
+ }
2294
+
2295
+ test("metadata", () => {
2296
+ expect(wk8503.id).toBe("WK8503");
2297
+ });
2298
+
2299
+ test("fires when nothing produces the consumed Secret", () => {
2300
+ const diags = wk8503.check(manifestsCtx(appConsuming("fountain-secrets")));
2301
+ expect(diags.length).toBe(1);
2302
+ expect(diags[0].checkId).toBe("WK8503");
2303
+ expect(diags[0].severity).toBe("error");
2304
+ expect(diags[0].entity).toBe("app");
2305
+ expect(diags[0].message).toContain('Secret "fountain-secrets"');
2306
+ expect(diags[0].message).toContain("envFrom.secretRef");
2307
+ });
2308
+
2309
+ test("quiet when a Secret manifest in the output produces it", () => {
2310
+ const ctx = manifestsCtx(
2311
+ appConsuming("fountain-secrets"),
2312
+ { apiVersion: "v1", kind: "Secret", metadata: { name: "fountain-secrets", namespace: "web" }, stringData: {} },
2313
+ );
2314
+ expect(wk8503.check(ctx)).toEqual([]);
2315
+ });
2316
+
2317
+ test("quiet when an ExternalSecret materializes the target name", () => {
2318
+ const ctx = manifestsCtx(
2319
+ appConsuming("fountain-secrets"),
2320
+ {
2321
+ apiVersion: "external-secrets.io/v1",
2322
+ kind: "ExternalSecret",
2323
+ metadata: { name: "fountain-secrets-eso", namespace: "web" },
2324
+ spec: {
2325
+ secretStoreRef: { name: "store", kind: "ClusterSecretStore" },
2326
+ target: { name: "fountain-secrets", creationPolicy: "Owner" },
2327
+ data: [],
2328
+ },
2329
+ },
2330
+ );
2331
+ expect(wk8503.check(ctx)).toEqual([]);
2332
+ });
2333
+
2334
+ test("an ExternalSecret with no explicit target produces its own name", () => {
2335
+ const ctx = manifestsCtx(
2336
+ appConsuming("fountain-secrets"),
2337
+ {
2338
+ apiVersion: "external-secrets.io/v1",
2339
+ kind: "ExternalSecret",
2340
+ metadata: { name: "fountain-secrets", namespace: "web" },
2341
+ spec: { secretStoreRef: { name: "store", kind: "ClusterSecretStore" }, data: [] },
2342
+ },
2343
+ );
2344
+ expect(wk8503.check(ctx)).toEqual([]);
2345
+ });
2346
+
2347
+ test("quiet when an InfisicalSecret's managedSecretReference produces it", () => {
2348
+ const ctx = manifestsCtx(
2349
+ appConsuming("fountain-secrets"),
2350
+ {
2351
+ apiVersion: "secrets.infisical.com/v1alpha1",
2352
+ kind: "InfisicalSecret",
2353
+ metadata: { name: "fountain-sync", namespace: "infisical-operator" },
2354
+ spec: {
2355
+ managedSecretReference: { secretName: "fountain-secrets", secretNamespace: "web" },
2356
+ },
2357
+ },
2358
+ );
2359
+ expect(wk8503.check(ctx)).toEqual([]);
2360
+ });
2361
+
2362
+ test("an InfisicalSecret targeting a different namespace does not cover the consumer", () => {
2363
+ const ctx = manifestsCtx(
2364
+ appConsuming("fountain-secrets"),
2365
+ {
2366
+ apiVersion: "secrets.infisical.com/v1alpha1",
2367
+ kind: "InfisicalSecret",
2368
+ metadata: { name: "fountain-sync", namespace: "web" },
2369
+ spec: {
2370
+ managedSecretReference: { secretName: "fountain-secrets", secretNamespace: "other" },
2371
+ },
2372
+ },
2373
+ );
2374
+ expect(wk8503.check(ctx).length).toBe(1);
2375
+ });
2376
+
2377
+ test("quiet when a cert-manager Certificate materializes the secretName", () => {
2378
+ const consumer = {
2379
+ apiVersion: "apps/v1",
2380
+ kind: "StatefulSet",
2381
+ metadata: { name: "db", namespace: "web" },
2382
+ spec: {
2383
+ template: {
2384
+ spec: {
2385
+ containers: [{ name: "db", image: "db:1" }],
2386
+ volumes: [{ name: "tls", secret: { secretName: "db-tls" } }],
2387
+ },
2388
+ },
2389
+ },
2390
+ };
2391
+ const ctx = manifestsCtx(consumer, {
2392
+ apiVersion: "cert-manager.io/v1",
2393
+ kind: "Certificate",
2394
+ metadata: { name: "db-cert", namespace: "web" },
2395
+ spec: { secretName: "db-tls", issuerRef: { name: "issuer" } },
2396
+ });
2397
+ expect(wk8503.check(ctx)).toEqual([]);
2398
+ });
2399
+
2400
+ test("a `referenced` provenance declaration waives the check", () => {
2401
+ const ctx = ctxWithDeclarations(
2402
+ [{ name: "fountain-secrets", provenance: "referenced", scope: "minted by `just secret`" }],
2403
+ appConsuming("fountain-secrets"),
2404
+ );
2405
+ expect(wk8503.check(ctx)).toEqual([]);
2406
+ });
2407
+
2408
+ test("a `generated-once` provenance declaration waives the check", () => {
2409
+ const ctx = ctxWithDeclarations(
2410
+ [{ name: "fountain-secrets", provenance: "generated-once", keys: ["token"] }],
2411
+ appConsuming("fountain-secrets"),
2412
+ );
2413
+ expect(wk8503.check(ctx)).toEqual([]);
2414
+ });
2415
+
2416
+ test("a declaration covering a different name does not waive the check", () => {
2417
+ const ctx = ctxWithDeclarations(
2418
+ [{ name: "some-other-secret", provenance: "referenced" }],
2419
+ appConsuming("fountain-secrets"),
2420
+ );
2421
+ const diags = wk8503.check(ctx);
2422
+ expect(diags.length).toBe(1);
2423
+ expect(diags[0].message).toContain('"fountain-secrets"');
2424
+ });
2425
+
2426
+ test("optional references are skipped", () => {
2427
+ const ctx = manifestsCtx({
2428
+ apiVersion: "apps/v1",
2429
+ kind: "Deployment",
2430
+ metadata: { name: "app", namespace: "web" },
2431
+ spec: {
2432
+ template: {
2433
+ spec: {
2434
+ containers: [
2435
+ {
2436
+ name: "app",
2437
+ image: "app:1.0",
2438
+ envFrom: [{ secretRef: { name: "maybe-env", optional: true } }],
2439
+ env: [{ name: "TOKEN", valueFrom: { secretKeyRef: { name: "maybe-key", key: "t", optional: true } } }],
2440
+ },
2441
+ ],
2442
+ volumes: [{ name: "v", secret: { secretName: "maybe-vol", optional: true } }],
2443
+ },
2444
+ },
2445
+ },
2446
+ });
2447
+ expect(wk8503.check(ctx)).toEqual([]);
2448
+ });
2449
+
2450
+ test("covers secretKeyRef, projected sources, imagePullSecrets, and initContainers", () => {
2451
+ const ctx = manifestsCtx({
2452
+ apiVersion: "batch/v1",
2453
+ kind: "CronJob",
2454
+ metadata: { name: "sync", namespace: "web" },
2455
+ spec: {
2456
+ jobTemplate: {
2457
+ spec: {
2458
+ template: {
2459
+ spec: {
2460
+ imagePullSecrets: [{ name: "registry-creds" }],
2461
+ initContainers: [
2462
+ { name: "init", image: "i:1", env: [{ name: "T", valueFrom: { secretKeyRef: { name: "api-token", key: "t" } } }] },
2463
+ ],
2464
+ containers: [{ name: "sync", image: "s:1" }],
2465
+ volumes: [
2466
+ { name: "p", projected: { sources: [{ secret: { name: "projected-secret" } }] } },
2467
+ ],
2468
+ },
2469
+ },
2470
+ },
2471
+ },
2472
+ },
2473
+ });
2474
+ const secrets = wk8503.check(ctx).map((d) => d.message.match(/consumes Secret "([^"]+)"/)?.[1]).sort();
2475
+ expect(secrets).toEqual(["api-token", "projected-secret", "registry-creds"]);
2476
+ });
2477
+
2478
+ test("a Secret in an explicit different namespace does not cover the consumer", () => {
2479
+ const ctx = manifestsCtx(
2480
+ appConsuming("fountain-secrets"),
2481
+ { apiVersion: "v1", kind: "Secret", metadata: { name: "fountain-secrets", namespace: "other" }, stringData: {} },
2482
+ );
2483
+ expect(wk8503.check(ctx).length).toBe(1);
2484
+ });
2485
+
2486
+ test("a Secret with no namespace covers a namespaced consumer", () => {
2487
+ const ctx = manifestsCtx(
2488
+ appConsuming("fountain-secrets"),
2489
+ { apiVersion: "v1", kind: "Secret", metadata: { name: "fountain-secrets" }, stringData: {} },
2490
+ );
2491
+ expect(wk8503.check(ctx)).toEqual([]);
2492
+ });
2493
+
2494
+ test("reports a missing Secret once per workload even when referenced twice", () => {
2495
+ const ctx = manifestsCtx({
2496
+ apiVersion: "apps/v1",
2497
+ kind: "Deployment",
2498
+ metadata: { name: "app", namespace: "web" },
2499
+ spec: {
2500
+ template: {
2501
+ spec: {
2502
+ containers: [
2503
+ {
2504
+ name: "app",
2505
+ image: "app:1.0",
2506
+ envFrom: [{ secretRef: { name: "fountain-secrets" } }],
2507
+ env: [{ name: "T", valueFrom: { secretKeyRef: { name: "fountain-secrets", key: "t" } } }],
2508
+ },
2509
+ ],
2510
+ },
2511
+ },
2512
+ },
2513
+ });
2514
+ expect(wk8503.check(ctx).length).toBe(1);
2515
+ });
2516
+ });
2517
+
2518
+ // ── WK8504: committed-encrypted declaration does not resolve ─────────
2519
+ //
2520
+ // The fixture is a real sops-shaped document (cleartext structure, ENC[...]
2521
+ // values, an age recipient block, a mac). Nothing here decrypts anything.
2522
+
2523
+ describe("WK8504: committed-encrypted secret declaration does not resolve", () => {
2524
+ const CIPHERTEXT = readFileSync(
2525
+ join(dirname(fileURLToPath(import.meta.url)), "..", "..", "testdata", "sops", "db-credentials.sops.yaml"),
2526
+ "utf-8",
2527
+ );
2528
+
2529
+ /**
2530
+ * A context whose k8s output carries `manifests` in the primary document and
2531
+ * the ciphertext as a SIDECAR — the only place the check can find it.
2532
+ */
2533
+ function sopsCtx(
2534
+ opts: { ciphertext?: string; file?: string; declaredName?: string; manifests?: unknown[]; omitSidecar?: boolean } = {},
2535
+ ): PostSynthContext {
2536
+ const primary = (opts.manifests ?? []).map((o) => JSON.stringify(o)).join("\n---\n");
2537
+ const file = opts.file ?? "secrets/db-credentials.sops.yaml";
2538
+ const output: SerializerResult = {
2539
+ primary,
2540
+ files: opts.omitSidecar
2541
+ ? {}
2542
+ : { "db-credentials.sops.yaml": opts.ciphertext ?? CIPHERTEXT },
2543
+ };
2544
+ const outputs = new Map<string, string | SerializerResult>([["k8s", output]]);
2545
+ const entities = new Map<string, Declarable>([
2546
+ [
2547
+ "dbCredentials",
2548
+ declareSecret({
2549
+ name: opts.declaredName ?? "db-credentials",
2550
+ provenance: "committed-encrypted",
2551
+ file,
2552
+ keys: ["POSTGRES_USER", "POSTGRES_PASSWORD"],
2553
+ }),
2554
+ ],
2555
+ ]);
2556
+ return {
2557
+ outputs,
2558
+ entities,
2559
+ buildResult: { outputs, entities, warnings: [], errors: [], sourceFileCount: 1 },
2560
+ };
2561
+ }
2562
+
2563
+ test("metadata", () => {
2564
+ expect(wk8504.id).toBe("WK8504");
2565
+ });
2566
+
2567
+ test("quiet when the declared file resolved to encrypted ciphertext", () => {
2568
+ expect(wk8504.check(sopsCtx())).toEqual([]);
2569
+ });
2570
+
2571
+ test("quiet when the project declares no committed-encrypted secret", () => {
2572
+ expect(wk8504.check(manifestsCtx({ apiVersion: "v1", kind: "ConfigMap", metadata: { name: "c" } }))).toEqual([]);
2573
+ });
2574
+
2575
+ test("fires when the build emitted no file for the declaration", () => {
2576
+ const diags = wk8504.check(sopsCtx({ omitSidecar: true }));
2577
+ expect(diags.length).toBe(1);
2578
+ expect(diags[0].checkId).toBe("WK8504");
2579
+ expect(diags[0].severity).toBe("error");
2580
+ expect(diags[0].entity).toBe("db-credentials");
2581
+ expect(diags[0].message).toContain("secrets/db-credentials.sops.yaml");
2582
+ expect(diags[0].message).toContain("did not resolve");
2583
+ });
2584
+
2585
+ test("fires when metadata.name disagrees with the declaration", () => {
2586
+ const diags = wk8504.check(sopsCtx({ declaredName: "other-credentials" }));
2587
+ // The name mismatch; the sidecar filename still matches, so it is found.
2588
+ expect(diags.some((d) => /metadata\.name "db-credentials"/.test(d.message))).toBe(true);
2589
+ });
2590
+
2591
+ test("fires when the document carries no sops block", () => {
2592
+ const decrypted = CIPHERTEXT.slice(0, CIPHERTEXT.indexOf("sops:"));
2593
+ const diags = wk8504.check(sopsCtx({ ciphertext: decrypted }));
2594
+ expect(diags.some((d) => /no top-level `sops` block/.test(d.message))).toBe(true);
2595
+ });
2596
+
2597
+ test("fires when a data value is not ENC[...], naming the key and not the value", () => {
2598
+ const leaked = CIPHERTEXT.replace(/POSTGRES_PASSWORD: ENC\[[^\]]*\]/, "POSTGRES_PASSWORD: hunter2");
2599
+ const diags = wk8504.check(sopsCtx({ ciphertext: leaked }));
2600
+ expect(diags.length).toBe(1);
2601
+ expect(diags[0].message).toContain('stringData."POSTGRES_PASSWORD" is not encrypted');
2602
+ expect(diags[0].message).not.toContain("hunter2");
2603
+ });
2604
+
2605
+ test("reports every problem on a declaration at once", () => {
2606
+ const broken = CIPHERTEXT.replace("kind: Secret", "kind: ConfigMap").replace(
2607
+ /POSTGRES_USER: ENC\[[^\]]*\]/,
2608
+ "POSTGRES_USER: postgres",
2609
+ );
2610
+ expect(wk8504.check(sopsCtx({ ciphertext: broken })).length).toBeGreaterThanOrEqual(2);
2611
+ });
2612
+
2613
+ test("post-synth checks that read only the primary output stay blind to the sidecar", () => {
2614
+ // The ciphertext must never reach the primary output — that is what makes
2615
+ // applying an undecrypted Secret structurally impossible — so the rules
2616
+ // that look for hardcoded material see nothing here.
2617
+ const ctx = sopsCtx({ manifests: [{ apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "app" } }] });
2618
+ expect(wk8005.check(ctx)).toEqual([]);
2619
+ expect(wk8041.check(ctx)).toEqual([]);
2620
+ expect(wk8042.check(ctx)).toEqual([]);
2621
+ });
2622
+
2623
+ describe("WK8503 is satisfied through the producer set, not the waiver set", () => {
2624
+ function consumerIn(namespace: string) {
2625
+ return {
2626
+ apiVersion: "apps/v1",
2627
+ kind: "Deployment",
2628
+ metadata: { name: "api", namespace },
2629
+ spec: {
2630
+ template: {
2631
+ spec: {
2632
+ containers: [
2633
+ { name: "api", image: "api:1.0", envFrom: [{ secretRef: { name: "db-credentials" } }] },
2634
+ ],
2635
+ },
2636
+ },
2637
+ },
2638
+ };
2639
+ }
2640
+
2641
+ test("a resolved declaration produces the Secret in its own namespace", () => {
2642
+ // The fixture is `namespace: apps`, read from the ciphertext itself.
2643
+ expect(wk8503.check(sopsCtx({ manifests: [consumerIn("apps")] }))).toEqual([]);
2644
+ });
2645
+
2646
+ test("namespace matching applies — another namespace is not covered", () => {
2647
+ const diags = wk8503.check(sopsCtx({ manifests: [consumerIn("platform")] }));
2648
+ expect(diags.length).toBe(1);
2649
+ expect(diags[0].message).toContain('Secret "db-credentials"');
2650
+ });
2651
+
2652
+ test("a declaration whose file did not resolve waives nothing", () => {
2653
+ const ctx = sopsCtx({ omitSidecar: true, manifests: [consumerIn("apps")] });
2654
+ expect(wk8504.check(ctx).length).toBe(1);
2655
+ expect(wk8503.check(ctx).length).toBe(1);
2656
+ });
2657
+
2658
+ test("a declaration whose ciphertext is not encrypted waives nothing", () => {
2659
+ const leaked = CIPHERTEXT.replace(/POSTGRES_USER: ENC\[[^\]]*\]/, "POSTGRES_USER: postgres");
2660
+ const ctx = sopsCtx({ ciphertext: leaked, manifests: [consumerIn("apps")] });
2661
+ expect(wk8504.check(ctx).length).toBe(1);
2662
+ expect(wk8503.check(ctx).length).toBe(1);
2663
+ });
2664
+ });
2665
+ });
2666
+
2667
+ // ── WK8505: committed-encrypted secret with no Flux decryption wiring ─────
2668
+ //
2669
+ // Reuses the FLUX002 fixture helpers (fluxKustomization, gitSourceRef) and
2670
+ // the same real sops-shaped ciphertext fixture WK8504 validates against.
2671
+
2672
+ describe("WK8505: committed-encrypted secret with no Flux decryption wiring", () => {
2673
+ const CIPHERTEXT = readFileSync(
2674
+ join(dirname(fileURLToPath(import.meta.url)), "..", "..", "testdata", "sops", "db-credentials.sops.yaml"),
2675
+ "utf-8",
2676
+ );
2677
+
2678
+ /**
2679
+ * A context carrying one committed-encrypted declaration (resolved to its
2680
+ * sidecar unless `omitSidecar`) plus whatever Flux/other manifests are
2681
+ * passed in the primary output.
2682
+ */
2683
+ function sopsCtx(opts: { manifests?: unknown[]; omitSidecar?: boolean } = {}): PostSynthContext {
2684
+ const primary = (opts.manifests ?? []).map((o) => JSON.stringify(o)).join("\n---\n");
2685
+ const output: SerializerResult = {
2686
+ primary,
2687
+ files: opts.omitSidecar ? {} : { "db-credentials.sops.yaml": CIPHERTEXT },
2688
+ };
2689
+ const outputs = new Map<string, string | SerializerResult>([["k8s", output]]);
2690
+ const entities = new Map<string, Declarable>([
2691
+ [
2692
+ "dbCredentials",
2693
+ declareSecret({
2694
+ name: "db-credentials",
2695
+ provenance: "committed-encrypted",
2696
+ file: "secrets/db-credentials.sops.yaml",
2697
+ keys: ["POSTGRES_USER", "POSTGRES_PASSWORD"],
2698
+ }),
2699
+ ],
2700
+ ]);
2701
+ return {
2702
+ outputs,
2703
+ entities,
2704
+ buildResult: { outputs, entities, warnings: [], errors: [], sourceFileCount: 1 },
2705
+ };
2706
+ }
2707
+
2708
+ test("metadata", () => {
2709
+ expect(wk8505.id).toBe("WK8505");
2710
+ });
2711
+
2712
+ test("quiet when the build has no Flux Kustomization at all", () => {
2713
+ expect(wk8505.check(sopsCtx())).toEqual([]);
2714
+ });
2715
+
2716
+ test("quiet when the build declares no committed-encrypted secret", () => {
2717
+ const ctx = manifestsCtx(fluxKustomization("app", gitSourceRef("flux-system")));
2718
+ expect(wk8505.check(ctx)).toEqual([]);
2719
+ });
2720
+
2721
+ test("fires when a Kustomization exists but none sets spec.decryption", () => {
2722
+ const ctx = sopsCtx({ manifests: [fluxKustomization("app", gitSourceRef("flux-system"))] });
2723
+ const diags = wk8505.check(ctx);
2724
+ expect(diags.length).toBe(1);
2725
+ expect(diags[0].checkId).toBe("WK8505");
2726
+ expect(diags[0].severity).toBe("warning");
2727
+ expect(diags[0].entity).toBe("db-credentials");
2728
+ expect(diags[0].message).toContain("spec.decryption");
2729
+ });
2730
+
2731
+ test("quiet when a Kustomization sets spec.decryption", () => {
2732
+ const ctx = sopsCtx({
2733
+ manifests: [
2734
+ fluxKustomization("app", {
2735
+ ...gitSourceRef("flux-system"),
2736
+ decryption: { provider: "sops", secretRef: { name: "sops-age" } },
2737
+ }),
2738
+ ],
2739
+ });
2740
+ expect(wk8505.check(ctx)).toEqual([]);
2741
+ });
2742
+
2743
+ test("a decrypting Kustomization elsewhere in the build satisfies every declaration", () => {
2744
+ const ctx = sopsCtx({
2745
+ manifests: [
2746
+ fluxKustomization("infra", gitSourceRef("flux-system")),
2747
+ fluxKustomization("secrets", {
2748
+ ...gitSourceRef("flux-system"),
2749
+ decryption: { provider: "sops", secretRef: { name: "sops-age" } },
2750
+ }),
2751
+ ],
2752
+ });
2753
+ expect(wk8505.check(ctx)).toEqual([]);
2754
+ });
2755
+
2756
+ test("an unresolved declaration does not also fire WK8505 — WK8504 already reports it", () => {
2757
+ const ctx = sopsCtx({ omitSidecar: true, manifests: [fluxKustomization("app", gitSourceRef("flux-system"))] });
2758
+ expect(wk8504.check(ctx).length).toBe(1);
2759
+ expect(wk8505.check(ctx)).toEqual([]);
2760
+ });
2761
+
2762
+ test("skips kustomize.config.k8s.io Kustomizations (not Flux CRs)", () => {
2763
+ const ctx = sopsCtx({
2764
+ manifests: [
2765
+ {
2766
+ apiVersion: "kustomize.config.k8s.io/v1beta1",
2767
+ kind: "Kustomization",
2768
+ metadata: { name: "overlay" },
2769
+ spec: {},
2770
+ },
2771
+ ],
2772
+ });
2773
+ expect(wk8505.check(ctx)).toEqual([]);
2774
+ });
2775
+ });