@intentius/chant-lexicon-k8s 0.124.30 → 0.124.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/dist/actions/apps.js +18 -0
  2. package/dist/actions/batch.js +6 -0
  3. package/dist/actions/core.js +48 -0
  4. package/dist/actions/index.js +13 -0
  5. package/dist/actions/networking.js +11 -0
  6. package/dist/actions/rbac.js +8 -0
  7. package/dist/api/classify.js +123 -0
  8. package/dist/api/connect.js +62 -0
  9. package/dist/api/fake-cluster.js +175 -0
  10. package/dist/api/operation-surface.d.ts.map +1 -1
  11. package/dist/api/operation-surface.js +109 -0
  12. package/dist/api/owner-chain.js +100 -0
  13. package/dist/api/sweep-types.js +35 -0
  14. package/dist/behaviour-kinds.js +82 -0
  15. package/dist/codegen/docs-cli.js +2 -0
  16. package/dist/codegen/docs.js +146 -0
  17. package/dist/codegen/generate-cli.js +30 -0
  18. package/dist/codegen/generate-lexicon.js +74 -0
  19. package/dist/codegen/generate-list-map-keys.js +88 -0
  20. package/dist/codegen/generate-operations.js +53 -0
  21. package/dist/codegen/generate-typescript.js +124 -0
  22. package/dist/codegen/generate.js +173 -0
  23. package/dist/codegen/naming.js +174 -0
  24. package/dist/codegen/package.js +36 -0
  25. package/dist/codegen/patches.js +88 -0
  26. package/dist/codegen/typecheck.js +4 -0
  27. package/dist/codegen/versions.js +41 -0
  28. package/dist/components/argo-app.js +51 -0
  29. package/dist/components/builders.js +22 -0
  30. package/dist/components/capability-plugin.js +31 -0
  31. package/dist/components/flux-reconcile.js +61 -0
  32. package/dist/components/index.d.ts +1 -1
  33. package/dist/components/index.d.ts.map +1 -1
  34. package/dist/components/index.js +18 -0
  35. package/dist/components/kubectl-apply.d.ts +83 -1
  36. package/dist/components/kubectl-apply.d.ts.map +1 -1
  37. package/dist/components/kubectl-apply.js +274 -0
  38. package/dist/components/kustomize-apply.js +25 -0
  39. package/dist/composites/adot-collector.js +184 -0
  40. package/dist/composites/agic-ingress.js +85 -0
  41. package/dist/composites/aks-external-dns-agent.js +146 -0
  42. package/dist/composites/alb-ingress.js +86 -0
  43. package/dist/composites/argo-app.js +199 -0
  44. package/dist/composites/autoscaled-service.js +174 -0
  45. package/dist/composites/azure-disk-storage-class.js +47 -0
  46. package/dist/composites/azure-file-storage-class.js +48 -0
  47. package/dist/composites/azure-monitor-collector.js +176 -0
  48. package/dist/composites/batch-job.js +139 -0
  49. package/dist/composites/catalog.js +4110 -0
  50. package/dist/composites/cockroachdb-cluster.js +371 -0
  51. package/dist/composites/cockroachdb-region-stack.js +369 -0
  52. package/dist/composites/config-connector-context.js +40 -0
  53. package/dist/composites/configured-app.js +135 -0
  54. package/dist/composites/cron-schedule.js +38 -0
  55. package/dist/composites/cron-workload.js +109 -0
  56. package/dist/composites/ebs-storage-class.js +54 -0
  57. package/dist/composites/efs-storage-class.js +46 -0
  58. package/dist/composites/external-dns-agent.js +122 -0
  59. package/dist/composites/filestore-storage-class.js +49 -0
  60. package/dist/composites/fluent-bit-agent.js +157 -0
  61. package/dist/composites/flux-app.js +156 -0
  62. package/dist/composites/gce-ingress.js +83 -0
  63. package/dist/composites/gce-pd-storage-class.js +49 -0
  64. package/dist/composites/gke-external-dns-agent.js +129 -0
  65. package/dist/composites/gke-fluent-bit-agent.js +165 -0
  66. package/dist/composites/gke-gateway.js +91 -0
  67. package/dist/composites/gke-otel-collector.js +94 -0
  68. package/dist/composites/index.js +51 -0
  69. package/dist/composites/inference-service.js +76 -0
  70. package/dist/composites/irsa-service-account.js +77 -0
  71. package/dist/composites/member-defaults.js +14 -0
  72. package/dist/composites/metrics-server.js +181 -0
  73. package/dist/composites/model.js +92 -0
  74. package/dist/composites/monitored-service.js +152 -0
  75. package/dist/composites/namespace-env.js +114 -0
  76. package/dist/composites/network-isolated-app.js +124 -0
  77. package/dist/composites/node-agent.js +149 -0
  78. package/dist/composites/operator-stack.js +268 -0
  79. package/dist/composites/otel-collector-agent.js +142 -0
  80. package/dist/composites/otel-collector-gateway.js +216 -0
  81. package/dist/composites/otel-collector-node.js +176 -0
  82. package/dist/composites/otel-collector-rbac.js +115 -0
  83. package/dist/composites/otel-collector-shape.js +128 -0
  84. package/dist/composites/otel-collector.js +115 -0
  85. package/dist/composites/otel-operator-collector.js +123 -0
  86. package/dist/composites/ray-cluster.js +378 -0
  87. package/dist/composites/ray-job.js +136 -0
  88. package/dist/composites/ray-service.js +178 -0
  89. package/dist/composites/secure-ingress.js +100 -0
  90. package/dist/composites/security-context.js +1 -0
  91. package/dist/composites/sidecar-app.js +105 -0
  92. package/dist/composites/stateful-app.js +111 -0
  93. package/dist/composites/vllm-serving-runtime.js +114 -0
  94. package/dist/composites/web-app.js +157 -0
  95. package/dist/composites/worker-pool.js +167 -0
  96. package/dist/composites/workload-identity-sa.js +81 -0
  97. package/dist/composites/workload-identity-service-account.js +77 -0
  98. package/dist/config-schema.js +72 -0
  99. package/dist/config.js +59 -0
  100. package/dist/coverage.js +27 -0
  101. package/dist/crd/crd-sources.js +615 -0
  102. package/dist/crd/loader.js +181 -0
  103. package/dist/crd/oci-chart.js +144 -0
  104. package/dist/crd/parser.js +382 -0
  105. package/dist/crd/project-codegen.js +273 -0
  106. package/dist/crd/types.js +7 -0
  107. package/dist/deep-observe-hooks.js +306 -0
  108. package/dist/deep-observe.js +234 -0
  109. package/dist/default-labels.js +87 -0
  110. package/dist/describe-resources.js +699 -0
  111. package/dist/describe-stack-status.js +99 -0
  112. package/dist/detect.js +11 -0
  113. package/dist/effect-receipt-row.js +205 -0
  114. package/dist/export-resources.js +39 -0
  115. package/dist/generated/index.js +1092 -0
  116. package/dist/generated/runtime.js +4 -0
  117. package/dist/group-namespace.js +96 -0
  118. package/dist/import/embedded.js +197 -0
  119. package/dist/import/generator.js +288 -0
  120. package/dist/import/live-export.js +85 -0
  121. package/dist/import/parser.js +204 -0
  122. package/dist/import/testdata/embedded/fixtures.js +60 -0
  123. package/dist/include/entity.js +78 -0
  124. package/dist/include/resolve.js +170 -0
  125. package/dist/index.js +59 -0
  126. package/dist/integrity.json +3 -3
  127. package/dist/kube/apply.js +90 -0
  128. package/dist/kube/connect.js +46 -0
  129. package/dist/kube/delete.js +138 -0
  130. package/dist/kube/describe.js +169 -0
  131. package/dist/kube/events.js +132 -0
  132. package/dist/kube/flags.js +91 -0
  133. package/dist/kube/get.js +159 -0
  134. package/dist/kube/group.js +123 -0
  135. package/dist/kube/logs.js +82 -0
  136. package/dist/kube/project.js +94 -0
  137. package/dist/kube/render.js +227 -0
  138. package/dist/kube/source.js +122 -0
  139. package/dist/kube/target.js +63 -0
  140. package/dist/kube/testing.js +28 -0
  141. package/dist/kube/top.js +99 -0
  142. package/dist/kube/verdict.js +72 -0
  143. package/dist/kube/wait.js +129 -0
  144. package/dist/kustomize/render.js +73 -0
  145. package/dist/kustomize/rendered-entity.js +67 -0
  146. package/dist/kustomize/root.js +68 -0
  147. package/dist/lint/audit-catalog.js +61 -0
  148. package/dist/lint/audit-lineage.js +118 -0
  149. package/dist/lint/post-synth/argo-helpers.js +36 -0
  150. package/dist/lint/post-synth/argo002.js +31 -0
  151. package/dist/lint/post-synth/argo003.js +53 -0
  152. package/dist/lint/post-synth/argo005.js +54 -0
  153. package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
  154. package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
  155. package/dist/lint/post-synth/flux002.js +56 -0
  156. package/dist/lint/post-synth/flux003.js +50 -0
  157. package/dist/lint/post-synth/index.js +104 -0
  158. package/dist/lint/post-synth/k8s-helpers.js +186 -0
  159. package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
  160. package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
  161. package/dist/lint/post-synth/sops-helpers.js +43 -0
  162. package/dist/lint/post-synth/wk8005.js +44 -0
  163. package/dist/lint/post-synth/wk8006.js +56 -0
  164. package/dist/lint/post-synth/wk8041.js +60 -0
  165. package/dist/lint/post-synth/wk8042.js +36 -0
  166. package/dist/lint/post-synth/wk8101.js +52 -0
  167. package/dist/lint/post-synth/wk8102.js +30 -0
  168. package/dist/lint/post-synth/wk8103.js +33 -0
  169. package/dist/lint/post-synth/wk8104.js +58 -0
  170. package/dist/lint/post-synth/wk8105.js +33 -0
  171. package/dist/lint/post-synth/wk8201.js +44 -0
  172. package/dist/lint/post-synth/wk8202.js +34 -0
  173. package/dist/lint/post-synth/wk8203.js +34 -0
  174. package/dist/lint/post-synth/wk8204.js +71 -0
  175. package/dist/lint/post-synth/wk8205.js +38 -0
  176. package/dist/lint/post-synth/wk8207.js +33 -0
  177. package/dist/lint/post-synth/wk8208.js +33 -0
  178. package/dist/lint/post-synth/wk8209.js +33 -0
  179. package/dist/lint/post-synth/wk8301.js +49 -0
  180. package/dist/lint/post-synth/wk8302.js +34 -0
  181. package/dist/lint/post-synth/wk8303.js +68 -0
  182. package/dist/lint/post-synth/wk8304.js +58 -0
  183. package/dist/lint/post-synth/wk8305.js +96 -0
  184. package/dist/lint/post-synth/wk8306.js +38 -0
  185. package/dist/lint/post-synth/wk8401.js +86 -0
  186. package/dist/lint/post-synth/wk8402.js +31 -0
  187. package/dist/lint/post-synth/wk8403.js +49 -0
  188. package/dist/lint/post-synth/wk8404.js +36 -0
  189. package/dist/lint/post-synth/wk8405.js +65 -0
  190. package/dist/lint/post-synth/wk8406.js +50 -0
  191. package/dist/lint/post-synth/wk8407.js +58 -0
  192. package/dist/lint/post-synth/wk8501.js +36 -0
  193. package/dist/lint/post-synth/wk8502.js +35 -0
  194. package/dist/lint/post-synth/wk8503.js +164 -0
  195. package/dist/lint/post-synth/wk8504.js +20 -0
  196. package/dist/lint/post-synth/wk8505.js +31 -0
  197. package/dist/lint/post-synth/wk8601.js +40 -0
  198. package/dist/lint/post-synth/wk8602.js +120 -0
  199. package/dist/lint/post-synth/wk8603.js +63 -0
  200. package/dist/lint/post-synth/wk8604.js +31 -0
  201. package/dist/lint/post-synth/wk8605.js +124 -0
  202. package/dist/lint/post-synth/wk8606.js +22 -0
  203. package/dist/lint/post-synth/wk8701.js +31 -0
  204. package/dist/lint/post-synth/wk8702.js +37 -0
  205. package/dist/lint/post-synth/wk8703.js +31 -0
  206. package/dist/lint/post-synth/wk8704.js +135 -0
  207. package/dist/lint/post-synth/wk8705.js +38 -0
  208. package/dist/lint/rules/argo-appset-single-project.js +56 -0
  209. package/dist/lint/rules/argo-ast.js +94 -0
  210. package/dist/lint/rules/argo-automated-prune.js +59 -0
  211. package/dist/lint/rules/flux-source-ref-pin.js +44 -0
  212. package/dist/lint/rules/hardcoded-namespace.js +57 -0
  213. package/dist/lint/rules/latest-image-tag.js +103 -0
  214. package/dist/lint/rules/missing-resource-limits.js +85 -0
  215. package/dist/lsp/completions.d.ts.map +1 -1
  216. package/dist/lsp/completions.js +18 -0
  217. package/dist/lsp/hover.d.ts.map +1 -1
  218. package/dist/lsp/hover.js +57 -0
  219. package/dist/manifest-entity.js +71 -0
  220. package/dist/manifest.json +1 -1
  221. package/dist/micro-time.js +49 -0
  222. package/dist/op/activities/apply-diff.d.ts +31 -0
  223. package/dist/op/activities/apply-diff.d.ts.map +1 -0
  224. package/dist/op/activities/apply-diff.js +79 -0
  225. package/dist/op/activities/argo.js +98 -0
  226. package/dist/op/activities/ensure-secret.js +38 -0
  227. package/dist/op/activities/index.js +51 -0
  228. package/dist/op/activities/kubectl.d.ts +64 -0
  229. package/dist/op/activities/kubectl.d.ts.map +1 -1
  230. package/dist/op/activities/kubectl.js +517 -0
  231. package/dist/op/activities/wait-for-ready.js +229 -0
  232. package/dist/op/builders.js +68 -0
  233. package/dist/package-cli.js +21 -0
  234. package/dist/patch/json-patch.js +187 -0
  235. package/dist/patch/rendered-patch.js +94 -0
  236. package/dist/plugin.js +741 -0
  237. package/dist/project-kinds.js +56 -0
  238. package/dist/receipt-store.js +216 -0
  239. package/dist/rules/crd-schema-helpers.ts +2 -1
  240. package/dist/secret-labels.js +33 -0
  241. package/dist/secret-store.js +116 -0
  242. package/dist/serializer.d.ts.map +1 -1
  243. package/dist/serializer.js +578 -0
  244. package/dist/sops/encrypted-secret-file.js +194 -0
  245. package/dist/sops/entity.js +32 -0
  246. package/dist/spec/fetch.js +58 -0
  247. package/dist/spec/parse.js +622 -0
  248. package/dist/subscribe-changes.js +168 -0
  249. package/dist/teardown.js +197 -0
  250. package/dist/telemetry-release.js +86 -0
  251. package/dist/validate-cli.js +17 -0
  252. package/dist/validate.js +47 -0
  253. package/dist/variables.js +34 -0
  254. package/dist/whoami.js +109 -0
  255. package/package.json +12 -12
  256. package/src/api/operation-surface.ts +2 -1
  257. package/src/codegen/package.ts +2 -2
  258. package/src/components/index.ts +3 -0
  259. package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
  260. package/src/components/kubectl-apply.test.ts +262 -0
  261. package/src/components/kubectl-apply.ts +341 -12
  262. package/src/deep-observe-hooks.ts +2 -2
  263. package/src/import/roundtrip-build.test.ts +46 -0
  264. package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
  265. package/src/lsp/completions.ts +2 -1
  266. package/src/lsp/hover.ts +2 -1
  267. package/src/op/activities/apply-diff.ts +87 -0
  268. package/src/op/activities/kubectl-plan.test.ts +224 -0
  269. package/src/op/activities/kubectl.ts +204 -16
  270. package/src/serializer.ts +2 -1
