@intentius/chant-lexicon-k8s 0.41.4 → 0.41.5

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":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAsC,MAAM,0BAA0B,CAAC;AAGtG,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAkBvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CA0BvD;AAgED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,EAAE,GAAG,SAAS,CAetE;AAQD,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,EACjC,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,iBAAiB,CAAC,CAuH5B"}
1
+ {"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAsC,MAAM,0BAA0B,CAAC;AAGtG,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAmBvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CA0BvD;AAgED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,EAAE,GAAG,SAAS,CAetE;AAQD,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,EACjC,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,iBAAiB,CAAC,CAmI5B"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "5f1a15a53cfc8e71e5472c94d89b7880a5f703b96979191a5c5f2c1774d156dd",
4
+ "manifest.json": "9cd5bebf0d9b42505b9b15cbde474c7ed1934e615d7d92b20459e0e6bfc850c7",
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": "1662e74e96d14ad3c614e8b38df06cc8266bbfbfdfa1aa769d88ee2a4abd0a4b"
53
+ "composite": "4b8491693100b59644a7d1794d71ee06dfa702c9ab9f85ea5552c356e6c3bff6"
54
54
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.41.4",
3
+ "version": "0.41.5",
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.41.4",
3
+ "version": "0.41.5",
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.4",
87
+ "@intentius/chant": "^0.41.5",
88
88
  "typescript": "^5.9.3"
89
89
  }
90
90
  }
