@intentius/chant-lexicon-k8s 0.124.31 → 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,517 @@
1
+ /**
2
+ * `kubectlApply` — apply a rendered manifest to a cluster.
3
+ *
4
+ * chant #1074 moved this off `kubectl apply -f`. The activity contract is
5
+ * unchanged (a manifest path, an optional context, `Promise<void>`, the
6
+ * `longInfra` profile) because an Op step names the activity by its export
7
+ * name and core's registry resolves it there; what changed is underneath. The
8
+ * name is kept for the same reason.
9
+ *
10
+ * chant #1075 finished the job on two axes:
11
+ *
12
+ * - **Identity.** The field manager is no longer the bare `chant` but
13
+ * `chant:<stack>`, derived from the project's `ownership.stack` — the same
14
+ * identity the ownership label already carries. See
15
+ * `@intentius/chant-k8s-client`'s `field-manager.ts`.
16
+ * - **Deletes.** `nativeApply`'s kubectl branch used to shell
17
+ * `kubectl apply --prune --selector <marker>`; that branch now routes here,
18
+ * so the prune had to come with it. {@link applyManifest} implements it
19
+ * against the typed client, scoped to the ownership marker exactly as the
20
+ * shelled prune was, and never touching an object that does not carry it.
21
+ *
22
+ * Consequences worth knowing:
23
+ *
24
+ * - **A worker image needs no `kubectl` binary.** That was the point.
25
+ * - **It is a server-side apply**, rather than the client-side three-way merge
26
+ * `kubectl apply` performs by default. A conflict with another field manager
27
+ * arrives as a `FieldManagerConflictError` naming the competing manager and
28
+ * the contested paths — never as a silent overwrite, and never force-resolved
29
+ * on chant's initiative.
30
+ * - **Documents apply in file order**, as `kubectl apply -f` does, and a
31
+ * directory's files are read in sorted order.
32
+ */
33
+ import { readFileSync, readdirSync, statSync } from "node:fs";
34
+ import { join } from "node:path";
35
+ import { loadAll } from "js-yaml";
36
+ import { loadChantConfig, resolveOwnershipStack } from "@intentius/chant/config";
37
+ import { hasOwnershipMarker, LABEL_OWNERSHIP_KEYS, OWNERSHIP_MANAGED_BY_VALUE, } from "@intentius/chant/ownership";
38
+ import { defaultK8sConnector } from "../../api/connect.js";
39
+ import { operationFor } from "../../api/operation-surface.js";
40
+ import { DEFAULT_IMPORT_TYPES } from "../../api/sweep-types.js";
41
+ import { isGeneratedOnce } from "../../secret-labels.js";
42
+ import { isEffectReceiptObject } from "../../effect-receipt-row.js";
43
+ import { diffFields, normalizeServerObject } from "./apply-diff.js";
44
+ /** Read a manifest path — one file, or every YAML/JSON file in a directory. */
45
+ export function readManifestDocuments(path) {
46
+ const files = statSync(path).isDirectory()
47
+ ? readdirSync(path)
48
+ .filter((f) => /\.(ya?ml|json)$/i.test(f))
49
+ .sort()
50
+ .map((f) => join(path, f))
51
+ : [path];
52
+ const documents = [];
53
+ for (const file of files) {
54
+ for (const doc of loadAll(readFileSync(file, "utf-8"))) {
55
+ // Multi-document YAML files routinely carry empty documents between
56
+ // separators; they are not objects to apply.
57
+ if (doc && typeof doc === "object")
58
+ documents.push(doc);
59
+ }
60
+ }
61
+ return documents;
62
+ }
63
+ /**
64
+ * The field manager to apply as: an explicit one, else `chant:<stack>` from a
65
+ * caller-supplied stack, else `chant:<stack>` from the project's `ownership`
66
+ * config, else the bare `chant`.
67
+ *
68
+ * A project config that cannot be read falls back to the bare `chant` with a
69
+ * warning rather than failing the apply — an activity handed a manifest path
70
+ * and nothing else is a supported way to call this, and its cwd need not be a
71
+ * chant project at all. The warning is there because a *silent* downgrade
72
+ * would change which fields chant owns without saying so.
73
+ */
74
+ export async function resolveFieldManager(args) {
75
+ return (await resolveApplyIdentity(args)).fieldManager;
76
+ }
77
+ /**
78
+ * The stack and the field manager together, resolved once. The prune needs the
79
+ * stack for its selector and the apply needs the manager; reading the project
80
+ * config twice for one apply would be silly, and could disagree with itself if
81
+ * the file changed in between.
82
+ */
83
+ async function resolveApplyIdentity(args) {
84
+ const { fieldManagerFor, assertValidFieldManager } = await import("@intentius/chant-k8s-client");
85
+ let stack = args.stack;
86
+ if (stack === undefined) {
87
+ try {
88
+ const { config } = await loadChantConfig(args.cwd ?? process.cwd());
89
+ stack = resolveOwnershipStack(config);
90
+ }
91
+ catch (err) {
92
+ console.warn(`[k8s] could not read the project config to derive a field manager ` +
93
+ `(${err instanceof Error ? err.message : String(err)}); applying as the unqualified "chant"`);
94
+ }
95
+ }
96
+ if (args.fieldManager !== undefined) {
97
+ assertValidFieldManager(args.fieldManager);
98
+ return { fieldManager: args.fieldManager, ...(stack !== undefined ? { stack } : {}) };
99
+ }
100
+ return {
101
+ fieldManager: fieldManagerFor(stack !== undefined ? { stack } : undefined),
102
+ ...(stack !== undefined ? { stack } : {}),
103
+ };
104
+ }
105
+ /** chant's reserved label channel — the `chant.intentius.io/*` keys the
106
+ * ownership marker lives under, as a conflict-cause field-path prefix. */
107
+ const MARKER_FIELD_PREFIX = `.metadata.labels.${LABEL_OWNERSHIP_KEYS.stack.split("/")[0]}/`;
108
+ /**
109
+ * Is this conflict one chant may retake without a human? Two shapes qualify,
110
+ * per contested field:
111
+ *
112
+ * - the owning manager is chant itself (`chant` bare or `chant:<stack>`) —
113
+ * the ownership-stack → unit-stack migration, or a renamed deploy unit; or
114
+ * - the field lives in chant's OWN reserved label channel
115
+ * (`chant.intentius.io/*`). A controller that reconciles whole objects
116
+ * echoes existing labels and becomes their SSA owner as a side effect
117
+ * (observed live: kubemicrovm's `microvmimagereconciler` owning
118
+ * `.metadata.labels.chant.intentius.io/stack`) — it never chose that value,
119
+ * and chant is the authority for its marker namespace no matter who last
120
+ * echoed it.
121
+ *
122
+ * Any contested field outside both shapes — a spec field, an app label —
123
+ * keeps the presented refusal: that IS a dispute with another tool.
124
+ */
125
+ function isChantSelfConflict(err) {
126
+ const e = err;
127
+ if (!e || e.name !== "FieldManagerConflictError" || !Array.isArray(e.conflicts) || e.conflicts.length === 0)
128
+ return false;
129
+ return e.conflicts.every((c) => {
130
+ const chantManager = typeof c.manager === "string" && (c.manager === "chant" || c.manager.startsWith("chant:"));
131
+ const markerField = typeof c.field === "string" && c.field.startsWith(MARKER_FIELD_PREFIX);
132
+ return chantManager || markerField;
133
+ });
134
+ }
135
+ /**
136
+ * Stamp the apply's own stack identity onto a document's labels.
137
+ *
138
+ * The serializer bakes `ownership.stack` — the *project* identity from
139
+ * `chant.config.ts` — into every manifest it emits. But the apply's identity
140
+ * is the *stack argument it resolved* (a component's `kubectl-apply` step
141
+ * names its own deploy unit, e.g. `kmv-workload`), and that is the value both
142
+ * consumers of the label query for: the owned-only prune's selector and
143
+ * `describeStackStatus`'s presence sweep. Applying the manifest verbatim left
144
+ * the label saying `kubemicrovm-ops` while both readers asked for
145
+ * `kmv-workload` — the prune silently matched nothing and every kubectl-apply
146
+ * unit read absent in `components status --live` (a green estate painted
147
+ * dead). A manifest chant never serialized (upstream CRDs pinned into a
148
+ * `crds` unit) carried no marker at all, with the same two failures.
149
+ *
150
+ * So the apply now re-stamps what it applies: `managed-by=chant` plus the
151
+ * resolved stack. The ownership *verdict* keys on `managed-by` alone
152
+ * (core/ownership.ts `hasOwnershipMarker`), so overlay classification is
153
+ * unchanged; the env label is the serializer's to write and is left as-is.
154
+ * No stack resolved (no explicit arg, no project ownership) applies verbatim,
155
+ * exactly as before.
156
+ */
157
+ function stampOwnership(document, stack) {
158
+ if (stack === undefined)
159
+ return document;
160
+ const metadata = (document.metadata ?? {});
161
+ return {
162
+ ...document,
163
+ metadata: {
164
+ ...metadata,
165
+ labels: {
166
+ ...(metadata.labels ?? {}),
167
+ [LABEL_OWNERSHIP_KEYS.managedBy]: OWNERSHIP_MANAGED_BY_VALUE,
168
+ [LABEL_OWNERSHIP_KEYS.stack]: stack,
169
+ },
170
+ },
171
+ };
172
+ }
173
+ /**
174
+ * Apply every document in `args.manifest`, then prune when asked.
175
+ *
176
+ * Returns what it did. `kubectlApply` is the registered activity and keeps its
177
+ * `Promise<void>` contract; this is the function `nativeApply` calls, because a
178
+ * dispatcher that reports "applied 12, pruned 2" is worth more than one that
179
+ * reports the shell command it ran.
180
+ */
181
+ export async function applyManifest(args, signal, connect = defaultK8sConnector) {
182
+ const documents = args.documents ?? readManifestDocuments(args.manifest);
183
+ const { fieldManager, stack } = await resolveApplyIdentity(args);
184
+ const { client } = await connect({
185
+ ...(args.environment !== undefined ? { environment: args.environment } : {}),
186
+ ...(args.context !== undefined ? { context: args.context } : {}),
187
+ ...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
188
+ });
189
+ const applied = [];
190
+ for (const document of documents) {
191
+ const stamped = stampOwnership(document, stack);
192
+ let result;
193
+ try {
194
+ result = await client.apply(stamped, {
195
+ fieldManager,
196
+ force: args.force ?? false,
197
+ dryRun: args.dryRun,
198
+ signal,
199
+ });
200
+ }
201
+ catch (err) {
202
+ // "chant never forces a conflict on its own" is about taking fields
203
+ // from ANOTHER tool. A conflict where every contested field is owned
204
+ // by another `chant:*` manager is chant contesting itself — the
205
+ // ownership-stack → unit-stack label migration, or a renamed deploy
206
+ // unit — and refusing that forever would strand every estate applied
207
+ // before the rename with no non-force path back. Retake those fields
208
+ // deliberately, once, and only when no foreign manager is involved.
209
+ if (!isChantSelfConflict(err))
210
+ throw err;
211
+ result = await client.apply(stamped, { fieldManager, force: true, dryRun: args.dryRun, signal });
212
+ }
213
+ const ref = {
214
+ apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
215
+ kind: String(result.kind ?? document.kind ?? ""),
216
+ name: String(result.metadata?.name ?? ""),
217
+ ...(result.metadata?.namespace !== undefined
218
+ ? { namespace: String(result.metadata.namespace) }
219
+ : {}),
220
+ };
221
+ applied.push(ref);
222
+ console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} applied${args.dryRun ? " (dry run — nothing persisted)" : ""}`);
223
+ }
224
+ const deleteMode = args.deleteMode ?? "never";
225
+ const { pruned, retained } = deleteMode === "never" || args.dryRun
226
+ ? { pruned: [], retained: [] }
227
+ : await pruneOrphans(client, applied, {
228
+ ...(stack !== undefined ? { stack } : {}),
229
+ signal,
230
+ });
231
+ return { fieldManager, applied, pruned, retained };
232
+ }
233
+ /** A 404 from the API server, matched by shape. */
234
+ function isNotFound(err) {
235
+ const e = err;
236
+ return !!e && e.name === "K8sApiError" && e.statusCode === 404;
237
+ }
238
+ /** A 422 from the API server whose message says a field is immutable. Matched by shape, as {@link isChantSelfConflict} is. */
239
+ function immutableRefusal(err) {
240
+ const e = err;
241
+ if (!e || e.name !== "K8sApiError" || e.statusCode !== 422)
242
+ return undefined;
243
+ const message = typeof e.apiMessage === "string" && e.apiMessage !== "" ? e.apiMessage : String(e.message ?? "");
244
+ return /immutable/i.test(message) ? message : undefined;
245
+ }
246
+ /**
247
+ * Say what {@link applyManifest} would do, without changing anything (#3998).
248
+ *
249
+ * Every document is stamped and dry-run applied exactly as the apply sends
250
+ * it: the same field manager, `force: false`, and the same retake of a
251
+ * conflict chant has only with itself. The live object is read first and
252
+ * compared with the dry-run result through `./apply-diff.ts`, so a field the
253
+ * server defaults counts as unchanged when the live object already has it.
254
+ *
255
+ * An object in a namespace an earlier document of the same plan creates
256
+ * cannot be dry-run (the server answers 404, as the namespace was never
257
+ * persisted), so it plans as a create of the document as sent.
258
+ *
259
+ * Deletes come from the apply's own prune scope (`findOrphans`): marker
260
+ * scoped, in the namespaces the apply set touches, generated-once Secrets
261
+ * and effect receipts kept. They are found only when `deleteMode` is not
262
+ * `never`, as the apply prunes only then.
263
+ */
264
+ export async function planManifest(args, signal, connect = defaultK8sConnector) {
265
+ const documents = args.documents ?? readManifestDocuments(args.manifest);
266
+ const { fieldManager, stack } = await resolveApplyIdentity(args);
267
+ const { client } = await connect({
268
+ ...(args.environment !== undefined ? { environment: args.environment } : {}),
269
+ ...(args.context !== undefined ? { context: args.context } : {}),
270
+ ...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
271
+ });
272
+ const objects = [];
273
+ /** Namespaces this plan creates. A dry run cannot place an object in one, since nothing persisted it. */
274
+ const creating = new Set();
275
+ for (const document of documents) {
276
+ const stamped = stampOwnership(document, stack);
277
+ const declaredRef = {
278
+ apiVersion: String(stamped.apiVersion ?? ""),
279
+ kind: String(stamped.kind ?? ""),
280
+ name: String(stamped.metadata?.name ?? ""),
281
+ ...(stamped.metadata?.namespace !== undefined ? { namespace: String(stamped.metadata.namespace) } : {}),
282
+ };
283
+ const live = await client.readIfPresent(declaredRef, signal ? { signal } : {});
284
+ let result;
285
+ try {
286
+ try {
287
+ result = await client.apply(stamped, { fieldManager, force: args.force ?? false, dryRun: true, signal });
288
+ }
289
+ catch (err) {
290
+ if (!isChantSelfConflict(err))
291
+ throw err;
292
+ result = await client.apply(stamped, { fieldManager, force: true, dryRun: true, signal });
293
+ }
294
+ }
295
+ catch (err) {
296
+ if (!live && declaredRef.namespace !== undefined && creating.has(declaredRef.namespace) && isNotFound(err)) {
297
+ objects.push({ ref: declaredRef, action: "create", object: stamped, changes: [] });
298
+ continue;
299
+ }
300
+ const refused = live ? immutableRefusal(err) : undefined;
301
+ if (refused === undefined)
302
+ throw err;
303
+ objects.push({ ref: refOf(live, document), action: "update", changes: [], refused });
304
+ continue;
305
+ }
306
+ const ref = refOf(result, document);
307
+ if (!live) {
308
+ if (ref.kind === "Namespace" && ref.apiVersion === "v1")
309
+ creating.add(ref.name);
310
+ objects.push({ ref, action: "create", object: stamped, changes: [] });
311
+ continue;
312
+ }
313
+ const changes = diffFields(normalizeServerObject(live), normalizeServerObject(result));
314
+ objects.push({ ref, action: changes.length === 0 ? "no-op" : "update", changes });
315
+ }
316
+ const deleteMode = args.deleteMode ?? "never";
317
+ const { candidates, retained } = deleteMode === "never"
318
+ ? { candidates: [], retained: [] }
319
+ : await findOrphans(client, objects.map((o) => o.ref), { ...(stack !== undefined ? { stack } : {}), ...(signal ? { signal } : {}) });
320
+ return { fieldManager, ...(stack !== undefined ? { stack } : {}), objects, deletes: candidates, retained };
321
+ }
322
+ /**
323
+ * Read each of `refs` as the cluster holds it now, `undefined` for one that
324
+ * does not exist (#3999). `kubectl-apply` reads the fields its step names as
325
+ * outputs from these.
326
+ */
327
+ export async function readLiveObjects(args, signal, connect = defaultK8sConnector) {
328
+ if (args.refs.length === 0)
329
+ return [];
330
+ const { client } = await connect({
331
+ ...(args.environment !== undefined ? { environment: args.environment } : {}),
332
+ ...(args.context !== undefined ? { context: args.context } : {}),
333
+ ...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
334
+ });
335
+ const out = [];
336
+ for (const ref of args.refs)
337
+ out.push((await client.readIfPresent(ref, signal ? { signal } : {})));
338
+ return out;
339
+ }
340
+ /** The ref an applied or dry-run object answers with, as {@link applyManifest} records it. */
341
+ function refOf(result, document) {
342
+ return {
343
+ apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
344
+ kind: String(result.kind ?? document.kind ?? ""),
345
+ name: String(result.metadata?.name ?? ""),
346
+ ...(result.metadata?.namespace !== undefined ? { namespace: String(result.metadata.namespace) } : {}),
347
+ };
348
+ }
349
+ /**
350
+ * Apply every document in `args.manifest`.
351
+ * Uses the longInfra profile: 20m timeout, three attempts backing off from 30s.
352
+ */
353
+ export async function kubectlApply(args, signal, connect = defaultK8sConnector) {
354
+ await applyManifest(args, signal, connect);
355
+ }
356
+ /**
357
+ * Delete chant-owned objects that the apply set no longer declares.
358
+ *
359
+ * This replaces `kubectl apply --prune --selector <marker>=chant`, and keeps
360
+ * its two safety properties exactly:
361
+ *
362
+ * - **Marker-scoped.** Only objects carrying chant's managed-by label are
363
+ * candidates; the label selector is sent to the server, and the marker is
364
+ * re-checked on every object before it is deleted. An unmarked resource is
365
+ * never a candidate, so a hand-made object in the same namespace is safe.
366
+ * - **Namespace-scoped to the apply.** Namespaced kinds are swept only in the
367
+ * namespaces the apply set touched (the context's default namespace when it
368
+ * named none), which is what kubectl's prune does. A manifest that mentions
369
+ * `prod` never reaches into `staging`. Whether a kind *is* namespaced comes
370
+ * from the cluster's own discovery, never from whether a document happened
371
+ * to name a namespace — guessing that wrong is the one way this could widen
372
+ * into a cluster-wide sweep.
373
+ *
374
+ * Which *kinds* get swept is the union of the kinds the apply set contains and
375
+ * chant's default sweep set (`DEFAULT_IMPORT_TYPES` — the same list `chant
376
+ * import` uses when nothing narrows it). The union matters: a kind removed
377
+ * from source entirely appears nowhere in the apply set, and pruning only what
378
+ * is still declared would leave it behind forever. Per-namespace lists rather
379
+ * than one cluster-wide list per kind, so a namespace-scoped service account
380
+ * can still prune.
381
+ */
382
+ async function pruneOrphans(client, applied, options) {
383
+ const { candidates, retained } = await findOrphans(client, applied, options);
384
+ for (const ref of retained) {
385
+ console.log(ref.kind === "Secret"
386
+ ? `${ref.apiVersion} ${ref.kind}/${ref.name} retained (generated-once — owned and no longer declared, ` +
387
+ `but never pruned; delete it explicitly if you mean to)`
388
+ : `${ref.apiVersion} ${ref.kind}/${ref.name} retained (effect receipt: owned and never declared ` +
389
+ `in an apply set, but never pruned; the effect() step is its sole writer)`);
390
+ }
391
+ const pruned = [];
392
+ for (const ref of candidates) {
393
+ await client.delete(ref, { ...(options.signal ? { signal: options.signal } : {}) });
394
+ pruned.push(ref);
395
+ console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} pruned (chant-owned, no longer declared)`);
396
+ }
397
+ return { pruned, retained };
398
+ }
399
+ /**
400
+ * The objects {@link pruneOrphans} would delete (`candidates`) and the owned,
401
+ * undeclared ones it keeps (`retained`), found without deleting anything. A
402
+ * plan (#3998) asks this for its delete entries, so the plan and the apply
403
+ * share one prune scope.
404
+ */
405
+ async function findOrphans(client, applied, options) {
406
+ const stack = options.stack;
407
+ const selector = [
408
+ `${LABEL_OWNERSHIP_KEYS.managedBy}=${OWNERSHIP_MANAGED_BY_VALUE}`,
409
+ ...(stack ? [`${LABEL_OWNERSHIP_KEYS.stack}=${stack}`] : []),
410
+ ].join(",");
411
+ const namespaces = [...new Set(applied.map((a) => a.namespace).filter((n) => !!n))];
412
+ if (namespaces.length === 0 && applied.length > 0)
413
+ namespaces.push(client.defaultNamespace);
414
+ const declared = new Set(applied.map(refKey));
415
+ // Every kind that could hold an orphan: the apply set's own, plus the
416
+ // default sweep set so a kind deleted from source entirely is still reached.
417
+ const sweeps = new Map();
418
+ for (const entityType of DEFAULT_IMPORT_TYPES) {
419
+ const operation = operationFor(entityType);
420
+ if (!operation)
421
+ continue;
422
+ sweeps.set(`${operation.apiVersion}/${operation.kind}`, {
423
+ apiVersion: operation.apiVersion,
424
+ kind: operation.kind,
425
+ });
426
+ }
427
+ for (const ref of applied) {
428
+ if (!ref.apiVersion || !ref.kind)
429
+ continue;
430
+ sweeps.set(`${ref.apiVersion}/${ref.kind}`, { apiVersion: ref.apiVersion, kind: ref.kind });
431
+ }
432
+ // (kind, namespace) pairs to list. Scope comes from the cluster's discovery,
433
+ // which is the only thing that knows it for the version this cluster serves;
434
+ // a kind discovery does not report is dropped rather than swept blindly.
435
+ const resolved = await client.concurrently([...sweeps.values()], async (sweep) => {
436
+ const info = await client
437
+ .resolve({ apiVersion: sweep.apiVersion, kind: sweep.kind }, options.signal)
438
+ .catch(() => undefined);
439
+ return info ? { ...sweep, namespaced: info.namespaced } : undefined;
440
+ });
441
+ const targets = [];
442
+ for (const sweep of resolved) {
443
+ if (!sweep)
444
+ continue;
445
+ if (!sweep.namespaced) {
446
+ targets.push({ apiVersion: sweep.apiVersion, kind: sweep.kind });
447
+ continue;
448
+ }
449
+ for (const namespace of namespaces) {
450
+ targets.push({ apiVersion: sweep.apiVersion, kind: sweep.kind, namespace });
451
+ }
452
+ }
453
+ const found = await client.concurrently(targets, async (target) => {
454
+ try {
455
+ const items = await client.list({ apiVersion: target.apiVersion, kind: target.kind }, {
456
+ labelSelector: selector,
457
+ ...(target.namespace !== undefined ? { namespace: target.namespace } : {}),
458
+ ...(options.signal ? { signal: options.signal } : {}),
459
+ });
460
+ return items.map((item) => ({ target, item }));
461
+ }
462
+ catch {
463
+ // Kind not served here, or the read was denied. Neither is evidence that
464
+ // something should be deleted, so the sweep skips it rather than failing
465
+ // the apply that already succeeded.
466
+ return [];
467
+ }
468
+ });
469
+ const candidates = [];
470
+ const retained = [];
471
+ const seen = new Set();
472
+ for (const { target, item } of found.flat()) {
473
+ const name = item.metadata?.name;
474
+ if (!name)
475
+ continue;
476
+ // The server filtered by label already; this re-check is the one that
477
+ // matters, because a delete is not undoable.
478
+ if (!hasOwnershipMarker(item.metadata?.labels, LABEL_OWNERSHIP_KEYS))
479
+ continue;
480
+ // Already terminating — deleting again is noise, not progress.
481
+ if (item.metadata?.deletionTimestamp !== undefined)
482
+ continue;
483
+ const ref = {
484
+ apiVersion: target.apiVersion,
485
+ kind: target.kind,
486
+ name,
487
+ ...(item.metadata?.namespace !== undefined ? { namespace: item.metadata.namespace } : {}),
488
+ };
489
+ const key = refKey(ref);
490
+ if (declared.has(key) || seen.has(key))
491
+ continue;
492
+ seen.add(key);
493
+ // A generated-once Secret never enters the prunable set (#1830, epic
494
+ // #1365 decision 5): its bytes are the only copy of material chant never
495
+ // held, so no sweep may destroy them. Kept, and said loudly — deletion is
496
+ // an explicit act (`kubectl delete`, or a future gated op).
497
+ if (target.kind === "Secret" && isGeneratedOnce(item.metadata?.labels)) {
498
+ retained.push(ref);
499
+ continue;
500
+ }
501
+ // An effect receipt never enters the prunable set either (#2074, epic
502
+ // #1703 decision 3). It is chant-owned and is never in ANY apply set,
503
+ // the `effect()` step being its sole writer, which is exactly the shape
504
+ // this sweep deletes. Pruning one would tell the next plan that an effect
505
+ // never ran and re-fire it, or worse, be re-stamped by nothing and leave
506
+ // the estate re-running a migration on every apply.
507
+ if (target.kind === "ConfigMap" && isEffectReceiptObject(item.metadata?.labels)) {
508
+ retained.push(ref);
509
+ continue;
510
+ }
511
+ candidates.push(ref);
512
+ }
513
+ return { candidates, retained };
514
+ }
515
+ function refKey(ref) {
516
+ return `${ref.apiVersion}/${ref.kind}/${ref.namespace ?? ""}/${ref.name}`;
517
+ }