@intentius/chant-lexicon-k8s 0.30.0 → 0.32.0

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.
Files changed (137) hide show
  1. package/dist/api/classify.d.ts +65 -0
  2. package/dist/api/classify.d.ts.map +1 -0
  3. package/dist/api/connect.d.ts +58 -0
  4. package/dist/api/connect.d.ts.map +1 -0
  5. package/dist/api/fake-cluster.d.ts +55 -0
  6. package/dist/api/fake-cluster.d.ts.map +1 -0
  7. package/dist/api/operation-surface.d.ts +64 -0
  8. package/dist/api/operation-surface.d.ts.map +1 -0
  9. package/dist/api/owner-chain.d.ts +46 -0
  10. package/dist/api/owner-chain.d.ts.map +1 -0
  11. package/dist/api/sweep-types.d.ts +17 -0
  12. package/dist/api/sweep-types.d.ts.map +1 -0
  13. package/dist/codegen/generate-operations.d.ts +29 -0
  14. package/dist/codegen/generate-operations.d.ts.map +1 -0
  15. package/dist/codegen/generate.d.ts.map +1 -1
  16. package/dist/config.d.ts +17 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/crd/parser.d.ts.map +1 -1
  19. package/dist/crd/types.d.ts +7 -0
  20. package/dist/crd/types.d.ts.map +1 -1
  21. package/dist/deep-observe-hooks.d.ts +86 -0
  22. package/dist/deep-observe-hooks.d.ts.map +1 -0
  23. package/dist/deep-observe.d.ts +146 -0
  24. package/dist/deep-observe.d.ts.map +1 -0
  25. package/dist/describe-resources.d.ts +58 -26
  26. package/dist/describe-resources.d.ts.map +1 -1
  27. package/dist/export-resources.d.ts +31 -1
  28. package/dist/export-resources.d.ts.map +1 -1
  29. package/dist/integrity.json +2 -2
  30. package/dist/kube/apply.d.ts +26 -0
  31. package/dist/kube/apply.d.ts.map +1 -0
  32. package/dist/kube/connect.d.ts +30 -0
  33. package/dist/kube/connect.d.ts.map +1 -0
  34. package/dist/kube/delete.d.ts +27 -0
  35. package/dist/kube/delete.d.ts.map +1 -0
  36. package/dist/kube/describe.d.ts +19 -0
  37. package/dist/kube/describe.d.ts.map +1 -0
  38. package/dist/kube/events.d.ts +34 -0
  39. package/dist/kube/events.d.ts.map +1 -0
  40. package/dist/kube/flags.d.ts +40 -0
  41. package/dist/kube/flags.d.ts.map +1 -0
  42. package/dist/kube/get.d.ts +21 -0
  43. package/dist/kube/get.d.ts.map +1 -0
  44. package/dist/kube/group.d.ts +19 -0
  45. package/dist/kube/group.d.ts.map +1 -0
  46. package/dist/kube/logs.d.ts +16 -0
  47. package/dist/kube/logs.d.ts.map +1 -0
  48. package/dist/kube/project.d.ts +59 -0
  49. package/dist/kube/project.d.ts.map +1 -0
  50. package/dist/kube/render.d.ts +75 -0
  51. package/dist/kube/render.d.ts.map +1 -0
  52. package/dist/kube/source.d.ts +19 -0
  53. package/dist/kube/source.d.ts.map +1 -0
  54. package/dist/kube/target.d.ts +52 -0
  55. package/dist/kube/target.d.ts.map +1 -0
  56. package/dist/kube/testing.d.ts +21 -0
  57. package/dist/kube/testing.d.ts.map +1 -0
  58. package/dist/kube/top.d.ts +23 -0
  59. package/dist/kube/top.d.ts.map +1 -0
  60. package/dist/kube/verdict.d.ts +45 -0
  61. package/dist/kube/verdict.d.ts.map +1 -0
  62. package/dist/kube/wait.d.ts +23 -0
  63. package/dist/kube/wait.d.ts.map +1 -0
  64. package/dist/manifest.json +1 -1
  65. package/dist/op/activities/index.d.ts +7 -4
  66. package/dist/op/activities/index.d.ts.map +1 -1
  67. package/dist/op/activities/kubectl.d.ts +120 -2
  68. package/dist/op/activities/kubectl.d.ts.map +1 -1
  69. package/dist/op/activities/wait-for-ready.d.ts +30 -3
  70. package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
  71. package/dist/plugin.d.ts.map +1 -1
  72. package/dist/spec/parse.d.ts +42 -0
  73. package/dist/spec/parse.d.ts.map +1 -1
  74. package/package.json +5 -2
  75. package/src/api/classify.test.ts +133 -0
  76. package/src/api/classify.ts +144 -0
  77. package/src/api/connect.ts +104 -0
  78. package/src/api/fake-cluster.ts +218 -0
  79. package/src/api/operation-surface.test.ts +116 -0
  80. package/src/api/operation-surface.ts +129 -0
  81. package/src/api/owner-chain.test.ts +171 -0
  82. package/src/api/owner-chain.ts +144 -0
  83. package/src/api/sweep-types.ts +36 -0
  84. package/src/codegen/generate-operations.ts +56 -0
  85. package/src/codegen/generate.ts +9 -0
  86. package/src/config.ts +17 -0
  87. package/src/crd/parser.ts +8 -0
  88. package/src/crd/types.ts +7 -0
  89. package/src/deep-observe-hooks.ts +125 -0
  90. package/src/deep-observe.test.ts +668 -0
  91. package/src/deep-observe.ts +299 -0
  92. package/src/describe-resources.test.ts +619 -191
  93. package/src/describe-resources.ts +218 -117
  94. package/src/export-resources-io.test.ts +76 -51
  95. package/src/export-resources.ts +47 -35
  96. package/src/generated/operations.json +2156 -0
  97. package/src/kube/apply.test.ts +105 -0
  98. package/src/kube/apply.ts +101 -0
  99. package/src/kube/connect.ts +54 -0
  100. package/src/kube/delete.test.ts +123 -0
  101. package/src/kube/delete.ts +174 -0
  102. package/src/kube/describe.test.ts +106 -0
  103. package/src/kube/describe.ts +195 -0
  104. package/src/kube/events.test.ts +115 -0
  105. package/src/kube/events.ts +160 -0
  106. package/src/kube/flags.test.ts +65 -0
  107. package/src/kube/flags.ts +114 -0
  108. package/src/kube/get.test.ts +195 -0
  109. package/src/kube/get.ts +190 -0
  110. package/src/kube/group.test.ts +33 -0
  111. package/src/kube/group.ts +125 -0
  112. package/src/kube/logs.test.ts +126 -0
  113. package/src/kube/logs.ts +96 -0
  114. package/src/kube/project.test.ts +57 -0
  115. package/src/kube/project.ts +106 -0
  116. package/src/kube/render.test.ts +142 -0
  117. package/src/kube/render.ts +252 -0
  118. package/src/kube/source.test.ts +138 -0
  119. package/src/kube/source.ts +139 -0
  120. package/src/kube/target.test.ts +67 -0
  121. package/src/kube/target.ts +89 -0
  122. package/src/kube/testing.ts +42 -0
  123. package/src/kube/top.test.ts +149 -0
  124. package/src/kube/top.ts +120 -0
  125. package/src/kube/verdict.test.ts +48 -0
  126. package/src/kube/verdict.ts +77 -0
  127. package/src/kube/wait.test.ts +125 -0
  128. package/src/kube/wait.ts +160 -0
  129. package/src/lifecycle-integration.test.ts +132 -92
  130. package/src/op/activities/index.ts +7 -3
  131. package/src/op/activities/kubectl.test.ts +420 -0
  132. package/src/op/activities/kubectl.ts +379 -13
  133. package/src/op/activities/wait-for-ready.test.ts +94 -0
  134. package/src/op/activities/wait-for-ready.ts +66 -15
  135. package/src/plugin.test.ts +58 -1
  136. package/src/plugin.ts +29 -0
  137. package/src/spec/parse.ts +93 -1
