@intentius/chant-lexicon-k8s 0.41.15 → 0.41.17

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.
@@ -1 +1 @@
1
- {"version":3,"file":"deep-observe-hooks.d.ts","sourceRoot":"","sources":["../src/deep-observe-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAY,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGnG;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAcxE,CAAC;AA+EF;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAYnF;AAMD,eAAO,MAAM,yBAAyB,EAAE,sBAWvC,CAAC"}
1
+ {"version":3,"file":"deep-observe-hooks.d.ts","sourceRoot":"","sources":["../src/deep-observe-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAY,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAYnG;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAcxE,CAAC;AA+EF;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAYnF;AAMD,eAAO,MAAM,yBAAyB,EAAE,sBAWvC,CAAC"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "de51ffb566f7b4346cfb33021dbca84d7e7d7a3d0c096640dbe1d38f831187ee",
4
+ "manifest.json": "579bf4095e47e58c245aa6e2531cabeaf1b6b558a25a5e1090810d9bf31f2675",
5
5
  "meta.json": "c0b141b882c51483c1d1aead748a0eb171aceca7432edd2a0404b062bf6d7831",
6
6
  "types/index.d.ts": "3be2af7494aca94adfa16269cb9c4e7a155529872207a3d70b0c4d39914f9eec",
7
7
  "rules/argo-appset-single-project.ts": "afa9f310753aa2d475f35012b13135b2dfaebed2a35d44edbe185ebc07673674",
@@ -50,5 +50,5 @@
50
50
  "skills/chant-k8s-aks.md": "e18f0e2b055f72cd7a37deaf258d7027c2d4d3e286e8fd4975b27a1f981a3ad9",
51
51
  "skills/chant-k8s-argo.md": "b1a0b826559d8c5033a479c5781efaf650320f0aee4419d8841170bd3393cea5"
52
52
  },
53
- "composite": "2a0b689bdb3e0a2d672e703b751bf91685ee52070385d52fb03c997cc06afb36"
53
+ "composite": "d2f25a5b946a9065204fc9c38ac8a5c39417015120bbdaeee93c12c2597dc397"
54
54
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.41.15",
3
+ "version": "0.41.17",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
@@ -1 +1 @@
1
- {"version":3,"file":"kubectl.d.ts","sourceRoot":"","sources":["../../../src/op/activities/kubectl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAaH,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAI3E;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,YAAY,GAAG,OAAO,CAAC;AAE/D,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,oCAAoC;AACpC,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,sCAAsC;AACtC,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,sFAAsF;IACtF,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAiB7E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjF;AAoCD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,mBAAmB,CAAC,CAoD9B;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,IAAI,CAAC,CAEf"}
1
+ {"version":3,"file":"kubectl.d.ts","sourceRoot":"","sources":["../../../src/op/activities/kubectl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAaH,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAI3E;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,YAAY,GAAG,OAAO,CAAC;AAE/D,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,oCAAoC;AACpC,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,sCAAsC;AACtC,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,sFAAsF;IACtF,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAiB7E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjF;AAmGD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,mBAAmB,CAAC,CAkE9B;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,IAAI,CAAC,CAEf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-k8s",
3
- "version": "0.41.15",
3
+ "version": "0.41.17",
4
4
  "description": "Kubernetes lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -84,7 +84,7 @@
84
84
  },
85
85
  "peerDependencies": {
86
86
  "zod": "^4.3.6",
87
- "@intentius/chant": "^0.41.15",
87
+ "@intentius/chant": "^0.41.17",
88
88
  "typescript": "^5.9.3"
89
89
  }
90
90
  }
@@ -41,9 +41,19 @@
41
41
  * keyed by chant's own k8s entityType catalog.
42
42
  */
43
43
 
44
+ import { createRequire } from "module";
44
45
  import type { DeepArrayElement, DeepNode, DeepNormalizationHooks } from "@intentius/chant/lexicon";
45
46
  import { K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS, k8sListMapOrderKey } from "@intentius/chant/managed-fields";
46
47
 
48
+ // This module ships as ESM (tsx strips types from src/ directly), where a bare
49
+ // `require` is not defined — the same reason serializer.ts and the LSP modules
50
+ // already build one. The bug only surfaced on a LIVE deep read that meets an
51
+ // associative list (every k8s estate does: a Deployment's containers), so
52
+ // `lifecycle diff --live` crashed with `require is not defined` on exactly the
53
+ // estates the generated table exists for (#1441 regression, found on
54
+ // kubemicrovm-ops).
55
+ const require = createRequire(import.meta.url);
56
+
47
57
  /**
48
58
  * Kubernetes-defaulted fields, per entity type, as index-erased property
49
59
  * paths. Subtracted only where source never declared the property
@@ -132,6 +132,122 @@ describe("kubectlApply", () => {
132
132
  }
133
133
  });
134
134
 
135
+ test("the apply stamps its resolved stack onto every document's labels — the identity the prune and the status sweep query", async () => {
136
+ // The serializer bakes the PROJECT stack (`ownership.stack`) into what it
137
+ // emits, but a component's kubectl-apply step applies under its own unit
138
+ // stack — and that is what both label readers select on. A manifest with
139
+ // the project label (or none at all: upstream CRDs) must leave the apply
140
+ // carrying the unit's identity, or the unit reads absent forever.
141
+ const file = join(dir, "k8s.yaml");
142
+ const labeled = deploymentYaml.replace(
143
+ " name: web\n namespace: prod\n",
144
+ " name: web\n namespace: prod\n labels:\n app.kubernetes.io/managed-by: chant\n chant.intentius.io/stack: whole-project\n chant.intentius.io/env: dev\n",
145
+ );
146
+ writeFileSync(file, labeled);
147
+ const cluster = fakeCluster({ respond: echoApplies });
148
+
149
+ await applyManifest({ manifest: file, stack: "kmv-workload" }, undefined, cluster.connector);
150
+
151
+ const bodies = cluster.layer.requests
152
+ .filter((r) => r.method === "PATCH")
153
+ .map((r) => JSON.parse(String(r.body)) as { metadata?: { labels?: Record<string, string> } });
154
+ expect(bodies).toHaveLength(2);
155
+ for (const body of bodies) {
156
+ expect(body.metadata?.labels).toMatchObject({
157
+ "app.kubernetes.io/managed-by": "chant",
158
+ "chant.intentius.io/stack": "kmv-workload",
159
+ });
160
+ }
161
+ // The env label is the serializer's channel — restamped never, kept as-is.
162
+ expect(bodies[0].metadata?.labels?.["chant.intentius.io/env"]).toBe("dev");
163
+ // The previously-unlabeled Service now carries the marker too.
164
+ expect(bodies[1].metadata?.labels?.["chant.intentius.io/env"]).toBeUndefined();
165
+ });
166
+
167
+ test("a conflict owned entirely by chant's own managers is retaken with force — the stack-label migration path", async () => {
168
+ // An estate applied before its components named per-unit stacks has every
169
+ // object's labels owned by `chant:<ownership.stack>`. The re-stamp under
170
+ // the unit's manager conflicts — with chant itself. Refusing that forever
171
+ // would strand the estate with no non-force path (the capability exposes
172
+ // no force flag), so chant retakes its OWN fields, once, per document.
173
+ const file = join(dir, "k8s.yaml");
174
+ writeFileSync(file, deploymentYaml);
175
+ let patches = 0;
176
+ const cluster = fakeCluster({
177
+ respond: (req) => {
178
+ if (req.method !== "PATCH") return undefined;
179
+ patches++;
180
+ const forced = req.query.force === "true";
181
+ if (forced) return { body: JSON.parse(String(req.body)) };
182
+ return {
183
+ status: 409,
184
+ body: {
185
+ ...statusBody(409, "Conflict", 'Apply failed with 1 conflict: conflict with "chant:kubemicrovm-ops"'),
186
+ details: {
187
+ causes: [
188
+ {
189
+ type: "FieldManagerConflict",
190
+ message: 'conflict with "chant:kubemicrovm-ops"',
191
+ field: ".metadata.labels.chant.intentius.io/stack",
192
+ },
193
+ ],
194
+ },
195
+ },
196
+ };
197
+ },
198
+ });
199
+
200
+ const result = await applyManifest({ manifest: file, stack: "kmv-workload" }, undefined, cluster.connector);
201
+ expect(result.applied).toHaveLength(2);
202
+ // Each of the two documents: one refused apply + one forced retake.
203
+ expect(patches).toBe(4);
204
+ const requests = cluster.layer.requests.filter((r) => r.method === "PATCH");
205
+ expect(requests.map((r) => r.query.force)).toEqual(["false", "true", "false", "true"]);
206
+ });
207
+
208
+ test("a conflict involving any FOREIGN manager still refuses — self-retake never widens", async () => {
209
+ const file = join(dir, "k8s.yaml");
210
+ writeFileSync(file, deploymentYaml);
211
+ const cluster = fakeCluster({
212
+ respond: (req) =>
213
+ req.method === "PATCH"
214
+ ? {
215
+ status: 409,
216
+ body: {
217
+ ...statusBody(409, "Conflict", "Apply failed with 2 conflicts"),
218
+ details: {
219
+ causes: [
220
+ { type: "FieldManagerConflict", message: 'conflict with "chant:old"', field: ".metadata.labels.chant.intentius.io/stack" },
221
+ { type: "FieldManagerConflict", message: 'conflict with "helm"', field: ".spec.replicas" },
222
+ ],
223
+ },
224
+ },
225
+ }
226
+ : undefined,
227
+ });
228
+
229
+ const err = (await applyManifest({ manifest: file, stack: "kmv-workload" }, undefined, cluster.connector).catch(
230
+ (e: unknown) => e,
231
+ )) as Error;
232
+ expect(err.name).toBe("FieldManagerConflictError");
233
+ // No forced retry was attempted.
234
+ expect(cluster.layer.requests.filter((r) => r.method === "PATCH" && r.query.force === "true")).toHaveLength(0);
235
+ });
236
+
237
+ test("no resolvable stack applies the documents verbatim — no marker invented", async () => {
238
+ const file = join(dir, "k8s.yaml");
239
+ writeFileSync(file, deploymentYaml);
240
+ const cluster = fakeCluster({ respond: echoApplies });
241
+
242
+ // No stack arg, no project config: identity resolution yields none.
243
+ await applyManifest({ manifest: file, cwd: dir }, undefined, cluster.connector);
244
+
245
+ const body = JSON.parse(
246
+ String(cluster.layer.requests.find((r) => r.method === "PATCH")!.body),
247
+ ) as { metadata?: { labels?: Record<string, string> } };
248
+ expect(body.metadata?.labels).toBeUndefined();
249
+ });
250
+
135
251
  test("an explicit context is honored and skips the environment lookup entirely", async () => {
136
252
  const file = join(dir, "k8s.yaml");
137
253
  writeFileSync(file, deploymentYaml);
@@ -191,6 +191,69 @@ async function resolveApplyIdentity(
191
191
  };
192
192
  }
193
193
 
194
+ /**
195
+ * Structural mirror of the client's `FieldManagerConflictError` surface —
196
+ * matched by `name` + shape, NOT `instanceof`, because a static value import
197
+ * of `@intentius/chant-k8s-client` here would put the API-client chain on the
198
+ * build path (the #1074 boundary; same reason the applier itself is reached
199
+ * by dynamic import).
200
+ */
201
+ interface ConflictErrorLike {
202
+ name: string;
203
+ conflicts?: Array<{ manager?: unknown }>;
204
+ }
205
+
206
+ /**
207
+ * Is every contested field in this conflict owned by chant itself (under any
208
+ * of its field managers — `chant` bare or `chant:<stack>`)? True means the
209
+ * conflict is a self-migration, never a dispute with another tool.
210
+ */
211
+ function isChantSelfConflict(err: unknown): boolean {
212
+ const e = err as ConflictErrorLike;
213
+ if (!e || e.name !== "FieldManagerConflictError" || !Array.isArray(e.conflicts) || e.conflicts.length === 0) return false;
214
+ return e.conflicts.every(
215
+ (c) => typeof c.manager === "string" && (c.manager === "chant" || c.manager.startsWith("chant:")),
216
+ );
217
+ }
218
+
219
+ /**
220
+ * Stamp the apply's own stack identity onto a document's labels.
221
+ *
222
+ * The serializer bakes `ownership.stack` — the *project* identity from
223
+ * `chant.config.ts` — into every manifest it emits. But the apply's identity
224
+ * is the *stack argument it resolved* (a component's `kubectl-apply` step
225
+ * names its own deploy unit, e.g. `kmv-workload`), and that is the value both
226
+ * consumers of the label query for: the owned-only prune's selector and
227
+ * `describeStackStatus`'s presence sweep. Applying the manifest verbatim left
228
+ * the label saying `kubemicrovm-ops` while both readers asked for
229
+ * `kmv-workload` — the prune silently matched nothing and every kubectl-apply
230
+ * unit read absent in `components status --live` (a green estate painted
231
+ * dead). A manifest chant never serialized (upstream CRDs pinned into a
232
+ * `crds` unit) carried no marker at all, with the same two failures.
233
+ *
234
+ * So the apply now re-stamps what it applies: `managed-by=chant` plus the
235
+ * resolved stack. The ownership *verdict* keys on `managed-by` alone
236
+ * (core/ownership.ts `hasOwnershipMarker`), so overlay classification is
237
+ * unchanged; the env label is the serializer's to write and is left as-is.
238
+ * No stack resolved (no explicit arg, no project ownership) applies verbatim,
239
+ * exactly as before.
240
+ */
241
+ function stampOwnership(document: K8sObject, stack: string | undefined): K8sObject {
242
+ if (stack === undefined) return document;
243
+ const metadata = (document.metadata ?? {}) as { labels?: Record<string, string> };
244
+ return {
245
+ ...document,
246
+ metadata: {
247
+ ...metadata,
248
+ labels: {
249
+ ...(metadata.labels ?? {}),
250
+ [LABEL_OWNERSHIP_KEYS.managedBy]: OWNERSHIP_MANAGED_BY_VALUE,
251
+ [LABEL_OWNERSHIP_KEYS.stack]: stack,
252
+ },
253
+ },
254
+ } as K8sObject;
255
+ }
256
+
194
257
  /**
195
258
  * Apply every document in `args.manifest`, then prune when asked.
196
259
  *
@@ -219,12 +282,26 @@ export async function applyManifest(
219
282
 
220
283
  const applied: AppliedRef[] = [];
221
284
  for (const document of documents) {
222
- const result = await client.apply(document as K8sObject, {
223
- fieldManager,
224
- force: args.force ?? false,
225
- dryRun: args.dryRun,
226
- signal,
227
- });
285
+ const stamped = stampOwnership(document as K8sObject, stack);
286
+ let result: K8sObject;
287
+ try {
288
+ result = await client.apply(stamped, {
289
+ fieldManager,
290
+ force: args.force ?? false,
291
+ dryRun: args.dryRun,
292
+ signal,
293
+ });
294
+ } catch (err) {
295
+ // "chant never forces a conflict on its own" is about taking fields
296
+ // from ANOTHER tool. A conflict where every contested field is owned
297
+ // by another `chant:*` manager is chant contesting itself — the
298
+ // ownership-stack → unit-stack label migration, or a renamed deploy
299
+ // unit — and refusing that forever would strand every estate applied
300
+ // before the rename with no non-force path back. Retake those fields
301
+ // deliberately, once, and only when no foreign manager is involved.
302
+ if (!isChantSelfConflict(err)) throw err;
303
+ result = await client.apply(stamped, { fieldManager, force: true, dryRun: args.dryRun, signal });
304
+ }
228
305
  const ref: AppliedRef = {
229
306
  apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
230
307
  kind: String(result.kind ?? document.kind ?? ""),