@intentius/chant-lexicon-cpln 0.53.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 (266) hide show
  1. package/README.md +85 -0
  2. package/dist/api.d.ts +77 -0
  3. package/dist/api.d.ts.map +1 -0
  4. package/dist/codegen/docs-cli.d.ts +3 -0
  5. package/dist/codegen/docs-cli.d.ts.map +1 -0
  6. package/dist/codegen/docs.d.ts +17 -0
  7. package/dist/codegen/docs.d.ts.map +1 -0
  8. package/dist/codegen/generate-cli.d.ts +6 -0
  9. package/dist/codegen/generate-cli.d.ts.map +1 -0
  10. package/dist/codegen/generate-lexicon.d.ts +36 -0
  11. package/dist/codegen/generate-lexicon.d.ts.map +1 -0
  12. package/dist/codegen/generate-typescript.d.ts +11 -0
  13. package/dist/codegen/generate-typescript.d.ts.map +1 -0
  14. package/dist/codegen/generate.d.ts +15 -0
  15. package/dist/codegen/generate.d.ts.map +1 -0
  16. package/dist/codegen/naming.d.ts +30 -0
  17. package/dist/codegen/naming.d.ts.map +1 -0
  18. package/dist/codegen/package.d.ts +17 -0
  19. package/dist/codegen/package.d.ts.map +1 -0
  20. package/dist/composites/cron-job.d.ts +60 -0
  21. package/dist/composites/cron-job.d.ts.map +1 -0
  22. package/dist/composites/gvc-environment.d.ts +47 -0
  23. package/dist/composites/gvc-environment.d.ts.map +1 -0
  24. package/dist/composites/index.d.ts +13 -0
  25. package/dist/composites/index.d.ts.map +1 -0
  26. package/dist/composites/public-domain.d.ts +68 -0
  27. package/dist/composites/public-domain.d.ts.map +1 -0
  28. package/dist/composites/secret-access.d.ts +78 -0
  29. package/dist/composites/secret-access.d.ts.map +1 -0
  30. package/dist/composites/serverless-service.d.ts +78 -0
  31. package/dist/composites/serverless-service.d.ts.map +1 -0
  32. package/dist/composites/stateful-service.d.ts +79 -0
  33. package/dist/composites/stateful-service.d.ts.map +1 -0
  34. package/dist/coverage.d.ts +59 -0
  35. package/dist/coverage.d.ts.map +1 -0
  36. package/dist/describe-resources.d.ts +71 -0
  37. package/dist/describe-resources.d.ts.map +1 -0
  38. package/dist/detect.d.ts +14 -0
  39. package/dist/detect.d.ts.map +1 -0
  40. package/dist/entity-props.d.ts +34 -0
  41. package/dist/entity-props.d.ts.map +1 -0
  42. package/dist/generated/index.d.ts +151 -0
  43. package/dist/generated/index.d.ts.map +1 -0
  44. package/dist/generated/runtime.d.ts +5 -0
  45. package/dist/generated/runtime.d.ts.map +1 -0
  46. package/dist/index.d.ts +8 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/init-templates.d.ts +16 -0
  49. package/dist/init-templates.d.ts.map +1 -0
  50. package/dist/integrity.json +35 -0
  51. package/dist/kinds.d.ts +70 -0
  52. package/dist/kinds.d.ts.map +1 -0
  53. package/dist/lint/audit-catalog.d.ts +19 -0
  54. package/dist/lint/audit-catalog.d.ts.map +1 -0
  55. package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts +12 -0
  56. package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts.map +1 -0
  57. package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts +11 -0
  58. package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts.map +1 -0
  59. package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts +11 -0
  60. package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts.map +1 -0
  61. package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts +11 -0
  62. package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts.map +1 -0
  63. package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts +14 -0
  64. package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts.map +1 -0
  65. package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts +10 -0
  66. package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts.map +1 -0
  67. package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts +11 -0
  68. package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts.map +1 -0
  69. package/dist/lint/post-synth/cpl022-container-ports.d.ts +10 -0
  70. package/dist/lint/post-synth/cpl022-container-ports.d.ts.map +1 -0
  71. package/dist/lint/post-synth/cpl023-container-resources.d.ts +12 -0
  72. package/dist/lint/post-synth/cpl023-container-resources.d.ts.map +1 -0
  73. package/dist/lint/post-synth/cpl024-probe-handler.d.ts +10 -0
  74. package/dist/lint/post-synth/cpl024-probe-handler.d.ts.map +1 -0
  75. package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts +11 -0
  76. package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts.map +1 -0
  77. package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts +11 -0
  78. package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts.map +1 -0
  79. package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts +13 -0
  80. package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts.map +1 -0
  81. package/dist/lint/post-synth/cpl028-volume-set.d.ts +3 -0
  82. package/dist/lint/post-synth/cpl028-volume-set.d.ts.map +1 -0
  83. package/dist/lint/post-synth/cpl029-link-targets.d.ts +3 -0
  84. package/dist/lint/post-synth/cpl029-link-targets.d.ts.map +1 -0
  85. package/dist/lint/post-synth/cpl030-domain-routing.d.ts +3 -0
  86. package/dist/lint/post-synth/cpl030-domain-routing.d.ts.map +1 -0
  87. package/dist/lint/post-synth/cpl040-pinned-image.d.ts +11 -0
  88. package/dist/lint/post-synth/cpl040-pinned-image.d.ts.map +1 -0
  89. package/dist/lint/post-synth/cpl041-image-reference-form.d.ts +12 -0
  90. package/dist/lint/post-synth/cpl041-image-reference-form.d.ts.map +1 -0
  91. package/dist/lint/post-synth/cpl042-gvc-placement.d.ts +10 -0
  92. package/dist/lint/post-synth/cpl042-gvc-placement.d.ts.map +1 -0
  93. package/dist/lint/post-synth/cpl043-policy-scope.d.ts +16 -0
  94. package/dist/lint/post-synth/cpl043-policy-scope.d.ts.map +1 -0
  95. package/dist/lint/post-synth/helpers.d.ts +45 -0
  96. package/dist/lint/post-synth/helpers.d.ts.map +1 -0
  97. package/dist/lint/post-synth/index.d.ts +3 -0
  98. package/dist/lint/post-synth/index.d.ts.map +1 -0
  99. package/dist/lint/rules/cpl001-no-secret-literals.d.ts +3 -0
  100. package/dist/lint/rules/cpl001-no-secret-literals.d.ts.map +1 -0
  101. package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts +3 -0
  102. package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts.map +1 -0
  103. package/dist/lint/rules/index.d.ts +6 -0
  104. package/dist/lint/rules/index.d.ts.map +1 -0
  105. package/dist/lsp/completions.d.ts +7 -0
  106. package/dist/lsp/completions.d.ts.map +1 -0
  107. package/dist/lsp/hover.d.ts +4 -0
  108. package/dist/lsp/hover.d.ts.map +1 -0
  109. package/dist/manifest.json +6 -0
  110. package/dist/meta.json +2110 -0
  111. package/dist/okf/index.md +39 -0
  112. package/dist/okf/rules/CPL001.md +22 -0
  113. package/dist/okf/rules/CPL002.md +22 -0
  114. package/dist/okf/rules/CPL010.md +15 -0
  115. package/dist/okf/rules/CPL011.md +15 -0
  116. package/dist/okf/rules/CPL012.md +15 -0
  117. package/dist/okf/rules/CPL013.md +15 -0
  118. package/dist/okf/rules/CPL014.md +16 -0
  119. package/dist/okf/rules/CPL020.md +11 -0
  120. package/dist/okf/rules/CPL021.md +15 -0
  121. package/dist/okf/rules/CPL022.md +15 -0
  122. package/dist/okf/rules/CPL023.md +15 -0
  123. package/dist/okf/rules/CPL024.md +15 -0
  124. package/dist/okf/rules/CPL025.md +15 -0
  125. package/dist/okf/rules/CPL026.md +15 -0
  126. package/dist/okf/rules/CPL027.md +15 -0
  127. package/dist/okf/rules/CPL028.md +15 -0
  128. package/dist/okf/rules/CPL029.md +15 -0
  129. package/dist/okf/rules/CPL030.md +16 -0
  130. package/dist/okf/rules/CPL040.md +15 -0
  131. package/dist/okf/rules/CPL041.md +15 -0
  132. package/dist/okf/rules/CPL042.md +11 -0
  133. package/dist/okf/rules/CPL043.md +15 -0
  134. package/dist/okf/types/Domain.md +22 -0
  135. package/dist/okf/types/Gvc.md +21 -0
  136. package/dist/okf/types/Identity.md +29 -0
  137. package/dist/okf/types/IpSet.md +21 -0
  138. package/dist/okf/types/Policy.md +28 -0
  139. package/dist/okf/types/Secret.md +23 -0
  140. package/dist/okf/types/VolumeSet.md +22 -0
  141. package/dist/okf/types/Workload.md +38 -0
  142. package/dist/ownership.d.ts +23 -0
  143. package/dist/ownership.d.ts.map +1 -0
  144. package/dist/package-cli.d.ts +3 -0
  145. package/dist/package-cli.d.ts.map +1 -0
  146. package/dist/plugin.d.ts +3 -0
  147. package/dist/plugin.d.ts.map +1 -0
  148. package/dist/reference-catalog.d.ts +24 -0
  149. package/dist/reference-catalog.d.ts.map +1 -0
  150. package/dist/rules/cpl001-no-secret-literals.ts +83 -0
  151. package/dist/rules/cpl002-prefer-resource-reference.ts +71 -0
  152. package/dist/rules/cpl010-unrestricted-outbound.ts +41 -0
  153. package/dist/rules/cpl011-internal-firewall-scope.ts +38 -0
  154. package/dist/rules/cpl012-no-literal-credential-env.ts +58 -0
  155. package/dist/rules/cpl013-qualified-identity-principal.ts +45 -0
  156. package/dist/rules/cpl014-qualified-secret-ref.ts +54 -0
  157. package/dist/rules/cpl020-serverless-single-port.ts +58 -0
  158. package/dist/rules/cpl021-cron-job-shape.ts +64 -0
  159. package/dist/rules/cpl022-container-ports.ts +64 -0
  160. package/dist/rules/cpl023-container-resources.ts +87 -0
  161. package/dist/rules/cpl024-probe-handler.ts +51 -0
  162. package/dist/rules/cpl025-autoscaling-shape.ts +100 -0
  163. package/dist/rules/cpl026-scale-to-zero.ts +67 -0
  164. package/dist/rules/cpl027-capacity-ai-conflict.ts +78 -0
  165. package/dist/rules/cpl028-volume-set.ts +183 -0
  166. package/dist/rules/cpl029-link-targets.ts +155 -0
  167. package/dist/rules/cpl030-domain-routing.ts +137 -0
  168. package/dist/rules/cpl040-pinned-image.ts +48 -0
  169. package/dist/rules/cpl041-image-reference-form.ts +60 -0
  170. package/dist/rules/cpl042-gvc-placement.ts +57 -0
  171. package/dist/rules/cpl043-policy-scope.ts +0 -0
  172. package/dist/rules/helpers.ts +114 -0
  173. package/dist/serializer.d.ts +69 -0
  174. package/dist/serializer.d.ts.map +1 -0
  175. package/dist/skills/chant-cpln-secrets.md +110 -0
  176. package/dist/skills/chant-cpln-workloads.md +78 -0
  177. package/dist/skills/chant-cpln.md +103 -0
  178. package/dist/spec/fetch.d.ts +43 -0
  179. package/dist/spec/fetch.d.ts.map +1 -0
  180. package/dist/spec/parse.d.ts +79 -0
  181. package/dist/spec/parse.d.ts.map +1 -0
  182. package/dist/spec/snapshot-cli.d.ts +18 -0
  183. package/dist/spec/snapshot-cli.d.ts.map +1 -0
  184. package/dist/types/index.d.ts +1649 -0
  185. package/dist/validate-cli.d.ts +3 -0
  186. package/dist/validate-cli.d.ts.map +1 -0
  187. package/dist/validate.d.ts +26 -0
  188. package/dist/validate.d.ts.map +1 -0
  189. package/package.json +74 -0
  190. package/src/api.ts +134 -0
  191. package/src/codegen/docs-cli.ts +7 -0
  192. package/src/codegen/docs.ts +94 -0
  193. package/src/codegen/generate-cli.ts +34 -0
  194. package/src/codegen/generate-lexicon.ts +88 -0
  195. package/src/codegen/generate-typescript.ts +71 -0
  196. package/src/codegen/generate.ts +110 -0
  197. package/src/codegen/naming.ts +61 -0
  198. package/src/codegen/package.ts +35 -0
  199. package/src/composites/composites.test.ts +252 -0
  200. package/src/composites/cron-job.ts +124 -0
  201. package/src/composites/gvc-environment.ts +104 -0
  202. package/src/composites/index.ts +17 -0
  203. package/src/composites/public-domain.ts +144 -0
  204. package/src/composites/secret-access.ts +136 -0
  205. package/src/composites/serverless-service.ts +143 -0
  206. package/src/composites/stateful-service.ts +179 -0
  207. package/src/coverage.ts +149 -0
  208. package/src/describe-resources.test.ts +252 -0
  209. package/src/describe-resources.ts +310 -0
  210. package/src/detect.ts +58 -0
  211. package/src/entity-props.ts +98 -0
  212. package/src/generated/index.d.ts +1649 -0
  213. package/src/generated/index.ts +156 -0
  214. package/src/generated/lexicon-cpln.json +2110 -0
  215. package/src/generated/runtime.ts +4 -0
  216. package/src/index.ts +31 -0
  217. package/src/init-templates.ts +131 -0
  218. package/src/kinds.ts +173 -0
  219. package/src/lint/audit-catalog.ts +230 -0
  220. package/src/lint/post-synth/cpl010-unrestricted-outbound.ts +41 -0
  221. package/src/lint/post-synth/cpl011-internal-firewall-scope.ts +38 -0
  222. package/src/lint/post-synth/cpl012-no-literal-credential-env.ts +58 -0
  223. package/src/lint/post-synth/cpl013-qualified-identity-principal.ts +45 -0
  224. package/src/lint/post-synth/cpl014-qualified-secret-ref.ts +54 -0
  225. package/src/lint/post-synth/cpl020-serverless-single-port.ts +58 -0
  226. package/src/lint/post-synth/cpl021-cron-job-shape.ts +64 -0
  227. package/src/lint/post-synth/cpl022-container-ports.ts +64 -0
  228. package/src/lint/post-synth/cpl023-container-resources.ts +87 -0
  229. package/src/lint/post-synth/cpl024-probe-handler.ts +51 -0
  230. package/src/lint/post-synth/cpl025-autoscaling-shape.ts +100 -0
  231. package/src/lint/post-synth/cpl026-scale-to-zero.ts +67 -0
  232. package/src/lint/post-synth/cpl027-capacity-ai-conflict.ts +78 -0
  233. package/src/lint/post-synth/cpl028-volume-set.ts +183 -0
  234. package/src/lint/post-synth/cpl029-link-targets.ts +155 -0
  235. package/src/lint/post-synth/cpl030-domain-routing.ts +137 -0
  236. package/src/lint/post-synth/cpl040-pinned-image.ts +48 -0
  237. package/src/lint/post-synth/cpl041-image-reference-form.ts +60 -0
  238. package/src/lint/post-synth/cpl042-gvc-placement.ts +57 -0
  239. package/src/lint/post-synth/cpl043-policy-scope.ts +0 -0
  240. package/src/lint/post-synth/helpers.ts +114 -0
  241. package/src/lint/post-synth/index.ts +45 -0
  242. package/src/lint/post-synth/post-synth.test.ts +737 -0
  243. package/src/lint/rules/cpl001-no-secret-literals.ts +83 -0
  244. package/src/lint/rules/cpl002-prefer-resource-reference.ts +71 -0
  245. package/src/lint/rules/index.ts +9 -0
  246. package/src/lint/rules/rules.test.ts +108 -0
  247. package/src/lsp/completions.test.ts +44 -0
  248. package/src/lsp/completions.ts +27 -0
  249. package/src/lsp/hover.test.ts +53 -0
  250. package/src/lsp/hover.ts +130 -0
  251. package/src/ownership.ts +28 -0
  252. package/src/package-cli.ts +25 -0
  253. package/src/plugin.test.ts +131 -0
  254. package/src/plugin.ts +187 -0
  255. package/src/reference-catalog.ts +110 -0
  256. package/src/serializer.test.ts +233 -0
  257. package/src/serializer.ts +253 -0
  258. package/src/skills/chant-cpln-secrets.md +110 -0
  259. package/src/skills/chant-cpln-workloads.md +78 -0
  260. package/src/skills/chant-cpln.md +103 -0
  261. package/src/spec/cpln-openapi.snapshot.json +10651 -0
  262. package/src/spec/fetch.ts +77 -0
  263. package/src/spec/parse.ts +451 -0
  264. package/src/spec/snapshot-cli.ts +71 -0
  265. package/src/validate-cli.ts +8 -0
  266. package/src/validate.ts +102 -0
