@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.
- package/dist/api/classify.d.ts +65 -0
- package/dist/api/classify.d.ts.map +1 -0
- package/dist/api/connect.d.ts +58 -0
- package/dist/api/connect.d.ts.map +1 -0
- package/dist/api/fake-cluster.d.ts +55 -0
- package/dist/api/fake-cluster.d.ts.map +1 -0
- package/dist/api/operation-surface.d.ts +64 -0
- package/dist/api/operation-surface.d.ts.map +1 -0
- package/dist/api/owner-chain.d.ts +46 -0
- package/dist/api/owner-chain.d.ts.map +1 -0
- package/dist/api/sweep-types.d.ts +17 -0
- package/dist/api/sweep-types.d.ts.map +1 -0
- package/dist/codegen/generate-operations.d.ts +29 -0
- package/dist/codegen/generate-operations.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/crd/parser.d.ts.map +1 -1
- package/dist/crd/types.d.ts +7 -0
- package/dist/crd/types.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts +86 -0
- package/dist/deep-observe-hooks.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +146 -0
- package/dist/deep-observe.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +58 -26
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/export-resources.d.ts +31 -1
- package/dist/export-resources.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/kube/apply.d.ts +26 -0
- package/dist/kube/apply.d.ts.map +1 -0
- package/dist/kube/connect.d.ts +30 -0
- package/dist/kube/connect.d.ts.map +1 -0
- package/dist/kube/delete.d.ts +27 -0
- package/dist/kube/delete.d.ts.map +1 -0
- package/dist/kube/describe.d.ts +19 -0
- package/dist/kube/describe.d.ts.map +1 -0
- package/dist/kube/events.d.ts +34 -0
- package/dist/kube/events.d.ts.map +1 -0
- package/dist/kube/flags.d.ts +40 -0
- package/dist/kube/flags.d.ts.map +1 -0
- package/dist/kube/get.d.ts +21 -0
- package/dist/kube/get.d.ts.map +1 -0
- package/dist/kube/group.d.ts +19 -0
- package/dist/kube/group.d.ts.map +1 -0
- package/dist/kube/logs.d.ts +16 -0
- package/dist/kube/logs.d.ts.map +1 -0
- package/dist/kube/project.d.ts +59 -0
- package/dist/kube/project.d.ts.map +1 -0
- package/dist/kube/render.d.ts +75 -0
- package/dist/kube/render.d.ts.map +1 -0
- package/dist/kube/source.d.ts +19 -0
- package/dist/kube/source.d.ts.map +1 -0
- package/dist/kube/target.d.ts +52 -0
- package/dist/kube/target.d.ts.map +1 -0
- package/dist/kube/testing.d.ts +21 -0
- package/dist/kube/testing.d.ts.map +1 -0
- package/dist/kube/top.d.ts +23 -0
- package/dist/kube/top.d.ts.map +1 -0
- package/dist/kube/verdict.d.ts +45 -0
- package/dist/kube/verdict.d.ts.map +1 -0
- package/dist/kube/wait.d.ts +23 -0
- package/dist/kube/wait.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +7 -4
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/kubectl.d.ts +120 -2
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/wait-for-ready.d.ts +30 -3
- package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/spec/parse.d.ts +42 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/api/classify.test.ts +133 -0
- package/src/api/classify.ts +144 -0
- package/src/api/connect.ts +104 -0
- package/src/api/fake-cluster.ts +218 -0
- package/src/api/operation-surface.test.ts +116 -0
- package/src/api/operation-surface.ts +129 -0
- package/src/api/owner-chain.test.ts +171 -0
- package/src/api/owner-chain.ts +144 -0
- package/src/api/sweep-types.ts +36 -0
- package/src/codegen/generate-operations.ts +56 -0
- package/src/codegen/generate.ts +9 -0
- package/src/config.ts +17 -0
- package/src/crd/parser.ts +8 -0
- package/src/crd/types.ts +7 -0
- package/src/deep-observe-hooks.ts +125 -0
- package/src/deep-observe.test.ts +668 -0
- package/src/deep-observe.ts +299 -0
- package/src/describe-resources.test.ts +619 -191
- package/src/describe-resources.ts +218 -117
- package/src/export-resources-io.test.ts +76 -51
- package/src/export-resources.ts +47 -35
- package/src/generated/operations.json +2156 -0
- package/src/kube/apply.test.ts +105 -0
- package/src/kube/apply.ts +101 -0
- package/src/kube/connect.ts +54 -0
- package/src/kube/delete.test.ts +123 -0
- package/src/kube/delete.ts +174 -0
- package/src/kube/describe.test.ts +106 -0
- package/src/kube/describe.ts +195 -0
- package/src/kube/events.test.ts +115 -0
- package/src/kube/events.ts +160 -0
- package/src/kube/flags.test.ts +65 -0
- package/src/kube/flags.ts +114 -0
- package/src/kube/get.test.ts +195 -0
- package/src/kube/get.ts +190 -0
- package/src/kube/group.test.ts +33 -0
- package/src/kube/group.ts +125 -0
- package/src/kube/logs.test.ts +126 -0
- package/src/kube/logs.ts +96 -0
- package/src/kube/project.test.ts +57 -0
- package/src/kube/project.ts +106 -0
- package/src/kube/render.test.ts +142 -0
- package/src/kube/render.ts +252 -0
- package/src/kube/source.test.ts +138 -0
- package/src/kube/source.ts +139 -0
- package/src/kube/target.test.ts +67 -0
- package/src/kube/target.ts +89 -0
- package/src/kube/testing.ts +42 -0
- package/src/kube/top.test.ts +149 -0
- package/src/kube/top.ts +120 -0
- package/src/kube/verdict.test.ts +48 -0
- package/src/kube/verdict.ts +77 -0
- package/src/kube/wait.test.ts +125 -0
- package/src/kube/wait.ts +160 -0
- package/src/lifecycle-integration.test.ts +132 -92
- package/src/op/activities/index.ts +7 -3
- package/src/op/activities/kubectl.test.ts +420 -0
- package/src/op/activities/kubectl.ts +379 -13
- package/src/op/activities/wait-for-ready.test.ts +94 -0
- package/src/op/activities/wait-for-ready.ts +66 -15
- package/src/plugin.test.ts +58 -1
- package/src/plugin.ts +29 -0
- package/src/spec/parse.ts +93 -1
package/src/plugin.test.ts
CHANGED
|
@@ -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)
|
|
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
|
*/
|