@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,58 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType } from "./helpers";
4
+
5
+ /** `cpln://secret/NAME.FIELD` — the field qualifier is the part that matters. */
6
+ const SECRET_REF = /^cpln:\/\/secret\/([^/.\s]+)(?:\.([^/\s]+))?$/;
7
+
8
+ /** Env var names that read as credentials. */
9
+ const CREDENTIAL_NAME = /(password|passwd|secret|token|apikey|api_key|access_key|private_key|credential)/i;
10
+
11
+ /**
12
+ * Values that look like a deliberate non-secret rather than a leaked one: an
13
+ * empty string, an obvious placeholder, or another `cpln://` reference.
14
+ */
15
+ const PLACEHOLDER = /^(|changeme|placeholder|todo|xxx+|\*+|<[^>]*>|\$\{[^}]*\})$/i;
16
+
17
+ /**
18
+ * CPL012: a credential-shaped env var carrying a literal value.
19
+ *
20
+ * The alternative is one string change — `cpln://secret/db.password` — so a
21
+ * literal here is almost always an oversight rather than a decision. Env values
22
+ * end up in the workload spec, which is readable by anyone with `get` on the
23
+ * workload, and in every diff of the manifest.
24
+ */
25
+ export const noLiteralCredentialEnvCheck: PostSynthCheck = {
26
+ id: "CPL012",
27
+ description: "Credential-shaped environment variables must reference a secret, not a literal",
28
+
29
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
30
+ const diagnostics: PostSynthDiagnostic[] = [];
31
+
32
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
33
+ for (const container of readArray(entity, "spec", "containers")) {
34
+ const containerName = readString(container, "name") ?? "?";
35
+ for (const env of readArray(container, "env")) {
36
+ const envName = readString(env, "name");
37
+ const value = readString(env, "value");
38
+ if (!envName || value === undefined) continue;
39
+ if (!CREDENTIAL_NAME.test(envName)) continue;
40
+ if (value.startsWith("cpln://") || PLACEHOLDER.test(value)) continue;
41
+
42
+ diagnostics.push({
43
+ checkId: "CPL012",
44
+ severity: "error",
45
+ message:
46
+ `Workload "${name}" container "${containerName}" sets env "${envName}" to a literal value. ` +
47
+ `Reference a secret instead — \`cpln://secret/NAME.FIELD\` — so the value is not stored in ` +
48
+ `the workload spec or its diffs.`,
49
+ entity: name,
50
+ lexicon: "cpln",
51
+ });
52
+ }
53
+ }
54
+ }
55
+
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,45 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { propsOf, readArray } from "../../entity-props";
3
+ import { GVC, POLICY, entitiesOfType } from "./helpers";
4
+
5
+ /**
6
+ * CPL013: an identity principal link that Control Plane will ignore.
7
+ *
8
+ * `//identity/NAME` is accepted by the policy API and then has no effect. Only
9
+ * the GVC-qualified `//gvc/GVC/identity/NAME` grants anything. Nothing reports
10
+ * this — the policy exists, the binding exists, and the permission is simply
11
+ * never granted.
12
+ */
13
+ export const qualifiedIdentityPrincipalCheck: PostSynthCheck = {
14
+ id: "CPL013",
15
+ description: "Policy bindings must name identities by their GVC-qualified link",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [name, entity] of entitiesOfType(ctx.entities, POLICY)) {
21
+ for (const binding of readArray(entity, "bindings")) {
22
+ const links = propsOf(binding).principalLinks;
23
+ if (!Array.isArray(links)) continue;
24
+
25
+ for (const link of links) {
26
+ if (typeof link !== "string") continue;
27
+ if (!/^\/\/identity\/[^/]+$/.test(link)) continue;
28
+
29
+ diagnostics.push({
30
+ checkId: "CPL013",
31
+ severity: "error",
32
+ message:
33
+ `Policy "${name}" binds the principal "${link}". Control Plane silently ignores the bare ` +
34
+ `//identity/NAME form — use //gvc/<gvc>/identity/${link.slice("//identity/".length)} instead. ` +
35
+ `The policy will apply cleanly and grant nothing as written.`,
36
+ entity: name,
37
+ lexicon: "cpln",
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ return diagnostics;
44
+ },
45
+ };
@@ -0,0 +1,54 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType } from "./helpers";
4
+
5
+ /** `cpln://secret/NAME.FIELD` — the field qualifier is the part that matters. */
6
+ const SECRET_REF = /^cpln:\/\/secret\/([^/.\s]+)(?:\.([^/\s]+))?$/;
7
+
8
+ /**
9
+ * CPL014: a secret reference with no field qualifier.
10
+ *
11
+ * `cpln://secret/db` resolves to nothing for every secret type except `gcp`,
12
+ * which is conventionally mounted as a whole JSON file. The field name is not
13
+ * optional: `opaque` needs `.payload`, `userpass` needs `.username`/`.password`,
14
+ * `tls` needs `.cert`/`.key`, and a `dictionary` needs one of its keys.
15
+ *
16
+ * Volume mounts are exempt — mounting a secret as a directory is the documented
17
+ * way to consume a `dictionary` or a `gcp` credential file.
18
+ */
19
+ export const qualifiedSecretRefCheck: PostSynthCheck = {
20
+ id: "CPL014",
21
+ description: "Secret references in environment variables must name a field",
22
+
23
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
24
+ const diagnostics: PostSynthDiagnostic[] = [];
25
+
26
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
27
+ for (const container of readArray(entity, "spec", "containers")) {
28
+ const containerName = readString(container, "name") ?? "?";
29
+ for (const env of readArray(container, "env")) {
30
+ const envName = readString(env, "name") ?? "?";
31
+ const value = readString(env, "value");
32
+ if (!value) continue;
33
+
34
+ const match = SECRET_REF.exec(value);
35
+ if (!match || match[2]) continue;
36
+
37
+ diagnostics.push({
38
+ checkId: "CPL014",
39
+ severity: "error",
40
+ message:
41
+ `Workload "${name}" container "${containerName}" sets env "${envName}" to "${value}", which ` +
42
+ `names no field. Use \`cpln://secret/${match[1]}.FIELD\` — \`.payload\` for opaque, ` +
43
+ `\`.username\`/\`.password\` for userpass, \`.cert\`/\`.key\` for tls, a key name for a ` +
44
+ `dictionary. An unqualified reference resolves to nothing at runtime.`,
45
+ entity: name,
46
+ lexicon: "cpln",
47
+ });
48
+ }
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ },
54
+ };
@@ -0,0 +1,58 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType, exposedPorts, workloadType } from "./helpers";
4
+
5
+ /**
6
+ * CPL020: a serverless workload must expose exactly one HTTP port.
7
+ *
8
+ * Not "at least one" and not "any number" — exactly one, and it must be HTTP.
9
+ * Zero is the more common mistake and the more confusing one, because the
10
+ * workload deploys and reports healthy while serving nothing.
11
+ */
12
+ export const serverlessSinglePortCheck: PostSynthCheck = {
13
+ id: "CPL020",
14
+ description: "Serverless workloads must expose exactly one HTTP port",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
20
+ if (workloadType(entity) !== "serverless") continue;
21
+
22
+ const ports = exposedPorts(entity);
23
+ if (ports.length !== 1) {
24
+ diagnostics.push({
25
+ checkId: "CPL020",
26
+ severity: "error",
27
+ message:
28
+ `Serverless workload "${name}" exposes ${ports.length} ports; exactly one HTTP port is required. ` +
29
+ (ports.length === 0
30
+ ? `It will deploy and serve nothing.`
31
+ : `Use a standard workload if it genuinely needs several.`),
32
+ entity: name,
33
+ lexicon: "cpln",
34
+ });
35
+ continue;
36
+ }
37
+
38
+ // The single port must be HTTP. `tcp` and `grpc` need a standard workload.
39
+ for (const container of readArray(entity, "spec", "containers")) {
40
+ for (const port of readArray(container, "ports")) {
41
+ const protocol = readString(port, "protocol") ?? "http";
42
+ if (protocol === "http" || protocol === "http2") continue;
43
+ diagnostics.push({
44
+ checkId: "CPL020",
45
+ severity: "error",
46
+ message:
47
+ `Serverless workload "${name}" exposes a "${protocol}" port. Serverless supports only http ` +
48
+ `and http2 — use a standard workload for ${protocol}.`,
49
+ entity: name,
50
+ lexicon: "cpln",
51
+ });
52
+ }
53
+ }
54
+ }
55
+
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,64 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readPath, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType, exposedPorts, workloadType } from "./helpers";
4
+
5
+ /**
6
+ * CPL021: a cron workload needs a schedule and must expose no ports.
7
+ *
8
+ * Cron cannot serve traffic, so a port on one is always a misunderstanding —
9
+ * usually a workload converted from serverless with the ports left behind. A
10
+ * missing `spec.job.schedule` is the other half: `spec.job` is *required* for
11
+ * cron and forbidden for every other type.
12
+ */
13
+ export const cronJobShapeCheck: PostSynthCheck = {
14
+ id: "CPL021",
15
+ description: "Cron workloads must declare a schedule and expose no ports",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
21
+ const type = workloadType(entity);
22
+ const job = readPath(entity, "spec", "job");
23
+ const schedule = readString(entity, "spec", "job", "schedule");
24
+
25
+ if (type === "cron") {
26
+ if (!schedule) {
27
+ diagnostics.push({
28
+ checkId: "CPL021",
29
+ severity: "error",
30
+ message: `Cron workload "${name}" has no spec.job.schedule. A cron workload requires one.`,
31
+ entity: name,
32
+ lexicon: "cpln",
33
+ });
34
+ }
35
+
36
+ const ports = exposedPorts(entity);
37
+ if (ports.length > 0) {
38
+ diagnostics.push({
39
+ checkId: "CPL021",
40
+ severity: "error",
41
+ message:
42
+ `Cron workload "${name}" exposes ${ports.length} port(s) (${ports.join(", ")}). Cron workloads ` +
43
+ `cannot serve traffic — remove the ports, or use a standard workload.`,
44
+ entity: name,
45
+ lexicon: "cpln",
46
+ });
47
+ }
48
+ continue;
49
+ }
50
+
51
+ if (job !== undefined) {
52
+ diagnostics.push({
53
+ checkId: "CPL021",
54
+ severity: "error",
55
+ message: `Workload "${name}" is type "${type}" but sets spec.job. Only cron workloads may declare one.`,
56
+ entity: name,
57
+ lexicon: "cpln",
58
+ });
59
+ }
60
+ }
61
+
62
+ return diagnostics;
63
+ },
64
+ };
@@ -0,0 +1,64 @@
1
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
2
+ import { readArray, readNumber, readString } from "../../entity-props";
3
+ import { WORKLOAD, entitiesOfType } from "./helpers";
4
+
5
+ /**
6
+ * CPL022: `port` and `ports` are mutually exclusive, and port numbers must be
7
+ * unique across every container in a workload.
8
+ *
9
+ * The duplicate case is the one worth catching: two containers each binding
10
+ * 8080 is accepted at author time, rejected at apply, and reads as correct.
11
+ */
12
+ export const containerPortsCheck: PostSynthCheck = {
13
+ id: "CPL022",
14
+ description: "Container ports must not mix `port` and `ports`, and must be unique across containers",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
20
+ const seen = new Map<number, string>();
21
+
22
+ for (const container of readArray(entity, "spec", "containers")) {
23
+ const containerName = readString(container, "name") ?? "?";
24
+ const single = readNumber(container, "port");
25
+ const list = readArray(container, "ports");
26
+
27
+ if (single !== undefined && list.length > 0) {
28
+ diagnostics.push({
29
+ checkId: "CPL022",
30
+ severity: "error",
31
+ message:
32
+ `Workload "${name}" container "${containerName}" sets both \`port\` and \`ports\`. ` +
33
+ `They are mutually exclusive — keep \`ports\`.`,
34
+ entity: name,
35
+ lexicon: "cpln",
36
+ });
37
+ }
38
+
39
+ const numbers = [
40
+ ...(single !== undefined ? [single] : []),
41
+ ...list.map((port) => readNumber(port, "number")).filter((n): n is number => n !== undefined),
42
+ ];
43
+
44
+ for (const number of numbers) {
45
+ const owner = seen.get(number);
46
+ if (owner !== undefined && owner !== containerName) {
47
+ diagnostics.push({
48
+ checkId: "CPL022",
49
+ severity: "error",
50
+ message:
51
+ `Workload "${name}" exposes port ${number} from both "${owner}" and "${containerName}". ` +
52
+ `Port numbers must be unique across the containers of one workload.`,
53
+ entity: name,
54
+ lexicon: "cpln",
55
+ });
56
+ }
57
+ seen.set(number, containerName);
58
+ }
59
+ }
60
+ }
61
+
62
+ return diagnostics;
63
+ },
64
+ };
@@ -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
+ };