@@ -1,4 +1,4 @@
1
- import { describe, test, expect } from "vitest";
1
+ import { describe, test, expect, vi } from "vitest";
2
2
  import { existsSync } from "fs";
3
3
  import { join, dirname } from "path";
4
4
  import { fileURLToPath } from "url";
@@ -211,4 +211,61 @@ describe("k8sPlugin", () => {
211
211
  test("package() method exists", () => {
212
212
  expect(typeof k8sPlugin.package).toBe("function");
213
213
  });
214
+
215
+ // chant #1078 — the command-group seam's first real tenant: `commands()`
216
+ // mounts a "kube" verb group, reserving the namespace #1079's `chant kube`
217
+ // terminal surface will add its own verbs to.
218
+ describe("commands() — the `kube` command group (#1078)", () => {
219
+ test("contributes a group named kube with a version verb", () => {
220
+ const group = k8sPlugin.commands!();
221
+ expect(group.name).toBe("kube");
222
+ expect(group.commands.map((c) => c.name)).toContain("version");
223
+ });
224
+
225
+ test("kube version prints the pinned K8S_SCHEMA_VERSION", async () => {
226
+ const { K8S_SCHEMA_VERSION } = await import("./spec/fetch");
227
+ const group = k8sPlugin.commands!();
228
+ const version = group.commands.find((c) => c.name === "version")!;
229
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
230
+ try {
231
+ const exitCode = await version.handler({ verb: "version", rawArgs: [] });
232
+ expect(exitCode).toBe(0);
233
+ expect(logSpy).toHaveBeenCalledWith(K8S_SCHEMA_VERSION);
234
+ } finally {
235
+ logSpy.mockRestore();
236
+ }
237
+ });
238
+
239
+ test("kube version --format json emits a JSON envelope", async () => {
240
+ const { K8S_SCHEMA_VERSION } = await import("./spec/fetch");
241
+ const group = k8sPlugin.commands!();
242
+ const version = group.commands.find((c) => c.name === "version")!;
243
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
244
+ try {
245
+ const exitCode = await version.handler({ verb: "version", rawArgs: ["--format", "json"] });
246
+ expect(exitCode).toBe(0);
247
+ expect(logSpy).toHaveBeenCalledWith(JSON.stringify({ schemaVersion: K8S_SCHEMA_VERSION }));
248
+ } finally {
249
+ logSpy.mockRestore();
250
+ }
251
+ });
252
+
253
+ test("kube version --format=json (joined form, #1127 discipline) works identically", async () => {
254
+ const group = k8sPlugin.commands!();
255
+ const version = group.commands.find((c) => c.name === "version")!;
256
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
257
+ try {
258
+ await version.handler({ verb: "version", rawArgs: ["--format=json"] });
259
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("schemaVersion"));
260
+ } finally {
261
+ logSpy.mockRestore();
262
+ }
263
+ });
264
+
265
+ test("kube version rejects an unknown flag (#1127 discipline applies to mounted commands)", async () => {
266
+ const group = k8sPlugin.commands!();
267
+ const version = group.commands.find((c) => c.name === "version")!;
268
+ await expect(version.handler({ verb: "version", rawArgs: ["--bogus"] })).rejects.toThrow(/Unknown flag: --bogus/);
269
+ });
270
+ });
214
271
  });
