@intentius/chant-lexicon-k8s 0.124.41 → 0.124.43

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.
@@ -159,6 +159,12 @@ export interface KubectlApplyOutputField {
159
159
  /** The field's dot path in the object; a list index is a number, `status.loadBalancer.ingress.0.hostname`. */
160
160
  field: string;
161
161
  }
162
+ /**
163
+ * The documents `targets` name (#4300), each `Kind/namespace/name` or
164
+ * `Kind/name` as {@link addressOf} writes it, in manifest order. Throws for
165
+ * a target the manifest does not declare.
166
+ */
167
+ export declare function targetedDocuments(member: string, manifest: string, documents: Record<string, unknown>[], targets: readonly string[]): Record<string, unknown>[];
162
168
  /** Factory with an injectable applier, so tests assert the delegation without
163
169
  * a cluster — the same seam `createGenerateSbomCapability` uses. The default
164
170
  * applier is resolved by dynamic import on first run, keeping the API-client
@@ -1 +1 @@
1
- {"version":3,"file":"kubectl-apply.d.ts","sourceRoot":"","sources":["../../src/components/kubectl-apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,UAAU,EAAiD,MAAM,wCAAwC,CAAC;AAOxH;;;;;;;;;;GAUG;AACH,UAAU,UAAU;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,4DAA4D;AAC5D,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,sFAAsF;IACtF,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;IACxB,2GAA2G;IAC3G,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,yGAAyG;IACzG,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,OAAO,CAAC;CAC/C;AAED,KAAK,OAAO,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;AACnE,KAAK,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;AAElE,wFAAwF;AACxF,UAAU,gBAAgB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,oDAAoD;AACpD,UAAU,kBAAkB;IAC1B,GAAG,EAAE,UAAU,CAAC;IAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,uFAAuF;AACvF,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,KAAK,OAAO,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAElE,0DAA0D;AAC1D,UAAU,cAAc;IACtB,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,gGAAgG;AAChG,KAAK,UAAU,GAAG,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;AAEhG,qEAAqE;AACrE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CACtC;AAOD;;;;;;GAMG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;aAIjC,MAAM,EAAE,MAAM;aACd,QAAQ,EAAE,MAAM;aAChB,KAAK,EAAE,MAAM;IAL/B,QAAQ,CAAC,SAAS,QAAQ;IAC1B,QAAQ,CAAC,QAAQ,KAAK;IACtB,YACkB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EAO9B;CACF;AA2GD,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;IAChC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;CACnD;AAED,oDAAoD;AACpD,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,8GAA8G;IAC9G,KAAK,EAAE,MAAM,CAAC;CACf;AA4CD;;;uCAGuC;AACvC,wBAAgB,4BAA4B,CAC1C,KAAK,CAAC,EAAE,OAAO,EACf,IAAI,CAAC,EAAE,cAAc,EACrB,MAAM,CAAC,EAAE,OAAO,EAChB,QAAQ,CAAC,EAAE,UAAU,GACpB,UAAU,CAAC,iBAAiB,EAAE,mBAAmB,CAAC,CAsGpD;AAED,eAAO,MAAM,sBAAsB,oDAAiC,CAAC"}
1
+ {"version":3,"file":"kubectl-apply.d.ts","sourceRoot":"","sources":["../../src/components/kubectl-apply.ts"],"names":[],"mappings":"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"}
@@ -157,6 +157,36 @@ async function readOutputs(read, refs, input, where) {
157
157
  }
158
158
  return out;
159
159
  }
160
+ /**
161
+ * The documents `targets` name (#4300), each `Kind/namespace/name` or
162
+ * `Kind/name` as {@link addressOf} writes it, in manifest order. Throws for
163
+ * a target the manifest does not declare.
164
+ */
165
+ export function targetedDocuments(member, manifest, documents, targets) {
166
+ const addressOfDoc = (doc) => {
167
+ const metadata = (doc.metadata ?? {});
168
+ return addressOf({
169
+ apiVersion: String(doc.apiVersion ?? ""),
170
+ kind: String(doc.kind ?? ""),
171
+ name: String(metadata.name ?? ""),
172
+ ...(metadata.namespace !== undefined ? { namespace: String(metadata.namespace) } : {}),
173
+ });
174
+ };
175
+ const declared = new Set(documents.map(addressOfDoc));
176
+ const missing = targets.filter((t) => !declared.has(t));
177
+ if (missing.length > 0) {
178
+ throw new Error(`kubectl-apply "${member}": --target ${missing.join(", ")} names no object ${manifest} declares ` +
179
+ `(it declares ${[...declared].join(", ") || "nothing"})`);
180
+ }
181
+ const wanted = new Set(targets);
182
+ return documents.filter((d) => wanted.has(addressOfDoc(d)));
183
+ }
184
+ /**
185
+ * The input a targeted step plans and applies with (#4300): the same, with no
186
+ * prune. A prune over a narrowed set would delete every owned object the
187
+ * targets leave out.
188
+ */
189
+ const narrowedInput = (ctx, input) => ctx.targets && ctx.targets.length > 0 ? { ...input, delete: "never" } : input;
160
190
  /** Factory with an injectable applier, so tests assert the delegation without
161
191
  * a cluster — the same seam `createGenerateSbomCapability` uses. The default
162
192
  * applier is resolved by dynamic import on first run, keeping the API-client
@@ -205,28 +235,61 @@ export function createKubectlApplyCapability(apply, read, dryRun, readLive) {
205
235
  }
206
236
  return { known, unknown: unknown.sort() };
207
237
  };
238
+ /**
239
+ * {@link plannedOutputs} for a targeted plan (#4300): an output of a
240
+ * targeted object as the plan says, and one of an object the targets leave
241
+ * out as the cluster holds it now, since the apply leaves that object alone.
242
+ */
243
+ const targetedPlannedOutputs = async (ctx, input, member, documents, dry, targets, declared) => {
244
+ if (!targets)
245
+ return plannedOutputs(ctx, input, member, documents, dry);
246
+ const wanted = new Set(targets);
247
+ const all = Object.entries(input.outputs ?? {});
248
+ const inside = Object.fromEntries(all.filter(([, spec]) => wanted.has(spec.object)));
249
+ const outside = Object.fromEntries(all.filter(([, spec]) => !wanted.has(spec.object)));
250
+ const planned = Object.keys(inside).length > 0 ? await plannedOutputs(ctx, { ...input, outputs: inside }, member, documents, dry) : { known: {}, unknown: [] };
251
+ if (Object.keys(outside).length === 0)
252
+ return planned;
253
+ const rest = { ...input, outputs: outside };
254
+ const live = await readOutputs(await liveReader(), outputRefs(member, rest, declared), rest, where(ctx, input));
255
+ const known = { ...planned.known, ...live };
256
+ const unknown = [...planned.unknown, ...Object.keys(outside).filter((n) => !(n in live))].sort();
257
+ return { known, unknown };
258
+ };
208
259
  return {
209
260
  kind: "kubectl-apply",
210
261
  // A server-side apply has no native undo (the previous object state is not
211
262
  // kept by the API server), so COMP003 requires the component to acknowledge
212
263
  // the compensation gap — the same posture as s3-sync/run-migration.
213
264
  rollbackPolicy: "needs-opt-out",
265
+ // #4300: a target names one object, `Kind/namespace/name`; the plan and the apply send those alone.
266
+ targets: "accept",
214
267
  /**
215
268
  * Dry-run the manifest against the cluster (#3998): the documents `run`
216
269
  * would apply, release attributes set, server-side applied with
217
270
  * `dryRun=All` as the same field manager, each compared with its live
218
271
  * object. The member is the step's `stack`, or the component.
219
272
  */
220
- async plan(ctx, input) {
273
+ async plan(ctx, stepInput) {
274
+ const input = narrowedInput(ctx, stepInput);
221
275
  const member = input.stack ?? ctx.component;
222
- const documents = (read ?? (await activities()).readManifestDocuments)(input.manifest);
276
+ const read_ = (read ?? (await activities()).readManifestDocuments)(input.manifest);
277
+ const targets = ctx.targets && ctx.targets.length > 0 ? ctx.targets : undefined;
278
+ const documents = targets ? targetedDocuments(member, input.manifest, read_, targets) : read_;
223
279
  const suffix = releaseAttributesSuffix(ctx.release);
224
280
  if (suffix !== "")
225
281
  annotateRelease(documents, suffix);
226
282
  const { result, planDigest } = await dryRunOf(ctx, input, member, documents);
227
283
  const artifact = { kind: "kubectl-apply-plan", planDigest, documents };
228
- const outputs = input.outputs ? await plannedOutputs(ctx, input, member, documents, result) : undefined;
229
- return { member, planDigest, artifact, changeSet: changeSetPartOf(member, planDigest, result), ...(outputs ? { outputs } : {}) };
284
+ const outputs = input.outputs ? await targetedPlannedOutputs(ctx, input, member, documents, result, targets, read_) : undefined;
285
+ return {
286
+ member,
287
+ planDigest,
288
+ artifact,
289
+ changeSet: changeSetPartOf(member, planDigest, result),
290
+ ...(outputs ? { outputs } : {}),
291
+ ...(targets ? { targets } : {}),
292
+ };
230
293
  },
231
294
  /**
232
295
  * The fields the step names as outputs, as the cluster holds them now
@@ -239,7 +302,8 @@ export function createKubectlApplyCapability(apply, read, dryRun, readLive) {
239
302
  const documents = (read ?? (await activities()).readManifestDocuments)(input.manifest);
240
303
  return liveOutputs(ctx, input, documents);
241
304
  },
242
- async run(ctx, input) {
305
+ async run(ctx, stepInput) {
306
+ const input = narrowedInput(ctx, stepInput);
243
307
  const member = input.stack ?? ctx.component;
244
308
  const approved = ctx.plans?.[member];
245
309
  if (isPlanArtifact(approved)) {
@@ -258,7 +322,13 @@ export function createKubectlApplyCapability(apply, read, dryRun, readLive) {
258
322
  // workload the build stamped, in what it applies; the committed manifest is not touched.
259
323
  const suffix = releaseAttributesSuffix(ctx.release);
260
324
  let documents;
261
- if (suffix !== "") {
325
+ if (ctx.targets && ctx.targets.length > 0) {
326
+ // #4300: only the targeted objects, and no prune (`narrowedInput`).
327
+ documents = targetedDocuments(member, input.manifest, (read ?? loaded.readManifestDocuments)(input.manifest), ctx.targets);
328
+ if (suffix !== "")
329
+ annotateRelease(documents, suffix);
330
+ }
331
+ else if (suffix !== "") {
262
332
  documents = (read ?? loaded.readManifestDocuments)(input.manifest);
263
333
  if (annotateRelease(documents, suffix) === 0)
264
334
  documents = undefined;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "9b2a09dba2af9e0c71158f1a6372fdc6c1e29cec5c9ade9f271e6d3d38b7375b",
4
+ "manifest.json": "0bb2e0d248aa5b1808d3d4ad070e78463f9c935177c09c7bb1fefaeb0d9b4b64",
5
5
  "meta.json": "c1831ac15083f5e0e45b29698d7b77f1c01646240fe6de5d6320753188594f69",
6
6
  "types/index.d.ts": "5c5340046d090ae9db5553039d923cf92a8b2eb57aa4dc941273ed5bcfb5bc9d",
7
7
  "rules/argo-appset-single-project.ts": "afa9f310753aa2d475f35012b13135b2dfaebed2a35d44edbe185ebc07673674",
@@ -78,5 +78,5 @@
78
78
  "skills/chant-k8s-argo.md": "24c9a85b78a2fc5c6657424baac9cc6d6caff6f47605b8146b2c0082133181cf",
79
79
  "skills/chant-k8s-flux.md": "9670efc2e9854bd14686b5adeb707396b9030a4dcaee6529b30abf56efe2c50c"
80
80
  },
81
- "composite": "5e8e9876fad16ad70ca9496f6f9a383ecb8632e45189ff2f89b1a57d28b0daf0"
81
+ "composite": "fe30957da3c3498c28db61f85f707151d9f8a5ef6297632f8e080bcbf6ff5f68"
82
82
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.124.41",
3
+ "version": "0.124.43",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-k8s",
3
- "version": "0.124.41",
3
+ "version": "0.124.43",
4
4
  "description": "Kubernetes lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -72,13 +72,13 @@
72
72
  "build": "../../scripts/tsc7.sh -p tsconfig.build.json --sourceMap false && tsc-alias -p tsconfig.build.json && cp src/generated/index.d.ts dist/generated/index.d.d.ts"
73
73
  },
74
74
  "dependencies": {
75
- "@intentius/chant-lexicon-otel": "^0.124.41",
76
- "@intentius/chant-lexicon-prometheus": "^0.124.41",
75
+ "@intentius/chant-lexicon-otel": "^0.124.43",
76
+ "@intentius/chant-lexicon-prometheus": "^0.124.43",
77
77
  "js-yaml": "^4.3.1",
78
78
  "@types/js-yaml": "^4.0.9"
79
79
  },
80
80
  "optionalDependencies": {
81
- "@intentius/chant-k8s-client": "^0.124.41"
81
+ "@intentius/chant-k8s-client": "^0.124.43"
82
82
  },
83
83
  "devDependencies": {
84
84
  "@intentius/chant": "*",
@@ -86,6 +86,6 @@
86
86
  },
87
87
  "peerDependencies": {
88
88
  "zod": "^4.3.6",
89
- "@intentius/chant": "^0.124.41"
89
+ "@intentius/chant": "^0.124.43"
90
90
  }
91
91
  }
@@ -388,3 +388,52 @@ describe("kubectl-apply outputs: the fields a step names (#3999)", () => {
388
388
  expect((await cap.plan!(ctx, { manifest: "k8s.yaml", stack: "web" })).outputs).toBeUndefined();
389
389
  });
390
390
  });
391
+
392
+ describe("kubectl-apply targets: one object at a time (#4300)", () => {
393
+ type DryRun = import("./kubectl-apply").KubectlApplyDryRun;
394
+ const doc = (kind: string, name: string, namespace?: string) => ({
395
+ apiVersion: kind === "Deployment" ? "apps/v1" : "v1",
396
+ kind,
397
+ metadata: { name, ...(namespace ? { namespace } : {}) },
398
+ });
399
+ const docs = () => [doc("Deployment", "web", "prod"), doc("Service", "web", "prod"), doc("Namespace", "prod")];
400
+ const none = (): DryRun => ({ fieldManager: "chant:web", stack: "web", objects: [], deletes: [], retained: [] });
401
+ const noApply = async (): Promise<never> => {
402
+ throw new Error("applied");
403
+ };
404
+
405
+ test("accepts targets", () => {
406
+ expect(createKubectlApplyCapability().targets).toBe("accept");
407
+ });
408
+
409
+ test("plans the targeted objects alone, with no prune, and gives the targets back", async () => {
410
+ let seen: Record<string, unknown> = {};
411
+ const cap = createKubectlApplyCapability(noApply, docs, async (args) => {
412
+ seen = args as never;
413
+ return none();
414
+ });
415
+ const ctx = { env: "prod", component: "api", targets: ["Namespace/prod", "Service/prod/web"] };
416
+ const planned = await cap.plan!(ctx, { manifest: "k8s.yaml", stack: "web", delete: "owned-only" });
417
+ expect(seen).toMatchObject({ documents: [doc("Service", "web", "prod"), doc("Namespace", "prod")], deleteMode: "never" });
418
+ expect(planned.targets).toEqual(["Namespace/prod", "Service/prod/web"]);
419
+ const whole = await cap.plan!({ env: "prod", component: "api" }, { manifest: "k8s.yaml", stack: "web", delete: "owned-only" });
420
+ expect(whole.planDigest).not.toBe(planned.planDigest);
421
+ });
422
+
423
+ test("a target the manifest does not declare refuses and names what it declares", async () => {
424
+ const cap = createKubectlApplyCapability(noApply, docs, async () => none());
425
+ await expect(cap.plan!({ env: "prod", component: "api", targets: ["Deployment/prod/api"] }, { manifest: "k8s.yaml" })).rejects.toThrow(
426
+ /--target Deployment\/prod\/api names no object k8s\.yaml declares \(it declares Deployment\/prod\/web, Service\/prod\/web, Namespace\/prod\)/,
427
+ );
428
+ });
429
+
430
+ test("run without a plan applies the targeted objects alone, with no prune", async () => {
431
+ let seen: Record<string, unknown> = {};
432
+ const cap = createKubectlApplyCapability(async (args) => {
433
+ seen = args as never;
434
+ return { fieldManager: "chant:web", applied: [], pruned: [] };
435
+ }, docs);
436
+ await cap.run({ env: "prod", component: "api", targets: ["Deployment/prod/web"] }, { manifest: "k8s.yaml", stack: "web", delete: "owned-only" });
437
+ expect(seen).toMatchObject({ documents: [doc("Deployment", "web", "prod")], deleteMode: "never" });
438
+ });
439
+ });
@@ -338,6 +338,41 @@ async function readOutputs(read: LiveReader, refs: Map<string, AppliedRef>, inpu
338
338
  return out;
339
339
  }
340
340
 
341
+ /**
342
+ * The documents `targets` name (#4300), each `Kind/namespace/name` or
343
+ * `Kind/name` as {@link addressOf} writes it, in manifest order. Throws for
344
+ * a target the manifest does not declare.
345
+ */
346
+ export function targetedDocuments(member: string, manifest: string, documents: Record<string, unknown>[], targets: readonly string[]): Record<string, unknown>[] {
347
+ const addressOfDoc = (doc: Record<string, unknown>): string => {
348
+ const metadata = (doc.metadata ?? {}) as { name?: unknown; namespace?: unknown };
349
+ return addressOf({
350
+ apiVersion: String(doc.apiVersion ?? ""),
351
+ kind: String(doc.kind ?? ""),
352
+ name: String(metadata.name ?? ""),
353
+ ...(metadata.namespace !== undefined ? { namespace: String(metadata.namespace) } : {}),
354
+ });
355
+ };
356
+ const declared = new Set(documents.map(addressOfDoc));
357
+ const missing = targets.filter((t) => !declared.has(t));
358
+ if (missing.length > 0) {
359
+ throw new Error(
360
+ `kubectl-apply "${member}": --target ${missing.join(", ")} names no object ${manifest} declares ` +
361
+ `(it declares ${[...declared].join(", ") || "nothing"})`,
362
+ );
363
+ }
364
+ const wanted = new Set(targets);
365
+ return documents.filter((d) => wanted.has(addressOfDoc(d)));
366
+ }
367
+
368
+ /**
369
+ * The input a targeted step plans and applies with (#4300): the same, with no
370
+ * prune. A prune over a narrowed set would delete every owned object the
371
+ * targets leave out.
372
+ */
373
+ const narrowedInput = (ctx: DeployContext, input: KubectlApplyInput): KubectlApplyInput =>
374
+ ctx.targets && ctx.targets.length > 0 ? { ...input, delete: "never" } : input;
375
+
341
376
  /** Factory with an injectable applier, so tests assert the delegation without
342
377
  * a cluster — the same seam `createGenerateSbomCapability` uses. The default
343
378
  * applier is resolved by dynamic import on first run, keeping the API-client
@@ -389,27 +424,66 @@ export function createKubectlApplyCapability(
389
424
  }
390
425
  return { known, unknown: unknown.sort() };
391
426
  };
427
+ /**
428
+ * {@link plannedOutputs} for a targeted plan (#4300): an output of a
429
+ * targeted object as the plan says, and one of an object the targets leave
430
+ * out as the cluster holds it now, since the apply leaves that object alone.
431
+ */
432
+ const targetedPlannedOutputs = async (
433
+ ctx: DeployContext,
434
+ input: KubectlApplyInput,
435
+ member: string,
436
+ documents: Record<string, unknown>[],
437
+ dry: KubectlApplyDryRun,
438
+ targets: readonly string[] | undefined,
439
+ declared: Record<string, unknown>[],
440
+ ): Promise<PlannedOutputs> => {
441
+ if (!targets) return plannedOutputs(ctx, input, member, documents, dry);
442
+ const wanted = new Set(targets);
443
+ const all = Object.entries(input.outputs ?? {});
444
+ const inside = Object.fromEntries(all.filter(([, spec]) => wanted.has(spec.object)));
445
+ const outside = Object.fromEntries(all.filter(([, spec]) => !wanted.has(spec.object)));
446
+ const planned = Object.keys(inside).length > 0 ? await plannedOutputs(ctx, { ...input, outputs: inside }, member, documents, dry) : { known: {}, unknown: [] };
447
+ if (Object.keys(outside).length === 0) return planned;
448
+ const rest = { ...input, outputs: outside };
449
+ const live = await readOutputs(await liveReader(), outputRefs(member, rest, declared), rest, where(ctx, input));
450
+ const known = { ...planned.known, ...live };
451
+ const unknown = [...planned.unknown, ...Object.keys(outside).filter((n) => !(n in live))].sort();
452
+ return { known, unknown };
453
+ };
392
454
  return {
393
455
  kind: "kubectl-apply",
394
456
  // A server-side apply has no native undo (the previous object state is not
395
457
  // kept by the API server), so COMP003 requires the component to acknowledge
396
458
  // the compensation gap — the same posture as s3-sync/run-migration.
397
459
  rollbackPolicy: "needs-opt-out",
460
+ // #4300: a target names one object, `Kind/namespace/name`; the plan and the apply send those alone.
461
+ targets: "accept",
398
462
  /**
399
463
  * Dry-run the manifest against the cluster (#3998): the documents `run`
400
464
  * would apply, release attributes set, server-side applied with
401
465
  * `dryRun=All` as the same field manager, each compared with its live
402
466
  * object. The member is the step's `stack`, or the component.
403
467
  */
404
- async plan(ctx: DeployContext, input: KubectlApplyInput): Promise<CapabilityPlan> {
468
+ async plan(ctx: DeployContext, stepInput: KubectlApplyInput): Promise<CapabilityPlan> {
469
+ const input = narrowedInput(ctx, stepInput);
405
470
  const member = input.stack ?? ctx.component;
406
- const documents = (read ?? (await activities()).readManifestDocuments)(input.manifest);
471
+ const read_ = (read ?? (await activities()).readManifestDocuments)(input.manifest);
472
+ const targets = ctx.targets && ctx.targets.length > 0 ? ctx.targets : undefined;
473
+ const documents = targets ? targetedDocuments(member, input.manifest, read_, targets) : read_;
407
474
  const suffix = releaseAttributesSuffix(ctx.release);
408
475
  if (suffix !== "") annotateRelease(documents, suffix);
409
476
  const { result, planDigest } = await dryRunOf(ctx, input, member, documents);
410
477
  const artifact: KubectlApplyPlanArtifact = { kind: "kubectl-apply-plan", planDigest, documents };
411
- const outputs = input.outputs ? await plannedOutputs(ctx, input, member, documents, result) : undefined;
412
- return { member, planDigest, artifact, changeSet: changeSetPartOf(member, planDigest, result), ...(outputs ? { outputs } : {}) };
478
+ const outputs = input.outputs ? await targetedPlannedOutputs(ctx, input, member, documents, result, targets, read_) : undefined;
479
+ return {
480
+ member,
481
+ planDigest,
482
+ artifact,
483
+ changeSet: changeSetPartOf(member, planDigest, result),
484
+ ...(outputs ? { outputs } : {}),
485
+ ...(targets ? { targets } : {}),
486
+ };
413
487
  },
414
488
  /**
415
489
  * The fields the step names as outputs, as the cluster holds them now
@@ -421,7 +495,8 @@ export function createKubectlApplyCapability(
421
495
  const documents = (read ?? (await activities()).readManifestDocuments)(input.manifest);
422
496
  return liveOutputs(ctx, input, documents);
423
497
  },
424
- async run(ctx: DeployContext, input: KubectlApplyInput): Promise<KubectlApplyOutcome> {
498
+ async run(ctx: DeployContext, stepInput: KubectlApplyInput): Promise<KubectlApplyOutcome> {
499
+ const input = narrowedInput(ctx, stepInput);
425
500
  const member = input.stack ?? ctx.component;
426
501
  const approved = ctx.plans?.[member];
427
502
  if (isPlanArtifact(approved)) {
@@ -439,7 +514,11 @@ export function createKubectlApplyCapability(
439
514
  // workload the build stamped, in what it applies; the committed manifest is not touched.
440
515
  const suffix = releaseAttributesSuffix(ctx.release);
441
516
  let documents: Record<string, unknown>[] | undefined;
442
- if (suffix !== "") {
517
+ if (ctx.targets && ctx.targets.length > 0) {
518
+ // #4300: only the targeted objects, and no prune (`narrowedInput`).
519
+ documents = targetedDocuments(member, input.manifest, (read ?? loaded!.readManifestDocuments)(input.manifest), ctx.targets);
520
+ if (suffix !== "") annotateRelease(documents, suffix);
521
+ } else if (suffix !== "") {
443
522
  documents = (read ?? loaded!.readManifestDocuments)(input.manifest);
444
523
  if (annotateRelease(documents, suffix) === 0) documents = undefined;
445
524
  }