@intentius/chant-lexicon-k8s 0.124.31 → 0.124.33

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,578 @@
1
+ /**
2
+ * Kubernetes YAML serializer.
3
+ *
4
+ * Converts Chant declarables to multi-document K8s YAML output with
5
+ * apiVersion, kind, metadata, and spec structure.
6
+ */
7
+ import { createRequire } from "module";
8
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
9
+ import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
10
+ import { mergeResourceAttributes, RELEASE_ATTRIBUTES_VARIABLE, telemetryEnvironment } from "@intentius/chant/telemetry-attribution";
11
+ import { podSpecOf, releaseAttributesEnv } from "./telemetry-release.js";
12
+ import { ownershipEntries, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
13
+ import { isEffectReceipt, receiptExpectation, referenceInputPaths, } from "@intentius/chant/effect-receipt";
14
+ import { RECEIPT_CONFIGMAP_REF, RECEIPT_DATA_KEY, RECEIPT_UNRESOLVED_VALUE_NOTE, receiptConfigMapRef, receiptNamespaceFrom, renderReceiptComment, } from "./effect-receipt-row.js";
15
+ import { walkValue } from "@intentius/chant/serializer-walker";
16
+ import { emitYAML } from "@intentius/chant/yaml";
17
+ import { isDefaultLabels, isDefaultAnnotations } from "./default-labels.js";
18
+ import { isRenderedManifestEntity } from "./manifest-entity.js";
19
+ import { projectKindForType } from "./project-kinds.js";
20
+ import { isEncryptedSecretFileEntity } from "./sops/entity.js";
21
+ import { isK8sInclude } from "./include/entity.js";
22
+ import { isRuleGroup, ruleGroupConfig } from "@intentius/chant-lexicon-prometheus";
23
+ const require = createRequire(moduleSourceUrl(import.meta.url));
24
+ /**
25
+ * K8s resource kinds whose properties live directly on the manifest
26
+ * (not nested under `spec`). These types use `data`, `stringData`,
27
+ * or have no spec field at all.
28
+ */
29
+ const SPECLESS_TYPES = new Set([
30
+ "ConfigMap",
31
+ "Secret",
32
+ "Namespace",
33
+ "ServiceAccount",
34
+ "ClusterRole",
35
+ "ClusterRoleBinding",
36
+ "Role",
37
+ "RoleBinding",
38
+ "StorageClass",
39
+ "PersistentVolume",
40
+ "APIService",
41
+ ]);
42
+ let cachedGVKMap = null;
43
+ function getGVKMap() {
44
+ if (cachedGVKMap)
45
+ return cachedGVKMap;
46
+ try {
47
+ cachedGVKMap = require("./generated/lexicon-k8s.json");
48
+ }
49
+ catch {
50
+ cachedGVKMap = {};
51
+ }
52
+ return cachedGVKMap;
53
+ }
54
+ /**
55
+ * Resolve entityType to apiVersion and kind.
56
+ */
57
+ function resolveGVK(entityType) {
58
+ // A kind the project generated from its own CRDs (`chant generate`) wins
59
+ // over the packaged map, which knows nothing of it or an older version.
60
+ const projectKind = projectKindForType(entityType);
61
+ if (projectKind)
62
+ return { apiVersion: projectKind.apiVersion, kind: projectKind.kind };
63
+ const gvkMap = getGVKMap();
64
+ // Search for matching entry by resourceType
65
+ for (const entry of Object.values(gvkMap)) {
66
+ if (entry.resourceType === entityType && entry.apiVersion && entry.gvkKind) {
67
+ return { apiVersion: entry.apiVersion, kind: entry.gvkKind };
68
+ }
69
+ }
70
+ // Fallback: derive from entity type string (K8s::Group::Kind → group/v1, Kind)
71
+ return deriveGVKFromType(entityType);
72
+ }
73
+ /**
74
+ * Well-known K8s API group → apiVersion mappings for fallback when
75
+ * the generated lexicon JSON is not available.
76
+ */
77
+ const API_GROUP_VERSIONS = {
78
+ Core: "v1",
79
+ Apps: "apps/v1",
80
+ Batch: "batch/v1",
81
+ Networking: "networking.k8s.io/v1",
82
+ Policy: "policy/v1",
83
+ Rbac: "rbac.authorization.k8s.io/v1",
84
+ Storage: "storage.k8s.io/v1",
85
+ Autoscaling: "autoscaling/v2",
86
+ Admissionregistration: "admissionregistration.k8s.io/v1",
87
+ GKE: "cloud.google.com/v1",
88
+ NetworkingGKE: "networking.gke.io/v1",
89
+ NetworkingGKEBeta: "networking.gke.io/v1beta1",
90
+ // Common Kubernetes operator CRDs
91
+ CertManager: "cert-manager.io/v1",
92
+ ExternalSecrets: "external-secrets.io/v1",
93
+ Monitoring: "monitoring.coreos.com/v1",
94
+ // KubeRay operator CRDs
95
+ Ray: "ray.io/v1",
96
+ };
97
+ function deriveGVKFromType(entityType) {
98
+ // Format: K8s::Group::Kind
99
+ const parts = entityType.split("::");
100
+ if (parts.length !== 3 || parts[0] !== "K8s")
101
+ return null;
102
+ const group = parts[1];
103
+ const kind = parts[2];
104
+ const apiVersion = API_GROUP_VERSIONS[group];
105
+ if (!apiVersion)
106
+ return null;
107
+ return { apiVersion, kind };
108
+ }
109
+ /**
110
+ * The name a resource gets when it does not declare one: the logical name,
111
+ * kebab-cased. Shared with the manifest builder below so a reference and the
112
+ * thing it references cannot derive different names (#1493).
113
+ */
114
+ function derivedName(logicalName) {
115
+ return logicalName.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
116
+ }
117
+ /**
118
+ * What a cross-resource reference resolves to on this substrate (#1493).
119
+ *
120
+ * Kubernetes YAML has no deploy-time reference mechanism — no `!Ref`, no
121
+ * `Fn::GetAtt`. Whatever a reference is going to mean has to be resolved here,
122
+ * before serialization, or it means nothing at all.
123
+ *
124
+ * This used to return the *logical* name for every attribute, so
125
+ * `claimName: pgClaim.name` emitted `claimName: pgClaim` and the manifest
126
+ * named a resource that does not exist. It applied cleanly and failed on the
127
+ * cluster, which is the failure this whole system exists to remove. The
128
+ * lexicon's own plugin comment already described the intended behaviour —
129
+ * "resolves to metadata.name" — so this brings the code to the documentation
130
+ * rather than the other way round.
131
+ *
132
+ * `uid` is refused rather than resolved: a UID is assigned by the API server
133
+ * at admission, so no build-time value exists and any string emitted here
134
+ * would be a fabrication.
135
+ */
136
+ function resolveK8sAttr(entity, logicalName, attr) {
137
+ if (attr === "uid") {
138
+ throw new Error(`Cannot reference "${logicalName}.uid": a Kubernetes UID is assigned by the API server at ` +
139
+ `admission, so it has no build-time value. Reference .name, or carry the UID at runtime ` +
140
+ `with fieldRef: { fieldPath: "metadata.uid" }.`);
141
+ }
142
+ const metadata = entity && isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null
143
+ ? entity.props.metadata
144
+ : undefined;
145
+ if (attr === "name") {
146
+ // Same fallback the manifest builder applies, so a reference to a resource
147
+ // that declares no name still resolves to the name it will actually get.
148
+ const declared = metadata?.name;
149
+ return typeof declared === "string" && declared.length > 0 ? declared : derivedName(logicalName);
150
+ }
151
+ if (attr === "namespace") {
152
+ const ns = metadata?.namespace;
153
+ if (typeof ns === "string" && ns.length > 0)
154
+ return ns;
155
+ throw new Error(`Cannot reference "${logicalName}.namespace": it declares no metadata.namespace, and the ` +
156
+ `namespace a manifest lands in is decided at apply time (kubectl -n, or the context's ` +
157
+ `default). Set metadata.namespace on "${logicalName}" if the reference needs to be stable.`);
158
+ }
159
+ throw new Error(`Cannot reference "${logicalName}.${attr}": the k8s lexicon resolves .name and .namespace at ` +
160
+ `build time, and Kubernetes YAML has no way to express any other attribute reference.`);
161
+ }
162
+ /** The rendered expectation: the synthesis-time value when the receipt is
163
+ * fully static, the placeholder note when reference inputs remain (#1703
164
+ * decision 5, since synthesis resolves nothing). */
165
+ function receiptRowValue(receipt) {
166
+ if (receipt.flavor === "hash" && referenceInputPaths(receipt).length > 0) {
167
+ return RECEIPT_UNRESOLVED_VALUE_NOTE;
168
+ }
169
+ return receiptExpectation(receipt);
170
+ }
171
+ /**
172
+ * Render the effect receipts (#2074) the build withheld from the apply-bound
173
+ * entity set (`SerializeContext.receipts`, #1832) as ConfigMap rows: named
174
+ * `chant-receipt.<stack>.<env>.<effect>` from the ownership marker fields
175
+ * (epic decision 4), in the project's receipt namespace, with the expectation
176
+ * under `data.expectation`.
177
+ *
178
+ * Visibility only. The rows ride a YAML comment at the end of the manifest
179
+ * stream (./effect-receipt-row.ts) rather than a document, because a document
180
+ * is what an applier applies and the `effect()` step is a receipt's sole
181
+ * writer (#1832, epic #1703 decision 3).
182
+ *
183
+ * The env segment is explicit: a receipt with no resolved `ownership.env` is
184
+ * an error, never a guessed segment.
185
+ */
186
+ function renderReceiptRows(receipts, ownership, namespace) {
187
+ const rows = {};
188
+ const names = [...receipts.keys()].join(", ");
189
+ if (!ownership?.stack) {
190
+ throw new Error(`k8s receipts (${names}): no ownership marker resolved. The receipt ConfigMap is named ` +
191
+ `chant-receipt.<stack>.<env>.<effect>, derived from the same ownership fields that ` +
192
+ `stamp markers (chant #1703, decision 4). Set ownership: { stack } in chant.config.ts.`);
193
+ }
194
+ if (!ownership.env) {
195
+ throw new Error(`k8s receipts (${names}): ownership resolved no env. The receipt name's <env> segment is ` +
196
+ `explicit (chant #1703, decision 4). Set ownership: { env } in chant.config.ts, or ` +
197
+ `build with an env-valued parameter that resolves it.`);
198
+ }
199
+ for (const [name, entity] of receipts) {
200
+ if (!isEffectReceipt(entity))
201
+ continue;
202
+ const ref = receiptConfigMapRef(ownership.stack, ownership.env, entity.effect, namespace);
203
+ rows[name] = {
204
+ kind: RECEIPT_CONFIGMAP_REF.kind,
205
+ namespace: ref.namespace,
206
+ name: ref.name,
207
+ data: { [RECEIPT_DATA_KEY]: receiptRowValue(entity) },
208
+ };
209
+ }
210
+ return rows;
211
+ }
212
+ /**
213
+ * K8s visitor for the generic serializer walker.
214
+ */
215
+ function k8sVisitor(entityNames) {
216
+ // name → entity, so an AttrRef can reach the resource it points at. The
217
+ // walker hands the visitor a logical name, not the Declarable.
218
+ const byName = new Map();
219
+ for (const [entity, name] of entityNames)
220
+ byName.set(name, entity);
221
+ return {
222
+ attrRef: (name, attr) => resolveK8sAttr(byName.get(name), name, attr),
223
+ // A prometheus-lexicon RuleGroup (in a PrometheusRule's spec.groups) is
224
+ // data, not a reference: it renders as the same group the rule file
225
+ // holds, whether it was exported as its own entity or written inline.
226
+ resourceRef: (name) => {
227
+ const entity = byName.get(name);
228
+ return isRuleGroup(entity) ? ruleGroupConfig(entity) : name;
229
+ },
230
+ propertyDeclarable: (entity, walk) => {
231
+ if (isRuleGroup(entity))
232
+ return ruleGroupConfig(entity);
233
+ if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
234
+ return undefined;
235
+ }
236
+ const props = entity.props;
237
+ const result = {};
238
+ for (const [key, value] of Object.entries(props)) {
239
+ if (value !== undefined) {
240
+ result[key] = walk(value);
241
+ }
242
+ }
243
+ return Object.keys(result).length > 0 ? result : undefined;
244
+ },
245
+ };
246
+ }
247
+ /**
248
+ * Convert a value to YAML-compatible form using the walker.
249
+ */
250
+ function toYAMLValue(value, entityNames) {
251
+ return walkValue(value, entityNames, k8sVisitor(entityNames));
252
+ }
253
+ /**
254
+ * Kubernetes YAML serializer implementation.
255
+ */
256
+ // ── Telemetry attribution (#2558, D22, #3059) ──────────────────────
257
+ /** The digest of an image reference pinned with `@sha256:...`, or undefined. */
258
+ function imageDigest(image) {
259
+ if (typeof image !== "string")
260
+ return undefined;
261
+ const at = image.lastIndexOf("@");
262
+ return at === -1 ? undefined : image.slice(at + 1);
263
+ }
264
+ /**
265
+ * Stamp `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES` into the `env` of
266
+ * each container of a workload. `service.name` is the workload's
267
+ * `metadata.name`, `chant.decl` its declaration id (`decl`), and an image
268
+ * pinned by digest gives `service.version`. A variable the container already
269
+ * sets is kept: its own `OTEL_SERVICE_NAME` wins, and its own resource
270
+ * attributes keep their keys while the missing ones are appended. One set
271
+ * through `valueFrom` is left alone. Init containers are not stamped.
272
+ *
273
+ * `OTEL_RESOURCE_ATTRIBUTES` ends with `$(CHANT_RELEASE_ATTRIBUTES)`, read
274
+ * from the pod annotation a release deploy sets (#3061, ws-081), so a
275
+ * release adds `service.version` and `vcs.ref.head.revision` without
276
+ * changing this output.
277
+ */
278
+ function stampWorkloadTelemetry(manifest, decl, telemetry) {
279
+ const podSpec = podSpecOf(manifest);
280
+ const containers = podSpec?.containers;
281
+ if (!Array.isArray(containers))
282
+ return;
283
+ const service = String(manifest.metadata?.name ?? decl);
284
+ const releaseRef = `$(${RELEASE_ATTRIBUTES_VARIABLE})`;
285
+ for (const container of containers) {
286
+ if (!container || typeof container !== "object")
287
+ continue;
288
+ const c = container;
289
+ const version = imageDigest(c.image);
290
+ const vars = telemetryEnvironment(telemetry, { service, decl, ...(version ? { version } : {}) });
291
+ if (c.env !== undefined && !Array.isArray(c.env) && typeof c.env === "object" && c.env !== null) {
292
+ // A map, as some composites and hand-written props give it before the API's list form. A map
293
+ // has no order, so it cannot hold the $(VAR) reference to the release attributes.
294
+ const map = { ...c.env };
295
+ if (!("OTEL_SERVICE_NAME" in map))
296
+ map.OTEL_SERVICE_NAME = vars.OTEL_SERVICE_NAME;
297
+ const own = map.OTEL_RESOURCE_ATTRIBUTES;
298
+ if (own === undefined)
299
+ map.OTEL_RESOURCE_ATTRIBUTES = vars.OTEL_RESOURCE_ATTRIBUTES;
300
+ else if (typeof own === "string")
301
+ map.OTEL_RESOURCE_ATTRIBUTES = mergeResourceAttributes(own, vars.OTEL_RESOURCE_ATTRIBUTES);
302
+ c.env = map;
303
+ continue;
304
+ }
305
+ const env = Array.isArray(c.env) ? [...c.env] : [];
306
+ const at = (key) => env.findIndex((e) => e && typeof e === "object" && e.name === key);
307
+ if (at("OTEL_SERVICE_NAME") === -1)
308
+ env.push({ name: "OTEL_SERVICE_NAME", value: vars.OTEL_SERVICE_NAME });
309
+ let i = at("OTEL_RESOURCE_ATTRIBUTES");
310
+ if (i === -1) {
311
+ env.push({ name: "OTEL_RESOURCE_ATTRIBUTES", value: vars.OTEL_RESOURCE_ATTRIBUTES });
312
+ i = env.length - 1;
313
+ }
314
+ else if (typeof env[i].value === "string") {
315
+ env[i] = { ...env[i], value: mergeResourceAttributes(env[i].value, vars.OTEL_RESOURCE_ATTRIBUTES) };
316
+ }
317
+ // The release attributes (#3061, ws-081): a $(VAR) reference expands only to a variable defined
318
+ // earlier in the list, so the downward-API entry goes just before OTEL_RESOURCE_ATTRIBUTES. A
319
+ // container that defines the variable itself keeps its own, and gets the reference only when
320
+ // its definition comes first.
321
+ if (typeof env[i].value === "string") {
322
+ if (at(RELEASE_ATTRIBUTES_VARIABLE) === -1) {
323
+ env.splice(i, 0, releaseAttributesEnv());
324
+ i++;
325
+ }
326
+ const value = env[i].value;
327
+ if (at(RELEASE_ATTRIBUTES_VARIABLE) < i && !value.includes(releaseRef))
328
+ env[i] = { ...env[i], value: `${value}${releaseRef}` };
329
+ }
330
+ c.env = env;
331
+ }
332
+ }
333
+ export const k8sSerializer = {
334
+ name: "k8s",
335
+ rulePrefix: "WK8",
336
+ // The Argo CD and Flux checks are distinct product surfaces this lexicon
337
+ // covers, and their ids are published — renaming them would break every
338
+ // `chant-disable ARGO001` already written (#1349).
339
+ extraRulePrefixes: ["ARGO", "FLUX"],
340
+ serialize(entities, _outputs, context) {
341
+ // Build reverse map: entity → name
342
+ const entityNames = new Map();
343
+ for (const [name, entity] of entities) {
344
+ entityNames.set(entity, name);
345
+ }
346
+ // Collect default labels and annotations. Ownership markers are stamped as
347
+ // labels, so they seed defaultLabelEntries and flow through the same merge
348
+ // (explicit resource labels still win).
349
+ let defaultLabelEntries = context?.ownership
350
+ ? { ...ownershipEntries(LABEL_OWNERSHIP_KEYS, context.ownership) }
351
+ : {};
352
+ let defaultAnnotationEntries = {};
353
+ for (const [, entity] of entities) {
354
+ if (isDefaultLabels(entity)) {
355
+ defaultLabelEntries = { ...defaultLabelEntries, ...entity.labels };
356
+ }
357
+ if (isDefaultAnnotations(entity)) {
358
+ defaultAnnotationEntries = { ...defaultAnnotationEntries, ...entity.annotations };
359
+ }
360
+ }
361
+ const namespaceDocs = [];
362
+ const otherDocs = [];
363
+ /** Sidecar files — committed ciphertext, copied byte-for-byte. */
364
+ const files = {};
365
+ /** Basenames within `files` that must never be JSON.parse round-tripped (chant#1937). */
366
+ const verbatimFiles = [];
367
+ const warnings = [];
368
+ for (const [name, entity] of entities) {
369
+ if (isPropertyDeclarable(entity))
370
+ continue;
371
+ if (isDefaultLabels(entity) || isDefaultAnnotations(entity))
372
+ continue;
373
+ // A `k8sInclude` declaration is resolved into its documents at
374
+ // `buildRoots()`; the declaration itself emits nothing.
375
+ if (isK8sInclude(entity))
376
+ continue;
377
+ // Committed SOPS ciphertext (epic lex00/iac-cd-bench#6) leaves as a
378
+ // SIDECAR and never as a document in the primary output. That is the
379
+ // load-bearing line: chant's appliers read the primary output, so an
380
+ // undecrypted Secret cannot reach a cluster through them. The bytes are
381
+ // copied exactly as committed — no re-emit, no label merge, no
382
+ // ownership stamp — because re-serializing would break the MAC the
383
+ // `sops` block carries over the plaintext.
384
+ if (isEncryptedSecretFileEntity(entity)) {
385
+ const existing = files[entity.filename];
386
+ if (existing !== undefined && existing !== entity.text) {
387
+ warnings.push(`k8s: two committed-encrypted secrets emit the same sidecar filename ` +
388
+ `"${entity.filename}" — the second (${entity.sourcePath}) wins`);
389
+ }
390
+ files[entity.filename] = entity.text;
391
+ if (!verbatimFiles.includes(entity.filename))
392
+ verbatimFiles.push(entity.filename);
393
+ continue;
394
+ }
395
+ // A kustomize build root's document (#1548 piece 3) is render-final:
396
+ // the overlay decided every field, so the props ARE the manifest and
397
+ // the spec-inference heuristics below (built for typed declarables)
398
+ // must not reshape it — a doc whose top-level fields aren't `spec`
399
+ // (webhooks, rules on a CRD instance) would otherwise be re-nested.
400
+ // It still gets the exact default-label/annotation merge every
401
+ // discovered resource gets, which is what stamps ownership on it.
402
+ if (isRenderedManifestEntity(entity)) {
403
+ const manifest = { ...entity.props };
404
+ const metadata = { ...(manifest.metadata ?? {}) };
405
+ if (Object.keys(defaultLabelEntries).length > 0) {
406
+ metadata.labels = { ...defaultLabelEntries, ...(metadata.labels ?? {}) };
407
+ }
408
+ if (Object.keys(defaultAnnotationEntries).length > 0) {
409
+ metadata.annotations = {
410
+ ...defaultAnnotationEntries,
411
+ ...(metadata.annotations ?? {}),
412
+ };
413
+ }
414
+ manifest.metadata = metadata;
415
+ const yamlDoc = emitK8sManifest(manifest);
416
+ if (manifest.kind === "Namespace") {
417
+ namespaceDocs.push(yamlDoc);
418
+ }
419
+ else {
420
+ otherDocs.push(yamlDoc);
421
+ }
422
+ continue;
423
+ }
424
+ const entityType = entity.entityType;
425
+ const gvk = resolveGVK(entityType);
426
+ if (!gvk)
427
+ continue;
428
+ const props = toYAMLValue(isResourceDeclarable(entity) ? entity.props : undefined, entityNames);
429
+ if (!props)
430
+ continue;
431
+ // Build the K8s manifest structure
432
+ const manifest = {
433
+ apiVersion: gvk.apiVersion,
434
+ kind: gvk.kind,
435
+ };
436
+ // Build metadata
437
+ const metadata = props.metadata ?? {};
438
+ if (!metadata.name) {
439
+ metadata.name = derivedName(name);
440
+ }
441
+ // Merge default labels
442
+ if (Object.keys(defaultLabelEntries).length > 0) {
443
+ const existingLabels = (metadata.labels ?? {});
444
+ metadata.labels = { ...defaultLabelEntries, ...existingLabels };
445
+ }
446
+ // Merge default annotations
447
+ if (Object.keys(defaultAnnotationEntries).length > 0) {
448
+ const existingAnnotations = (metadata.annotations ?? {});
449
+ metadata.annotations = { ...defaultAnnotationEntries, ...existingAnnotations };
450
+ }
451
+ manifest.metadata = metadata;
452
+ // Properties that always belong at the manifest root, never inside
453
+ // spec. apiVersion/kind allow consumers to override the gvk-derived
454
+ // defaults (the "CRD wrapper" trick used to declare arbitrary K8s
455
+ // resources via a generic Declarable class). rules/subjects/roleRef
456
+ // are the top-level fields for RBAC kinds. data/stringData are for
457
+ // ConfigMap/Secret. binaryData covers ConfigMap binary entries.
458
+ const TOP_LEVEL_PROPS = new Set([
459
+ "apiVersion",
460
+ "kind",
461
+ "rules",
462
+ "subjects",
463
+ "roleRef",
464
+ "data",
465
+ "stringData",
466
+ "binaryData",
467
+ "type",
468
+ "immutable",
469
+ "automountServiceAccountToken",
470
+ "secrets",
471
+ "imagePullSecrets",
472
+ ]);
473
+ // The remaining properties go under spec (or directly on the manifest for certain types)
474
+ if (SPECLESS_TYPES.has(gvk.kind)) {
475
+ // These types have their data directly on the manifest (data, stringData, etc.)
476
+ for (const [key, value] of Object.entries(props)) {
477
+ if (key !== "metadata") {
478
+ manifest[key] = value;
479
+ }
480
+ }
481
+ }
482
+ else if (props.spec !== undefined) {
483
+ // If spec is already set, use it directly
484
+ manifest.spec = props.spec;
485
+ for (const [key, value] of Object.entries(props)) {
486
+ if (key !== "metadata" && key !== "spec") {
487
+ manifest[key] = value;
488
+ }
489
+ }
490
+ }
491
+ else {
492
+ // Place remaining props under spec — except known top-level fields
493
+ // (apiVersion/kind for CRD-wrapper overrides; rules/subjects for RBAC).
494
+ const spec = {};
495
+ for (const [key, value] of Object.entries(props)) {
496
+ if (key === "metadata")
497
+ continue;
498
+ if (TOP_LEVEL_PROPS.has(key)) {
499
+ manifest[key] = value;
500
+ }
501
+ else {
502
+ spec[key] = value;
503
+ }
504
+ }
505
+ if (Object.keys(spec).length > 0) {
506
+ manifest.spec = spec;
507
+ }
508
+ }
509
+ if (context?.telemetry)
510
+ stampWorkloadTelemetry(manifest, name, context.telemetry);
511
+ // Emit as YAML — sort Namespaces first so kubectl apply succeeds
512
+ const yamlDoc = emitK8sManifest(manifest);
513
+ if (gvk.kind === "Namespace") {
514
+ namespaceDocs.push(yamlDoc);
515
+ }
516
+ else {
517
+ otherDocs.push(yamlDoc);
518
+ }
519
+ }
520
+ let primary = [...namespaceDocs, ...otherDocs].join("\n---\n");
521
+ // Effect receipt rows (#2074): visibility only, deliberately NOT a
522
+ // document: appliers apply documents, and the `effect()` step is a
523
+ // receipt's sole writer (#1832, epic #1703 decision 3). The comment rides
524
+ // the same build output the observation leg is handed, which is how it
525
+ // learns each receipt's ConfigMap address.
526
+ if (context?.receipts && context.receipts.size > 0) {
527
+ const rows = renderReceiptRows(context.receipts, context.ownership, receiptNamespaceFrom(context.config));
528
+ if (Object.keys(rows).length > 0) {
529
+ primary = primary.length > 0 ? `${primary}\n${renderReceiptComment(rows)}\n` : `${renderReceiptComment(rows)}\n`;
530
+ }
531
+ }
532
+ // A bare string when there is nothing extra to write, so the common case
533
+ // stays byte-identical to what every existing consumer already reads.
534
+ if (Object.keys(files).length === 0 && warnings.length === 0)
535
+ return primary;
536
+ return {
537
+ primary,
538
+ files,
539
+ // Every verbatim file here is committed ciphertext, so each is also a
540
+ // secret: a build with no --output refuses rather than echo it (chant#3738).
541
+ ...(verbatimFiles.length > 0 ? { verbatimFiles, secretFiles: [...verbatimFiles] } : {}),
542
+ ...(warnings.length > 0 ? { warnings } : {}),
543
+ };
544
+ },
545
+ };
546
+ /**
547
+ * Emit a K8s manifest object as YAML.
548
+ * Preserves key ordering: apiVersion, kind, metadata, spec, then rest.
549
+ */
550
+ /**
551
+ * Emit a key-value pair as YAML. Scalars get ` value` suffix; objects get
552
+ * block-style indented below the key.
553
+ */
554
+ function emitKeyValue(key, value) {
555
+ const yamlStr = emitYAML(value, 1);
556
+ // If the YAML starts with a newline, it's a block value (object/array)
557
+ if (yamlStr.startsWith("\n")) {
558
+ return `${key}:${yamlStr}`;
559
+ }
560
+ return `${key}: ${yamlStr}`;
561
+ }
562
+ function emitK8sManifest(manifest) {
563
+ const orderedKeys = ["apiVersion", "kind", "metadata", "spec"];
564
+ const lines = [];
565
+ // Emit ordered keys first
566
+ for (const key of orderedKeys) {
567
+ if (manifest[key] !== undefined) {
568
+ lines.push(emitKeyValue(key, manifest[key]));
569
+ }
570
+ }
571
+ // Emit remaining keys
572
+ for (const [key, value] of Object.entries(manifest)) {
573
+ if (!orderedKeys.includes(key) && value !== undefined) {
574
+ lines.push(emitKeyValue(key, value));
575
+ }
576
+ }
577
+ return lines.join("\n") + "\n";
578
+ }