@@ -0,0 +1,87 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { propsOf, readArray, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType, parseCpuMillicores, parseMemoryMib } from "./helpers";
4
+
5
+ /** The tag that raises the memory-to-CPU ceiling from 8 to 32. */
6
+ const RELAX_RATIO_TAG = "cpln/relaxMemoryToCpuRatio";
7
+
8
+ const CPU_MIN_MILLICORES = 25;
9
+ const MEMORY_MIN_MIB = 32;
10
+ const RATIO_MAX = 8;
11
+ const RATIO_MAX_RELAXED = 32;
12
+
13
+ /**
14
+ * CPL023: CPU and memory floors, and the memory-to-CPU ratio.
15
+ *
16
+ * The ratio is the non-obvious one. `memory(MiB) / cpu(millicores)` must be
17
+ * ≤ 8, so `2Gi` of memory needs at least 256 millicores of CPU — a workload
18
+ * asking for lots of memory and a little CPU is rejected, which surprises
19
+ * people coming from Kubernetes where the two are independent. The
20
+ * `cpln/relaxMemoryToCpuRatio` tag raises the ceiling to 32.
21
+ */
22
+ export const containerResourcesCheck: PostSynthCheck = {
23
+ id: "CPL023",
24
+ description: "Container CPU and memory must clear the platform floors and the memory-to-CPU ratio",
25
+
26
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
27
+ const diagnostics: PostSynthDiagnostic[] = [];
28
+
29
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
30
+ const tags = propsOf(entity).tags;
31
+ const relaxed = !!tags && typeof tags === "object" && RELAX_RATIO_TAG in (tags as Record<string, unknown>);
32
+ const ceiling = relaxed ? RATIO_MAX_RELAXED : RATIO_MAX;
33
+
34
+ for (const container of readArray(entity, "spec", "containers")) {
35
+ const containerName = readString(container, "name") ?? "?";
36
+ // Control Plane's defaults, so an omitted value is checked as what it
37
+ // will actually become rather than skipped.
38
+ const cpu = parseCpuMillicores(readString(container, "cpu") ?? "50m");
39
+ const memory = parseMemoryMib(readString(container, "memory") ?? "128Mi");
40
+
41
+ if (cpu !== undefined && cpu < CPU_MIN_MILLICORES) {
42
+ diagnostics.push({
43
+ checkId: "CPL023",
44
+ severity: "error",
45
+ message:
46
+ `Workload "${name}" container "${containerName}" requests ${cpu}m CPU; the minimum is ` +
47
+ `${CPU_MIN_MILLICORES}m.`,
48
+ entity: name,
49
+ lexicon: "cpln",
50
+ });
51
+ }
52
+
53
+ if (memory !== undefined && memory < MEMORY_MIN_MIB) {
54
+ diagnostics.push({
55
+ checkId: "CPL023",
56
+ severity: "error",
57
+ message:
58
+ `Workload "${name}" container "${containerName}" requests ${Math.round(memory)}Mi memory; ` +
59
+ `the minimum is ${MEMORY_MIN_MIB}Mi.`,
60
+ entity: name,
61
+ lexicon: "cpln",
62
+ });
63
+ }
64
+
65
+ if (cpu !== undefined && memory !== undefined && cpu > 0) {
66
+ const ratio = memory / cpu;
67
+ if (ratio > ceiling) {
68
+ const needed = Math.ceil(memory / ceiling);
69
+ diagnostics.push({
70
+ checkId: "CPL023",
71
+ severity: "error",
72
+ message:
73
+ `Workload "${name}" container "${containerName}" has a memory-to-CPU ratio of ` +
74
+ `${ratio.toFixed(1)} (${Math.round(memory)}Mi / ${cpu}m), above the limit of ${ceiling}. ` +
75
+ `Raise CPU to at least ${needed}m` +
76
+ (relaxed ? "." : `, or add the \`${RELAX_RATIO_TAG}\` tag to raise the ceiling to ${RATIO_MAX_RELAXED}.`),
77
+ entity: name,
78
+ lexicon: "cpln",
79
+ });
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ return diagnostics;
86
+ },
87
+ };
@@ -0,0 +1,51 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readPath, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType } from "./helpers";
4
+
5
+ /** The four mutually exclusive probe handlers. */
6
+ const PROBE_HANDLERS = ["exec", "grpc", "tcpSocket", "httpGet"] as const;
7
+
8
+ /**
9
+ * CPL024: a probe must set exactly one handler.
10
+ *
11
+ * Zero is the interesting case: a `readinessProbe` with only
12
+ * `initialDelaySeconds` and `periodSeconds` set looks configured and probes
13
+ * nothing.
14
+ */
15
+ export const probeHandlerCheck: PostSynthCheck = {
16
+ id: "CPL024",
17
+ description: "Each health-check probe must set exactly one of exec, grpc, tcpSocket, httpGet",
18
+
19
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
20
+ const diagnostics: PostSynthDiagnostic[] = [];
21
+
22
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
23
+ for (const container of readArray(entity, "spec", "containers")) {
24
+ const containerName = readString(container, "name") ?? "?";
25
+
26
+ for (const probeName of ["readinessProbe", "livenessProbe"]) {
27
+ const probe = readPath(container, probeName);
28
+ if (!probe || typeof probe !== "object") continue;
29
+
30
+ const set = PROBE_HANDLERS.filter((handler) => readPath(probe, handler) !== undefined);
31
+ if (set.length === 1) continue;
32
+
33
+ diagnostics.push({
34
+ checkId: "CPL024",
35
+ severity: "error",
36
+ message:
37
+ set.length === 0
38
+ ? `Workload "${name}" container "${containerName}" declares a ${probeName} with no handler. ` +
39
+ `Set exactly one of ${PROBE_HANDLERS.join(", ")} — as written the probe checks nothing.`
40
+ : `Workload "${name}" container "${containerName}" ${probeName} sets ${set.length} handlers ` +
41
+ `(${set.join(", ")}). They are mutually exclusive; keep one.`,
42
+ entity: name,
43
+ lexicon: "cpln",
44
+ });
45
+ }
46
+ }
47
+ }
48
+
49
+ return diagnostics;
50
+ },
51
+ };
@@ -0,0 +1,100 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readNumber, readString } from "../../entity-props";
3
+ import { WORKLOAD, autoscalingOf, entitiesOfType, workloadType } from "./helpers";
4
+
5
+ /** Which strategies each workload type supports at all. */
6
+ const SUPPORTED_METRICS: Record<string, string[]> = {
7
+ serverless: ["concurrency", "rps", "cpu", "memory", "disabled"],
8
+ standard: ["rps", "cpu", "memory", "latency", "keda", "disabled"],
9
+ stateful: ["rps", "cpu", "memory", "latency", "keda", "disabled"],
10
+ };
11
+
12
+ /**
13
+ * CPL025: mutually exclusive autoscaling fields, and the `target` ceiling.
14
+ *
15
+ * `metric` and `multi` are alternatives, not layers; `target` belongs to the
16
+ * single-metric form and is rejected alongside `multi`; and with cpu or memory
17
+ * as the metric, `target` is a utilization percentage, so above 100 it is not
18
+ * an aggressive setting but an invalid one.
19
+ */
20
+ export const autoscalingShapeCheck: PostSynthCheck = {
21
+ id: "CPL025",
22
+ description: "Autoscaling `metric`, `multi` and `target` must form a valid combination",
23
+
24
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
25
+ const diagnostics: PostSynthDiagnostic[] = [];
26
+
27
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
28
+ const scaling = autoscalingOf(entity);
29
+ if (!scaling || typeof scaling !== "object") continue;
30
+
31
+ const metric = readString(scaling, "metric");
32
+ const multi = readArray(scaling, "multi");
33
+ const target = readNumber(scaling, "target");
34
+ const type = workloadType(entity);
35
+
36
+ if (metric && multi.length > 0) {
37
+ diagnostics.push({
38
+ checkId: "CPL025",
39
+ severity: "error",
40
+ message:
41
+ `Workload "${name}" sets both autoscaling.metric ("${metric}") and autoscaling.multi. ` +
42
+ `They are mutually exclusive — multi is the multi-metric form and replaces metric.`,
43
+ entity: name,
44
+ lexicon: "cpln",
45
+ });
46
+ }
47
+
48
+ if (target !== undefined && multi.length > 0) {
49
+ diagnostics.push({
50
+ checkId: "CPL025",
51
+ severity: "error",
52
+ message:
53
+ `Workload "${name}" sets autoscaling.target alongside autoscaling.multi. ` +
54
+ `Each entry in multi carries its own target.`,
55
+ entity: name,
56
+ lexicon: "cpln",
57
+ });
58
+ }
59
+
60
+ if (target !== undefined && (metric === "cpu" || metric === "memory") && target > 100) {
61
+ diagnostics.push({
62
+ checkId: "CPL025",
63
+ severity: "error",
64
+ message:
65
+ `Workload "${name}" sets autoscaling.target ${target} with metric "${metric}", which is a ` +
66
+ `utilization percentage capped at 100.`,
67
+ entity: name,
68
+ lexicon: "cpln",
69
+ });
70
+ }
71
+
72
+ const supported = SUPPORTED_METRICS[type];
73
+ if (metric && supported && !supported.includes(metric)) {
74
+ diagnostics.push({
75
+ checkId: "CPL025",
76
+ severity: "error",
77
+ message:
78
+ `Workload "${name}" is type "${type}" and uses autoscaling metric "${metric}", which that type ` +
79
+ `does not support. Available: ${supported.join(", ")}.`,
80
+ entity: name,
81
+ lexicon: "cpln",
82
+ });
83
+ }
84
+
85
+ if (multi.length > 0 && type === "serverless") {
86
+ diagnostics.push({
87
+ checkId: "CPL025",
88
+ severity: "error",
89
+ message:
90
+ `Workload "${name}" is serverless and uses multi-metric autoscaling, which is available only ` +
91
+ `on standard and stateful workloads.`,
92
+ entity: name,
93
+ lexicon: "cpln",
94
+ });
95
+ }
96
+ }
97
+
98
+ return diagnostics;
99
+ },
100
+ };
@@ -0,0 +1,67 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readNumber, readPath, readString } from "../../entity-props";
3
+ import { WORKLOAD, autoscalingOf, entitiesOfType, workloadType } from "./helpers";
4
+
5
+ /** Strategies that let a workload scale to zero, by workload type. */
6
+ const SCALE_TO_ZERO: Record<string, string[]> = {
7
+ serverless: ["rps", "concurrency"],
8
+ standard: ["keda"],
9
+ stateful: ["keda"],
10
+ };
11
+
12
+ /**
13
+ * CPL026: scale-to-zero that will not happen.
14
+ *
15
+ * `minScale: 0` is accepted on any workload type. It only *takes effect* for
16
+ * serverless with `rps` or `concurrency`, or for standard/stateful under KEDA.
17
+ * Everywhere else the workload holds at one replica and the cost saving that
18
+ * motivated the setting never arrives — with nothing reported.
19
+ */
20
+ export const scaleToZeroCheck: PostSynthCheck = {
21
+ id: "CPL026",
22
+ description: "minScale 0 must pair with a strategy that can actually scale to zero",
23
+
24
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
25
+ const diagnostics: PostSynthDiagnostic[] = [];
26
+
27
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
28
+ const scaling = autoscalingOf(entity);
29
+ if (!scaling || typeof scaling !== "object") continue;
30
+ if (readNumber(scaling, "minScale") !== 0) continue;
31
+
32
+ const type = workloadType(entity);
33
+ const metric = readString(scaling, "metric") ?? (type === "serverless" ? "concurrency" : undefined);
34
+
35
+ if (type === "cron") {
36
+ diagnostics.push({
37
+ checkId: "CPL026",
38
+ severity: "error",
39
+ message: `Cron workload "${name}" sets minScale 0. Cron workloads cannot scale to zero.`,
40
+ entity: name,
41
+ lexicon: "cpln",
42
+ });
43
+ continue;
44
+ }
45
+
46
+ const allowed = SCALE_TO_ZERO[type] ?? [];
47
+ const usingKeda = metric === "keda" || readPath(scaling, "keda") !== undefined;
48
+ if (allowed.includes(metric ?? "") || (allowed.includes("keda") && usingKeda)) continue;
49
+
50
+ diagnostics.push({
51
+ checkId: "CPL026",
52
+ severity: "warning",
53
+ message:
54
+ `Workload "${name}" (type "${type}") sets minScale 0 with metric "${metric ?? "unset"}", which ` +
55
+ `cannot scale to zero. ` +
56
+ (type === "serverless"
57
+ ? `Serverless scales to zero only with "rps" or "concurrency".`
58
+ : `Standard and stateful workloads scale to zero only under KEDA, which must be enabled on the GVC.`) +
59
+ ` As written the workload holds at one replica with no error.`,
60
+ entity: name,
61
+ lexicon: "cpln",
62
+ });
63
+ }
64
+
65
+ return diagnostics;
66
+ },
67
+ };
@@ -0,0 +1,78 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readBoolean, readPath, readString } from "../../entity-props";
3
+ import { WORKLOAD, autoscalingOf, entitiesOfType, workloadType } from "./helpers";
4
+
5
+ /**
6
+ * CPL027: Capacity AI combined with something it excludes.
7
+ *
8
+ * Capacity AI resizes CPU and memory from observed usage, and it is **on by
9
+ * default** for serverless, standard and cron — so these conflicts are usually
10
+ * reached by adding CPU autoscaling or a GPU to a workload that never opted
11
+ * into Capacity AI in the first place. Each conflict has the same root: the
12
+ * other feature needs a stable resource baseline that Capacity AI is actively
13
+ * moving.
14
+ */
15
+ export const capacityAiConflictCheck: PostSynthCheck = {
16
+ id: "CPL027",
17
+ description: "Capacity AI conflicts with CPU-utilization autoscaling, multi-metric autoscaling and GPUs",
18
+
19
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
20
+ const diagnostics: PostSynthDiagnostic[] = [];
21
+
22
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
23
+ const type = workloadType(entity);
24
+ // Default-on for every type except stateful, so `undefined` means enabled
25
+ // for those — which is exactly how these conflicts get reached unnoticed.
26
+ const declared = readBoolean(entity, "spec", "defaultOptions", "capacityAI");
27
+ const enabled = declared ?? type !== "stateful";
28
+ if (!enabled) continue;
29
+
30
+ const implicit = declared === undefined;
31
+ const how = implicit ? "Capacity AI is on by default for this workload type and" : "Capacity AI";
32
+
33
+ const scaling = autoscalingOf(entity);
34
+ const metric = scaling && typeof scaling === "object" ? readString(scaling, "metric") : undefined;
35
+ const multi = scaling && typeof scaling === "object" ? readArray(scaling, "multi") : [];
36
+
37
+ if (metric === "cpu") {
38
+ diagnostics.push({
39
+ checkId: "CPL027",
40
+ severity: "error",
41
+ message:
42
+ `Workload "${name}": ${how} conflicts with CPU-utilization autoscaling — dynamic CPU allocation ` +
43
+ `moves the baseline the metric scales against. Set defaultOptions.capacityAI false, or scale on ` +
44
+ `rps or memory.`,
45
+ entity: name,
46
+ lexicon: "cpln",
47
+ });
48
+ }
49
+
50
+ if (multi.length > 0) {
51
+ diagnostics.push({
52
+ checkId: "CPL027",
53
+ severity: "error",
54
+ message:
55
+ `Workload "${name}": ${how} conflicts with multi-metric autoscaling, which requires stable ` +
56
+ `resource baselines. Set defaultOptions.capacityAI false.`,
57
+ entity: name,
58
+ lexicon: "cpln",
59
+ });
60
+ }
61
+
62
+ for (const container of readArray(entity, "spec", "containers")) {
63
+ if (readPath(container, "gpu") === undefined) continue;
64
+ diagnostics.push({
65
+ checkId: "CPL027",
66
+ severity: "error",
67
+ message:
68
+ `Workload "${name}" container "${readString(container, "name") ?? "?"}" requests a GPU, and ` +
69
+ `${how} conflicts with it — GPU allocation is fixed. Set defaultOptions.capacityAI false.`,
70
+ entity: name,
71
+ lexicon: "cpln",
72
+ });
73
+ }
74
+ }
75
+
76
+ return diagnostics;
77
+ },
78
+ };
@@ -0,0 +1,183 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readNumber, readString } from "../../entity-props";
3
+ import { GVC, VOLUMESET, WORKLOAD, entitiesOfType } from "./helpers";
4
+
5
+ /** Minimum initial capacity in GB, by performance class. */
6
+ const MIN_CAPACITY_GB: Record<string, number> = {
7
+ "general-purpose-ssd": 10,
8
+ "high-throughput-ssd": 200,
9
+ shared: 10,
10
+ };
11
+
12
+ const MAX_CAPACITY_GB = 65_536;
13
+
14
+ /** Filesystems that bind to exactly one stateful workload. */
15
+ const EXCLUSIVE_FILESYSTEMS = new Set(["ext4", "xfs"]);
16
+
17
+ /** Control Plane caps a workload at 15 volumes. */
18
+ const MAX_VOLUMES = 15;
19
+
20
+ const VOLUMESET_URI = /^cpln:\/\/volumeset\/(.+)$/;
21
+
22
+ export const volumeSetCheck: PostSynthCheck = {
23
+ id: "CPL028",
24
+ description: "Volume set capacity, filesystem binding, and workload mount rules",
25
+
26
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
27
+ const diagnostics: PostSynthDiagnostic[] = [];
28
+
29
+ // Volume sets by their declared name, so a mount can be checked against the
30
+ // set it names rather than only against itself.
31
+ const volumeSets = new Map<string, { entity: unknown; gvc?: string; fileSystemType: string }>();
32
+
33
+ for (const [name, entity] of entitiesOfType(ctx.entities, VOLUMESET)) {
34
+ const declaredName = readString(entity, "name") ?? name;
35
+ const fileSystemType = readString(entity, "spec", "fileSystemType") ?? "ext4";
36
+ volumeSets.set(declaredName, { entity, gvc: readString(entity, "gvc"), fileSystemType });
37
+
38
+ const performanceClass =
39
+ readString(entity, "spec", "performanceClass") ?? (fileSystemType === "shared" ? "shared" : "general-purpose-ssd");
40
+ const capacity = readNumber(entity, "spec", "initialCapacity");
41
+
42
+ const floor = MIN_CAPACITY_GB[performanceClass];
43
+ if (capacity !== undefined && floor !== undefined && capacity < floor) {
44
+ diagnostics.push({
45
+ checkId: "CPL028",
46
+ severity: "error",
47
+ message:
48
+ `Volume set "${name}" requests ${capacity} GB with performanceClass "${performanceClass}", ` +
49
+ `below its ${floor} GB minimum. performanceClass is immutable after creation, so correcting ` +
50
+ `this later means recreating the volume set and losing its data.`,
51
+ entity: name,
52
+ lexicon: "cpln",
53
+ });
54
+ }
55
+
56
+ if (capacity !== undefined && capacity > MAX_CAPACITY_GB) {
57
+ diagnostics.push({
58
+ checkId: "CPL028",
59
+ severity: "error",
60
+ message: `Volume set "${name}" requests ${capacity} GB, above the ${MAX_CAPACITY_GB} GB maximum.`,
61
+ entity: name,
62
+ lexicon: "cpln",
63
+ });
64
+ }
65
+
66
+ if (fileSystemType === "shared" && performanceClass !== "shared") {
67
+ diagnostics.push({
68
+ checkId: "CPL028",
69
+ severity: "warning",
70
+ message:
71
+ `Volume set "${name}" has fileSystemType "shared" but performanceClass "${performanceClass}". ` +
72
+ `Control Plane forces "shared" for a shared filesystem, so the declared value will not match ` +
73
+ `what comes back and will read as drift on every plan.`,
74
+ entity: name,
75
+ lexicon: "cpln",
76
+ });
77
+ }
78
+
79
+ // Custom encryption is AWS KMS on a block device; a shared filesystem has
80
+ // no device to encrypt.
81
+ if (fileSystemType === "shared" && readString(entity, "spec", "customEncryption", "secretLink")) {
82
+ diagnostics.push({
83
+ checkId: "CPL028",
84
+ severity: "error",
85
+ message: `Volume set "${name}" sets customEncryption with fileSystemType "shared"; it is supported on ext4 and xfs only.`,
86
+ entity: name,
87
+ lexicon: "cpln",
88
+ });
89
+ }
90
+ }
91
+
92
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
93
+ const type = readString(entity, "spec", "type") ?? "serverless";
94
+ const workloadGvc = readString(entity, "gvc");
95
+ const paths: string[] = [];
96
+ let mountCount = 0;
97
+
98
+ for (const container of readArray(entity, "spec", "containers")) {
99
+ const containerName = readString(container, "name") ?? "?";
100
+
101
+ for (const volume of readArray(container, "volumes")) {
102
+ mountCount++;
103
+ const uri = readString(volume, "uri");
104
+ const path = readString(volume, "path");
105
+ if (path) paths.push(path);
106
+
107
+ const match = uri ? VOLUMESET_URI.exec(uri) : null;
108
+ if (!match) continue;
109
+
110
+ const target = volumeSets.get(match[1]);
111
+ if (!target) continue; // declared elsewhere; CPL029 covers dangling references
112
+
113
+ if (EXCLUSIVE_FILESYSTEMS.has(target.fileSystemType) && type !== "stateful") {
114
+ diagnostics.push({
115
+ checkId: "CPL028",
116
+ severity: "error",
117
+ message:
118
+ `Workload "${name}" is type "${type}" and container "${containerName}" mounts volume set ` +
119
+ `"${match[1]}", whose fileSystemType "${target.fileSystemType}" binds to exactly one ` +
120
+ `stateful workload. Use a "shared" volume set, or make the workload stateful.`,
121
+ entity: name,
122
+ lexicon: "cpln",
123
+ });
124
+ }
125
+
126
+ if (target.gvc && workloadGvc && target.gvc !== workloadGvc) {
127
+ diagnostics.push({
128
+ checkId: "CPL028",
129
+ severity: "error",
130
+ message:
131
+ `Workload "${name}" (GVC "${workloadGvc}") mounts volume set "${match[1]}" from GVC ` +
132
+ `"${target.gvc}". Volume sets are only mountable from within their own GVC.`,
133
+ entity: name,
134
+ lexicon: "cpln",
135
+ });
136
+ }
137
+ }
138
+ }
139
+
140
+ if (mountCount > MAX_VOLUMES) {
141
+ diagnostics.push({
142
+ checkId: "CPL028",
143
+ severity: "error",
144
+ message: `Workload "${name}" mounts ${mountCount} volumes; the maximum is ${MAX_VOLUMES}.`,
145
+ entity: name,
146
+ lexicon: "cpln",
147
+ });
148
+ }
149
+
150
+ for (const [i, path] of paths.entries()) {
151
+ for (const other of paths.slice(i + 1)) {
152
+ if (path === other) {
153
+ diagnostics.push({
154
+ checkId: "CPL028",
155
+ severity: "error",
156
+ message: `Workload "${name}" mounts two volumes at the same path "${path}".`,
157
+ entity: name,
158
+ lexicon: "cpln",
159
+ });
160
+ } else if (isParentPath(path, other) || isParentPath(other, path)) {
161
+ diagnostics.push({
162
+ checkId: "CPL028",
163
+ severity: "error",
164
+ message:
165
+ `Workload "${name}" mounts volumes at "${path}" and "${other}"; no mount path may be a ` +
166
+ `parent of another.`,
167
+ entity: name,
168
+ lexicon: "cpln",
169
+ });
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ return diagnostics;
176
+ },
177
+ };
178
+
179
+ /** Whether `parent` is a proper ancestor directory of `child`. */
180
+ function isParentPath(parent: string, child: string): boolean {
181
+ const normalized = parent.endsWith("/") ? parent : `${parent}/`;
182
+ return child.startsWith(normalized);
183
+ }