@intentius/chant-lexicon-k8s 0.1.8 → 0.1.9
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/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +3 -3
- package/src/index.ts +3 -0
- package/src/micro-time.test.ts +48 -0
- package/src/micro-time.ts +51 -0
- package/src/plugin.ts +0 -90
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "3b5e686ba2f5230ae91d763af4ce3eacde217b82c51db5063ae79415383ce2bc",
|
|
5
5
|
"meta.json": "970c70eb6eea1686f7cb8ce6ceccc46ce2e57d696d344f1dd52460e266f9a56c",
|
|
6
6
|
"types/index.d.ts": "07473e029254345488bc9952585e88bcf18da79d1026cc26744027683f472554",
|
|
7
7
|
"rules/hardcoded-namespace.ts": "ba3f43f2adbffdd87db20a2c45839354ceecda1b9f04f29ae31c4c077dddc7ec",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"skills/chant-k8s-gke.md": "8938840bf9ef5ed58d6333fdd773b3dd54ecaf25a9df35e58f7f5c3355d4928f",
|
|
43
43
|
"skills/chant-k8s-aks.md": "e18f0e2b055f72cd7a37deaf258d7027c2d4d3e286e8fd4975b27a1f981a3ad9"
|
|
44
44
|
},
|
|
45
|
-
"composite": "
|
|
45
|
+
"composite": "1cf502ba76b37beb544a97555310f41a48fc251e44330f3ac67faebeee530e2f"
|
|
46
46
|
}
|
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.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Kubernetes lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"prepack": "npm run generate && npm run bundle && npm run validate"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"js-yaml": "^4.1.1"
|
|
46
|
+
"js-yaml": "^4.1.1",
|
|
47
|
+
"@types/js-yaml": "^4.0.9"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@intentius/chant": "*",
|
|
50
|
-
"@types/js-yaml": "^4.0.9",
|
|
51
51
|
"typescript": "^5.9.3"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,9 @@ export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-lab
|
|
|
11
11
|
// Variables / label constants
|
|
12
12
|
export { K8sLabels, K8sAnnotations } from "./variables";
|
|
13
13
|
|
|
14
|
+
// MicroTime helpers — see micro-time.ts for why this exists.
|
|
15
|
+
export { microTime, isMicroTimeFormatted } from "./micro-time";
|
|
16
|
+
|
|
14
17
|
// Generated entities — export everything from generated index
|
|
15
18
|
// After running `chant generate`, this re-exports all K8s resource classes
|
|
16
19
|
export * from "./generated/index";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { microTime, isMicroTimeFormatted } from "./micro-time";
|
|
3
|
+
|
|
4
|
+
describe("microTime", () => {
|
|
5
|
+
test("formats a Date with 6 fractional digits + Z", () => {
|
|
6
|
+
const d = new Date("2026-05-11T21:25:36.495Z");
|
|
7
|
+
expect(microTime(d)).toBe("2026-05-11T21:25:36.495000Z");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("handles dates whose ms is zero", () => {
|
|
11
|
+
const d = new Date("2026-01-01T00:00:00.000Z");
|
|
12
|
+
expect(microTime(d)).toBe("2026-01-01T00:00:00.000000Z");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("defaults to current time when called with no args", () => {
|
|
16
|
+
const out = microTime();
|
|
17
|
+
expect(out).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("output is accepted by isMicroTimeFormatted()", () => {
|
|
21
|
+
expect(isMicroTimeFormatted(microTime(new Date()))).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("isMicroTimeFormatted", () => {
|
|
26
|
+
test("accepts canonical MicroTime strings", () => {
|
|
27
|
+
expect(isMicroTimeFormatted("2026-05-11T21:25:36.495000Z")).toBe(true);
|
|
28
|
+
expect(isMicroTimeFormatted("2026-05-11T21:25:36.000000Z")).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("rejects nanosecond precision (the bug this helper avoids)", () => {
|
|
32
|
+
// time.RFC3339Nano in Go: 9 fractional digits
|
|
33
|
+
expect(isMicroTimeFormatted("2026-05-11T21:25:36.495095754Z")).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("rejects millisecond precision (Date.toISOString() shape)", () => {
|
|
37
|
+
expect(isMicroTimeFormatted("2026-05-11T21:25:36.495Z")).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("rejects non-UTC offset", () => {
|
|
41
|
+
expect(isMicroTimeFormatted("2026-05-11T21:25:36.495000-05:00")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("rejects garbage", () => {
|
|
45
|
+
expect(isMicroTimeFormatted("yesterday")).toBe(false);
|
|
46
|
+
expect(isMicroTimeFormatted("")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for K8s `MicroTime` fields.
|
|
3
|
+
*
|
|
4
|
+
* The K8s API server validates fields like `coordination.k8s.io/v1.Lease.spec.renewTime`
|
|
5
|
+
* (kind: MicroTime) with the exact format `2006-01-02T15:04:05.000000Z07:00`
|
|
6
|
+
* — microsecond precision, always 6 fractional digits, UTC offset.
|
|
7
|
+
*
|
|
8
|
+
* The naive JS `Date.toISOString()` (millisecond precision, 3 fractional
|
|
9
|
+
* digits) is accepted by the API server too — the schema is more permissive
|
|
10
|
+
* than the documented format string — but `time.RFC3339Nano`-style strings
|
|
11
|
+
* with nanosecond precision (9 fractional digits) are rejected as
|
|
12
|
+
* `422 Unprocessable Entity` with:
|
|
13
|
+
*
|
|
14
|
+
* parsing time "...754Z" as "2006-01-02T15:04:05.000000Z07:00":
|
|
15
|
+
* cannot parse "754Z" as "Z07:00"
|
|
16
|
+
*
|
|
17
|
+
* Pass a JS Date through `microTime()` to get a string the API server
|
|
18
|
+
* always accepts. This is the canonical way to populate MicroTime fields
|
|
19
|
+
* when authoring chant resources by hand.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format a Date as a K8s MicroTime string (UTC, exactly 6 fractional digits).
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* import { Lease } from "@intentius/chant-lexicon-k8s";
|
|
27
|
+
* import { microTime } from "@intentius/chant-lexicon-k8s/micro-time";
|
|
28
|
+
*
|
|
29
|
+
* new Lease({
|
|
30
|
+
* metadata: { name: "my-lease", namespace: "default" },
|
|
31
|
+
* spec: {
|
|
32
|
+
* holderIdentity: "node-1",
|
|
33
|
+
* leaseDurationSeconds: 15,
|
|
34
|
+
* renewTime: microTime(new Date()),
|
|
35
|
+
* },
|
|
36
|
+
* });
|
|
37
|
+
*/
|
|
38
|
+
export function microTime(date: Date = new Date()): string {
|
|
39
|
+
const iso = date.toISOString(); // "2026-05-11T21:25:36.495Z" (3 digits)
|
|
40
|
+
// Replace the millisecond fragment with a 6-digit microsecond fragment.
|
|
41
|
+
// toISOString() always produces ms precision; pad to 6 by appending 3 zeros.
|
|
42
|
+
return iso.replace(/\.(\d{3})Z$/, ".$1000Z");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns true if `value` is a string in the K8s MicroTime canonical format
|
|
47
|
+
* (UTC, microsecond precision). Useful in lint/validation contexts.
|
|
48
|
+
*/
|
|
49
|
+
export function isMicroTimeFormatted(value: string): boolean {
|
|
50
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/.test(value);
|
|
51
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -273,96 +273,6 @@ export const service = new Service({
|
|
|
273
273
|
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
274
274
|
},
|
|
275
275
|
|
|
276
|
-
async describeResources(options: {
|
|
277
|
-
environment: string;
|
|
278
|
-
buildOutput: string;
|
|
279
|
-
entityNames: string[];
|
|
280
|
-
}): Promise<Record<string, ResourceMetadata>> {
|
|
281
|
-
const { getRuntime } = await import("@intentius/chant/runtime-adapter");
|
|
282
|
-
const rt = getRuntime();
|
|
283
|
-
const resources: Record<string, ResourceMetadata> = {};
|
|
284
|
-
|
|
285
|
-
// Resolve namespace from environment (convention: env name = namespace)
|
|
286
|
-
const namespace = options.environment;
|
|
287
|
-
|
|
288
|
-
// Parse build output to extract kind/name pairs for each entity
|
|
289
|
-
let manifests: Array<{ kind: string; metadata: { name: string; namespace?: string }; apiVersion: string }> = [];
|
|
290
|
-
try {
|
|
291
|
-
// K8s build output is YAML with --- separators
|
|
292
|
-
const docs = options.buildOutput.split(/^---$/m).filter((d) => d.trim());
|
|
293
|
-
for (const doc of docs) {
|
|
294
|
-
// Simple YAML parsing — look for kind: and metadata.name:
|
|
295
|
-
const kindMatch = doc.match(/^kind:\s*(.+)$/m);
|
|
296
|
-
const nameMatch = doc.match(/^\s+name:\s*(.+)$/m);
|
|
297
|
-
const apiVersionMatch = doc.match(/^apiVersion:\s*(.+)$/m);
|
|
298
|
-
if (kindMatch && nameMatch && apiVersionMatch) {
|
|
299
|
-
manifests.push({
|
|
300
|
-
kind: kindMatch[1].trim(),
|
|
301
|
-
metadata: { name: nameMatch[1].trim() },
|
|
302
|
-
apiVersion: apiVersionMatch[1].trim(),
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
} catch {
|
|
307
|
-
// If build output parsing fails, skip
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Query each resource
|
|
311
|
-
for (const entityName of options.entityNames) {
|
|
312
|
-
// Find the corresponding manifest
|
|
313
|
-
const manifest = manifests.find((m) => {
|
|
314
|
-
// Try matching by entity name convention
|
|
315
|
-
return m.metadata.name === entityName ||
|
|
316
|
-
entityName.toLowerCase().includes(m.metadata.name.toLowerCase());
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
if (!manifest) continue;
|
|
320
|
-
|
|
321
|
-
// Build kubectl resource type from apiVersion + kind
|
|
322
|
-
const resourceType = manifest.kind.toLowerCase();
|
|
323
|
-
const getResult = await rt.spawn([
|
|
324
|
-
"kubectl", "get", resourceType, manifest.metadata.name,
|
|
325
|
-
"-n", namespace,
|
|
326
|
-
"-o", "json",
|
|
327
|
-
]);
|
|
328
|
-
|
|
329
|
-
if (getResult.exitCode !== 0) continue;
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
const obj = JSON.parse(getResult.stdout) as {
|
|
333
|
-
metadata: { name: string; uid: string; creationTimestamp: string };
|
|
334
|
-
status?: { phase?: string; conditions?: Array<{ type: string; status: string }> };
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
// Derive status from conditions or phase
|
|
338
|
-
let status = "Unknown";
|
|
339
|
-
if (obj.status?.phase) {
|
|
340
|
-
status = obj.status.phase;
|
|
341
|
-
} else if (obj.status?.conditions) {
|
|
342
|
-
const ready = obj.status.conditions.find((c) => c.type === "Ready" || c.type === "Available");
|
|
343
|
-
status = ready?.status === "True" ? "Ready" : "NotReady";
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Build attributes, scrubbing sensitive data
|
|
347
|
-
const attributes: Record<string, unknown> = {
|
|
348
|
-
uid: obj.metadata.uid,
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
resources[entityName] = {
|
|
352
|
-
type: `${manifest.apiVersion}/${manifest.kind}`,
|
|
353
|
-
physicalId: obj.metadata.name,
|
|
354
|
-
status,
|
|
355
|
-
lastUpdated: obj.metadata.creationTimestamp,
|
|
356
|
-
attributes,
|
|
357
|
-
};
|
|
358
|
-
} catch {
|
|
359
|
-
// Skip parse failures
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return resources;
|
|
364
|
-
},
|
|
365
|
-
|
|
366
276
|
mcpTools() {
|
|
367
277
|
return [createDiffTool(k8sSerializer, "Compare current build output against previous output for Kubernetes manifests", "k8s")];
|
|
368
278
|
},
|