@intentius/chant-lexicon-k8s 0.41.11 → 0.41.13

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.
@@ -14,11 +14,18 @@
14
14
  * a caller observes with, and a mismatch would read a deployed unit as absent
15
15
  * — the failure mode the tri-state exists to prevent.
16
16
  *
17
- * The sweep is bounded to the kinds chant's own k8s surface deploys as
18
- * stack-scoped objects (workloads + the service/config plumbing around them).
19
- * A CRD-only unit is out of this bound and reports absent rather than null —
20
- * a real limit, stated here rather than papered over; widening the sweep to
21
- * discovery-driven kinds is a follow-up, not a silent claim.
17
+ * The sweep starts from the kinds chant's own k8s surface deploys as
18
+ * stack-scoped objects (workloads + the service/config plumbing around them),
19
+ * then widens to what the cluster itself serves: the CRDs (a unit that
20
+ * deploys definitions is a real unit kubemicrovm-ops pins them
21
+ * `delete: never`), and every CRD-backed kind those definitions declare. A
22
+ * kubemicrovm workload stack is *entirely* custom resources, and under the
23
+ * fixed bound it read as absent while its VMs ran (#1528) — and because a
24
+ * component is live only when every unit is, that one false absence painted
25
+ * whole components dead. The CRD list is read unfiltered, since the unit
26
+ * under observation rarely owns the definitions its objects instantiate;
27
+ * each derived kind is then read with the same stack selector, so the cost
28
+ * is one label-filtered list per served CRD kind.
22
29
  */
23
30
  import type { StackStatusObservation } from "@intentius/chant/lexicon";
24
31
  import { type K8sConnector } from "./api/connect.js";
@@ -1 +1 @@
1
- {"version":3,"file":"describe-stack-status.d.ts","sourceRoot":"","sources":["../src/describe-stack-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGvE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAgCvE,wBAAsB,mBAAmB,CACvC,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC/C,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAuCxC"}
1
+ {"version":3,"file":"describe-stack-status.d.ts","sourceRoot":"","sources":["../src/describe-stack-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGvE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAmEvE,wBAAsB,mBAAmB,CACvC,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC/C,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAwCxC"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "9399085f9a236e05e251e77ed9836ea682c29d2a5a685ec4609f48a426ce7c48",
4
+ "manifest.json": "9ba4951b328537999953cb55679d420add94305817fab50d31b487841236689f",
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": "eb5beb679ae3d4c4bddabe46475489b75f48613276910005eef1276d2ae4666e"
53
+ "composite": "3ad81fead558e4d2546ec00f031acaaa0fb4c1800794a5147b2cd0b20d9ef4ed"
54
54
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.41.11",
3
+ "version": "0.41.13",
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.11",
3
+ "version": "0.41.13",
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.11",
87
+ "@intentius/chant": "^0.41.13",
88
88
  "typescript": "^5.9.3"
89
89
  }
90
90
  }
@@ -2,13 +2,23 @@ import { describe, test, expect } from "vitest";
2
2
  import { describeStackStatus } from "./describe-stack-status";
3
3
  import type { K8sConnector } from "./api/connect";
4
4
 
5
- /** A connector whose client answers list() from a canned map keyed by kind. */
6
- function fakeConnector(byKind: Record<string, unknown[]>, opts: { failKinds?: string[]; recordSelectors?: string[] } = {}): K8sConnector {
5
+ /**
6
+ * A connector whose client answers list() from a canned map keyed by kind.
7
+ * Selector-bearing lists model the server having filtered to the stack's
8
+ * objects (`byKind` holds what matched); the selector-less list is the CRD
9
+ * discovery read (#1528), answered from `discovery` so a test can serve
10
+ * definitions without them counting as the stack's own objects.
11
+ */
12
+ function fakeConnector(
13
+ byKind: Record<string, unknown[]>,
14
+ opts: { failKinds?: string[]; recordSelectors?: string[]; discovery?: Record<string, unknown[]> } = {},
15
+ ): K8sConnector {
7
16
  return (async () => ({
8
17
  client: {
9
18
  async list(selector: { kind: string }, listOpts?: { labelSelector?: string }) {
10
- if (listOpts?.labelSelector && opts.recordSelectors) opts.recordSelectors.push(listOpts.labelSelector);
11
19
  if (opts.failKinds?.includes(selector.kind)) throw new Error("rbac denied");
20
+ if (!listOpts?.labelSelector) return opts.discovery?.[selector.kind] ?? [];
21
+ if (opts.recordSelectors) opts.recordSelectors.push(listOpts.labelSelector);
12
22
  return byKind[selector.kind] ?? [];
13
23
  },
14
24
  },
@@ -23,6 +33,26 @@ const readyDeployment = {
23
33
  const laggingDeployment = { ...readyDeployment, status: { replicas: 2, readyReplicas: 1 } };
24
34
  const service = { kind: "Service", metadata: { name: "cc-api" } };
25
35
 
36
+ /** A served CRD, as the discovery read returns it — not the stack's object. */
37
+ const microvmCrd = {
38
+ kind: "CustomResourceDefinition",
39
+ metadata: { name: "microvms.lambda.aws.amazon.com" },
40
+ spec: {
41
+ group: "lambda.aws.amazon.com",
42
+ names: { kind: "MicroVM" },
43
+ versions: [{ name: "v1alpha1", served: true, storage: true }],
44
+ },
45
+ };
46
+ const runningMicroVm = {
47
+ kind: "MicroVM",
48
+ metadata: { name: "kmv-vm", labels: { "chant.intentius.io/stack": "kmv-workload" } },
49
+ };
50
+ const microvmReplicaSet = {
51
+ kind: "MicroVMReplicaSet",
52
+ metadata: { name: "kmv-rs", labels: { "chant.intentius.io/stack": "kmv-workload" } },
53
+ status: { replicas: 2, readyReplicas: 2 },
54
+ };
55
+
26
56
  describe("k8s describeStackStatus (#1495 piece 3)", () => {
27
57
  test("selects on the labels chant's serializer stamps", async () => {
28
58
  const selectors: string[] = [];
@@ -68,8 +98,72 @@ describe("k8s describeStackStatus (#1495 piece 3)", () => {
68
98
  test("every read failing is indeterminate too", async () => {
69
99
  const out = await describeStackStatus(
70
100
  { environment: "local", stack: "cc" },
71
- fakeConnector({}, { failKinds: ["Deployment", "StatefulSet", "DaemonSet", "Service", "ConfigMap", "Secret", "ServiceAccount", "Namespace", "Ingress", "PodDisruptionBudget", "HorizontalPodAutoscaler", "CronJob"] }),
101
+ fakeConnector({}, { failKinds: ["Deployment", "StatefulSet", "DaemonSet", "Service", "ConfigMap", "Secret", "ServiceAccount", "Namespace", "Ingress", "PodDisruptionBudget", "HorizontalPodAutoscaler", "CronJob", "CustomResourceDefinition"] }),
72
102
  );
73
103
  expect(out).toBeNull();
74
104
  });
75
105
  });
106
+
107
+ describe("the sweep reaches what the cluster serves, not only built-ins (#1528)", () => {
108
+ // A kubemicrovm estate made both gaps real: its crds unit deploys nothing
109
+ // but definitions, its workload unit nothing but custom resources, and
110
+ // under the fixed bound both read absent while the estate ran — which
111
+ // painted every component touching them dead in `components status --live`.
112
+ test("a unit that deploys the definitions themselves is present", async () => {
113
+ const crdWithLabel = {
114
+ ...microvmCrd,
115
+ metadata: { ...microvmCrd.metadata, labels: { "chant.intentius.io/stack": "kmv-crds" } },
116
+ };
117
+ const out = await describeStackStatus(
118
+ { environment: "local", stack: "kmv-crds" },
119
+ fakeConnector({ CustomResourceDefinition: [crdWithLabel] }),
120
+ );
121
+ expect(out).toMatchObject({ present: true });
122
+ });
123
+
124
+ test("a unit of custom resources is present via the kinds the CRDs declare", async () => {
125
+ const out = await describeStackStatus(
126
+ { environment: "local", stack: "kmv-workload" },
127
+ fakeConnector({ MicroVM: [runningMicroVm] }, { discovery: { CustomResourceDefinition: [microvmCrd] } }),
128
+ );
129
+ expect(out).toMatchObject({ present: true });
130
+ });
131
+
132
+ test("a custom controller reporting replica readiness asserts health like a built-in", async () => {
133
+ const out = await describeStackStatus(
134
+ { environment: "local", stack: "kmv-workload" },
135
+ fakeConnector(
136
+ { MicroVMReplicaSet: [microvmReplicaSet] },
137
+ {
138
+ discovery: {
139
+ CustomResourceDefinition: [
140
+ {
141
+ ...microvmCrd,
142
+ spec: { ...microvmCrd.spec, names: { kind: "MicroVMReplicaSet" } },
143
+ },
144
+ ],
145
+ },
146
+ },
147
+ ),
148
+ );
149
+ expect(out).toMatchObject({ present: true, healthy: true, status: "1/1 workloads ready" });
150
+ });
151
+
152
+ test("an unserved or malformed CRD derives nothing and breaks nothing", async () => {
153
+ const out = await describeStackStatus(
154
+ { environment: "local", stack: "cc" },
155
+ fakeConnector(
156
+ { Deployment: [readyDeployment] },
157
+ {
158
+ discovery: {
159
+ CustomResourceDefinition: [
160
+ { kind: "CustomResourceDefinition", metadata: { name: "half" }, spec: { group: "x.io" } },
161
+ { ...microvmCrd, spec: { ...microvmCrd.spec, versions: [{ name: "v1", served: false }] } },
162
+ ],
163
+ },
164
+ },
165
+ ),
166
+ );
167
+ expect(out).toMatchObject({ present: true, healthy: true });
168
+ });
169
+ });
@@ -14,11 +14,18 @@
14
14
  * a caller observes with, and a mismatch would read a deployed unit as absent
15
15
  * — the failure mode the tri-state exists to prevent.
16
16
  *
17
- * The sweep is bounded to the kinds chant's own k8s surface deploys as
18
- * stack-scoped objects (workloads + the service/config plumbing around them).
19
- * A CRD-only unit is out of this bound and reports absent rather than null —
20
- * a real limit, stated here rather than papered over; widening the sweep to
21
- * discovery-driven kinds is a follow-up, not a silent claim.
17
+ * The sweep starts from the kinds chant's own k8s surface deploys as
18
+ * stack-scoped objects (workloads + the service/config plumbing around them),
19
+ * then widens to what the cluster itself serves: the CRDs (a unit that
20
+ * deploys definitions is a real unit kubemicrovm-ops pins them
21
+ * `delete: never`), and every CRD-backed kind those definitions declare. A
22
+ * kubemicrovm workload stack is *entirely* custom resources, and under the
23
+ * fixed bound it read as absent while its VMs ran (#1528) — and because a
24
+ * component is live only when every unit is, that one false absence painted
25
+ * whole components dead. The CRD list is read unfiltered, since the unit
26
+ * under observation rarely owns the definitions its objects instantiate;
27
+ * each derived kind is then read with the same stack selector, so the cost
28
+ * is one label-filtered list per served CRD kind.
22
29
  */
23
30
  import type { StackStatusObservation } from "@intentius/chant/lexicon";
24
31
  import { LABEL_OWNERSHIP_KEYS, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
@@ -39,8 +46,43 @@ const SWEEP_KINDS: ReadonlyArray<{ apiVersion: string; kind: string }> = [
39
46
  { apiVersion: "policy/v1", kind: "PodDisruptionBudget" },
40
47
  { apiVersion: "autoscaling/v2", kind: "HorizontalPodAutoscaler" },
41
48
  { apiVersion: "batch/v1", kind: "CronJob" },
49
+ // The definitions themselves: a crds unit deploys these and nothing else.
50
+ { apiVersion: "apiextensions.k8s.io/v1", kind: "CustomResourceDefinition" },
42
51
  ];
43
52
 
53
+ /**
54
+ * Every kind the cluster's CRDs declare, one served version each (storage
55
+ * preferred). Read unfiltered — see the module doc. Failure to read the CRD
56
+ * list narrows the sweep back to the fixed bound rather than failing it.
57
+ */
58
+ async function crdBackedKinds(client: {
59
+ list(target: { apiVersion: string; kind: string }, opts?: object): Promise<K8sObject[]>;
60
+ }): Promise<Array<{ apiVersion: string; kind: string }>> {
61
+ let crds: K8sObject[];
62
+ try {
63
+ crds = await client.list({ apiVersion: "apiextensions.k8s.io/v1", kind: "CustomResourceDefinition" });
64
+ } catch {
65
+ return [];
66
+ }
67
+ const kinds: Array<{ apiVersion: string; kind: string }> = [];
68
+ for (const crd of crds) {
69
+ const spec = (crd as {
70
+ spec?: {
71
+ group?: string;
72
+ names?: { kind?: string };
73
+ versions?: Array<{ name: string; served?: boolean; storage?: boolean }>;
74
+ };
75
+ }).spec;
76
+ const group = spec?.group;
77
+ const kind = spec?.names?.kind;
78
+ const versions = spec?.versions ?? [];
79
+ const version = versions.find((v) => v.storage && v.served) ?? versions.find((v) => v.served);
80
+ if (!group || !kind || !version) continue;
81
+ kinds.push({ apiVersion: `${group}/${version.name}`, kind });
82
+ }
83
+ return kinds;
84
+ }
85
+
44
86
  /** A workload is healthy when its controller reports every replica ready; a
45
87
  * kind with no readiness story (a ConfigMap, a Service) asserts nothing. */
46
88
  function objectHealthy(obj: K8sObject): boolean | undefined {
@@ -74,7 +116,8 @@ export async function describeStackStatus(
74
116
 
75
117
  const matched: K8sObject[] = [];
76
118
  let anyRead = false;
77
- for (const target of SWEEP_KINDS) {
119
+ const targets = [...SWEEP_KINDS, ...(await crdBackedKinds(client))];
120
+ for (const target of targets) {
78
121
  try {
79
122
  const items = await client.list(target, { labelSelector: selector });
80
123
  anyRead = true;