@@ -0,0 +1,44 @@
1
+ import { findResourceLiterals, getNestedObject, getNestedString, getProp, lineCol } from "./argo-ast.js";
2
+ /**
3
+ * FLUX001: GitRepository must pin spec.ref
4
+ *
5
+ * A Flux `GitRepository` with no `spec.ref` falls back to the `master` branch,
6
+ * which on most repos no longer exists — the source stalls with a checkout
7
+ * error, and every Kustomization downstream of it stalls too. Real estates pin
8
+ * a branch (or tag) on every source; `FluxGitSource` always emits one. Flag
9
+ * hand-written `GitRepository` literals whose spec carries a `url` but no
10
+ * `ref`.
11
+ *
12
+ * Bad: new GitRepository({ spec: { url: "https://github.com/acme/infra" } })
13
+ * Good: new GitRepository({ spec: { url: "https://github.com/acme/infra", ref: { branch: "main" } } })
14
+ */
15
+ export const fluxSourceRefPinRule = {
16
+ id: "FLUX001",
17
+ severity: "warning",
18
+ category: "correctness",
19
+ description: "Flux GitRepository must pin spec.ref (branch, tag, semver, or commit) — the unset default is the master branch",
20
+ check(context) {
21
+ const { sourceFile } = context;
22
+ const diagnostics = [];
23
+ for (const { literal } of findResourceLiterals(sourceFile, new Set(["GitRepository"]))) {
24
+ const spec = getNestedObject(literal, ["spec"]);
25
+ // No spec object literal to inspect — leave it to other tooling.
26
+ if (!spec)
27
+ continue;
28
+ if (getProp(spec, "ref") !== undefined)
29
+ continue;
30
+ const anchor = spec;
31
+ const { line, column } = lineCol(sourceFile, anchor);
32
+ const name = getNestedString(literal, ["metadata", "name"]) ?? "(unnamed)";
33
+ diagnostics.push({
34
+ file: sourceFile.fileName,
35
+ line,
36
+ column,
37
+ ruleId: "FLUX001",
38
+ severity: "warning",
39
+ message: `GitRepository "${name}" has no spec.ref — Flux falls back to the master branch. Pin a branch, tag, semver range, or commit.`,
40
+ });
41
+ }
42
+ return diagnostics;
43
+ },
44
+ };
@@ -0,0 +1,57 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /**
3
+ * WK8001: Hardcoded Namespace
4
+ *
5
+ * Detects hardcoded namespace strings in a Kubernetes resource's `metadata`.
6
+ * Namespaces should be parameterized or derived from configuration, not
7
+ * hardcoded as string literals.
8
+ *
9
+ * Bad: new Deployment({ metadata: { namespace: "production" } })
10
+ * Good: new Deployment({ metadata: { namespace: config.namespace } })
11
+ */
12
+ /**
13
+ * Only `metadata: { namespace }` is a Kubernetes namespace. Other `namespace`
14
+ * keys are another tool's setting (an OpenTelemetry connector's metric prefix,
15
+ * for one) and a k8s rule has no business flagging them.
16
+ */
17
+ function isMetadataEntry(node) {
18
+ const owner = node.parent.parent;
19
+ return (ts.isPropertyAssignment(owner) &&
20
+ ts.isIdentifier(owner.name) &&
21
+ owner.name.text === "metadata");
22
+ }
23
+ export const hardcodedNamespaceRule = {
24
+ id: "WK8001",
25
+ severity: "warning",
26
+ category: "correctness",
27
+ description: "Detects hardcoded namespace strings — namespaces should be parameterized",
28
+ check(context) {
29
+ const { sourceFile } = context;
30
+ const diagnostics = [];
31
+ function visit(node) {
32
+ // Look for property assignments like `namespace: "production"`
33
+ if (ts.isPropertyAssignment(node) &&
34
+ ts.isIdentifier(node.name) &&
35
+ node.name.text === "namespace" &&
36
+ ts.isStringLiteral(node.initializer) &&
37
+ isMetadataEntry(node)) {
38
+ const value = node.initializer.text;
39
+ // Skip empty strings — those are likely intentional placeholders
40
+ if (value !== "") {
41
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.initializer.getStart());
42
+ diagnostics.push({
43
+ file: sourceFile.fileName,
44
+ line: line + 1,
45
+ column: character + 1,
46
+ ruleId: "WK8001",
47
+ severity: "warning",
48
+ message: `Hardcoded namespace "${value}" detected. Use a variable or configuration parameter instead.`,
49
+ });
50
+ }
51
+ }
52
+ ts.forEachChild(node, visit);
53
+ }
54
+ visit(sourceFile);
55
+ return diagnostics;
56
+ },
57
+ };
@@ -0,0 +1,103 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /**
3
+ * WK8002: Latest Image Tag
4
+ *
5
+ * Detects when a K8s workload resource uses the `:latest` image tag or no tag
6
+ * at all in a container image string literal. Untagged or `:latest` images are
7
+ * non-deterministic and can cause unexpected rollouts.
8
+ *
9
+ * Bad: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx:latest" }] } } } })
10
+ * Bad: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx" }] } } } })
11
+ * Good: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx:1.25" }] } } } })
12
+ */
13
+ const WORKLOAD_KINDS = new Set([
14
+ "Deployment",
15
+ "StatefulSet",
16
+ "DaemonSet",
17
+ "CronJob",
18
+ "Job",
19
+ "ReplicaSet",
20
+ "Pod",
21
+ ]);
22
+ /**
23
+ * Returns true if the string looks like a container image reference that is
24
+ * either untagged or using `:latest`.
25
+ *
26
+ * A string is considered a container image if it:
27
+ * - Contains at least one alphabetic character
28
+ * - Does not contain spaces
29
+ * - Is not a simple keyword like "true", "false", etc.
30
+ */
31
+ function isProblematicImage(value) {
32
+ if (!value || value.includes(" ") || value.length === 0)
33
+ return false;
34
+ // Skip values that are clearly not images
35
+ const nonImagePatterns = [
36
+ /^(true|false|null|undefined|yes|no)$/i,
37
+ /^\d+$/, // pure numbers
38
+ /^[.\/]/, // relative/absolute paths without image-like structure
39
+ ];
40
+ for (const pat of nonImagePatterns) {
41
+ if (pat.test(value))
42
+ return false;
43
+ }
44
+ // Check for :latest explicitly
45
+ if (value.endsWith(":latest"))
46
+ return true;
47
+ // Check for untagged image: no colon at all, but looks like an image name
48
+ // Images contain alphanumeric chars and may have / for registry prefix
49
+ // Must have at least one alpha char and match image naming conventions
50
+ if (!value.includes(":") && !value.includes("@") && /^[a-zA-Z0-9._\-\/]+$/.test(value) && /[a-zA-Z]/.test(value)) {
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ export const latestImageTagRule = {
56
+ id: "WK8002",
57
+ severity: "warning",
58
+ category: "security",
59
+ description: "Detects :latest or untagged container images — use explicit version tags for reproducibility",
60
+ check(context) {
61
+ const { sourceFile } = context;
62
+ const diagnostics = [];
63
+ function isInsideWorkloadConstructor(node) {
64
+ let current = node.parent;
65
+ while (current) {
66
+ if (ts.isNewExpression(current) &&
67
+ ts.isIdentifier(current.expression) &&
68
+ WORKLOAD_KINDS.has(current.expression.text)) {
69
+ return true;
70
+ }
71
+ current = current.parent;
72
+ }
73
+ return false;
74
+ }
75
+ function visit(node) {
76
+ // Look for property assignments like `image: "nginx:latest"` or `image: "nginx"`
77
+ if (ts.isPropertyAssignment(node) &&
78
+ ts.isIdentifier(node.name) &&
79
+ node.name.text === "image" &&
80
+ ts.isStringLiteral(node.initializer) &&
81
+ isInsideWorkloadConstructor(node)) {
82
+ const value = node.initializer.text;
83
+ if (isProblematicImage(value)) {
84
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.initializer.getStart());
85
+ const isLatest = value.endsWith(":latest");
86
+ diagnostics.push({
87
+ file: sourceFile.fileName,
88
+ line: line + 1,
89
+ column: character + 1,
90
+ ruleId: "WK8002",
91
+ severity: "warning",
92
+ message: isLatest
93
+ ? `Container image "${value}" uses the :latest tag. Pin to a specific version for reproducibility.`
94
+ : `Container image "${value}" has no tag. Pin to a specific version for reproducibility.`,
95
+ });
96
+ }
97
+ }
98
+ ts.forEachChild(node, visit);
99
+ }
100
+ visit(sourceFile);
101
+ return diagnostics;
102
+ },
103
+ };
@@ -0,0 +1,85 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /**
3
+ * WK8003: Missing Resource Limits
4
+ *
5
+ * Detects when a container in a Deployment/StatefulSet spec doesn't have
6
+ * resource limits or requests. Without resource limits, a container can
7
+ * consume unbounded cluster resources and cause noisy-neighbour issues.
8
+ *
9
+ * Bad: new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "app:1.0" }] } } } })
10
+ * Good: new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "app:1.0", resources: { limits: { cpu: "500m", memory: "256Mi" } } }] } } } })
11
+ */
12
+ const WORKLOAD_KINDS = new Set([
13
+ "Deployment",
14
+ "StatefulSet",
15
+ "DaemonSet",
16
+ "CronJob",
17
+ "Job",
18
+ "ReplicaSet",
19
+ ]);
20
+ export const missingResourceLimitsRule = {
21
+ id: "WK8003",
22
+ severity: "warning",
23
+ category: "correctness",
24
+ description: "Detects containers without resource limits/requests — always set resource constraints",
25
+ check(context) {
26
+ const { sourceFile } = context;
27
+ const diagnostics = [];
28
+ function isInsideWorkloadConstructor(node) {
29
+ let current = node.parent;
30
+ while (current) {
31
+ if (ts.isNewExpression(current) &&
32
+ ts.isIdentifier(current.expression) &&
33
+ WORKLOAD_KINDS.has(current.expression.text)) {
34
+ return true;
35
+ }
36
+ current = current.parent;
37
+ }
38
+ return false;
39
+ }
40
+ function objectLiteralHasProperty(obj, name) {
41
+ return obj.properties.some((p) => ts.isPropertyAssignment(p) &&
42
+ ts.isIdentifier(p.name) &&
43
+ p.name.text === name);
44
+ }
45
+ function visit(node) {
46
+ // Look for object literals inside arrays that represent container specs.
47
+ // A container object literal typically has "name" and "image" properties.
48
+ // We flag it if it lacks a "resources" property.
49
+ if (ts.isObjectLiteralExpression(node) &&
50
+ isInsideWorkloadConstructor(node)) {
51
+ const hasName = objectLiteralHasProperty(node, "name");
52
+ const hasImage = objectLiteralHasProperty(node, "image");
53
+ const hasResources = objectLiteralHasProperty(node, "resources");
54
+ if (hasName && hasImage && !hasResources) {
55
+ // Confirm we're inside an array literal (containers array)
56
+ if (node.parent && ts.isArrayLiteralExpression(node.parent)) {
57
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
58
+ // Try to extract the container name for a better message
59
+ let containerName = "unknown";
60
+ for (const prop of node.properties) {
61
+ if (ts.isPropertyAssignment(prop) &&
62
+ ts.isIdentifier(prop.name) &&
63
+ prop.name.text === "name" &&
64
+ ts.isStringLiteral(prop.initializer)) {
65
+ containerName = prop.initializer.text;
66
+ break;
67
+ }
68
+ }
69
+ diagnostics.push({
70
+ file: sourceFile.fileName,
71
+ line: line + 1,
72
+ column: character + 1,
73
+ ruleId: "WK8003",
74
+ severity: "warning",
75
+ message: `Container "${containerName}" is missing resource limits/requests. Set resources.limits and resources.requests to prevent unbounded resource consumption.`,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ ts.forEachChild(node, visit);
81
+ }
82
+ visit(sourceFile);
83
+ return diagnostics;
84
+ },
85
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAapF;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAEvE"}
1
+ {"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAapF;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAEvE"}
@@ -0,0 +1,18 @@
1
+ import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
3
+ import { LexiconIndex, lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
4
+ const require = createRequire(moduleSourceUrl(import.meta.url));
5
+ let cachedIndex = null;
6
+ function getIndex() {
7
+ if (cachedIndex)
8
+ return cachedIndex;
9
+ const data = require("../generated/lexicon-k8s.json");
10
+ cachedIndex = new LexiconIndex(data);
11
+ return cachedIndex;
12
+ }
13
+ /**
14
+ * Provide Kubernetes resource completions based on context.
15
+ */
16
+ export function k8sCompletions(ctx) {
17
+ return lexiconCompletions(ctx, getIndex(), "Kubernetes resource");
18
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAa1E;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAEjE"}
1
+ {"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAa1E;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAEjE"}
@@ -0,0 +1,57 @@
1
+ import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
3
+ import { LexiconIndex, lexiconHover } from "@intentius/chant/lsp/lexicon-providers";
4
+ const require = createRequire(moduleSourceUrl(import.meta.url));
5
+ let cachedIndex = null;
6
+ function getIndex() {
7
+ if (cachedIndex)
8
+ return cachedIndex;
9
+ const data = require("../generated/lexicon-k8s.json");
10
+ cachedIndex = new LexiconIndex(data);
11
+ return cachedIndex;
12
+ }
13
+ /**
14
+ * Provide hover information for Kubernetes resource types.
15
+ */
16
+ export function k8sHover(ctx) {
17
+ return lexiconHover(ctx, getIndex(), resourceHover);
18
+ }
19
+ function resourceHover(className, entry) {
20
+ const lines = [];
21
+ lines.push(`**${className}**`);
22
+ lines.push("");
23
+ lines.push(`K8s resource type: \`${entry.resourceType}\``);
24
+ // Show apiVersion if stored in attrs
25
+ const apiVersion = entry.attrs?.apiVersion;
26
+ if (apiVersion) {
27
+ lines.push(`apiVersion: \`${apiVersion}\``);
28
+ }
29
+ // Show GVK kind if stored in attrs
30
+ const gvkKind = entry.attrs?.gvkKind;
31
+ if (gvkKind) {
32
+ lines.push(`kind: \`${gvkKind}\``);
33
+ }
34
+ if (entry.kind === "resource") {
35
+ lines.push("");
36
+ lines.push("*Resource — serialized as a top-level Kubernetes object*");
37
+ }
38
+ else {
39
+ lines.push("");
40
+ lines.push("*Property — used as a nested value in resource specs*");
41
+ }
42
+ if (entry.attrs && Object.keys(entry.attrs).length > 0) {
43
+ const displayAttrs = Object.entries(entry.attrs).filter(([k]) => k !== "apiVersion" && k !== "gvkKind");
44
+ if (displayAttrs.length > 0) {
45
+ lines.push("");
46
+ lines.push("**Attributes:**");
47
+ for (const [key, value] of displayAttrs) {
48
+ lines.push(`- \`${key}\` \u2192 \`${value}\``);
49
+ }
50
+ }
51
+ }
52
+ if (entry.primaryIdentifier && entry.primaryIdentifier.length > 0) {
53
+ lines.push("");
54
+ lines.push(`**Primary identifier:** ${entry.primaryIdentifier.map((p) => `\`${p}\``).join(", ")}`);
55
+ }
56
+ return { contents: lines.join("\n") };
57
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * A complete Kubernetes manifest carried into a build as a chant entity.
3
+ *
4
+ * Two callers produce one: a kustomize build root, whose output is a set of
5
+ * finished documents rather than typed chant source (#1548), and `chant carve
6
+ * emit` adopting a Terraform `kubernetes_manifest` (#999), whose whole content
7
+ * is a manifest body read out of state. In both cases the document already
8
+ * decided every field, so `props` IS the manifest: the serializer emits it
9
+ * as-is — plus the default-label / ownership merge every discovered resource
10
+ * gets — instead of running the spec-inference heuristics built for typed
11
+ * declarables, which would re-nest a top-level field that is not `spec`
12
+ * (`rules` on a ClusterRole, `webhooks` on a webhook config).
13
+ *
14
+ * The marker is what tells the serializer to take that path — `Symbol.for`, so
15
+ * a lexicon built against a separate copy of core still matches (the #1137
16
+ * lesson).
17
+ *
18
+ * `entityType` comes from `gvkToTypeName`, the one group→namespace rule
19
+ * (`./group-namespace`), so a manifest declared this way is typed exactly as a
20
+ * generated class would be and `lifecycle diff --live` observes it through the
21
+ * reader a declared resource uses. A kind the generated operation surface does
22
+ * not know reports NOT-OBSERVED with the existing "no generated operation
23
+ * surface" reason — the honest verdict, not a silent hole.
24
+ */
25
+ import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
26
+ import { gvkToTypeName } from "./spec/parse.js";
27
+ /** Marks an entity whose `props` are a complete, render-final manifest. */
28
+ export const RENDERED_MANIFEST_MARKER = Symbol.for("chant.k8s.renderedManifest");
29
+ export function isRenderedManifestEntity(entity) {
30
+ return entity[RENDERED_MANIFEST_MARKER] === true;
31
+ }
32
+ /**
33
+ * Wrap one manifest. Returns null when the document has no string
34
+ * `apiVersion`/`kind` — not a Kubernetes object, nothing to declare.
35
+ */
36
+ export function manifestEntity(doc) {
37
+ const apiVersion = doc.apiVersion;
38
+ const kind = doc.kind;
39
+ if (typeof apiVersion !== "string" || typeof kind !== "string" || !apiVersion || !kind)
40
+ return null;
41
+ const slash = apiVersion.indexOf("/");
42
+ const group = slash === -1 ? "" : apiVersion.slice(0, slash);
43
+ const version = slash === -1 ? apiVersion : apiVersion.slice(slash + 1);
44
+ return {
45
+ lexicon: "k8s",
46
+ entityType: gvkToTypeName({ group, version, kind }),
47
+ kind: "resource",
48
+ props: { ...doc },
49
+ [DECLARABLE_MARKER]: true,
50
+ [RENDERED_MANIFEST_MARKER]: true,
51
+ };
52
+ }
53
+ /**
54
+ * Declare an arbitrary Kubernetes object from its manifest — the escape hatch
55
+ * for a kind the lexicon ships no generated class for, and what `chant carve
56
+ * emit` writes when it adopts a `kubernetes_manifest` (#999). The typed
57
+ * constructors cannot serve either case: their props are the kind's own schema,
58
+ * so an unknown CRD has no class at all and a known one rejects an
59
+ * `apiVersion`/`kind` pair that differs from the generated default.
60
+ *
61
+ * Throws on a document without a string `apiVersion` and `kind`: those two
62
+ * fields are what makes it addressable, and a silently dropped resource is the
63
+ * failure this is guarding against.
64
+ */
65
+ export function k8sManifest(doc) {
66
+ const entity = manifestEntity(doc);
67
+ if (!entity) {
68
+ throw new Error("k8sManifest requires a document with string `apiVersion` and `kind` fields.");
69
+ }
70
+ return entity;
71
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.124.30",
3
+ "version": "0.124.32",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Helpers for K8s `MicroTime` fields.
3
+ *
4
+ * The K8s API server validates fields like `coordination.k8s.io/v1.Lease.spec.renewTime`
5
+ * (kind: MicroTime) with the exact format `2006-01-02T15:04:05.000000Z07:00`
6
+ * — microsecond precision, always 6 fractional digits, UTC offset.
7
+ *
8
+ * The naive JS `Date.toISOString()` (millisecond precision, 3 fractional
9
+ * digits) is accepted by the API server too — the schema is more permissive
10
+ * than the documented format string — but `time.RFC3339Nano`-style strings
11
+ * with nanosecond precision (9 fractional digits) are rejected as
12
+ * `422 Unprocessable Entity` with:
13
+ *
14
+ * parsing time "...754Z" as "2006-01-02T15:04:05.000000Z07:00":
15
+ * cannot parse "754Z" as "Z07:00"
16
+ *
17
+ * Pass a JS Date through `microTime()` to get a string the API server
18
+ * always accepts. This is the canonical way to populate MicroTime fields
19
+ * when authoring chant resources by hand.
20
+ */
21
+ /**
22
+ * Format a Date as a K8s MicroTime string (UTC, exactly 6 fractional digits).
23
+ *
24
+ * @example
25
+ * import { Lease } from "@intentius/chant-lexicon-k8s";
26
+ * import { microTime } from "@intentius/chant-lexicon-k8s/micro-time";
27
+ *
28
+ * new Lease({
29
+ * metadata: { name: "my-lease", namespace: "default" },
30
+ * spec: {
31
+ * holderIdentity: "node-1",
32
+ * leaseDurationSeconds: 15,
33
+ * renewTime: microTime(new Date()),
34
+ * },
35
+ * });
36
+ */
37
+ export function microTime(date = new Date()) {
38
+ const iso = date.toISOString(); // "2026-05-11T21:25:36.495Z" (3 digits)
39
+ // Replace the millisecond fragment with a 6-digit microsecond fragment.
40
+ // toISOString() always produces ms precision; pad to 6 by appending 3 zeros.
41
+ return iso.replace(/\.(\d{3})Z$/, ".$1000Z");
42
+ }
43
+ /**
44
+ * Returns true if `value` is a string in the K8s MicroTime canonical format
45
+ * (UTC, microsecond precision). Useful in lint/validation contexts.
46
+ */
47
+ export function isMicroTimeFormatted(value) {
48
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/.test(value);
49
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * What a server-side apply would change in one object (#3998): the live
3
+ * object against the result of a dry-run apply of the declared one.
4
+ *
5
+ * Both sides come from the API server, so a field the server defaults or
6
+ * fills in appears on both with the same value and drops out of the diff.
7
+ * What is left out before comparing is the bookkeeping the server writes on
8
+ * every request and that says nothing about the declared state: managed
9
+ * fields, the resource version, the uid, the creation timestamp, the
10
+ * generation (a dry run of a spec change bumps it) and `status`.
11
+ *
12
+ * Pure functions only, with no client import, so the capability module can
13
+ * share them without reaching the API-client chain (#1074).
14
+ */
15
+ /** One field whose value the apply would change. `before` is absent on an added field, `after` on a removed one. */
16
+ export interface FieldChange {
17
+ path: string;
18
+ before?: unknown;
19
+ after?: unknown;
20
+ }
21
+ /** A copy of `object` without the fields the server writes on every request. */
22
+ export declare function normalizeServerObject(object: Record<string, unknown>): Record<string, unknown>;
23
+ /** Deep equality over JSON values. */
24
+ export declare function jsonEqual(a: unknown, b: unknown): boolean;
25
+ /**
26
+ * The fields that differ between `live` and `next`, both normalized. Objects
27
+ * are walked to their leaves; a list is compared whole and reported at its
28
+ * own path, since a list's items have no stable address in general.
29
+ */
30
+ export declare function diffFields(live: Record<string, unknown>, next: Record<string, unknown>, prefix?: string): FieldChange[];
31
+ //# sourceMappingURL=apply-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-diff.d.ts","sourceRoot":"","sources":["../../../src/op/activities/apply-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,oHAAoH;AACpH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAKD,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAS9F;AAKD,sCAAsC;AACtC,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CASzD;AAQD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,SAAK,GAAG,WAAW,EAAE,CAuBnH"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * What a server-side apply would change in one object (#3998): the live
3
+ * object against the result of a dry-run apply of the declared one.
4
+ *
5
+ * Both sides come from the API server, so a field the server defaults or
6
+ * fills in appears on both with the same value and drops out of the diff.
7
+ * What is left out before comparing is the bookkeeping the server writes on
8
+ * every request and that says nothing about the declared state: managed
9
+ * fields, the resource version, the uid, the creation timestamp, the
10
+ * generation (a dry run of a spec change bumps it) and `status`.
11
+ *
12
+ * Pure functions only, with no client import, so the capability module can
13
+ * share them without reaching the API-client chain (#1074).
14
+ */
15
+ /** The `metadata` keys the server writes on every request. */
16
+ const SERVER_METADATA = ["managedFields", "resourceVersion", "uid", "creationTimestamp", "generation", "selfLink"];
17
+ /** A copy of `object` without the fields the server writes on every request. */
18
+ export function normalizeServerObject(object) {
19
+ const { status: _status, ...rest } = object;
20
+ const metadata = rest.metadata;
21
+ if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
22
+ const copy = { ...metadata };
23
+ for (const key of SERVER_METADATA)
24
+ delete copy[key];
25
+ rest.metadata = copy;
26
+ }
27
+ return rest;
28
+ }
29
+ const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
30
+ /** Deep equality over JSON values. */
31
+ export function jsonEqual(a, b) {
32
+ if (a === b)
33
+ return true;
34
+ if (Array.isArray(a) && Array.isArray(b))
35
+ return a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
36
+ if (isPlainObject(a) && isPlainObject(b)) {
37
+ const ka = Object.keys(a);
38
+ if (ka.length !== Object.keys(b).length)
39
+ return false;
40
+ return ka.every((k) => Object.prototype.hasOwnProperty.call(b, k) && jsonEqual(a[k], b[k]));
41
+ }
42
+ return false;
43
+ }
44
+ /** `spec.replicas`, or `metadata.labels["app.kubernetes.io/name"]` for a key that is not a plain identifier. */
45
+ function joinPath(prefix, key) {
46
+ if (/^[A-Za-z_$][\w$-]*$/.test(key))
47
+ return prefix ? `${prefix}.${key}` : key;
48
+ return `${prefix}[${JSON.stringify(key)}]`;
49
+ }
50
+ /**
51
+ * The fields that differ between `live` and `next`, both normalized. Objects
52
+ * are walked to their leaves; a list is compared whole and reported at its
53
+ * own path, since a list's items have no stable address in general.
54
+ */
55
+ export function diffFields(live, next, prefix = "") {
56
+ const changes = [];
57
+ const keys = [...new Set([...Object.keys(live), ...Object.keys(next)])].sort();
58
+ for (const key of keys) {
59
+ const path = joinPath(prefix, key);
60
+ const hasBefore = Object.prototype.hasOwnProperty.call(live, key);
61
+ const hasAfter = Object.prototype.hasOwnProperty.call(next, key);
62
+ const before = live[key];
63
+ const after = next[key];
64
+ // An object on both sides, or added or removed whole, is walked to its
65
+ // leaves. An empty object added or removed has none, so it stands as itself.
66
+ const walkable = (has, v) => !has || isPlainObject(v);
67
+ if ((isPlainObject(before) || isPlainObject(after)) && walkable(hasBefore, before) && walkable(hasAfter, after)) {
68
+ const inner = diffFields(hasBefore ? before : {}, hasAfter ? after : {}, path);
69
+ if (inner.length > 0 || hasBefore === hasAfter) {
70
+ changes.push(...inner);
71
+ continue;
72
+ }
73
+ }
74
+ if (hasBefore && hasAfter && jsonEqual(before, after))
75
+ continue;
76
+ changes.push({ path, ...(hasBefore ? { before } : {}), ...(hasAfter ? { after } : {}) });
77
+ }
78
+ return changes;
79
+ }