@intentius/chant-lexicon-k8s 0.41.16 → 0.41.18

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,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "528b5cb5e20dcb4ab991f8ab6d98da91f361d833b65d8e40bdb62867498ddc0e",
4
+ "manifest.json": "c2f3553d27f37ccedf78b608acf76ee427dcd03bdc832f0a9ffbb4da78ff9bd9",
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": "f895a3588933ce1fa536eeceb58265524ea731115d96b1ca7b3a93df9c19d1e1"
53
+ "composite": "719807c3da16afea57ea7a7709cc29007e28cd3c8d7ab094c273794fa95fbb9e"
54
54
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.41.16",
3
+ "version": "0.41.18",
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;AA0ED;;;;;;;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;AAqHD;;;;;;;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.16",
3
+ "version": "0.41.18",
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.16",
87
+ "@intentius/chant": "^0.41.18",
88
88
  "typescript": "^5.9.3"
89
89
  }
90
90
  }
@@ -164,6 +164,109 @@ describe("kubectlApply", () => {
164
164
  expect(bodies[1].metadata?.labels?.["chant.intentius.io/env"]).toBeUndefined();
165
165
  });
166
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 controller echoing chant's marker label is retaken too — chant owns its reserved channel", async () => {
209
+ // An operator that reconciles whole objects becomes SSA owner of labels it
210
+ // merely echoed (observed live: kubemicrovm's microvmimagereconciler
211
+ // owning .metadata.labels.chant.intentius.io/stack). It never chose that
212
+ // value; the marker namespace is chant's.
213
+ const file = join(dir, "k8s.yaml");
214
+ writeFileSync(file, deploymentYaml);
215
+ const cluster = fakeCluster({
216
+ respond: (req) => {
217
+ if (req.method !== "PATCH") return undefined;
218
+ if (req.query.force === "true") return { body: JSON.parse(String(req.body)) };
219
+ return {
220
+ status: 409,
221
+ body: {
222
+ ...statusBody(409, "Conflict", 'Apply failed with 1 conflict: conflict with "microvmimagereconciler"'),
223
+ details: {
224
+ causes: [
225
+ {
226
+ type: "FieldManagerConflict",
227
+ message: 'conflict with "microvmimagereconciler"',
228
+ field: ".metadata.labels.chant.intentius.io/stack",
229
+ },
230
+ ],
231
+ },
232
+ },
233
+ };
234
+ },
235
+ });
236
+
237
+ const result = await applyManifest({ manifest: file, stack: "kmv-workload" }, undefined, cluster.connector);
238
+ expect(result.applied).toHaveLength(2);
239
+ });
240
+
241
+ test("a conflict involving any FOREIGN manager still refuses — self-retake never widens", async () => {
242
+ const file = join(dir, "k8s.yaml");
243
+ writeFileSync(file, deploymentYaml);
244
+ const cluster = fakeCluster({
245
+ respond: (req) =>
246
+ req.method === "PATCH"
247
+ ? {
248
+ status: 409,
249
+ body: {
250
+ ...statusBody(409, "Conflict", "Apply failed with 2 conflicts"),
251
+ details: {
252
+ causes: [
253
+ { type: "FieldManagerConflict", message: 'conflict with "chant:old"', field: ".metadata.labels.chant.intentius.io/stack" },
254
+ { type: "FieldManagerConflict", message: 'conflict with "helm"', field: ".spec.replicas" },
255
+ ],
256
+ },
257
+ },
258
+ }
259
+ : undefined,
260
+ });
261
+
262
+ const err = (await applyManifest({ manifest: file, stack: "kmv-workload" }, undefined, cluster.connector).catch(
263
+ (e: unknown) => e,
264
+ )) as Error;
265
+ expect(err.name).toBe("FieldManagerConflictError");
266
+ // No forced retry was attempted.
267
+ expect(cluster.layer.requests.filter((r) => r.method === "PATCH" && r.query.force === "true")).toHaveLength(0);
268
+ });
269
+
167
270
  test("no resolvable stack applies the documents verbatim — no marker invented", async () => {
168
271
  const file = join(dir, "k8s.yaml");
169
272
  writeFileSync(file, deploymentYaml);
@@ -191,6 +191,49 @@ 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; field?: unknown }>;
204
+ }
205
+
206
+ /** chant's reserved label channel — the `chant.intentius.io/*` keys the
207
+ * ownership marker lives under, as a conflict-cause field-path prefix. */
208
+ const MARKER_FIELD_PREFIX = `.metadata.labels.${LABEL_OWNERSHIP_KEYS.stack.split("/")[0]}/`;
209
+
210
+ /**
211
+ * Is this conflict one chant may retake without a human? Two shapes qualify,
212
+ * per contested field:
213
+ *
214
+ * - the owning manager is chant itself (`chant` bare or `chant:<stack>`) —
215
+ * the ownership-stack → unit-stack migration, or a renamed deploy unit; or
216
+ * - the field lives in chant's OWN reserved label channel
217
+ * (`chant.intentius.io/*`). A controller that reconciles whole objects
218
+ * echoes existing labels and becomes their SSA owner as a side effect
219
+ * (observed live: kubemicrovm's `microvmimagereconciler` owning
220
+ * `.metadata.labels.chant.intentius.io/stack`) — it never chose that value,
221
+ * and chant is the authority for its marker namespace no matter who last
222
+ * echoed it.
223
+ *
224
+ * Any contested field outside both shapes — a spec field, an app label —
225
+ * keeps the presented refusal: that IS a dispute with another tool.
226
+ */
227
+ function isChantSelfConflict(err: unknown): boolean {
228
+ const e = err as ConflictErrorLike;
229
+ if (!e || e.name !== "FieldManagerConflictError" || !Array.isArray(e.conflicts) || e.conflicts.length === 0) return false;
230
+ return e.conflicts.every((c) => {
231
+ const chantManager = typeof c.manager === "string" && (c.manager === "chant" || c.manager.startsWith("chant:"));
232
+ const markerField = typeof c.field === "string" && c.field.startsWith(MARKER_FIELD_PREFIX);
233
+ return chantManager || markerField;
234
+ });
235
+ }
236
+
194
237
  /**
195
238
  * Stamp the apply's own stack identity onto a document's labels.
196
239
  *
@@ -257,12 +300,26 @@ export async function applyManifest(
257
300
 
258
301
  const applied: AppliedRef[] = [];
259
302
  for (const document of documents) {
260
- const result = await client.apply(stampOwnership(document as K8sObject, stack), {
261
- fieldManager,
262
- force: args.force ?? false,
263
- dryRun: args.dryRun,
264
- signal,
265
- });
303
+ const stamped = stampOwnership(document as K8sObject, stack);
304
+ let result: K8sObject;
305
+ try {
306
+ result = await client.apply(stamped, {
307
+ fieldManager,
308
+ force: args.force ?? false,
309
+ dryRun: args.dryRun,
310
+ signal,
311
+ });
312
+ } catch (err) {
313
+ // "chant never forces a conflict on its own" is about taking fields
314
+ // from ANOTHER tool. A conflict where every contested field is owned
315
+ // by another `chant:*` manager is chant contesting itself — the
316
+ // ownership-stack → unit-stack label migration, or a renamed deploy
317
+ // unit — and refusing that forever would strand every estate applied
318
+ // before the rename with no non-force path back. Retake those fields
319
+ // deliberately, once, and only when no foreign manager is involved.
320
+ if (!isChantSelfConflict(err)) throw err;
321
+ result = await client.apply(stamped, { fieldManager, force: true, dryRun: args.dryRun, signal });
322
+ }
266
323
  const ref: AppliedRef = {
267
324
  apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
268
325
  kind: String(result.kind ?? document.kind ?? ""),