package/src/plugin.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  */
7
7
 
8
8
  import type { LexiconPlugin, InitTemplateSet, ResourceMetadata } from "@intentius/chant/lexicon";
9
+ import type { CommandGroup } from "@intentius/chant/cli/command-group";
10
+ import { kubeCommandGroup } from "./kube/group";
9
11
  import { detectTemplate } from "./detect";
10
12
  import type { LintRule } from "@intentius/chant/lint/rule";
11
13
  import { postSynthChecks as postSynthCheckList } from "./lint/post-synth";
@@ -23,6 +25,7 @@ import { k8sCompletions } from "./lsp/completions";
23
25
  import { k8sHover } from "./lsp/hover";
24
26
  import { K8sParser } from "./import/parser";
25
27
  import { K8sGenerator } from "./import/generator";
28
+ import { k8sDeepNormalizationHooks } from "./deep-observe-hooks";
26
29
 
27
30
  export const k8sPlugin: LexiconPlugin = {
28
31
  name: "k8s",
@@ -37,6 +40,17 @@ export const k8sPlugin: LexiconPlugin = {
37
40
  upstream: { owner: "kubernetes", repo: "kubernetes", kind: "releases" },
38
41
  },
39
42
 
43
+ // chant #1078/#1079 — the command-group seam's tenant. `kube` mounts the
44
+ // terminal surface over the typed client: kubectl-compatible reads (get,
45
+ // describe, logs, events, top, wait), chant's own additions (source, the
46
+ // `get` verdict column, -o chant), and gated writes (apply, delete) that
47
+ // route through the exact same machinery the Op activities use — never a
48
+ // parallel implementation. See ./kube/group.ts for the full verb list;
49
+ // this plugin only mounts it.
50
+ commands(): CommandGroup {
51
+ return kubeCommandGroup();
52
+ },
53
+
40
54
  lintRules(): LintRule[] {
41
55
  return [
42
56
  hardcodedNamespaceRule,
@@ -617,4 +631,19 @@ const { deployment, service, serviceMonitor, prometheusRule } = MonitoredService
617
631
  const { exportResources } = await import("./export-resources");
618
632
  return exportResources(options);
619
633
  },
634
+
635
+ // Property-level live drift via SSA managed-fields (#1076, epic #1073).
636
+ // The reader lives in ./deep-observe.ts, loaded only through this dynamic
637
+ // import — same reason describeResources/exportResources are — so the API
638
+ // client never becomes reachable from the build path (chant #1074,
639
+ // examples/k8s-client-boundary.test.ts). deepNormalizationHooks is plain
640
+ // data with no client dependency (./deep-observe-hooks.ts) and is imported
641
+ // statically above, because core applies it to the *declared* tree whether
642
+ // or not a live read ever happens.
643
+ async observeResourcesDeep(options) {
644
+ const { observeResourcesDeepK8s } = await import("./deep-observe");
645
+ return observeResourcesDeepK8s(options);
646
+ },
647
+
648
+ deepNormalizationHooks: k8sDeepNormalizationHooks,
620
649
  };
package/src/spec/parse.ts CHANGED
@@ -53,6 +53,26 @@ export interface GroupVersionKind {
53
53
  kind: string;
54
54
  }
55
55
 
56
+ /**
57
+ * How this resource is addressed over the API — chant #1074.
58
+ *
59
+ * Read out of the same document the resource's types come from (the OpenAPI
60
+ * `paths` for core kinds, the CRD's `spec.names` / `spec.scope` for custom
61
+ * ones), so the operation surface and the declarable surface cannot drift
62
+ * apart the way a hand-maintained `kind → kubectl resource` table did.
63
+ *
64
+ * It is a starting point, not the authority: the live client confirms plural
65
+ * and scope against the cluster's own discovery, which is the only thing that
66
+ * knows what a given cluster actually serves.
67
+ */
68
+ export interface ParsedOperation {
69
+ /** Plural path segment, e.g. `deployments`. */
70
+ plural: string;
71
+ scope: "Namespaced" | "Cluster";
72
+ /** Verbs the schema documents for the named resource, e.g. `get`, `patch`. */
73
+ verbs: string[];
74
+ }
75
+
56
76
  export interface K8sParseResult {
57
77
  resource: ParsedResource;
58
78
  propertyTypes: ParsedPropertyType[];
@@ -60,6 +80,8 @@ export interface K8sParseResult {
60
80
  gvk: GroupVersionKind;
61
81
  /** Whether this entity is a property type (nested inside resources) */
62
82
  isProperty?: boolean;
83
+ /** How the API addresses this resource. Absent for property types. */
84
+ operation?: ParsedOperation;
63
85
  }
64
86
 
65
87
  // ── Swagger types ──────────────────────────────────────────────────
@@ -90,9 +112,17 @@ interface SwaggerProperty extends SwaggerDefinition {
90
112
 
91
113
  interface SwaggerSpec {
92
114
  definitions?: Record<string, SwaggerDefinition>;
115
+ paths?: Record<string, SwaggerPathItem>;
93
116
  [key: string]: unknown;
94
117
  }
95
118
 
119
+ interface SwaggerOperation {
120
+ "x-kubernetes-group-version-kind"?: GroupVersionKind;
121
+ "x-kubernetes-action"?: string;
122
+ }
123
+
124
+ type SwaggerPathItem = Record<string, SwaggerOperation | unknown>;
125
+
96
126
  // ── Well-known property type definitions ───────────────────────────
97
127
 
98
128
  /**
@@ -162,6 +192,7 @@ export function parseK8sSwagger(data: string | Buffer): K8sParseResult[] {
162
192
  const spec: SwaggerSpec = JSON.parse(typeof data === "string" ? data : data.toString("utf-8"));
163
193
  const definitions = spec.definitions ?? {};
164
194
  const results: K8sParseResult[] = [];
195
+ const operations = parseOperations(spec.paths);
165
196
 
166
197
  // Phase 1: Extract top-level resources (definitions with GVK)
167
198
  for (const [defKey, def] of Object.entries(definitions)) {
@@ -176,7 +207,11 @@ export function parseK8sSwagger(data: string | Buffer): K8sParseResult[] {
176
207
 
177
208
  const typeName = gvkToTypeName(gvk);
178
209
  const result = extractResource(defKey, def, typeName, gvk, definitions);
179
- if (result) results.push(result);
210
+ if (result) {
211
+ const operation = operations.get(gvkKey(gvk));
212
+ if (operation) result.operation = operation;
213
+ results.push(result);
214
+ }
180
215
  }
181
216
 
182
217
  // Phase 2: Extract well-known property types
@@ -191,6 +226,63 @@ export function parseK8sSwagger(data: string | Buffer): K8sParseResult[] {
191
226
  return results;
192
227
  }
193
228
 
229
+ /** Stable key for a GVK, used to join the `paths` pass onto the `definitions` pass. */
230
+ export function gvkKey(gvk: GroupVersionKind): string {
231
+ return `${gvk.group}|${gvk.version}|${gvk.kind}`;
232
+ }
233
+
234
+ /**
235
+ * Derive the operation surface from the OpenAPI `paths` — chant #1074.
236
+ *
237
+ * Every Kubernetes operation carries `x-kubernetes-group-version-kind` and
238
+ * `x-kubernetes-action`, and the path itself carries the two facts a REST call
239
+ * needs and a definition does not have: the plural segment, and whether the
240
+ * resource is namespaced (`/namespaces/{namespace}/` appears in its path).
241
+ *
242
+ * Only paths addressing a single named object (`.../{plural}/{name}`) are read,
243
+ * so subresource paths (`.../{name}/status`, `.../{name}/scale`) and collection
244
+ * paths do not supply the plural — but their verbs are collected, because
245
+ * "this kind can be listed" is worth knowing.
246
+ */
247
+ export function parseOperations(paths: Record<string, SwaggerPathItem> | undefined): Map<string, ParsedOperation> {
248
+ const out = new Map<string, ParsedOperation>();
249
+ if (!paths) return out;
250
+
251
+ for (const [path, item] of Object.entries(paths)) {
252
+ for (const operation of Object.values(item ?? {})) {
253
+ if (!operation || typeof operation !== "object") continue;
254
+ const op = operation as SwaggerOperation;
255
+ const gvk = op["x-kubernetes-group-version-kind"];
256
+ const action = op["x-kubernetes-action"];
257
+ if (!gvk || !action) continue;
258
+
259
+ const segments = path.split("/").filter(Boolean);
260
+ const last = segments[segments.length - 1];
261
+ // `.../{plural}/{name}` — the only shape that names the plural
262
+ // unambiguously. `/api/v1/namespaces/{name}` is such a shape too, and
263
+ // correctly yields plural `namespaces`, cluster-scoped.
264
+ if (last !== "{name}") continue;
265
+ const plural = segments[segments.length - 2];
266
+ if (!plural || plural.startsWith("{")) continue;
267
+
268
+ const key = gvkKey(gvk);
269
+ const existing = out.get(key);
270
+ if (existing) {
271
+ if (!existing.verbs.includes(action)) existing.verbs.push(action);
272
+ continue;
273
+ }
274
+ out.set(key, {
275
+ plural,
276
+ scope: path.includes("/namespaces/{namespace}/") ? "Namespaced" : "Cluster",
277
+ verbs: [action],
278
+ });
279
+ }
280
+ }
281
+
282
+ for (const operation of out.values()) operation.verbs.sort();
283
+ return out;
284
+ }
285
+
194
286
  /**
195
287
  * Convert GVK to our type name convention: K8s::{Group}::{Kind}
196
288
  */