@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.
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/deep-observe-hooks.ts +10 -0
- package/src/op/activities/kubectl.test.ts +116 -0
- package/src/op/activities/kubectl.ts +83 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deep-observe-hooks.d.ts","sourceRoot":"","sources":["../src/deep-observe-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;
|
|
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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
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": "
|
|
53
|
+
"composite": "d2f25a5b946a9065204fc9c38ac8a5c39417015120bbdaeee93c12c2597dc397"
|
|
54
54
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -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;
|
|
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.
|
|
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.
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 ?? ""),
|