@intentius/chant-lexicon-k8s 0.41.4 → 0.41.6
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;
|
|
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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "9777722464099017a5407212d25a6f5b6d9f4e483e0ca6c171f1dbb78f566bfb",
|
|
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": "3c980aa69c171e478b779b411bc3f6d22651561530cfdfde7f7082be7448d93f"
|
|
54
54
|
}
|
package/dist/manifest.json
CHANGED
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.6",
|
|
4
4
|
"description": "Kubernetes lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"@types/js-yaml": "^4.0.9"
|
|
77
77
|
},
|
|
78
78
|
"optionalDependencies": {
|
|
79
|
-
"@intentius/chant-k8s-client": "
|
|
79
|
+
"@intentius/chant-k8s-client": "^0.41.5"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@intentius/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.6",
|
|
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
|
-
|
|
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
|
|
330
|
-
* classify each one's owner-reference chain (#1077). Mutates
|
|
331
|
-
* place, adding an entry per
|
|
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
|
|
334
|
-
* a transient error) is simply skipped rather than failing the
|
|
335
|
-
*
|
|
336
|
-
* to report against, the same way an out-of-band AWS child
|
|
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
|
-
|
|
357
|
-
|
|
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
|
-
|
|
390
|
+
infos = await client.resources(gv);
|
|
360
391
|
} catch {
|
|
361
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
}
|