@intentius/chant-lexicon-k8s 0.44.1 → 0.44.3
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.
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +3 -3
- package/src/api/classify.test.ts +19 -2
- package/src/api/classify.ts +5 -2
- package/src/describe-resources.test.ts +75 -0
- package/src/describe-resources.ts +38 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAwD,MAAM,0BAA0B,CAAC;AAGxH,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,CA2CvD;AA4FD,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,
|
|
1
|
+
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAwD,MAAM,0BAA0B,CAAC;AAGxH,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,CA2CvD;AA4FD,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,CAoJ5B"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "d94d393cea4fe4e3bebaa9590a85d8966e15e27d6cf2233ed07395182146501b",
|
|
5
5
|
"meta.json": "f568fb8a0cbd4adf041baccaa3e1164c2e3c5c0090186236f887f0084f5521a8",
|
|
6
6
|
"types/index.d.ts": "ef2c9df6a4d1ebfbe33f96c094fbbe3fc7909522ea38e086666fbf3c8ac477fd",
|
|
7
7
|
"rules/argo-appset-single-project.ts": "afa9f310753aa2d475f35012b13135b2dfaebed2a35d44edbe185ebc07673674",
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"skills/chant-k8s-argo.md": "b1a0b826559d8c5033a479c5781efaf650320f0aee4419d8841170bd3393cea5",
|
|
55
55
|
"skills/chant-k8s-flux.md": "9670efc2e9854bd14686b5adeb707396b9030a4dcaee6529b30abf56efe2c50c"
|
|
56
56
|
},
|
|
57
|
-
"composite": "
|
|
57
|
+
"composite": "1031cf6d53da6ad5ddf631aaa8160f1a5ca914c95a124b0827e42b40b99ab270"
|
|
58
58
|
}
|
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.44.
|
|
3
|
+
"version": "0.44.3",
|
|
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": "^0.44.
|
|
79
|
+
"@intentius/chant-k8s-client": "^0.44.3"
|
|
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.44.
|
|
87
|
+
"@intentius/chant": "^0.44.3",
|
|
88
88
|
"typescript": "^5.9.3"
|
|
89
89
|
}
|
|
90
90
|
}
|
package/src/api/classify.test.ts
CHANGED
|
@@ -103,13 +103,30 @@ describe("classifyApiFailure", () => {
|
|
|
103
103
|
}
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
test("the context note survives the one-line cap
|
|
106
|
+
test("the context note leads the detail, so it survives the one-line cap and any consumer truncation (#1488, #1620)", () => {
|
|
107
107
|
const err = new K8sApiError(500, "InternalError", "x".repeat(500));
|
|
108
108
|
err.contextNote = 'context "prod-eks" (bound by k8s.profiles.prod.context)';
|
|
109
109
|
const outcome = classifyApiFailure(err);
|
|
110
110
|
expect(outcome.kind).toBe("unobserved");
|
|
111
111
|
if (outcome.kind === "unobserved") {
|
|
112
|
-
expect(outcome.detail.
|
|
112
|
+
expect(outcome.detail.startsWith('context "prod-eks" (bound by k8s.profiles.prod.context)')).toBe(true);
|
|
113
|
+
// The error itself still follows — leading with the cluster does not
|
|
114
|
+
// cost the message.
|
|
115
|
+
expect(outcome.detail).toContain("xxx");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("the context appears within the first 160 chars of a long multi-line error's detail (#1620)", () => {
|
|
120
|
+
// behold's estate overlay truncates a reason to its first line / 160 chars;
|
|
121
|
+
// the context must land inside that window or it vanishes exactly on the
|
|
122
|
+
// failures long enough to need it.
|
|
123
|
+
const err = new K8sApiError(500, "InternalError", `${"y".repeat(300)}\n${"z".repeat(300)}`);
|
|
124
|
+
err.contextNote = 'context "prod-eks" (bound by k8s.profiles.prod.context)';
|
|
125
|
+
const outcome = classifyApiFailure(err);
|
|
126
|
+
expect(outcome.kind).toBe("unobserved");
|
|
127
|
+
if (outcome.kind === "unobserved") {
|
|
128
|
+
const firstLine = outcome.detail.split("\n")[0];
|
|
129
|
+
expect(firstLine.slice(0, 160)).toContain('context "prod-eks" (bound by k8s.profiles.prod.context)');
|
|
113
130
|
}
|
|
114
131
|
});
|
|
115
132
|
|
package/src/api/classify.ts
CHANGED
|
@@ -145,7 +145,10 @@ function detailOf(err: unknown): string {
|
|
|
145
145
|
const capped = line.length > 200 ? `${line.slice(0, 197)}...` : line;
|
|
146
146
|
// #1488 — a reason that does not say which cluster was read turned an
|
|
147
147
|
// ambient-context switch into an afternoon. The client stamps the context it
|
|
148
|
-
// resolved onto every failure
|
|
148
|
+
// resolved onto every failure. The note LEADS the detail (#1620): consumers
|
|
149
|
+
// truncate a reason to its first line and ~160 chars, and a note appended
|
|
150
|
+
// after a long error is cut off exactly when the error is long enough to
|
|
151
|
+
// need it. Leading, it survives any suffix truncation.
|
|
149
152
|
const note = shapeOf(err).contextNote;
|
|
150
|
-
return note ? `${
|
|
153
|
+
return note ? `${note} — ${capped}` : capped;
|
|
151
154
|
}
|
|
@@ -105,6 +105,81 @@ describe("k8s describeResources", () => {
|
|
|
105
105
|
expect(result.unobserved ?? {}).toEqual({});
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
// The behold#192 shape (#1620): a Flux-deployed object declares no
|
|
109
|
+
// metadata.namespace (the controller stamps targetNamespace at apply), so the
|
|
110
|
+
// live read scopes to the client's default namespace, finds nothing, and
|
|
111
|
+
// correctly reports absence. Every layer is spec-correct and the picture
|
|
112
|
+
// still says the opposite of the truth — unless the report carries the
|
|
113
|
+
// address it actually asked, which is what `queried` is.
|
|
114
|
+
test("an absent verdict carries the resolved query address, namespace defaulted and visible (#1620)", async () => {
|
|
115
|
+
const cluster = fakeCluster({
|
|
116
|
+
// The object lives where Flux put it — not where the read will look.
|
|
117
|
+
objects: { [objectKey("apps/v1", "Deployment", "web", "flux-target")]: web },
|
|
118
|
+
});
|
|
119
|
+
const result = await describeResources(
|
|
120
|
+
{
|
|
121
|
+
environment: "prod",
|
|
122
|
+
buildOutput: "",
|
|
123
|
+
entityNames: ["web"],
|
|
124
|
+
entities: makeEntities([
|
|
125
|
+
// No metadata.namespace declared — Flux stamps it at apply time.
|
|
126
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web" } } },
|
|
127
|
+
]),
|
|
128
|
+
},
|
|
129
|
+
cluster.connector,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Still a clean absence: in neither map, the tri-state unshifted.
|
|
133
|
+
expect(result.resources).toEqual({});
|
|
134
|
+
expect(result.unobserved ?? {}).toEqual({});
|
|
135
|
+
// But the report can now explain itself: the read went to `default`.
|
|
136
|
+
expect(result.queried?.web).toBe("/apis/apps/v1/namespaces/default/deployments/web");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a present entity carries its query address too — the map is total over issued reads (#1620)", async () => {
|
|
140
|
+
const cluster = fakeCluster({
|
|
141
|
+
objects: { [objectKey("apps/v1", "Deployment", "web", "prod")]: web },
|
|
142
|
+
});
|
|
143
|
+
const result = await describeResources(
|
|
144
|
+
{
|
|
145
|
+
environment: "prod",
|
|
146
|
+
buildOutput: "",
|
|
147
|
+
entityNames: ["web"],
|
|
148
|
+
entities: makeEntities([
|
|
149
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
|
|
150
|
+
]),
|
|
151
|
+
},
|
|
152
|
+
cluster.connector,
|
|
153
|
+
);
|
|
154
|
+
expect(result.resources.web).toBeDefined();
|
|
155
|
+
expect(result.queried?.web).toBe("/apis/apps/v1/namespaces/prod/deployments/web");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a read-failed unobserved entry carries the address the failing read was issued against (#1620)", async () => {
|
|
159
|
+
const cluster = fakeCluster({
|
|
160
|
+
respond: (req) =>
|
|
161
|
+
req.path.endsWith("/deployments/web")
|
|
162
|
+
? { status: 500, body: statusBody(500, "InternalError", "etcdserver: request timed out") }
|
|
163
|
+
: undefined,
|
|
164
|
+
});
|
|
165
|
+
const result = await describeResources(
|
|
166
|
+
{
|
|
167
|
+
environment: "prod",
|
|
168
|
+
buildOutput: "",
|
|
169
|
+
entityNames: ["web"],
|
|
170
|
+
entities: makeEntities([
|
|
171
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
|
|
172
|
+
]),
|
|
173
|
+
},
|
|
174
|
+
cluster.connector,
|
|
175
|
+
);
|
|
176
|
+
expect(result.unobserved?.web).toMatchObject({
|
|
177
|
+
reason: "read-failed",
|
|
178
|
+
queried: "/apis/apps/v1/namespaces/prod/deployments/web",
|
|
179
|
+
});
|
|
180
|
+
expect(result.queried?.web).toBe("/apis/apps/v1/namespaces/prod/deployments/web");
|
|
181
|
+
});
|
|
182
|
+
|
|
108
183
|
// The headline of chant #1074: the twenty-entry map is gone, so a CRD is an
|
|
109
184
|
// ordinary read rather than a permanent hole.
|
|
110
185
|
test("a CRD is observed like anything else — no KUBECTL_RESOURCE entry required", async () => {
|
|
@@ -255,6 +255,14 @@ export async function describeResources(
|
|
|
255
255
|
): Promise<ObservationResult> {
|
|
256
256
|
const resources: Record<string, ResourceMetadata> = {};
|
|
257
257
|
const unobserved: Record<string, UnobservedEntity> = {};
|
|
258
|
+
// The resolved query address per declared entity (#1620) — the exact request
|
|
259
|
+
// path each read was issued against, with the namespace the client actually
|
|
260
|
+
// defaulted to when the declaration carries none. This is what lets an
|
|
261
|
+
// absence explain itself: a Flux-deployed object declaring no
|
|
262
|
+
// metadata.namespace reads from `default`, correctly comes back 404, and
|
|
263
|
+
// without the address the report says "not there" when the truth is "not
|
|
264
|
+
// looked for where it lives".
|
|
265
|
+
const queried: Record<string, string> = {};
|
|
258
266
|
|
|
259
267
|
const declared: Declared[] = [...options.entities].map(([entityName, entity]) => ({
|
|
260
268
|
entityName,
|
|
@@ -323,13 +331,27 @@ export async function describeResources(
|
|
|
323
331
|
return;
|
|
324
332
|
}
|
|
325
333
|
|
|
334
|
+
const ref = {
|
|
335
|
+
apiVersion: operation.apiVersion,
|
|
336
|
+
kind: operation.kind,
|
|
337
|
+
name,
|
|
338
|
+
...(metadata?.namespace ? { namespace: metadata.namespace } : {}),
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Resolve the address BEFORE the read, so every verdict below — present,
|
|
342
|
+
// absent, filtered, read-failed — carries the same record of what was
|
|
343
|
+
// asked. Resolution rides the client's cached discovery; a kind discovery
|
|
344
|
+
// does not serve has no path, and a resolution failure classifies through
|
|
345
|
+
// the read below, never here.
|
|
346
|
+
try {
|
|
347
|
+
const address = typeof client.pathFor === "function" ? await client.pathFor(ref) : undefined;
|
|
348
|
+
if (address) queried[entityName] = address;
|
|
349
|
+
} catch {
|
|
350
|
+
// The read below hits the same resolution and classifies its failure.
|
|
351
|
+
}
|
|
352
|
+
|
|
326
353
|
try {
|
|
327
|
-
const obj = await client.read(
|
|
328
|
-
apiVersion: operation.apiVersion,
|
|
329
|
-
kind: operation.kind,
|
|
330
|
-
name,
|
|
331
|
-
...(metadata?.namespace ? { namespace: metadata.namespace } : {}),
|
|
332
|
-
});
|
|
354
|
+
const obj = await client.read(ref);
|
|
333
355
|
|
|
334
356
|
// owned filter: withhold resources not carrying chant's marker label.
|
|
335
357
|
// Withheld is not absent (chant #1089) — this object exists, it just
|
|
@@ -340,6 +362,7 @@ export async function describeResources(
|
|
|
340
362
|
type: entityType,
|
|
341
363
|
reason: "filtered",
|
|
342
364
|
detail: "live object carries no chant ownership marker and --owned was requested",
|
|
365
|
+
...(queried[entityName] ? { queried: queried[entityName] } : {}),
|
|
343
366
|
};
|
|
344
367
|
return;
|
|
345
368
|
}
|
|
@@ -361,16 +384,22 @@ export async function describeResources(
|
|
|
361
384
|
} catch (err) {
|
|
362
385
|
const outcome = classifyApiFailure(err);
|
|
363
386
|
if (outcome.kind === "unobserved") {
|
|
364
|
-
unobserved[entityName] = {
|
|
387
|
+
unobserved[entityName] = {
|
|
388
|
+
type: entityType,
|
|
389
|
+
reason: outcome.reason,
|
|
390
|
+
detail: outcome.detail,
|
|
391
|
+
...(queried[entityName] ? { queried: queried[entityName] } : {}),
|
|
392
|
+
};
|
|
365
393
|
}
|
|
366
394
|
// `absent` deliberately records nothing: in neither map is how the
|
|
367
|
-
// contract spells "asked, and it is not there".
|
|
395
|
+
// contract spells "asked, and it is not there". Its address stays in
|
|
396
|
+
// `queried` (#1620), which is the only record an absence gets.
|
|
368
397
|
}
|
|
369
398
|
});
|
|
370
399
|
|
|
371
400
|
await addRuntimeChildren(client, resources, unobserved, options.owned, declared);
|
|
372
401
|
|
|
373
|
-
return observation(resources, unobserved);
|
|
402
|
+
return observation(resources, unobserved, queried);
|
|
374
403
|
}
|
|
375
404
|
|
|
376
405
|
/**
|