@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,306 @@
1
+ /**
2
+ * The k8s lexicon's *static* deep-observation noise rules (#1076, epic #1073).
3
+ *
4
+ * Split out from `./deep-observe.ts` for one reason: this file must be safe
5
+ * to import from `plugin.ts` at module load time, because
6
+ * `LexiconPlugin.deepNormalizationHooks` is plain data core reads to
7
+ * normalize the *declared* tree — the half of the contract that runs whether
8
+ * or not a cluster is ever touched (`lifecycle diff` without `--live`,
9
+ * `chant build`, tests that only exercise normalization). `./deep-observe.ts`
10
+ * itself imports `@intentius/chant-k8s-client` for the live read, and chant
11
+ * #1074 made that package's reachability from the build path a structural
12
+ * property (`examples/k8s-client-boundary.test.ts`) rather than a lint rule —
13
+ * so nothing this file exports may pull that package in, directly or
14
+ * transitively. It imports the core contract's own types, and one generated
15
+ * JSON data file (`./generated/list-map-keys.json`, chant #1441) — data, not a
16
+ * package, so the boundary property is unchanged.
17
+ *
18
+ * What lives here is deliberately the *entityType-keyed, resource-agnostic*
19
+ * half of the rules: which fields the API server always populates regardless
20
+ * of what a manager wrote (`status`, `metadata.uid`, …), which fields
21
+ * Kubernetes defaults when a manifest is silent about them, and which arrays
22
+ * are sets addressed by a well-known identity (containers by name, ports by
23
+ * containerPort+protocol). None of that needs a live object in hand.
24
+ *
25
+ * What does *not* live here is the managed-fields ownership walk — which
26
+ * manager owns one specific field on one specific live object. That is
27
+ * inherently per-object (it depends on *that* object's
28
+ * `metadata.managedFields`, which the declared tree never carries and which
29
+ * differs between two Deployments of the same type), so it cannot be
30
+ * expressed as a fixed rule keyed only by entity type and path — the shape
31
+ * every other hook in this file takes. `./deep-observe.ts` computes it once
32
+ * per resource and attaches it to the observation as `fieldOwners`. Since
33
+ * chant #1191 ownership no longer prunes anything: a foreign-owned field
34
+ * nobody declared is reported as `undeclared` drift, and the only static
35
+ * subtraction for foreign writes is `K8S_SYSTEM_METADATA_PRUNE_PATTERNS`
36
+ * (core's `managed-fields`), applied below.
37
+ *
38
+ * The *entity-type-agnostic* half of these rules — which fields every
39
+ * Kubernetes API object carries regardless of kind, and the well-known
40
+ * list-map-key ordering conventions (containers/env/volumes/ports) — lives in
41
+ * `@intentius/chant/managed-fields` (chant #1087), because a GCP Config
42
+ * Connector custom resource is a Kubernetes object too and needs the exact
43
+ * same rules without depending on this lexicon's package. What stays here is
44
+ * only what's genuinely k8s-*lexicon*-specific: {@link K8S_SERVICE_DEFAULTS},
45
+ * keyed by chant's own k8s entityType catalog.
46
+ */
47
+ import { existsSync, readFileSync } from "fs";
48
+ import { join } from "path";
49
+ import { moduleSourceDir } from "@intentius/chant/module-source";
50
+ import { K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS, K8S_SYSTEM_METADATA_PRUNE_PATTERNS, k8sListMapOrderKey, } from "@intentius/chant/managed-fields";
51
+ import { LABEL_OWNERSHIP_KEYS, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
52
+ import { GENERATED_ONCE_LABEL_KEY } from "./secret-labels.js";
53
+ /**
54
+ * Kubernetes-defaulted fields, per entity type, as index-erased property
55
+ * paths. Subtracted only where source never declared the property
56
+ * (`side === "live" && counterpart === "absent"`) — cdk-real-drift's default
57
+ * subtraction, same convention as the AWS and Azure tables
58
+ * (`AWS_SERVICE_DEFAULTS`, `AZURE_SERVICE_DEFAULTS`).
59
+ *
60
+ * Sparse and evidence-based rather than derived from the generated schema:
61
+ * the k8s OpenAPI spec this lexicon's codegen consumes
62
+ * (`lexicons/k8s/src/spec/parse.ts`) does not carry a `default` value for
63
+ * these fields the way ARM's schema sometimes does, so "per discovery" is not
64
+ * actually expressible today. Widening this table is additive and needs no
65
+ * contract change.
66
+ *
67
+ * `spec.strategy` is listed whole rather than as `spec.strategy.type`, because
68
+ * pruning only the leaf would still recurse into the object, and a nested default the
69
+ * table does not separately name (`rollingUpdate.maxSurge`/`maxUnavailable`,
70
+ * both `"25%"` when `spec.strategy` is omitted entirely) would leave behind an
71
+ * empty `strategy: {}` — a value distinct from no `strategy` key at all, and
72
+ * itself a spurious "undeclared" finding. Matching the whole node first, before
73
+ * its children are ever visited, drops the wrapper outright when every field
74
+ * under it is exactly at its default.
75
+ */
76
+ export const K8S_SERVICE_DEFAULTS = {
77
+ "K8s::Apps::Deployment": {
78
+ "spec.strategy": { type: "RollingUpdate", rollingUpdate: { maxSurge: "25%", maxUnavailable: "25%" } },
79
+ "spec.revisionHistoryLimit": 10,
80
+ "spec.progressDeadlineSeconds": 600,
81
+ "spec.template.spec.dnsPolicy": "ClusterFirst",
82
+ "spec.template.spec.restartPolicy": "Always",
83
+ "spec.template.spec.terminationGracePeriodSeconds": 30,
84
+ "spec.template.spec.schedulerName": "default-scheduler",
85
+ // Widened on the CC canonical estate (#1214): a container declaring an
86
+ // httpGet probe, a named port, or nothing about termination gets exactly
87
+ // these from the API server. SSA attributes a server-defaulted field to no
88
+ // manager, so the managed-fields prune deliberately leaves them diffable
89
+ // (see ./deep-observe.ts) — this table is where they subtract.
90
+ "spec.template.spec.containers[].livenessProbe.failureThreshold": 3,
91
+ "spec.template.spec.containers[].livenessProbe.periodSeconds": 10,
92
+ "spec.template.spec.containers[].livenessProbe.successThreshold": 1,
93
+ "spec.template.spec.containers[].livenessProbe.timeoutSeconds": 1,
94
+ "spec.template.spec.containers[].livenessProbe.httpGet.scheme": "HTTP",
95
+ "spec.template.spec.containers[].readinessProbe.failureThreshold": 3,
96
+ "spec.template.spec.containers[].readinessProbe.periodSeconds": 10,
97
+ "spec.template.spec.containers[].readinessProbe.successThreshold": 1,
98
+ "spec.template.spec.containers[].readinessProbe.timeoutSeconds": 1,
99
+ "spec.template.spec.containers[].readinessProbe.httpGet.scheme": "HTTP",
100
+ "spec.template.spec.containers[].ports[].protocol": "TCP",
101
+ "spec.template.spec.containers[].terminationMessagePath": "/dev/termination-log",
102
+ "spec.template.spec.containers[].terminationMessagePolicy": "File",
103
+ "spec.template.spec.securityContext": {},
104
+ },
105
+ "K8s::Core::Service": {
106
+ "spec.sessionAffinity": "None",
107
+ "spec.type": "ClusterIP",
108
+ "spec.internalTrafficPolicy": "Cluster",
109
+ "spec.ipFamilyPolicy": "SingleStack",
110
+ "spec.ports[].protocol": "TCP",
111
+ },
112
+ "K8s::Core::Secret": {
113
+ // The API server defaults an untyped Secret to Opaque (#1830).
114
+ "type": "Opaque",
115
+ },
116
+ };
117
+ /**
118
+ * Server-ASSIGNED fields — populated by the API server with a value that
119
+ * varies per object (an allocated ClusterIP, the resolved IP family list), so
120
+ * unlike {@link K8S_SERVICE_DEFAULTS} there is no fixed value to compare
121
+ * against. Counterpart-gated like the defaults table: a declared `clusterIP`
122
+ * is still compared, only the purely server-filled appearance is noise.
123
+ */
124
+ export const K8S_SERVER_ASSIGNED_PATTERNS = {
125
+ "K8s::Core::Service": new Set(["spec.clusterIP", "spec.clusterIPs", "spec.ipFamilies"]),
126
+ };
127
+ /**
128
+ * chant's own ownership marker, as label paths (#1214 — the k8s edition of
129
+ * azure's #1213 rule). The build stamps `app.kubernetes.io/managed-by` +
130
+ * `chant.intentius.io/{stack,env}` into every manifest it emits, so a managed
131
+ * object always carries them live while the declared *source* the diff
132
+ * compares does not — chant reading its own signature back as drift. The
133
+ * managed-fields prune cannot subtract them: they are in the applied config,
134
+ * so chant's field manager owns them, and chant-owned is always diffable.
135
+ * Counterpart-gated at the call site; managed-by is additionally gated on the
136
+ * value `"chant"`, so `managed-by: helm` appearing out of band still surfaces.
137
+ */
138
+ const K8S_OWNERSHIP_LABEL_PATTERNS = new Set([
139
+ `metadata.labels.${LABEL_OWNERSHIP_KEYS.stack}`,
140
+ `metadata.labels.${LABEL_OWNERSHIP_KEYS.env}`,
141
+ // The generated-once marker (#1830) is stamped by the secret store at
142
+ // mint time, never by a serializer — chant's own signature, not drift.
143
+ `metadata.labels.${GENERATED_ONCE_LABEL_KEY}`,
144
+ ]);
145
+ const K8S_MANAGED_BY_LABEL_PATTERN = `metadata.labels.${LABEL_OWNERSHIP_KEYS.managedBy}`;
146
+ /** Stable JSON with sorted keys — the fallback ordering key for a set-like array without a natural identity field. */
147
+ function canonicalJson(value) {
148
+ return (JSON.stringify(value, (_k, v) => v && typeof v === "object" && !Array.isArray(v)
149
+ ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
150
+ : v) ?? "");
151
+ }
152
+ let cachedListMapKeys;
153
+ /**
154
+ * Where the generated table lives: next to this module, resolved from
155
+ * `import.meta.url`. This module ships as ESM (tsx strips types from src/
156
+ * directly), where a bare `require` is not defined — the #1441 regression
157
+ * crashed `lifecycle diff --live` with `require is not defined` on every
158
+ * estate with a Deployment, and azure's apiVersion registry hit the same
159
+ * class of bug in #1581. A plain fs read has no module-system dependency.
160
+ */
161
+ export function listMapKeyTablePath() {
162
+ return join(moduleSourceDir(import.meta.url), "generated", "list-map-keys.json");
163
+ }
164
+ /**
165
+ * Load the spec-derived table once. A missing or unreadable file degrades to
166
+ * an EMPTY table, which routes every list through `k8sListMapOrderKey`'s
167
+ * hand-written conventions — the same fallback an unlisted property already
168
+ * takes. The empty table is cached by the caller too, so an absent artifact
169
+ * costs one failed read and one warning, not one per array element
170
+ * (chant #1476).
171
+ *
172
+ * Degrading is the right call here, unlike azure's apiVersion registry
173
+ * (#1581) where a silent fallback bypasses correctness-bearing pins. Without
174
+ * the table, drift detection narrows back to the six hardcoded properties:
175
+ * less coverage, never wrong answers. The warning names the regen command so
176
+ * the narrowing is not silent.
177
+ */
178
+ export function loadListMapKeyTable(path = listMapKeyTablePath()) {
179
+ if (!existsSync(path)) {
180
+ warnMissingTable(path, "not found");
181
+ return {};
182
+ }
183
+ try {
184
+ return JSON.parse(readFileSync(path, "utf-8"));
185
+ }
186
+ catch (err) {
187
+ warnMissingTable(path, `unreadable: ${err instanceof Error ? err.message : String(err)}`);
188
+ return {};
189
+ }
190
+ }
191
+ function warnMissingTable(path, reason) {
192
+ console.warn(`k8s: list-map-keys table ${reason} (${path}); list identity falls back to the ` +
193
+ "hand-written conventions in @intentius/chant/managed-fields. Run `npm run generate` " +
194
+ "in lexicons/k8s (dev checkout) or reinstall @intentius/chant-lexicon-k8s.");
195
+ }
196
+ function listMapKeyTable() {
197
+ cachedListMapKeys ?? (cachedListMapKeys = loadListMapKeyTable());
198
+ return cachedListMapKeys;
199
+ }
200
+ /** Test hook: replace (or drop) the cached table so the next lookup reloads it. */
201
+ export function resetListMapKeyTableCache(table) {
202
+ cachedListMapKeys = table;
203
+ }
204
+ /** Last dotted segment of an index-erased path: `spec.template.spec.containers` → `containers`. */
205
+ function lastSegment(pattern) {
206
+ const at = pattern.lastIndexOf(".");
207
+ return at === -1 ? pattern : pattern.slice(at + 1);
208
+ }
209
+ /**
210
+ * Render one key field's value into a sort key fragment. Numbers are
211
+ * zero-padded so the key orders numerically rather than lexicographically —
212
+ * `"443"` would otherwise sort before `"80"`. Cosmetic, since either order
213
+ * canonicalizes the two sides identically, but a human-sensible order is free
214
+ * to have here (the same reasoning `k8sListMapOrderKey` applies to ports).
215
+ */
216
+ function keyFragment(value) {
217
+ if (typeof value === "number")
218
+ return String(value).padStart(5, "0");
219
+ return typeof value === "string" ? value : canonicalJson(value);
220
+ }
221
+ /**
222
+ * Join a key set's values the way `k8sListMapOrderKey` already does: the bare
223
+ * values, `/`-separated, with no field names.
224
+ *
225
+ * This is not only a sort key. Core renders it as the element's ADDRESS in
226
+ * drift output — `spec.template.spec.containers[#app].image` — so the format
227
+ * is user-visible and load-bearing. Emitting `name=app` here would rewrite
228
+ * every existing drift path. Bare values keep the six properties that were
229
+ * hardcoded byte-identical, and extend to the rest for free: a container port
230
+ * reads `[#08080/TCP]` exactly as before, a condition reads `[#Ready]`.
231
+ */
232
+ function joinKeyValues(el, keys) {
233
+ return keys.map((k) => keyFragment(el[k])).join("/");
234
+ }
235
+ /**
236
+ * Order an associative list's element by the identity the SPEC declares for it
237
+ * (chant #1441), falling back to `@intentius/chant/managed-fields`'s hand-written
238
+ * conventions for anything the generated table does not cover.
239
+ *
240
+ * The fallback is not vestigial. The table is built from the properties this
241
+ * lexicon's codegen parsed, so it covers core Kubernetes and any CRD that
242
+ * declares the extensions — but a CRD that declares an associative list
243
+ * WITHOUT `x-kubernetes-list-map-keys`, or a Config Connector resource read
244
+ * through GCP's hooks, still lands on the by-name conventions. Keeping both
245
+ * means widening the spec-derived table can never narrow what was already
246
+ * identified.
247
+ */
248
+ export function schemaListMapOrderKey(element) {
249
+ const candidates = listMapKeyTable()[lastSegment(element.pattern)];
250
+ const el = element.element;
251
+ if (candidates && isRecordLike(el)) {
252
+ for (const keys of candidates) {
253
+ if (!keys.every((k) => el[k] !== undefined))
254
+ continue;
255
+ return joinKeyValues(el, keys);
256
+ }
257
+ }
258
+ return k8sListMapOrderKey(element);
259
+ }
260
+ function isRecordLike(value) {
261
+ return typeof value === "object" && value !== null && !Array.isArray(value);
262
+ }
263
+ /**
264
+ * The paths on a `K8s::Core::Secret` whose values are secret material —
265
+ * `data.<key>` (live and declared base64) and `stringData.<key>` (declared
266
+ * plaintext). Masked on BOTH sides by core's normalization pass, so presence
267
+ * and key names still classify as drift while no value (or value-derived
268
+ * comparison) ever reaches a diff row — #1365 decision 6, the contract #1830's
269
+ * generated-once secrets rely on. Key-name masking alone cannot do this:
270
+ * Secret keys are arbitrary (`app.conf`), not name-shaped.
271
+ */
272
+ const K8S_SECRET_ENTITY_TYPE = "K8s::Core::Secret";
273
+ const K8S_SECRET_VALUE_PATTERN = /^(?:data|stringData)\./;
274
+ export const k8sDeepNormalizationHooks = {
275
+ mask(node) {
276
+ return node.entityType === K8S_SECRET_ENTITY_TYPE && K8S_SECRET_VALUE_PATTERN.test(node.pattern);
277
+ },
278
+ prune(node) {
279
+ if (K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS.has(node.pattern))
280
+ return true;
281
+ if (node.side !== "live" || node.counterpart !== "absent")
282
+ return false;
283
+ // What Kubernetes' own controllers stamp on every object of a kind
284
+ // (rollout counters, selector hashes, client-side-apply bookkeeping) is
285
+ // not drift. This is the static allowlist that stands in for the
286
+ // managed-fields prune since #1191: any *other* foreign-owned field
287
+ // nobody declared — a `kubectl label` from the console — now reaches the
288
+ // diff and reports as `undeclared`, with the accepted baseline as the
289
+ // valve for the ones an estate has decided to live with.
290
+ if (K8S_SYSTEM_METADATA_PRUNE_PATTERNS.has(node.pattern))
291
+ return true;
292
+ // chant's own ownership marker is not drift (see the sets' docs).
293
+ if (K8S_OWNERSHIP_LABEL_PATTERNS.has(node.pattern))
294
+ return true;
295
+ if (node.pattern === K8S_MANAGED_BY_LABEL_PATTERN && node.value === OWNERSHIP_MANAGED_BY_VALUE)
296
+ return true;
297
+ // A server-assigned value nobody declared is allocation, not drift.
298
+ if (K8S_SERVER_ASSIGNED_PATTERNS[node.entityType]?.has(node.pattern))
299
+ return true;
300
+ const defaults = K8S_SERVICE_DEFAULTS[node.entityType];
301
+ if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, node.pattern))
302
+ return false;
303
+ return canonicalJson(defaults[node.pattern]) === canonicalJson(node.value);
304
+ },
305
+ orderKey: schemaListMapOrderKey,
306
+ };
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Kubernetes deep observation (#1076) — the k8s row of the deep-observe
3
+ * contract (#1014), and the epic's (#1073) thesis made real: chant compiled
4
+ * the manifest, so it knows which fields it authored; the API server records,
5
+ * per field, which manager last wrote it (`metadata.managedFields`, chant
6
+ * #1075/#1178). Intersecting those two makes Kubernetes drift *derived*
7
+ * rather than the hand-maintained `ignoreDifferences` list every Argo user
8
+ * keeps.
9
+ *
10
+ * ## What managedFields decides, and what it does not
11
+ *
12
+ * The AWS and Azure rows both prune by a **static, entityType-keyed** table
13
+ * (`AWS_SERVICE_DEFAULTS`, `AZURE_SERVICE_DEFAULTS`): an ARN always looks like
14
+ * an ARN, `provisioningState` is always server-populated. Neither needs the
15
+ * specific live object in hand — it is exactly
16
+ * what `./deep-observe-hooks.ts`'s `k8sDeepNormalizationHooks` is, and it
17
+ * covers Kubernetes' *equivalent* static noise (`status`,
18
+ * `metadata.{uid,resourceVersion,generation,creationTimestamp}`, a handful of
19
+ * server-defaulted pod-spec fields, and the annotations and labels
20
+ * Kubernetes' own controllers stamp on every object of a kind —
21
+ * `K8S_SYSTEM_METADATA_PRUNE_PATTERNS` in `@intentius/chant/managed-fields`).
22
+ *
23
+ * managedFields is a different kind of fact. Which manager owns
24
+ * `spec.replicas` on *this* Deployment differs between two Deployments of
25
+ * the same type, and the declared tree never carries it at all (chant's
26
+ * source has no `managedFields` key to normalize). It has to be resolved once
27
+ * per resource, here, against the object in hand.
28
+ *
29
+ * Until chant #1191 that resolution *pruned*: a foreign-owned path nobody
30
+ * declared was dropped before the tree reached core, on the theory that it
31
+ * was controller noise. It silenced exactly the case the deep read exists
32
+ * for — `kubectl label deploy web team=platform` is foreign-owned
33
+ * (`kubectl-edit`) and undeclared, and never appeared in fieldDrift. The
34
+ * policy now (recorded on #1202, refined by #2160) is:
35
+ *
36
+ * 1. **Chant-owned** (`chant`, `chant:<stack>` — chant #1075, matched on the
37
+ * family so a stack rename does not stop recognizing its own history) is
38
+ * always diffable.
39
+ * 2. **Declared, whoever owns it live** (a contested field) is always
40
+ * diffable: chant's source is a statement of intent independent of which
41
+ * write currently holds the field, and a foreign write that overrides a
42
+ * value chant's manifest asks for is the thing `lifecycle diff --live`
43
+ * exists to surface. It reports as `changed`.
44
+ * 3. **Undeclared** — a path outside the declaration's claimed-field set
45
+ * (`@intentius/chant/claimed-fields`) — is reported as unclaimed and
46
+ * is not drift at all. Before #2160 it was `undeclared` drift that the
47
+ * accepted baseline had to absorb one path at a time; the declaration
48
+ * answers on the first read instead, so an HPA that owns `spec.replicas`
49
+ * on a Deployment whose source is silent about replicas is quiet with
50
+ * nothing recorded anywhere.
51
+ *
52
+ * So ownership decides nothing about *whether* a path is compared, and since
53
+ * #2160 it no longer decides the verdict either — the declaration does. What
54
+ * it contributes is `fieldOwners` (#1189), and that is worth more here than
55
+ * anywhere else: on every other substrate a held field can only be reported
56
+ * as "not chant's", while here it is reported as `hpa-controller`'s or
57
+ * `kubectl-edit`'s by name, which is the difference between a controller
58
+ * doing its job and somebody bypassing the pipeline. Core takes the manager
59
+ * where this reader supplies one and falls back to the claim where it does
60
+ * not (a path inside a key-addressed list, which no `fieldsV1` entry names in
61
+ * that form). The only fields subtracted because a foreign manager wrote them
62
+ * are the well-known system ones on the static allowlist.
63
+ *
64
+ * ## Resolving a managedFields entry against a live array
65
+ *
66
+ * `metadata.managedFields[].fieldsV1` encodes a list item three ways —
67
+ * `k:{"name":"web"}` (by key), `v:"blue"` (by value), `i:3` (by index) — and
68
+ * chant #1178's `managed-fields.ts` renders all three to a path *string*
69
+ * (`.spec.containers[name="web"]`) for display. Parsing that string back
70
+ * would be ambiguous in general (a label key may itself contain `.`, e.g.
71
+ * `app.kubernetes.io/name`, indistinguishable in the rendered form from
72
+ * nested fields), so this reader does not parse strings at all: it walks the
73
+ * *structured* `fieldsV1` tree directly (`walkOwnership`, below), matching
74
+ * each `k:`/`v:` entry against the actual live array to find the concrete
75
+ * index, and against the actual declared array (by the same key or value) to
76
+ * answer question 3 for that element specifically — a declared container
77
+ * named `app` at index 0 is still "declared" if a sidecar gets injected in
78
+ * front of it and shifts it to live index 1.
79
+ *
80
+ * ## What is deliberately not modeled
81
+ *
82
+ * A manager's `.` entry (SSA's "I own this element's presence") on a
83
+ * container that also sets no scalar sub-fields (unusual — a real apply
84
+ * setting a container almost always sets `image`/`name` too) would not confer
85
+ * ownership onto a sibling field nobody's `fieldsV1` entry lists explicitly.
86
+ * A path absent from every manager's fields simply has no `owner` on its
87
+ * drift line; it is compared like any other.
88
+ *
89
+ * ## `buildOwnershipSets` lives in core now
90
+ *
91
+ * The ownership walk itself (`walkOwnership`, resolving a `fieldsV1` tree
92
+ * against the live and declared trees) has nothing k8s-*lexicon*-specific
93
+ * about it — it is generic Kubernetes SSA machinery. Chant #1087 (GCP's row,
94
+ * reusing this one, since a Config Connector CR is a Kubernetes object too)
95
+ * moved it to `@intentius/chant/managed-fields` rather than have gcp depend
96
+ * on this lexicon's package, the same reason chant #1100's
97
+ * `resolveClusterTarget` lives in core. This module re-exports
98
+ * `buildOwnershipSets`/`OwnershipSets` from there so nothing here changes for
99
+ * an existing importer of this file.
100
+ */
101
+ import { deepObservation, normalizeDeepProperties } from "@intentius/chant/deep-observation";
102
+ import { unobservedAll } from "@intentius/chant/observation";
103
+ import { hasOwnershipMarker, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
104
+ import { buildOwnershipSets } from "@intentius/chant/managed-fields";
105
+ import { defaultK8sConnector } from "./api/connect.js";
106
+ import { classifyApiFailure, isMissingClientPackage, isWholeLexiconFailure, MISSING_CLIENT_DETAIL, } from "./api/classify.js";
107
+ import { operationFor } from "./api/operation-surface.js";
108
+ import { k8sDeepNormalizationHooks } from "./deep-observe-hooks.js";
109
+ import { observeReceiptRowsDeep, receiptRowsFor } from "./receipt-store.js";
110
+ // Re-exported so a dynamic importer of this module (plugin.ts's
111
+ // `observeResourcesDeep`, a test) can get the reader and its hooks from one
112
+ // place, the same shape the AWS and Azure deep-observe.ts modules offer.
113
+ // `plugin.ts`'s own `deepNormalizationHooks` field imports the hooks
114
+ // separately, directly from `./deep-observe-hooks` — that file has no
115
+ // dependency on `@intentius/chant-k8s-client`, so it is safe to import
116
+ // statically; this module is not (see the module doc).
117
+ export { k8sDeepNormalizationHooks, buildOwnershipSets };
118
+ /** The live object minus the envelope fields that live outside `properties` on {@link DeepResourceObservation} (mirrors `type`/`physicalId`). */
119
+ function propertiesTreeOf(obj) {
120
+ const { apiVersion: _apiVersion, kind: _kind, ...rest } = obj;
121
+ return rest;
122
+ }
123
+ /**
124
+ * Read the live property tree for each declared entity, pruning by
125
+ * `metadata.managedFields` (see the module doc). Reuses the same connector,
126
+ * discovery/caching client and tri-state failure classification as the thin
127
+ * read (`./describe-resources.ts`) — the binding check (chant #1100/#1155)
128
+ * still refuses before any resource is touched, and a connect failure still
129
+ * becomes NOT-OBSERVED for every declared entity rather than an empty result.
130
+ */
131
+ export async function observeResourcesDeepK8s(options, connect = defaultK8sConnector) {
132
+ const { managedFieldsOf, isChantFieldManager } = await import("@intentius/chant-k8s-client");
133
+ // Effect receipts (#2074) are read by their own leg at the end: they carry
134
+ // no declared props, so every live path would be an unclaimed field (#2160)
135
+ // and their staleness is an `effect` row from the plan (#1832), never
136
+ // property drift. Reading them here keeps the deep read from calling a
137
+ // declared entity a hole; contributing an empty tree keeps it from calling
138
+ // one drift.
139
+ const receiptRows = receiptRowsFor(options.entityNames, options.buildOutput);
140
+ const declared = [...options.entities]
141
+ .filter(([entityName]) => !receiptRows.has(entityName))
142
+ .map(([entityName, entity]) => ({
143
+ entityName,
144
+ entityType: entity.entityType,
145
+ props: entity.props,
146
+ }));
147
+ const everyName = [...declared.map((d) => d.entityName), ...receiptRows.keys()];
148
+ let client;
149
+ try {
150
+ ({ client } = await connect({ environment: options.environment, cwd: options.cwd }));
151
+ }
152
+ catch (err) {
153
+ if (isMissingClientPackage(err)) {
154
+ return deepObservation({}, unobservedAll(everyName, "read-failed", MISSING_CLIENT_DETAIL, options.entities));
155
+ }
156
+ if (isWholeLexiconFailure(err)) {
157
+ const outcome = classifyApiFailure(err);
158
+ return deepObservation({}, unobservedAll(everyName, outcome.kind === "unobserved" ? outcome.reason : "read-failed", outcome.kind === "unobserved" ? outcome.detail : undefined, options.entities));
159
+ }
160
+ // A cluster-binding mismatch (chant #1100) — refuse loudly, same as the
161
+ // thin path; core turns the throw into NOT-OBSERVED for every entity.
162
+ throw err;
163
+ }
164
+ const resources = {};
165
+ const unobserved = {};
166
+ await client.concurrently(declared, async ({ entityName, entityType, props }) => {
167
+ const operation = operationFor(entityType);
168
+ if (!operation) {
169
+ unobserved[entityName] = {
170
+ type: entityType,
171
+ reason: "unsupported-kind",
172
+ detail: `no generated operation surface for ${entityType} — run \`chant generate\` in the k8s lexicon, or declare the CRD as a codegen source`,
173
+ };
174
+ return;
175
+ }
176
+ const metadata = props.metadata;
177
+ const name = metadata?.name;
178
+ if (!name) {
179
+ unobserved[entityName] = {
180
+ type: entityType,
181
+ reason: "read-failed",
182
+ detail: "declared entity has no metadata.name to query by",
183
+ };
184
+ return;
185
+ }
186
+ try {
187
+ const obj = await client.read({
188
+ apiVersion: operation.apiVersion,
189
+ kind: operation.kind,
190
+ name,
191
+ ...(metadata?.namespace ? { namespace: metadata.namespace } : {}),
192
+ });
193
+ if (options.owned && !hasOwnershipMarker(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS)) {
194
+ unobserved[entityName] = {
195
+ type: entityType,
196
+ reason: "filtered",
197
+ detail: "live object carries no chant ownership marker and --owned was requested",
198
+ };
199
+ return;
200
+ }
201
+ const liveRoot = propertiesTreeOf(obj);
202
+ const sets = buildOwnershipSets(managedFieldsOf(obj), liveRoot, props, isChantFieldManager);
203
+ resources[entityName] = {
204
+ type: entityType,
205
+ physicalId: obj.metadata?.uid,
206
+ properties: normalizeDeepProperties(liveRoot, {
207
+ entityType,
208
+ side: "live",
209
+ hooks: k8sDeepNormalizationHooks,
210
+ }),
211
+ // Who owns each path (#1189). Nothing is pruned by ownership any more
212
+ // (#1191), so a foreign-owned undeclared field still reaches the diff
213
+ // — as a held field since #2160 — and naming the manager is the whole
214
+ // question there: an operator needs to tell `hpa-controller` doing its
215
+ // job from somebody running `kubectl edit`.
216
+ ...(sets.owners.size > 0 ? { fieldOwners: Object.fromEntries(sets.owners) } : {}),
217
+ };
218
+ }
219
+ catch (err) {
220
+ const outcome = classifyApiFailure(err);
221
+ if (outcome.kind === "unobserved") {
222
+ unobserved[entityName] = { type: entityType, reason: outcome.reason, detail: outcome.detail };
223
+ }
224
+ // `absent` records nothing — the thin read already reports the
225
+ // non-existence; restating it here would turn one finding into two.
226
+ }
227
+ });
228
+ if (receiptRows.size > 0) {
229
+ const receiptObs = await observeReceiptRowsDeep(client, receiptRows);
230
+ Object.assign(resources, receiptObs.resources);
231
+ Object.assign(unobserved, receiptObs.unobserved);
232
+ }
233
+ return deepObservation(resources, unobserved);
234
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Default Labels & Annotations — declares project-wide labels/annotations
3
+ * for all Kubernetes resources.
4
+ *
5
+ * When a project exports a `defaultLabels(...)` or `defaultAnnotations(...)`
6
+ * declaration, the serializer automatically injects them into every resource's
7
+ * metadata at synthesis time. Explicit labels/annotations on individual
8
+ * resources take precedence.
9
+ */
10
+ import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
11
+ /**
12
+ * Marker symbol for default labels identification.
13
+ */
14
+ export const DEFAULT_LABELS_MARKER = Symbol.for("chant.k8s.defaultLabels");
15
+ /**
16
+ * Marker symbol for default annotations identification.
17
+ */
18
+ export const DEFAULT_ANNOTATIONS_MARKER = Symbol.for("chant.k8s.defaultAnnotations");
19
+ /**
20
+ * Type guard for DefaultLabels.
21
+ */
22
+ export function isDefaultLabels(value) {
23
+ return (typeof value === "object" &&
24
+ value !== null &&
25
+ DEFAULT_LABELS_MARKER in value &&
26
+ value[DEFAULT_LABELS_MARKER] === true);
27
+ }
28
+ /**
29
+ * Type guard for DefaultAnnotations.
30
+ */
31
+ export function isDefaultAnnotations(value) {
32
+ return (typeof value === "object" &&
33
+ value !== null &&
34
+ DEFAULT_ANNOTATIONS_MARKER in value &&
35
+ value[DEFAULT_ANNOTATIONS_MARKER] === true);
36
+ }
37
+ /**
38
+ * Declare project-wide default labels for all Kubernetes resources.
39
+ *
40
+ * Labels are injected at synthesis time into every resource's
41
+ * `metadata.labels`. If a resource has an explicit label with the
42
+ * same key, the explicit value wins.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { defaultLabels } from "@intentius/chant-lexicon-k8s";
47
+ *
48
+ * export const labels = defaultLabels({
49
+ * "app.kubernetes.io/managed-by": "chant",
50
+ * "app.kubernetes.io/part-of": "my-app",
51
+ * });
52
+ * ```
53
+ */
54
+ export function defaultLabels(labels) {
55
+ return {
56
+ [DEFAULT_LABELS_MARKER]: true,
57
+ [DECLARABLE_MARKER]: true,
58
+ lexicon: "k8s",
59
+ entityType: "chant:k8s:defaultLabels",
60
+ labels,
61
+ };
62
+ }
63
+ /**
64
+ * Declare project-wide default annotations for all Kubernetes resources.
65
+ *
66
+ * Annotations are injected at synthesis time into every resource's
67
+ * `metadata.annotations`. If a resource has an explicit annotation with
68
+ * the same key, the explicit value wins.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * import { defaultAnnotations } from "@intentius/chant-lexicon-k8s";
73
+ *
74
+ * export const annotations = defaultAnnotations({
75
+ * "app.kubernetes.io/managed-by": "chant",
76
+ * });
77
+ * ```
78
+ */
79
+ export function defaultAnnotations(annotations) {
80
+ return {
81
+ [DEFAULT_ANNOTATIONS_MARKER]: true,
82
+ [DECLARABLE_MARKER]: true,
83
+ lexicon: "k8s",
84
+ entityType: "chant:k8s:defaultAnnotations",
85
+ annotations,
86
+ };
87
+ }