@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
|
|
18
|
-
* stack-scoped objects (workloads + the service/config plumbing around them)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
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": "
|
|
53
|
+
"composite": "3ad81fead558e4d2546ec00f031acaaa0fb4c1800794a5147b2cd0b20d9ef4ed"
|
|
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.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.
|
|
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
|
-
/**
|
|
6
|
-
|
|
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
|
|
18
|
-
* stack-scoped objects (workloads + the service/config plumbing around them)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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;
|