@@ -508,6 +508,96 @@ describe("k8s describeResources", () => {
508
508
 
509
509
  // ── Runtime children: owner-reference chain classification (chant #1077) ──
510
510
 
511
+ describe("runtime children of the estate's own API groups (#1517)", () => {
512
+ function declaredCronJob() {
513
+ return makeEntities([
514
+ { name: "backup", entityType: "K8s::Batch::CronJob", props: { metadata: { name: "backup", namespace: "prod" } } },
515
+ ]);
516
+ }
517
+ const cronJob = {
518
+ apiVersion: "batch/v1",
519
+ kind: "CronJob",
520
+ metadata: { name: "backup", namespace: "prod", uid: "cj-uid" },
521
+ };
522
+
523
+ test("a controller-made child in a declared non-core group surfaces with its own kind and a declared chain", async () => {
524
+ const job = {
525
+ apiVersion: "batch/v1",
526
+ kind: "Job",
527
+ metadata: {
528
+ name: "backup-29157",
529
+ namespace: "prod",
530
+ uid: "job-uid",
531
+ ownerReferences: [{ apiVersion: "batch/v1", kind: "CronJob", name: "backup", uid: "cj-uid", controller: true }],
532
+ },
533
+ status: { conditions: [{ type: "Complete", status: "True" }] },
534
+ };
535
+ const cluster = fakeCluster({
536
+ objects: {
537
+ [objectKey("batch/v1", "CronJob", "backup", "prod")]: cronJob,
538
+ [objectKey("batch/v1", "Job", "backup-29157", "prod")]: job,
539
+ },
540
+ });
541
+
542
+ const result = await describeResources(
543
+ { environment: "prod", buildOutput: "", entityNames: ["backup"], entities: declaredCronJob() },
544
+ cluster.connector,
545
+ );
546
+
547
+ const child = result.resources["prod/backup-29157"];
548
+ expect(child).toMatchObject({
549
+ type: "K8s::Batch::Job",
550
+ physicalId: "job-uid",
551
+ ownerChain: { root: "declared", entity: "backup" },
552
+ });
553
+ });
554
+
555
+ test("a loose object in a swept group with no owner chain is NOT reported — the scan is not an inventory", async () => {
556
+ const looseJob = {
557
+ apiVersion: "batch/v1",
558
+ kind: "Job",
559
+ metadata: { name: "hand-made", namespace: "prod", uid: "loose-uid" },
560
+ };
561
+ const cluster = fakeCluster({
562
+ objects: {
563
+ [objectKey("batch/v1", "CronJob", "backup", "prod")]: cronJob,
564
+ [objectKey("batch/v1", "Job", "hand-made", "prod")]: looseJob,
565
+ },
566
+ });
567
+
568
+ const result = await describeResources(
569
+ { environment: "prod", buildOutput: "", entityNames: ["backup"], entities: declaredCronJob() },
570
+ cluster.connector,
571
+ );
572
+
573
+ expect(result.resources["prod/hand-made"]).toBeUndefined();
574
+ });
575
+
576
+ test("a core-only estate sweeps no extra groups — Pods stay the core representative", async () => {
577
+ const svc = { apiVersion: "v1", kind: "Service", metadata: { name: "web", namespace: "prod", uid: "svc-uid" } };
578
+ const cluster = fakeCluster({
579
+ objects: { [objectKey("v1", "Service", "web", "prod")]: svc },
580
+ });
581
+
582
+ await describeResources(
583
+ {
584
+ environment: "prod",
585
+ buildOutput: "",
586
+ entityNames: ["web"],
587
+ entities: makeEntities([
588
+ { name: "web", entityType: "K8s::Core::Service", props: { metadata: { name: "web", namespace: "prod" } } },
589
+ ]),
590
+ },
591
+ cluster.connector,
592
+ );
593
+
594
+ // Discovery was consulted for no non-core groupVersion: the only list
595
+ // sweeps are the namespace's pods.
596
+ const groupDiscovery = cluster.layer.paths().filter((p) => /^\/apis\/[^/]+\/[^/]+$/.test(p) && !p.includes("namespaces"));
597
+ expect(groupDiscovery.filter((p) => !p.startsWith("/apis/apps"))).toEqual([]);
598
+ });
599
+ });
600
+
511
601
  describe("runtime children (#1077)", () => {
512
602
  function declaredDeployment() {
513
603
  return makeEntities([
@@ -65,6 +65,7 @@ import {
65
65
  } from "./api/classify";
66
66
  import { operationFor } from "./api/operation-surface";
67
67
  import { resolveK8sOwnerChain } from "./api/owner-chain";
68
+ import { gvkToTypeName } from "./spec/parse";
68
69
 
69
70
  function pruneUndefined<T extends Record<string, unknown>>(obj: T): Record<string, unknown> {
70
71
  const out: Record<string, unknown> = {};
@@ -320,26 +321,50 @@ export async function describeResources(
320
321
  }
321
322
  });
322
323
 
323
- await addRuntimeChildren(client, resources, options.owned);
324
+ // chant #1517 — the API groups this estate's own declarations use. A
325
+ // controller's children usually live in its controller's group (a
326
+ // MicroVMReplicaSet makes MicroVMs), so the runtime scan sweeps those
327
+ // groupVersions' kinds besides Pods. Core (`v1`) is excluded from the group
328
+ // sweep — Pods represent it; sweeping every core kind would walk endpoints
329
+ // and events for nothing.
330
+ const runtimeGroupVersions = new Set<string>();
331
+ for (const { entityType } of declared) {
332
+ const op = operationFor(entityType);
333
+ if (op && op.apiVersion.includes("/")) runtimeGroupVersions.add(op.apiVersion);
334
+ }
335
+
336
+ await addRuntimeChildren(client, resources, options.owned, runtimeGroupVersions);
324
337
 
325
338
  return observation(resources, unobserved);
326
339
  }
327
340
 
328
341
  /**
329
- * Scan Pods in the namespaces of entities this call just resolved, and
330
- * classify each one's owner-reference chain (#1077). Mutates `resources` in
331
- * place, adding an entry per Pod whose chain was worth reporting.
342
+ * Scan runtime-child candidates in the namespaces of entities this call just
343
+ * resolved, and classify each one's owner-reference chain (#1077). Mutates
344
+ * `resources` in place, adding an entry per object whose chain was worth
345
+ * reporting.
346
+ *
347
+ * Two candidate pools:
348
+ * - **Pods** — the built-in workload leaf, always scanned.
349
+ * - **The estate's own API groups** (#1517) — every namespaced, listable
350
+ * kind in each `groupVersions` entry, from the cluster's own discovery.
351
+ * An operator estate's interesting children are custom resources its
352
+ * controllers made (a MicroVMReplicaSet's MicroVMs), which no kind table
353
+ * could anticipate — the same open-world inversion behold#74 made for
354
+ * declared CRDs, applied to the runtime axis. Bounded by what the estate
355
+ * declares, never a cluster-wide sweep.
332
356
  *
333
- * Best-effort and additive: a namespace this scan cannot list (RBAC denial,
334
- * a transient error) is simply skipped rather than failing the observation —
335
- * Pods are not declared entities, so there is no NOT-OBSERVED axis for them
336
- * to report against, the same way an out-of-band AWS child resource has none
337
- * either.
357
+ * Best-effort and additive: a namespace or kind this scan cannot list (RBAC
358
+ * denial, a transient error) is simply skipped rather than failing the
359
+ * observation — these are not declared entities, so there is no NOT-OBSERVED
360
+ * axis for them to report against, the same way an out-of-band AWS child
361
+ * resource has none either.
338
362
  */
339
363
  async function addRuntimeChildren(
340
364
  client: K8sClient,
341
365
  resources: Record<string, ResourceMetadata>,
342
366
  owned: boolean | undefined,
367
+ groupVersions: ReadonlySet<string> = new Set(),
343
368
  ): Promise<void> {
344
369
  const declaredByUid = new Map<string, string>();
345
370
  for (const [entityName, meta] of Object.entries(resources)) {
@@ -353,42 +378,73 @@ async function addRuntimeChildren(
353
378
  }
354
379
  if (namespaces.size === 0) return;
355
380
 
356
- await client.concurrently([...namespaces], async (namespace) => {
357
- let pods: K8sObject[];
381
+ // The kinds to scan: Pods, plus each declared groupVersion's namespaced,
382
+ // listable kinds from discovery (#1517). A groupVersion the cluster does
383
+ // not serve contributes nothing — an answer, not a failure.
384
+ const kinds: Array<{ apiVersion: string; kind: string; typeName: string }> = [
385
+ { apiVersion: "v1", kind: "Pod", typeName: "K8s::Core::Pod" },
386
+ ];
387
+ for (const gv of [...groupVersions].sort()) {
388
+ let infos;
358
389
  try {
359
- pods = await client.list({ apiVersion: "v1", kind: "Pod" }, { namespace });
390
+ infos = await client.resources(gv);
360
391
  } catch {
361
- return; // best-effort — see the function doc
392
+ continue; // best-effort — see the function doc
393
+ }
394
+ const [group, version] = [gv.slice(0, gv.indexOf("/")), gv.slice(gv.indexOf("/") + 1)];
395
+ for (const info of infos) {
396
+ if (!info.namespaced || !info.verbs.includes("list")) continue;
397
+ kinds.push({ apiVersion: gv, kind: info.kind, typeName: gvkToTypeName({ group, version, kind: info.kind }) });
362
398
  }
399
+ }
363
400
 
364
- await client.concurrently(pods, async (pod) => {
365
- const uid = pod.metadata?.uid;
366
- const name = pod.metadata?.name;
367
- if (!uid || !name || declaredByUid.has(uid)) return; // declared directly, or unaddressable
368
-
369
- const ownerChain = await resolveK8sOwnerChain(pod, { declaredByUid, reader: client, namespace });
370
-
371
- // `--owned`: withhold a Pod that is neither a runtime child of a
372
- // declared entity nor carrying chant's own marker — the same rule the
373
- // declared-entity read above applies, extended to the undeclared axis
374
- // this scan introduces.
375
- const marker = hasOwnershipMarker(pod.metadata?.labels, LABEL_OWNERSHIP_KEYS);
376
- if (owned && ownerChain.root !== "declared" && !marker) return;
377
-
378
- resources[`${namespace}/${name}`] = {
379
- type: "K8s::Core::Pod",
380
- physicalId: uid,
381
- status: statusFromObject(pod),
382
- lastUpdated: pod.metadata?.creationTimestamp,
383
- ownership: classifyOwnership(pod.metadata?.labels, LABEL_OWNERSHIP_KEYS),
384
- ownerChain,
385
- attributes: pruneUndefined({
386
- namespace,
387
- labels: pod.metadata?.labels,
388
- resourceVersion: pod.metadata?.resourceVersion,
389
- conditions: unhappyConditions(pod),
390
- }),
391
- };
392
- });
401
+ await client.concurrently([...namespaces], async (namespace) => {
402
+ for (const { apiVersion, kind, typeName } of kinds) {
403
+ let objects: K8sObject[];
404
+ try {
405
+ objects = await client.list({ apiVersion, kind }, { namespace });
406
+ } catch {
407
+ continue; // best-effort — see the function doc
408
+ }
409
+
410
+ await client.concurrently(objects, async (obj) => {
411
+ const uid = obj.metadata?.uid;
412
+ const name = obj.metadata?.name;
413
+ if (!uid || !name || declaredByUid.has(uid)) return; // declared directly, or unaddressable
414
+ if (resources[`${namespace}/${name}`]) return; // already reported by an earlier kind
415
+
416
+ const ownerChain = await resolveK8sOwnerChain(obj, { declaredByUid, reader: client, namespace });
417
+
418
+ // `--owned`: withhold an object that is neither a runtime child of a
419
+ // declared entity nor carrying chant's own marker — the same rule the
420
+ // declared-entity read above applies, extended to the undeclared axis
421
+ // this scan introduces.
422
+ const marker = hasOwnershipMarker(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS);
423
+ if (owned && ownerChain.root !== "declared" && !marker) return;
424
+
425
+ // An undeclared object with NO owner chain at all is not a runtime
426
+ // child — reporting every loose object a group serves would turn the
427
+ // scan into an inventory. Pods keep their pre-#1517 reporting (their
428
+ // chain verdicts, including foreign, were already part of #1077's
429
+ // contract); swept group kinds only report when the chain reaches a
430
+ // declared entity.
431
+ if (kind !== "Pod" && ownerChain.root !== "declared") return;
432
+
433
+ resources[`${namespace}/${name}`] = {
434
+ type: typeName,
435
+ physicalId: uid,
436
+ status: statusFromObject(obj),
437
+ lastUpdated: obj.metadata?.creationTimestamp,
438
+ ownership: classifyOwnership(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS),
439
+ ownerChain,
440
+ attributes: pruneUndefined({
441
+ namespace,
442
+ labels: obj.metadata?.labels,
443
+ resourceVersion: obj.metadata?.resourceVersion,
444
+ conditions: unhappyConditions(obj),
445
+ }),
446
+ };
447
+ });
448
+ }
393
449
  });
394
450
  }