@intentius/chant-lexicon-k8s 0.30.0 → 0.31.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 +55 -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/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/describe-resources.d.ts +41 -24
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/export-resources.d.ts +27 -1
- package/dist/export-resources.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +2 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/kubectl.d.ts +38 -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/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 +131 -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/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/describe-resources.test.ts +396 -191
- package/src/describe-resources.ts +134 -118
- package/src/export-resources-io.test.ts +76 -51
- package/src/export-resources.ts +63 -35
- package/src/generated/operations.json +2156 -0
- package/src/lifecycle-integration.test.ts +132 -92
- package/src/op/activities/index.ts +2 -1
- package/src/op/activities/kubectl.test.ts +148 -0
- package/src/op/activities/kubectl.ts +86 -13
- package/src/op/activities/wait-for-ready.test.ts +94 -0
- package/src/op/activities/wait-for-ready.ts +66 -15
- package/src/spec/parse.ts +93 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
waitForReady,
|
|
4
|
+
apiResourceFetcher,
|
|
4
5
|
ReadinessFailedError,
|
|
5
6
|
readinessFor,
|
|
6
7
|
isReady,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
} from "./wait-for-ready";
|
|
12
13
|
// The k8sWait profile marks ReadinessFailedError non-retryable for this activity.
|
|
13
14
|
import { TEMPORAL_ACTIVITY_PROFILES } from "@intentius/chant-lexicon-temporal/config";
|
|
15
|
+
import { fakeCluster, objectKey } from "../../api/fake-cluster";
|
|
14
16
|
|
|
15
17
|
/** A fetcher returning a scripted sequence of objects, repeating the last. */
|
|
16
18
|
function scriptedFetcher(sequence: Array<Record<string, unknown>>): ResourceFetcher {
|
|
@@ -103,3 +105,95 @@ describe("waitForReady", () => {
|
|
|
103
105
|
expect((obj as any).status.state).toBe("running");
|
|
104
106
|
});
|
|
105
107
|
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* chant #1074 — the reader underneath. The activity's contract is unchanged
|
|
111
|
+
* (`kind` is still whatever `kubectl get` accepts), so what has to be proven is
|
|
112
|
+
* that the same strings still resolve, now through the cluster's own API
|
|
113
|
+
* discovery rather than by handing them to a `kubectl` process.
|
|
114
|
+
*/
|
|
115
|
+
describe("apiResourceFetcher (chant #1074)", () => {
|
|
116
|
+
const readyObject = (apiVersion: string, kind: string, name: string, namespace?: string) => ({
|
|
117
|
+
apiVersion,
|
|
118
|
+
kind,
|
|
119
|
+
metadata: { name, ...(namespace ? { namespace } : {}), generation: 1 },
|
|
120
|
+
status: { observedGeneration: 1, conditions: [{ type: "Ready", status: "True" }] },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test.each([
|
|
124
|
+
["raycluster.ray.io", "ray.io/v1", "RayCluster", "rayclusters", "K8s::Ray::RayCluster"],
|
|
125
|
+
["certificates", "cert-manager.io/v1", "Certificate", "certificates", "K8s::CertManager::Certificate"],
|
|
126
|
+
["Certificate", "cert-manager.io/v1", "Certificate", "certificates", "K8s::CertManager::Certificate"],
|
|
127
|
+
["deployments", "apps/v1", "Deployment", "deployments", "K8s::Apps::Deployment"],
|
|
128
|
+
])("`%s` resolves to %s %s via discovery", async (kindArg, apiVersion, kind, plural, entityType) => {
|
|
129
|
+
const cluster = fakeCluster({
|
|
130
|
+
serves: [entityType],
|
|
131
|
+
objects: { [objectKey(apiVersion, kind, "thing", "prod")]: readyObject(apiVersion, kind, "thing", "prod") },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const obj = await waitForReady(
|
|
135
|
+
{ kind: kindArg, name: "thing", namespace: "prod", intervalMs: 0 },
|
|
136
|
+
undefined,
|
|
137
|
+
apiResourceFetcher(cluster.connector),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect((obj as Record<string, unknown>).kind).toBe(kind);
|
|
141
|
+
expect(cluster.layer.paths()).toContain(
|
|
142
|
+
`${apiVersion.includes("/") ? `/apis/${apiVersion}` : `/api/${apiVersion}`}/namespaces/prod/${plural}/thing`,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("resolution and the connection are done once, not once per poll", async () => {
|
|
147
|
+
const notReady = {
|
|
148
|
+
apiVersion: "cert-manager.io/v1",
|
|
149
|
+
kind: "Certificate",
|
|
150
|
+
metadata: { name: "tls", namespace: "prod", generation: 1 },
|
|
151
|
+
status: { observedGeneration: 1, conditions: [{ type: "Ready", status: "False" }] },
|
|
152
|
+
};
|
|
153
|
+
let polls = 0;
|
|
154
|
+
const cluster = fakeCluster({
|
|
155
|
+
serves: ["K8s::CertManager::Certificate"],
|
|
156
|
+
objects: { [objectKey("cert-manager.io/v1", "Certificate", "tls", "prod")]: notReady },
|
|
157
|
+
respond: (req) => {
|
|
158
|
+
if (!req.path.endsWith("/certificates/tls")) return undefined;
|
|
159
|
+
polls++;
|
|
160
|
+
return polls < 3
|
|
161
|
+
? { body: notReady }
|
|
162
|
+
: { body: { ...notReady, status: { observedGeneration: 1, conditions: [{ type: "Ready", status: "True" }] } } };
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await waitForReady(
|
|
167
|
+
{ kind: "certificate", name: "tls", namespace: "prod", intervalMs: 0 },
|
|
168
|
+
undefined,
|
|
169
|
+
apiResourceFetcher(cluster.connector),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
expect(polls).toBe(3);
|
|
173
|
+
expect(cluster.connects).toHaveLength(1);
|
|
174
|
+
// Discovery once; the three object reads reuse the cached resource list.
|
|
175
|
+
expect(cluster.layer.paths().filter((p) => p === "/apis/cert-manager.io/v1")).toHaveLength(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a kind the cluster does not serve fails loudly instead of polling forever", async () => {
|
|
179
|
+
const cluster = fakeCluster({ serves: ["K8s::Apps::Deployment"] });
|
|
180
|
+
await expect(
|
|
181
|
+
waitForReady({ kind: "widgets", name: "w", intervalMs: 0 }, undefined, apiResourceFetcher(cluster.connector)),
|
|
182
|
+
).rejects.toThrow(/no resource matching "widgets"/);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("an explicit context is passed to the connector, closing the read/write split", async () => {
|
|
186
|
+
const cluster = fakeCluster({
|
|
187
|
+
serves: ["K8s::Apps::Deployment"],
|
|
188
|
+
objects: { [objectKey("apps/v1", "Deployment", "web", "prod")]: readyObject("apps/v1", "Deployment", "web", "prod") },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await waitForReady(
|
|
192
|
+
{ kind: "deployments", name: "web", namespace: "prod", context: "test-context", intervalMs: 0 },
|
|
193
|
+
undefined,
|
|
194
|
+
apiResourceFetcher(cluster.connector),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(cluster.connects[0]).toMatchObject({ context: "test-context" });
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
1
|
import { safeHeartbeat, sleep } from "@intentius/chant/op";
|
|
4
|
-
|
|
5
|
-
const execAsync = promisify(exec);
|
|
2
|
+
import { defaultK8sConnector, type K8sConnector } from "../../api/connect";
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
* waitForReady — block until any operator-backed Kubernetes resource reports
|
|
@@ -11,9 +8,16 @@ const execAsync = promisify(exec);
|
|
|
11
8
|
* Like `waitForArgoSync`, this activity is intentionally **dependency-light**:
|
|
12
9
|
* its signature is primitives + a plain readiness spec, so a Temporal worker
|
|
13
10
|
* loads it without importing the generated CRD declarable surface. It reads the
|
|
14
|
-
* resource
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* resource and evaluates the spec's predicates. It generalizes the bespoke
|
|
12
|
+
* `waitForArgoSync` / `waitForStack` waits — see #365.
|
|
13
|
+
*
|
|
14
|
+
* chant #1074 moved the read from `kubectl get -o json` to the typed API
|
|
15
|
+
* client, so a worker image needs no `kubectl` binary. The signature is
|
|
16
|
+
* unchanged — `kind` is still whatever `kubectl get` accepts, because that is
|
|
17
|
+
* what every existing caller passes, and the client resolves it through the
|
|
18
|
+
* cluster's own API discovery exactly as kubectl does: plural, then singular,
|
|
19
|
+
* then kind, then short name, with anything after the first dot read as the
|
|
20
|
+
* API group.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
// ── Readiness spec (plain data — no generated-type imports) ──────────
|
|
@@ -154,6 +158,11 @@ export interface WaitForReadyArgs {
|
|
|
154
158
|
* `.context` through.
|
|
155
159
|
*/
|
|
156
160
|
context?: string;
|
|
161
|
+
/**
|
|
162
|
+
* chant environment, used to resolve `k8s.profiles.<env>.context` when no
|
|
163
|
+
* explicit `context` is given. Optional and additive.
|
|
164
|
+
*/
|
|
165
|
+
environment?: string;
|
|
157
166
|
/** API group, used to pick a readiness override when `spec` is not given. */
|
|
158
167
|
group?: string;
|
|
159
168
|
/** Explicit readiness spec — wins over the registry/default. */
|
|
@@ -168,15 +177,57 @@ export type ResourceFetcher = (
|
|
|
168
177
|
signal?: AbortSignal,
|
|
169
178
|
) => Promise<Record<string, unknown>>;
|
|
170
179
|
|
|
171
|
-
/**
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Read the resource through the typed API client.
|
|
182
|
+
*
|
|
183
|
+
* A client is built once per `waitForReady` call and reused for every poll, so
|
|
184
|
+
* a 20-minute wait does not re-parse the kubeconfig or re-invoke an exec
|
|
185
|
+
* credential plugin on each iteration — and neither does it re-run discovery,
|
|
186
|
+
* which the client caches per API version.
|
|
187
|
+
*/
|
|
188
|
+
export function apiResourceFetcher(connect: K8sConnector = defaultK8sConnector): ResourceFetcher {
|
|
189
|
+
let connection: ReturnType<K8sConnector> | undefined;
|
|
190
|
+
let resolved: { apiVersion: string; kind: string } | undefined;
|
|
191
|
+
|
|
192
|
+
return async (args, signal) => {
|
|
193
|
+
connection ??= connect({
|
|
194
|
+
...(args.environment !== undefined ? { environment: args.environment } : {}),
|
|
195
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
196
|
+
});
|
|
197
|
+
const { client } = await connection;
|
|
198
|
+
|
|
199
|
+
if (!resolved) {
|
|
200
|
+
const info = await client.resolve(
|
|
201
|
+
{ resource: args.kind, ...(args.group ? { group: args.group } : {}) },
|
|
202
|
+
signal,
|
|
203
|
+
);
|
|
204
|
+
if (!info) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`waitForReady: the cluster's API discovery reports no resource matching "${args.kind}"` +
|
|
207
|
+
`${args.group ? ` in group "${args.group}"` : ""} — nothing to wait for`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
resolved = { apiVersion: info.apiVersion, kind: info.kind };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return (await client.read(
|
|
214
|
+
{
|
|
215
|
+
apiVersion: resolved.apiVersion,
|
|
216
|
+
kind: resolved.kind,
|
|
217
|
+
name: args.name,
|
|
218
|
+
...(args.namespace ? { namespace: args.namespace } : {}),
|
|
219
|
+
},
|
|
220
|
+
{ signal },
|
|
221
|
+
)) as Record<string, unknown>;
|
|
222
|
+
};
|
|
177
223
|
}
|
|
178
224
|
|
|
179
|
-
|
|
225
|
+
/**
|
|
226
|
+
* The production reader. Each call builds its own fetcher, so nothing is
|
|
227
|
+
* shared between two unrelated waits; a single `waitForReady` passes one
|
|
228
|
+
* fetcher through all of its polls, which is where the caching matters.
|
|
229
|
+
*/
|
|
230
|
+
export const defaultResourceFetcher: ResourceFetcher = (args, signal) => apiResourceFetcher()(args, signal);
|
|
180
231
|
|
|
181
232
|
/**
|
|
182
233
|
* Poll until the resource satisfies its readiness spec. Throws
|
|
@@ -189,7 +240,7 @@ export const defaultResourceFetcher: ResourceFetcher = (args, signal) => fetchVi
|
|
|
189
240
|
export async function waitForReady(
|
|
190
241
|
args: WaitForReadyArgs,
|
|
191
242
|
signal?: AbortSignal,
|
|
192
|
-
fetcher: ResourceFetcher =
|
|
243
|
+
fetcher: ResourceFetcher = apiResourceFetcher(),
|
|
193
244
|
): Promise<Record<string, unknown>> {
|
|
194
245
|
const spec = args.spec ?? readinessFor(args.group, args.kind);
|
|
195
246
|
const interval = args.intervalMs ?? 15_000;
|
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
|
*/
|