@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "26168ff9d5f4d906e451b0e2c9e7a868b9ae3ab366579d9abf76a45ace9a3b95",
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": "de04872487ff8b14bc40143791702adeea08ff52942eca65e90e6611623ed767"
45
+ "composite": "1cf502ba76b37beb544a97555310f41a48fc251e44330f3ac67faebeee530e2f"
46
46
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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.1.8",
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
  },