@numa-tech/numa 1.12.11 → 1.13.1
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/README.md +111 -4
- package/dist/application-onboarding/client.d.ts +83 -8
- package/dist/application-onboarding/client.js +23 -1
- package/dist/application-onboarding/client.js.map +1 -1
- package/dist/application-onboarding/commands.js +72 -6
- package/dist/application-onboarding/commands.js.map +1 -1
- package/dist/application-onboarding/schemas.d.ts +301 -58
- package/dist/application-onboarding/schemas.js +134 -8
- package/dist/application-onboarding/schemas.js.map +1 -1
- package/dist/application-onboarding/tui-model.d.ts +20 -0
- package/dist/application-onboarding/tui-model.js +362 -20
- package/dist/application-onboarding/tui-model.js.map +1 -1
- package/dist/application-onboarding/tui.d.ts +6 -1
- package/dist/application-onboarding/tui.js +104 -7
- package/dist/application-onboarding/tui.js.map +1 -1
- package/dist/cli.js +50 -8
- package/dist/cli.js.map +1 -1
- package/dist/clusters/client.d.ts +436 -0
- package/dist/clusters/client.js +207 -0
- package/dist/clusters/client.js.map +1 -0
- package/dist/clusters/commands.d.ts +11 -0
- package/dist/clusters/commands.js +373 -0
- package/dist/clusters/commands.js.map +1 -0
- package/dist/clusters/errors.d.ts +10 -0
- package/dist/clusters/errors.js +61 -0
- package/dist/clusters/errors.js.map +1 -0
- package/dist/clusters/kubeconfig-discovery.d.ts +48 -0
- package/dist/clusters/kubeconfig-discovery.js +280 -0
- package/dist/clusters/kubeconfig-discovery.js.map +1 -0
- package/dist/clusters/schemas.d.ts +510 -0
- package/dist/clusters/schemas.js +201 -0
- package/dist/clusters/schemas.js.map +1 -0
- package/dist/command-catalog.js +370 -1
- package/dist/command-catalog.js.map +1 -1
- package/dist/gitops/client.d.ts +499 -0
- package/dist/gitops/client.js +163 -0
- package/dist/gitops/client.js.map +1 -0
- package/dist/gitops/commands.d.ts +11 -0
- package/dist/gitops/commands.js +367 -0
- package/dist/gitops/commands.js.map +1 -0
- package/dist/gitops/errors.d.ts +10 -0
- package/dist/gitops/errors.js +59 -0
- package/dist/gitops/errors.js.map +1 -0
- package/dist/gitops/schemas.d.ts +1712 -0
- package/dist/gitops/schemas.js +360 -0
- package/dist/gitops/schemas.js.map +1 -0
- package/dist/repositories/client.d.ts +558 -0
- package/dist/repositories/client.js +215 -0
- package/dist/repositories/client.js.map +1 -0
- package/dist/repositories/commands.d.ts +13 -0
- package/dist/repositories/commands.js +368 -0
- package/dist/repositories/commands.js.map +1 -0
- package/dist/repositories/errors.d.ts +10 -0
- package/dist/repositories/errors.js +60 -0
- package/dist/repositories/errors.js.map +1 -0
- package/dist/repositories/schemas.d.ts +1724 -0
- package/dist/repositories/schemas.js +261 -0
- package/dist/repositories/schemas.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { resolveBackendUrl } from "../backend.js";
|
|
2
|
+
import { ensureAccessToken, fetchWithKeycloakIdentity } from "../oauth.js";
|
|
3
|
+
import { ClusterError } from "./errors.js";
|
|
4
|
+
import { ClusterAdoptRequestSchema, ClusterApiErrorSchema, ClusterCandidateDecisionViewSchema, ClusterCandidatePageSchema, ClusterCandidateViewSchema, ClusterDetailSchema, ClusterDiscoveryRunViewSchema, ClusterIgnoreRequestSchema, ClusterLinkRequestSchema, ClusterObservationBatchReceiptSchema, ClusterObservationBatchRequestSchema, ClusterObservationPageSchema, ClusterObservationViewSchema, ClusterSourceCreateSchema, ClusterSourceListSchema, ClusterSourceUpdateSchema, ClusterSourceViewSchema, ClusterSummaryListSchema } from "./schemas.js";
|
|
5
|
+
export const CLUSTER_CONTROL_PLANE_API_ROOT = "/api/v1/cluster-control-plane";
|
|
6
|
+
export const CLUSTER_CONTROL_PLANE_CONTRACT_VERSION = "v1";
|
|
7
|
+
function safeIdentifier(value, label) {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!trimmed || trimmed.length > 256 || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(trimmed)) {
|
|
10
|
+
throw new ClusterError(`${label} must be a stable platform identifier.`, "CLUSTER_CLI_INPUT_INVALID");
|
|
11
|
+
}
|
|
12
|
+
return trimmed;
|
|
13
|
+
}
|
|
14
|
+
function safeIdempotencyKey(value) {
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
if (!trimmed || trimmed.length > 200 || !/^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/u.test(trimmed)) {
|
|
17
|
+
throw new ClusterError("idempotency-key must be 1-200 safe opaque characters.", "CLUSTER_IDEMPOTENCY_INVALID");
|
|
18
|
+
}
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
21
|
+
function positiveInteger(value, label, maximum) {
|
|
22
|
+
if (value == null)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (!Number.isInteger(value) || value < 1 || value > maximum) {
|
|
25
|
+
throw new ClusterError(`${label} must be an integer between 1 and ${maximum}.`, "CLUSTER_CLI_INPUT_INVALID");
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function queryPath(path, values) {
|
|
30
|
+
const query = new URLSearchParams();
|
|
31
|
+
for (const [key, value] of Object.entries(values)) {
|
|
32
|
+
if (value != null && value !== "")
|
|
33
|
+
query.set(key, String(value));
|
|
34
|
+
}
|
|
35
|
+
return query.size ? `${path}?${query.toString()}` : path;
|
|
36
|
+
}
|
|
37
|
+
async function responseBody(response) {
|
|
38
|
+
const text = await response.text();
|
|
39
|
+
if (!text)
|
|
40
|
+
return {};
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(text);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function requestIdFrom(response, body) {
|
|
49
|
+
if (body && typeof body === "object" && "requestId" in body && typeof body.requestId === "string")
|
|
50
|
+
return body.requestId;
|
|
51
|
+
return response.headers.get("x-request-id") ?? undefined;
|
|
52
|
+
}
|
|
53
|
+
function apiFailure(response, body) {
|
|
54
|
+
const parsed = ClusterApiErrorSchema.safeParse(body);
|
|
55
|
+
return new ClusterError(parsed.success && parsed.data.message
|
|
56
|
+
? `Cluster Control Plane HTTP ${response.status}: ${parsed.data.message}`
|
|
57
|
+
: `Cluster Control Plane request failed with HTTP ${response.status}.`, parsed.success ? parsed.data.code ?? "CLUSTER_API_ERROR" : "CLUSTER_API_ERROR", response.status, requestIdFrom(response, body), parsed.success ? parsed.data.details : undefined);
|
|
58
|
+
}
|
|
59
|
+
function shouldRetryUnknown(error) {
|
|
60
|
+
if (error instanceof ClusterError) {
|
|
61
|
+
return [502, 503, 504].includes(error.status ?? 0) || /RESULT_UNKNOWN/u.test(error.code);
|
|
62
|
+
}
|
|
63
|
+
return error instanceof TypeError || (error instanceof Error && error.name === "AbortError");
|
|
64
|
+
}
|
|
65
|
+
export class ClusterControlPlaneClient {
|
|
66
|
+
config;
|
|
67
|
+
fetchImpl;
|
|
68
|
+
tokenProvider;
|
|
69
|
+
constructor(config, dependencies = {}) {
|
|
70
|
+
this.config = config;
|
|
71
|
+
this.fetchImpl = dependencies.fetch ?? fetch;
|
|
72
|
+
this.tokenProvider = dependencies.tokenProvider ?? ensureAccessToken;
|
|
73
|
+
}
|
|
74
|
+
async authenticatedFetch(path, init = {}) {
|
|
75
|
+
const method = String(init.method ?? "GET").toUpperCase();
|
|
76
|
+
const url = resolveBackendUrl(this.config, { method, path });
|
|
77
|
+
const headers = new Headers(init.headers);
|
|
78
|
+
headers.set("Accept", "application/json");
|
|
79
|
+
headers.set("X-Numa-Cluster-Contract", CLUSTER_CONTROL_PLANE_CONTRACT_VERSION);
|
|
80
|
+
if (init.body != null)
|
|
81
|
+
headers.set("Content-Type", "application/json");
|
|
82
|
+
return fetchWithKeycloakIdentity(this.config, url, () => ({ ...init, headers }), {
|
|
83
|
+
fetch: this.fetchImpl,
|
|
84
|
+
tokenProvider: this.tokenProvider
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async json(path, schema, init = {}, expectedStatuses) {
|
|
88
|
+
const response = await this.authenticatedFetch(path, init);
|
|
89
|
+
const body = await responseBody(response);
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
throw apiFailure(response, body);
|
|
92
|
+
if (expectedStatuses && !expectedStatuses.includes(response.status)) {
|
|
93
|
+
throw new ClusterError(`Cluster Control Plane returned HTTP ${response.status}; expected ${expectedStatuses.join(" or ")}.`, "CLUSTER_INVALID_RESPONSE", response.status, requestIdFrom(response, body));
|
|
94
|
+
}
|
|
95
|
+
const parsed = schema.safeParse(body);
|
|
96
|
+
if (!parsed.success) {
|
|
97
|
+
throw new ClusterError(`Cluster Control Plane returned an invalid response: ${parsed.error.message}`, "CLUSTER_INVALID_RESPONSE", response.status, requestIdFrom(response, body));
|
|
98
|
+
}
|
|
99
|
+
return parsed.data;
|
|
100
|
+
}
|
|
101
|
+
async mutation(path, schema, method, body, idempotencyKey, expectedStatuses) {
|
|
102
|
+
const key = safeIdempotencyKey(idempotencyKey);
|
|
103
|
+
const serialized = body == null ? undefined : JSON.stringify(body);
|
|
104
|
+
let lastError;
|
|
105
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
106
|
+
try {
|
|
107
|
+
return await this.json(path, schema, {
|
|
108
|
+
method,
|
|
109
|
+
headers: { "Idempotency-Key": key },
|
|
110
|
+
...(serialized == null ? {} : { body: serialized })
|
|
111
|
+
}, expectedStatuses);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
lastError = error;
|
|
115
|
+
if (!shouldRetryUnknown(error) || attempt === 1)
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (shouldRetryUnknown(lastError)) {
|
|
120
|
+
throw new ClusterError("Cluster mutation result is unknown. Retry the same command with the same Idempotency-Key and unchanged input.", "CLUSTER_RESULT_UNKNOWN", lastError instanceof ClusterError ? lastError.status : undefined, lastError instanceof ClusterError ? lastError.requestId : undefined);
|
|
121
|
+
}
|
|
122
|
+
throw lastError;
|
|
123
|
+
}
|
|
124
|
+
listClusters() {
|
|
125
|
+
return this.json(`${CLUSTER_CONTROL_PLANE_API_ROOT}/clusters`, ClusterSummaryListSchema);
|
|
126
|
+
}
|
|
127
|
+
getCluster(clusterKey) {
|
|
128
|
+
return this.json(`${CLUSTER_CONTROL_PLANE_API_ROOT}/clusters/${encodeURIComponent(safeIdentifier(clusterKey, "cluster-key"))}`, ClusterDetailSchema);
|
|
129
|
+
}
|
|
130
|
+
listSources() {
|
|
131
|
+
return this.json(`${CLUSTER_CONTROL_PLANE_API_ROOT}/sources`, ClusterSourceListSchema);
|
|
132
|
+
}
|
|
133
|
+
getSource(sourceKey) {
|
|
134
|
+
return this.json(`${CLUSTER_CONTROL_PLANE_API_ROOT}/sources/${encodeURIComponent(safeIdentifier(sourceKey, "source-key"))}`, ClusterSourceViewSchema);
|
|
135
|
+
}
|
|
136
|
+
listCandidates(options = {}) {
|
|
137
|
+
return this.json(queryPath(`${CLUSTER_CONTROL_PLANE_API_ROOT}/candidates`, {
|
|
138
|
+
status: options.status,
|
|
139
|
+
sourceKey: options.sourceKey,
|
|
140
|
+
page: positiveInteger(options.page, "page", 1_000_000),
|
|
141
|
+
size: positiveInteger(options.size, "size", 100)
|
|
142
|
+
}), ClusterCandidatePageSchema);
|
|
143
|
+
}
|
|
144
|
+
getCandidate(candidateKey) {
|
|
145
|
+
return this.json(`${CLUSTER_CONTROL_PLANE_API_ROOT}/candidates/${encodeURIComponent(safeIdentifier(candidateKey, "candidate-key"))}`, ClusterCandidateViewSchema);
|
|
146
|
+
}
|
|
147
|
+
listObservations(options = {}) {
|
|
148
|
+
return this.json(queryPath(`${CLUSTER_CONTROL_PLANE_API_ROOT}/observations`, {
|
|
149
|
+
sourceKey: options.sourceKey,
|
|
150
|
+
candidateKey: options.candidateKey,
|
|
151
|
+
page: positiveInteger(options.page, "page", 1_000_000),
|
|
152
|
+
size: positiveInteger(options.size, "size", 100)
|
|
153
|
+
}), ClusterObservationPageSchema);
|
|
154
|
+
}
|
|
155
|
+
getObservation(observationKey) {
|
|
156
|
+
return this.json(`${CLUSTER_CONTROL_PLANE_API_ROOT}/observations/${encodeURIComponent(safeIdentifier(observationKey, "observation-key"))}`, ClusterObservationViewSchema);
|
|
157
|
+
}
|
|
158
|
+
createSource(input, idempotencyKey) {
|
|
159
|
+
const parsed = ClusterSourceCreateSchema.safeParse(input);
|
|
160
|
+
if (!parsed.success)
|
|
161
|
+
throw new ClusterError(`Source input is invalid: ${parsed.error.message}`, "CLUSTER_CLI_INPUT_INVALID");
|
|
162
|
+
return this.mutation(`${CLUSTER_CONTROL_PLANE_API_ROOT}/sources`, ClusterSourceViewSchema, "POST", parsed.data, idempotencyKey, [201]);
|
|
163
|
+
}
|
|
164
|
+
updateSource(sourceKey, input, idempotencyKey) {
|
|
165
|
+
const parsed = ClusterSourceUpdateSchema.safeParse(input);
|
|
166
|
+
if (!parsed.success)
|
|
167
|
+
throw new ClusterError(`Source update is invalid: ${parsed.error.message}`, "CLUSTER_CLI_INPUT_INVALID");
|
|
168
|
+
const source = encodeURIComponent(safeIdentifier(sourceKey, "source-key"));
|
|
169
|
+
return this.mutation(`${CLUSTER_CONTROL_PLANE_API_ROOT}/sources/${source}`, ClusterSourceViewSchema, "PUT", parsed.data, idempotencyKey, [200]);
|
|
170
|
+
}
|
|
171
|
+
discoverSource(sourceKey, idempotencyKey) {
|
|
172
|
+
const source = encodeURIComponent(safeIdentifier(sourceKey, "source-key"));
|
|
173
|
+
return this.mutation(`${CLUSTER_CONTROL_PLANE_API_ROOT}/sources/${source}/discover`, ClusterDiscoveryRunViewSchema, "POST", undefined, idempotencyKey, [202]);
|
|
174
|
+
}
|
|
175
|
+
getRun(runNo) {
|
|
176
|
+
return this.json(`${CLUSTER_CONTROL_PLANE_API_ROOT}/runs/${encodeURIComponent(safeIdentifier(runNo, "run-no"))}`, ClusterDiscoveryRunViewSchema);
|
|
177
|
+
}
|
|
178
|
+
submitObservations(sourceKey, input, idempotencyKey) {
|
|
179
|
+
const parsed = ClusterObservationBatchRequestSchema.safeParse(input);
|
|
180
|
+
if (!parsed.success)
|
|
181
|
+
throw new ClusterError(`Observation plan is invalid: ${parsed.error.message}`, "CLUSTER_PLAN_INVALID");
|
|
182
|
+
const source = encodeURIComponent(safeIdentifier(sourceKey, "source-key"));
|
|
183
|
+
return this.mutation(`${CLUSTER_CONTROL_PLANE_API_ROOT}/sources/${source}/observations`, ClusterObservationBatchReceiptSchema, "POST", parsed.data, idempotencyKey, [202]);
|
|
184
|
+
}
|
|
185
|
+
adoptCandidate(candidateKey, input, idempotencyKey) {
|
|
186
|
+
const parsed = ClusterAdoptRequestSchema.safeParse(input);
|
|
187
|
+
if (!parsed.success)
|
|
188
|
+
throw new ClusterError(`Adopt input is invalid: ${parsed.error.message}`, "CLUSTER_CLI_INPUT_INVALID");
|
|
189
|
+
const candidate = encodeURIComponent(safeIdentifier(candidateKey, "candidate-key"));
|
|
190
|
+
return this.mutation(`${CLUSTER_CONTROL_PLANE_API_ROOT}/candidates/${candidate}/adopt`, ClusterCandidateDecisionViewSchema, "POST", parsed.data, idempotencyKey, [200]);
|
|
191
|
+
}
|
|
192
|
+
linkCandidate(candidateKey, input, idempotencyKey) {
|
|
193
|
+
const parsed = ClusterLinkRequestSchema.safeParse(input);
|
|
194
|
+
if (!parsed.success)
|
|
195
|
+
throw new ClusterError(`Link input is invalid: ${parsed.error.message}`, "CLUSTER_CLI_INPUT_INVALID");
|
|
196
|
+
const candidate = encodeURIComponent(safeIdentifier(candidateKey, "candidate-key"));
|
|
197
|
+
return this.mutation(`${CLUSTER_CONTROL_PLANE_API_ROOT}/candidates/${candidate}/link`, ClusterCandidateDecisionViewSchema, "POST", parsed.data, idempotencyKey, [200]);
|
|
198
|
+
}
|
|
199
|
+
ignoreCandidate(candidateKey, input, idempotencyKey) {
|
|
200
|
+
const parsed = ClusterIgnoreRequestSchema.safeParse(input);
|
|
201
|
+
if (!parsed.success)
|
|
202
|
+
throw new ClusterError(`Ignore input is invalid: ${parsed.error.message}`, "CLUSTER_CLI_INPUT_INVALID");
|
|
203
|
+
const candidate = encodeURIComponent(safeIdentifier(candidateKey, "candidate-key"));
|
|
204
|
+
return this.mutation(`${CLUSTER_CONTROL_PLANE_API_ROOT}/candidates/${candidate}/ignore`, ClusterCandidateDecisionViewSchema, "POST", parsed.data, idempotencyKey, [200]);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/clusters/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAA8B,MAAM,aAAa,CAAC;AACvG,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,kCAAkC,EAClC,0BAA0B,EAC1B,0BAA0B,EAC1B,mBAAmB,EACnB,6BAA6B,EAC7B,0BAA0B,EAC1B,wBAAwB,EACxB,oCAAoC,EACpC,oCAAoC,EACpC,4BAA4B,EAC5B,4BAA4B,EAC5B,yBAAyB,EACzB,uBAAuB,EACvB,yBAAyB,EACzB,uBAAuB,EACvB,wBAAwB,EAOzB,MAAM,cAAc,CAAC;AAEtB,MAAM,CAAC,MAAM,8BAA8B,GAAG,+BAA+B,CAAC;AAC9E,MAAM,CAAC,MAAM,sCAAsC,GAAG,IAAI,CAAC;AA2B3D,SAAS,cAAc,CAAC,KAAa,EAAE,KAAa;IAClD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG,IAAI,CAAC,gCAAgC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACxF,MAAM,IAAI,YAAY,CAAC,GAAG,KAAK,wCAAwC,EAAE,2BAA2B,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAa;IACvC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG,IAAI,CAAC,kCAAkC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1F,MAAM,IAAI,YAAY,CAAC,uDAAuD,EAAE,6BAA6B,CAAC,CAAC;IACjH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,eAAe,CAAC,KAAyB,EAAE,KAAa,EAAE,OAAe;IAChF,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IACpC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,OAAO,EAAE,CAAC;QAC7D,MAAM,IAAI,YAAY,CAAC,GAAG,KAAK,qCAAqC,OAAO,GAAG,EAAE,2BAA2B,CAAC,CAAC;IAC/G,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,SAAS,CAAC,IAAY,EAAE,MAAmD;IAClF,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,EAAE;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC3D,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,QAAkB;IAC5C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,IAAI,CAAC;QAAC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;AACpE,CAAC;AAED,SAAS,aAAa,CAAC,QAAkB,EAAE,IAAa;IACtD,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,WAAW,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC;IACzH,OAAO,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,SAAS,CAAC;AAC3D,CAAC;AAED,SAAS,UAAU,CAAC,QAAkB,EAAE,IAAa;IACnD,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACrD,OAAO,IAAI,YAAY,CACrB,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO;QACnC,CAAC,CAAC,8BAA8B,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE;QACzE,CAAC,CAAC,kDAAkD,QAAQ,CAAC,MAAM,GAAG,EACxE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,mBAAmB,CAAC,CAAC,CAAC,mBAAmB,EAC9E,QAAQ,CAAC,MAAM,EACf,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,EAC7B,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CACjD,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAc;IACxC,IAAI,KAAK,YAAY,YAAY,EAAE,CAAC;QAClC,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,KAAK,YAAY,SAAS,IAAI,CAAC,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC;AAC/F,CAAC;AAED,MAAM,OAAO,yBAAyB;IAIP,MAAM;IAHlB,SAAS,CAAe;IACxB,aAAa,CAAwB;IAEtD,YAA6B,MAAkB,EAAE,YAAY,GAA0C,EAAE;sBAA5E,MAAM;QACjC,IAAI,CAAC,SAAS,GAAG,YAAY,CAAC,KAAK,IAAI,KAAK,CAAC;QAC7C,IAAI,CAAC,aAAa,GAAG,YAAY,CAAC,aAAa,IAAI,iBAAiB,CAAC;IACvE,CAAC;IAEO,KAAK,CAAC,kBAAkB,CAAC,IAAY,EAAE,IAAI,GAAgB,EAAE;QACnE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAiD,CAAC;QACzG,MAAM,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,sCAAsC,CAAC,CAAC;QAC/E,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI;YAAE,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;QACvE,OAAO,yBAAyB,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE;YAC/E,KAAK,EAAE,IAAI,CAAC,SAAS;YACrB,aAAa,EAAE,IAAI,CAAC,aAAa;SAClC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,IAAI,CAAI,IAAY,EAAE,MAAiB,EAAE,IAAI,GAAgB,EAAE,EAAE,gBAA2B;QACxG,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACnD,IAAI,gBAAgB,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACpE,MAAM,IAAI,YAAY,CACpB,uCAAuC,QAAQ,CAAC,MAAM,cAAc,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EACpG,0BAA0B,EAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAC3E,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,IAAI,YAAY,CACpB,uDAAuD,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAC7E,0BAA0B,EAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAC3E,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;IAEO,KAAK,CAAC,QAAQ,CACpB,IAAY,EACZ,MAAiB,EACjB,MAAsB,EACtB,IAAyB,EACzB,cAAsB,EACtB,gBAA0B;QAE1B,MAAM,GAAG,GAAG,kBAAkB,CAAC,cAAc,CAAC,CAAC;QAC/C,MAAM,UAAU,GAAG,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACnE,IAAI,SAAkB,CAAC;QACvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE;oBACnC,MAAM;oBACN,OAAO,EAAE,EAAE,iBAAiB,EAAE,GAAG,EAAE;oBACnC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;iBACpD,EAAE,gBAAgB,CAAC,CAAC;YACvB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,SAAS,GAAG,KAAK,CAAC;gBAClB,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC;oBAAE,MAAM;YACzD,CAAC;QACH,CAAC;QACD,IAAI,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,YAAY,CACpB,+GAA+G,EAC/G,wBAAwB,EACxB,SAAS,YAAY,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAChE,SAAS,YAAY,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CACpE,CAAC;QACJ,CAAC;QACD,MAAM,SAAS,CAAC;IAClB,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,8BAA8B,WAAW,EAAE,wBAAwB,CAAC,CAAC;IAC3F,CAAC;IAED,UAAU,CAAC,UAAkB;QAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,8BAA8B,aAAa,kBAAkB,CAAC,cAAc,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC,EAAE,EAAE,mBAAmB,CAAC,CAAC;IACvJ,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,8BAA8B,UAAU,EAAE,uBAAuB,CAAC,CAAC;IACzF,CAAC;IAED,SAAS,CAAC,SAAiB;QACzB,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,8BAA8B,YAAY,kBAAkB,CAAC,cAAc,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,EAAE,EAAE,uBAAuB,CAAC,CAAC;IACxJ,CAAC;IAED,cAAc,CAAC,OAAO,GAAgC,EAAE;QACtD,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,8BAA8B,aAAa,EAAE;YACzE,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,IAAI,EAAE,eAAe,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC;YACtD,IAAI,EAAE,eAAe,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC;SACjD,CAAC,EAAE,0BAA0B,CAAC,CAAC;IAClC,CAAC;IAED,YAAY,CAAC,YAAoB;QAC/B,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,8BAA8B,eAAe,kBAAkB,CAAC,cAAc,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC,EAAE,EAAE,0BAA0B,CAAC,CAAC;IACpK,CAAC;IAED,gBAAgB,CAAC,OAAO,GAAkC,EAAE;QAC1D,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,8BAA8B,eAAe,EAAE;YAC3E,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,IAAI,EAAE,eAAe,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC;YACtD,IAAI,EAAE,eAAe,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC;SACjD,CAAC,EAAE,4BAA4B,CAAC,CAAC;IACpC,CAAC;IAED,cAAc,CAAC,cAAsB;QACnC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,8BAA8B,iBAAiB,kBAAkB,CAAC,cAAc,CAAC,cAAc,EAAE,iBAAiB,CAAC,CAAC,EAAE,EAAE,4BAA4B,CAAC,CAAC;IAC5K,CAAC;IAED,YAAY,CAAC,KAA0B,EAAE,cAAsB;QAC7D,MAAM,MAAM,GAAG,yBAAyB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,MAAM,IAAI,YAAY,CAAC,4BAA4B,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,2BAA2B,CAAC,CAAC;QAC7H,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,8BAA8B,UAAU,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzI,CAAC;IAED,YAAY,CAAC,SAAiB,EAAE,KAA0B,EAAE,cAAsB;QAChF,MAAM,MAAM,GAAG,yBAAyB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,MAAM,IAAI,YAAY,CAAC,6BAA6B,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,2BAA2B,CAAC,CAAC;QAC9H,MAAM,MAAM,GAAG,kBAAkB,CAAC,cAAc,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;QAC3E,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,8BAA8B,YAAY,MAAM,EAAE,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAClJ,CAAC;IAED,cAAc,CAAC,SAAiB,EAAE,cAAsB;QACtD,MAAM,MAAM,GAAG,kBAAkB,CAAC,cAAc,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;QAC3E,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,8BAA8B,YAAY,MAAM,WAAW,EAAE,6BAA6B,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAChK,CAAC;IAED,MAAM,CAAC,KAAa;QAClB,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,8BAA8B,SAAS,kBAAkB,CAAC,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,EAAE,EAAE,6BAA6B,CAAC,CAAC;IACnJ,CAAC;IAED,kBAAkB,CAAC,SAAiB,EAAE,KAAqC,EAAE,cAAsB;QACjG,MAAM,MAAM,GAAG,oCAAoC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACrE,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,MAAM,IAAI,YAAY,CAAC,gCAAgC,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,sBAAsB,CAAC,CAAC;QAC5H,MAAM,MAAM,GAAG,kBAAkB,CAAC,cAAc,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;QAC3E,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,8BAA8B,YAAY,MAAM,eAAe,EAAE,oCAAoC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7K,CAAC;IAED,cAAc,CAAC,YAAoB,EAAE,KAA0B,EAAE,cAAsB;QACrF,MAAM,MAAM,GAAG,yBAAyB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,MAAM,IAAI,YAAY,CAAC,2BAA2B,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,2BAA2B,CAAC,CAAC;QAC5H,MAAM,SAAS,GAAG,kBAAkB,CAAC,cAAc,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC,CAAC;QACpF,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,8BAA8B,eAAe,SAAS,QAAQ,EAAE,kCAAkC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1K,CAAC;IAED,aAAa,CAAC,YAAoB,EAAE,KAAyB,EAAE,cAAsB;QACnF,MAAM,MAAM,GAAG,wBAAwB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACzD,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,MAAM,IAAI,YAAY,CAAC,0BAA0B,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,2BAA2B,CAAC,CAAC;QAC3H,MAAM,SAAS,GAAG,kBAAkB,CAAC,cAAc,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC,CAAC;QACpF,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,8BAA8B,eAAe,SAAS,OAAO,EAAE,kCAAkC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzK,CAAC;IAED,eAAe,CAAC,YAAoB,EAAE,KAA2B,EAAE,cAAsB;QACvF,MAAM,MAAM,GAAG,0BAA0B,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC3D,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,MAAM,IAAI,YAAY,CAAC,4BAA4B,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,2BAA2B,CAAC,CAAC;QAC7H,MAAM,SAAS,GAAG,kBAAkB,CAAC,cAAc,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC,CAAC;QACpF,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,8BAA8B,eAAe,SAAS,SAAS,EAAE,kCAAkC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3K,CAAC;CACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { ClusterControlPlaneClient } from "./client.js";
|
|
3
|
+
import { type KubeconfigDiscoveryOptions, type LocalKubeconfigDiscoveryPlan } from "./kubeconfig-discovery.js";
|
|
4
|
+
export interface ClusterCommandDependencies {
|
|
5
|
+
clientFactory?: () => ClusterControlPlaneClient;
|
|
6
|
+
discoverKubeconfig?: (options: KubeconfigDiscoveryOptions) => Promise<LocalKubeconfigDiscoveryPlan>;
|
|
7
|
+
readPlan?: (path: string) => Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseClusterIdentifier(value: string | undefined, label: string): string;
|
|
10
|
+
export declare function parseClusterIdempotencyKey(value: string | undefined): string;
|
|
11
|
+
export declare function registerClusterCommands(program: Command, dependencies?: ClusterCommandDependencies): void;
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { loadAppConfig } from "../app-config.js";
|
|
4
|
+
import { loadConfig } from "../config.js";
|
|
5
|
+
import { ClusterControlPlaneClient } from "./client.js";
|
|
6
|
+
import { ClusterError, sanitizeClusterOutput } from "./errors.js";
|
|
7
|
+
import { discoverKubeconfig, LocalKubeconfigDiscoveryPlanSchema, observationBatchFromPlan } from "./kubeconfig-discovery.js";
|
|
8
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u;
|
|
9
|
+
const SAFE_KEY = /^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/u;
|
|
10
|
+
function addExamples(command, examples, notes = []) {
|
|
11
|
+
const noteSection = notes.length ? `\n说明:\n${notes.map((item) => ` - ${item}`).join("\n")}\n` : "";
|
|
12
|
+
return command.addHelpText("after", `${noteSection}\nExamples:\n${examples.map((item) => ` ${item}`).join("\n")}\n`);
|
|
13
|
+
}
|
|
14
|
+
function snakeCase(key) {
|
|
15
|
+
return key.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
function stableOutput(value) {
|
|
18
|
+
const safe = sanitizeClusterOutput(value);
|
|
19
|
+
if (Array.isArray(safe))
|
|
20
|
+
return safe.map(stableOutput);
|
|
21
|
+
if (safe && typeof safe === "object") {
|
|
22
|
+
return Object.fromEntries(Object.entries(safe).map(([key, item]) => [snakeCase(key), stableOutput(item)]));
|
|
23
|
+
}
|
|
24
|
+
return safe;
|
|
25
|
+
}
|
|
26
|
+
async function jsonEnabled(command) {
|
|
27
|
+
const configured = await loadAppConfig();
|
|
28
|
+
return Boolean(command.optsWithGlobals().json || configured.output === "json");
|
|
29
|
+
}
|
|
30
|
+
async function output(command, payload, textValue) {
|
|
31
|
+
if (await jsonEnabled(command))
|
|
32
|
+
process.stdout.write(`${JSON.stringify(stableOutput(payload), null, 2)}\n`);
|
|
33
|
+
else
|
|
34
|
+
process.stdout.write(`${String(sanitizeClusterOutput(textValue))}\n`);
|
|
35
|
+
}
|
|
36
|
+
function outputPlan(plan) {
|
|
37
|
+
process.stdout.write(`${JSON.stringify(sanitizeClusterOutput(plan), null, 2)}\n`);
|
|
38
|
+
}
|
|
39
|
+
export function parseClusterIdentifier(value, label) {
|
|
40
|
+
const trimmed = value?.trim() ?? "";
|
|
41
|
+
if (!trimmed || trimmed.length > 256 || !SAFE_ID.test(trimmed)) {
|
|
42
|
+
throw new ClusterError(`${label} must be a stable platform identifier.`, "CLUSTER_CLI_INPUT_INVALID");
|
|
43
|
+
}
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
export function parseClusterIdempotencyKey(value) {
|
|
47
|
+
const trimmed = value?.trim() ?? "";
|
|
48
|
+
if (!trimmed || trimmed.length > 200 || !SAFE_KEY.test(trimmed)) {
|
|
49
|
+
throw new ClusterError("idempotency-key must be 1-200 safe opaque characters.", "CLUSTER_IDEMPOTENCY_INVALID");
|
|
50
|
+
}
|
|
51
|
+
return trimmed;
|
|
52
|
+
}
|
|
53
|
+
function requireYes(value) {
|
|
54
|
+
if (!value)
|
|
55
|
+
throw new ClusterError("Mutation requires --yes after reviewing the target and input.", "CLUSTER_CONFIRMATION_REQUIRED");
|
|
56
|
+
}
|
|
57
|
+
function integer(value, label, minimum = 0, maximum = 1_000_000) {
|
|
58
|
+
const parsed = Number(value);
|
|
59
|
+
if (value == null || !Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
|
|
60
|
+
throw new ClusterError(`${label} must be an integer between ${minimum} and ${maximum}.`, "CLUSTER_CLI_INPUT_INVALID");
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
function optionalPage(value, label, maximum) {
|
|
65
|
+
return value == null ? undefined : integer(value, label, 1, maximum);
|
|
66
|
+
}
|
|
67
|
+
function positiveId(value, label) {
|
|
68
|
+
const trimmed = value?.trim() ?? "";
|
|
69
|
+
if (!/^[1-9]\d*$/u.test(trimmed))
|
|
70
|
+
throw new ClusterError(`${label} must be a positive platform ID.`, "CLUSTER_CLI_INPUT_INVALID");
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
function reason(value) {
|
|
74
|
+
const trimmed = value?.trim() ?? "";
|
|
75
|
+
if (trimmed.length < 5 || trimmed.length > 1000) {
|
|
76
|
+
throw new ClusterError("reason must contain 5-1000 characters.", "CLUSTER_CLI_INPUT_INVALID");
|
|
77
|
+
}
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|
|
80
|
+
function regions(values) {
|
|
81
|
+
const normalized = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))].sort();
|
|
82
|
+
if (normalized.length > 100 || normalized.some((value) => !/^[a-z0-9-]{1,80}$/u.test(value))) {
|
|
83
|
+
throw new ClusterError("region values must be lowercase region codes.", "CLUSTER_CLI_INPUT_INVALID");
|
|
84
|
+
}
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
function collect(value, previous = []) {
|
|
88
|
+
return [...previous, value];
|
|
89
|
+
}
|
|
90
|
+
function sourceType(value) {
|
|
91
|
+
const normalized = value.trim().toLowerCase();
|
|
92
|
+
if (normalized === "kubeconfig" || normalized === "kubeconfig-cli")
|
|
93
|
+
return "KUBECONFIG_CLI";
|
|
94
|
+
if (normalized === "ack" || normalized === "aliyun-ack-assume-role")
|
|
95
|
+
return "ALIYUN_ACK_ASSUME_ROLE";
|
|
96
|
+
throw new ClusterError("type must be kubeconfig or ack.", "CLUSTER_CLI_INPUT_INVALID");
|
|
97
|
+
}
|
|
98
|
+
function sourceLine(item) {
|
|
99
|
+
return `${String(item.sourceKey ?? "-")}\t${String(item.sourceType ?? "-")}\t${String(item.status ?? "-")}\tv${String(item.version ?? "-")}`;
|
|
100
|
+
}
|
|
101
|
+
function candidateLine(item) {
|
|
102
|
+
return `${String(item.candidateKey ?? "-")}\t${String(item.status ?? "-")}\t${String(item.matchKind ?? "-")}\t${String(item.matchedClusterKey ?? "-")}`;
|
|
103
|
+
}
|
|
104
|
+
function observationLine(item) {
|
|
105
|
+
return `${String(item.observationKey ?? "-")}\t${String(item.candidateKey ?? "-")}\t${String(item.provider ?? "-")}\t${String(item.disposition ?? "-")}`;
|
|
106
|
+
}
|
|
107
|
+
async function defaultReadPlan(path) {
|
|
108
|
+
let contents;
|
|
109
|
+
try {
|
|
110
|
+
contents = await readFile(path, "utf8");
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new ClusterError("Discovery plan file could not be read.", "CLUSTER_PLAN_INVALID");
|
|
114
|
+
}
|
|
115
|
+
if (Buffer.byteLength(contents) > 2 * 1024 * 1024)
|
|
116
|
+
throw new ClusterError("Discovery plan file is too large.", "CLUSTER_PLAN_INVALID");
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(contents);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new ClusterError("Discovery plan file is not valid JSON.", "CLUSTER_PLAN_INVALID");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function sourceUpdateBody(current, options) {
|
|
125
|
+
if (options.enable && options.disable)
|
|
126
|
+
throw new ClusterError("Choose only one of --enable or --disable.", "CLUSTER_CLI_INPUT_INVALID");
|
|
127
|
+
const expectedVersion = integer(options.expectedVersion, "expected-version");
|
|
128
|
+
if (current.version !== expectedVersion) {
|
|
129
|
+
throw new ClusterError("expected-version does not match the current source; review it before retrying.", "CLUSTER_SOURCE_VERSION_MISMATCH", 412);
|
|
130
|
+
}
|
|
131
|
+
const aliyunAccountId = options.aliyunAccountId == null ? current.aliyunAccountId ?? null : positiveId(options.aliyunAccountId, "aliyun-account-id");
|
|
132
|
+
const aliyunAssumeRoleId = options.aliyunAssumeRoleId == null ? current.aliyunAssumeRoleId ?? null : positiveId(options.aliyunAssumeRoleId, "aliyun-assume-role-id");
|
|
133
|
+
const selectedRegions = options.region == null ? current.regions : regions(options.region);
|
|
134
|
+
if (current.sourceType === "KUBECONFIG_CLI" && (aliyunAccountId != null || aliyunAssumeRoleId != null || selectedRegions.length)) {
|
|
135
|
+
throw new ClusterError("KUBECONFIG_CLI source cannot reference Alibaba Cloud identity or regions.", "CLUSTER_CLI_INPUT_INVALID");
|
|
136
|
+
}
|
|
137
|
+
if (current.sourceType === "ALIYUN_ACK_ASSUME_ROLE" && (aliyunAccountId == null || aliyunAssumeRoleId == null || !selectedRegions.length)) {
|
|
138
|
+
throw new ClusterError("ACK source requires account, assume Role, and at least one region.", "CLUSTER_CLI_INPUT_INVALID");
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
displayName: options.displayName?.trim() || current.displayName,
|
|
142
|
+
aliyunAccountId,
|
|
143
|
+
aliyunAssumeRoleId,
|
|
144
|
+
regions: selectedRegions,
|
|
145
|
+
enabled: options.enable ? true : options.disable ? false : current.enabled,
|
|
146
|
+
expectedVersion
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function registerClusterCommands(program, dependencies = {}) {
|
|
150
|
+
const client = () => dependencies.clientFactory?.() ?? new ClusterControlPlaneClient(loadConfig());
|
|
151
|
+
const cluster = program.command("cluster")
|
|
152
|
+
.description("发现、核对并治理跨集群 Cluster Control Plane inventory")
|
|
153
|
+
.addHelpText("after", `
|
|
154
|
+
安全边界:
|
|
155
|
+
kubeconfig discovery 仅在本机调用 kubectl 并输出脱敏 plan;不会上传 kubeconfig、endpoint、UID、CA、用户或凭据。
|
|
156
|
+
ACK source 只引用平台 accountId 和必选 assumeRoleId;CLI 不读取或传递 AK/SK。
|
|
157
|
+
所有写操作要求稳定 Idempotency-Key 和 --yes;结果未知时必须原键、原输入重试。
|
|
158
|
+
`);
|
|
159
|
+
addExamples(cluster.command("list").description("列出受管集群")
|
|
160
|
+
.action(async (_options, command) => {
|
|
161
|
+
const result = await client().listClusters();
|
|
162
|
+
await output(command, { clusters: result, count: result.length }, result.map((item) => `${item.clusterKey}\t${item.status}\t${item.code}\t${item.name}`).join("\n") || "没有集群");
|
|
163
|
+
}), ["numa cluster list --json"]);
|
|
164
|
+
addExamples(cluster.command("read <cluster-key>").description("按 stable clusterKey 查看集群详情")
|
|
165
|
+
.action(async (clusterKey, _options, command) => {
|
|
166
|
+
const result = await client().getCluster(parseClusterIdentifier(clusterKey, "cluster-key"));
|
|
167
|
+
await output(command, result, `${result.cluster.clusterKey}\t${result.cluster.status}\t${result.cluster.name}`);
|
|
168
|
+
}), ["numa cluster read clu_0123456789abcdef0123456789abcdef --json"]);
|
|
169
|
+
addExamples(cluster.command("run <run-no>").description("查看 ACK discovery 运行状态")
|
|
170
|
+
.action(async (runNo, _options, command) => {
|
|
171
|
+
const result = await client().getRun(parseClusterIdentifier(runNo, "run-no"));
|
|
172
|
+
await output(command, result, `${result.runNo}\t${result.state}\t${result.sourceKey}`);
|
|
173
|
+
}), ["numa cluster run cdr_01 --json"]);
|
|
174
|
+
addExamples(cluster.command("sources [source-key]").description("列出 discovery sources,或查看一个 source")
|
|
175
|
+
.action(async (sourceKey, _options, command) => {
|
|
176
|
+
if (sourceKey) {
|
|
177
|
+
const result = await client().getSource(parseClusterIdentifier(sourceKey, "source-key"));
|
|
178
|
+
await output(command, result, sourceLine(result));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const result = await client().listSources();
|
|
182
|
+
await output(command, { sources: result, count: result.length }, result.map(sourceLine).join("\n") || "没有 discovery source");
|
|
183
|
+
}
|
|
184
|
+
}), ["numa cluster sources --json", "numa cluster sources local-kubeconfig --json"]);
|
|
185
|
+
const candidates = cluster.command("candidates [candidate-key]").description("分页列出 candidates,或查看一个 candidate")
|
|
186
|
+
.option("--status <status>", "candidate 状态过滤")
|
|
187
|
+
.option("--source <source-key>", "sourceKey 过滤")
|
|
188
|
+
.option("--page <number>", "1-based 页码")
|
|
189
|
+
.option("--size <number>", "每页 1-100 条");
|
|
190
|
+
addExamples(candidates.action(async (candidateKey, options, command) => {
|
|
191
|
+
if (candidateKey) {
|
|
192
|
+
const result = await client().getCandidate(parseClusterIdentifier(candidateKey, "candidate-key"));
|
|
193
|
+
await output(command, result, candidateLine(result));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const result = await client().listCandidates({
|
|
197
|
+
...(options.status ? { status: options.status.trim().toUpperCase() } : {}),
|
|
198
|
+
...(options.source ? { sourceKey: parseClusterIdentifier(options.source, "source") } : {}),
|
|
199
|
+
...(options.page ? { page: optionalPage(options.page, "page", 1_000_000) } : {}),
|
|
200
|
+
...(options.size ? { size: optionalPage(options.size, "size", 100) } : {})
|
|
201
|
+
});
|
|
202
|
+
await output(command, result, result.items.map(candidateLine).join("\n") || "没有 candidate");
|
|
203
|
+
}
|
|
204
|
+
}), ["numa cluster candidates --status pending --source local-kubeconfig --json"]);
|
|
205
|
+
const observations = cluster.command("observations [observation-key]").description("分页列出脱敏 observations,或查看一条 observation")
|
|
206
|
+
.option("--source <source-key>", "sourceKey 过滤")
|
|
207
|
+
.option("--candidate <candidate-key>", "candidateKey 过滤")
|
|
208
|
+
.option("--page <number>", "1-based 页码")
|
|
209
|
+
.option("--size <number>", "每页 1-100 条");
|
|
210
|
+
addExamples(observations.action(async (observationKey, options, command) => {
|
|
211
|
+
if (observationKey) {
|
|
212
|
+
const result = await client().getObservation(parseClusterIdentifier(observationKey, "observation-key"));
|
|
213
|
+
await output(command, result, observationLine(result));
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const result = await client().listObservations({
|
|
217
|
+
...(options.source ? { sourceKey: parseClusterIdentifier(options.source, "source") } : {}),
|
|
218
|
+
...(options.candidate ? { candidateKey: parseClusterIdentifier(options.candidate, "candidate") } : {}),
|
|
219
|
+
...(options.page ? { page: optionalPage(options.page, "page", 1_000_000) } : {}),
|
|
220
|
+
...(options.size ? { size: optionalPage(options.size, "size", 100) } : {})
|
|
221
|
+
});
|
|
222
|
+
await output(command, result, result.items.map(observationLine).join("\n") || "没有 observation");
|
|
223
|
+
}
|
|
224
|
+
}), ["numa cluster observations --source local-kubeconfig --json"]);
|
|
225
|
+
const source = cluster.command("source").description("管理受控 discovery source(ops-admin)");
|
|
226
|
+
const create = source.command("create").description("登记 kubeconfig CLI 或 ACK assume-role source")
|
|
227
|
+
.requiredOption("--source-key <key>", "stable source key")
|
|
228
|
+
.requiredOption("--type <kubeconfig|ack>", "source 类型")
|
|
229
|
+
.requiredOption("--display-name <name>", "显示名称")
|
|
230
|
+
.option("--aliyun-account-id <id>", "平台 Alibaba Cloud account ID")
|
|
231
|
+
.option("--aliyun-assume-role-id <id>", "平台 ACTIVE assume Role ID")
|
|
232
|
+
.option("--region <region>", "ACK region,可重复", collect)
|
|
233
|
+
.option("--disabled", "创建为 DISABLED")
|
|
234
|
+
.requiredOption("--idempotency-key <key>", "稳定幂等键")
|
|
235
|
+
.option("--yes", "确认登记");
|
|
236
|
+
addExamples(create.action(async (options, command) => {
|
|
237
|
+
requireYes(options.yes);
|
|
238
|
+
const type = sourceType(options.type);
|
|
239
|
+
const selectedRegions = regions(options.region);
|
|
240
|
+
const input = {
|
|
241
|
+
sourceKey: parseClusterIdentifier(options.sourceKey, "source-key"),
|
|
242
|
+
sourceType: type,
|
|
243
|
+
displayName: options.displayName.trim(),
|
|
244
|
+
...(options.aliyunAccountId ? { aliyunAccountId: positiveId(options.aliyunAccountId, "aliyun-account-id") } : {}),
|
|
245
|
+
...(options.aliyunAssumeRoleId ? { aliyunAssumeRoleId: positiveId(options.aliyunAssumeRoleId, "aliyun-assume-role-id") } : {}),
|
|
246
|
+
regions: selectedRegions,
|
|
247
|
+
enabled: !options.disabled
|
|
248
|
+
};
|
|
249
|
+
const result = await client().createSource(input, parseClusterIdempotencyKey(options.idempotencyKey));
|
|
250
|
+
await output(command, result, sourceLine(result));
|
|
251
|
+
}), [
|
|
252
|
+
"numa cluster source create --source-key local-kubeconfig --type kubeconfig --display-name 'Local kubeconfig' --idempotency-key source-local-1 --yes --json",
|
|
253
|
+
"numa cluster source create --source-key ack-prod --type ack --display-name 'ACK production' --aliyun-account-id 7 --aliyun-assume-role-id 12 --region cn-shenzhen --idempotency-key source-ack-prod-1 --yes --json"
|
|
254
|
+
]);
|
|
255
|
+
const update = source.command("update <source-key>").description("乐观锁更新 discovery source")
|
|
256
|
+
.requiredOption("--expected-version <number>", "当前 source version")
|
|
257
|
+
.option("--display-name <name>", "新显示名称")
|
|
258
|
+
.option("--aliyun-account-id <id>", "平台 Alibaba Cloud account ID")
|
|
259
|
+
.option("--aliyun-assume-role-id <id>", "平台 ACTIVE assume Role ID")
|
|
260
|
+
.option("--region <region>", "替换 ACK region 集合,可重复", collect)
|
|
261
|
+
.option("--enable", "启用 source")
|
|
262
|
+
.option("--disable", "禁用 source")
|
|
263
|
+
.requiredOption("--idempotency-key <key>", "稳定幂等键")
|
|
264
|
+
.option("--yes", "确认更新");
|
|
265
|
+
addExamples(update.action(async (sourceKey, options, command) => {
|
|
266
|
+
requireYes(options.yes);
|
|
267
|
+
const stableSource = parseClusterIdentifier(sourceKey, "source-key");
|
|
268
|
+
const api = client();
|
|
269
|
+
const current = await api.getSource(stableSource);
|
|
270
|
+
const result = await api.updateSource(stableSource, sourceUpdateBody(current, options), parseClusterIdempotencyKey(options.idempotencyKey));
|
|
271
|
+
await output(command, result, sourceLine(result));
|
|
272
|
+
}), ["numa cluster source update ack-prod --expected-version 2 --region cn-shenzhen --region cn-shanghai --idempotency-key source-ack-prod-v2 --yes --json"]);
|
|
273
|
+
const remoteDiscover = source.command("discover <source-key>").description("启动 ACK source 服务端发现运行")
|
|
274
|
+
.requiredOption("--idempotency-key <key>", "稳定幂等键")
|
|
275
|
+
.option("--yes", "确认调用云只读发现");
|
|
276
|
+
addExamples(remoteDiscover.action(async (sourceKey, options, command) => {
|
|
277
|
+
requireYes(options.yes);
|
|
278
|
+
const stableSource = parseClusterIdentifier(sourceKey, "source-key");
|
|
279
|
+
const api = client();
|
|
280
|
+
const current = await api.getSource(stableSource);
|
|
281
|
+
if (current.sourceType !== "ALIYUN_ACK_ASSUME_ROLE") {
|
|
282
|
+
throw new ClusterError("source discover is only valid for ALIYUN_ACK_ASSUME_ROLE; use cluster discover kubeconfig locally.", "CLUSTER_CLI_INPUT_INVALID");
|
|
283
|
+
}
|
|
284
|
+
const result = await api.discoverSource(stableSource, parseClusterIdempotencyKey(options.idempotencyKey));
|
|
285
|
+
await output(command, result, `${result.runNo}\t${result.state}\t${result.sourceKey}`);
|
|
286
|
+
}), ["numa cluster source discover ack-prod --idempotency-key discover-ack-prod-20260822 --yes --json"]);
|
|
287
|
+
const candidate = cluster.command("candidate").description("治理 discovery candidate(ops-admin)");
|
|
288
|
+
const adopt = candidate.command("adopt <candidate-key>").description("把 candidate 登记成新集群")
|
|
289
|
+
.requiredOption("--code <code>", "新集群 code")
|
|
290
|
+
.requiredOption("--name <name>", "新集群名称")
|
|
291
|
+
.option("--description <text>", "说明")
|
|
292
|
+
.requiredOption("--expected-version <number>", "candidate version")
|
|
293
|
+
.requiredOption("--reason <text>", "5-1000 字符治理原因")
|
|
294
|
+
.requiredOption("--idempotency-key <key>", "稳定幂等键")
|
|
295
|
+
.option("--yes", "确认 adopt");
|
|
296
|
+
addExamples(adopt.action(async (candidateKey, options, command) => {
|
|
297
|
+
requireYes(options.yes);
|
|
298
|
+
const result = await client().adoptCandidate(parseClusterIdentifier(candidateKey, "candidate-key"), {
|
|
299
|
+
code: options.code.trim(), name: options.name.trim(), ...(options.description ? { description: options.description.trim() } : {}),
|
|
300
|
+
expectedVersion: integer(options.expectedVersion, "expected-version"), reason: reason(options.reason)
|
|
301
|
+
}, parseClusterIdempotencyKey(options.idempotencyKey));
|
|
302
|
+
await output(command, result, `${result.action}\t${result.candidate.candidateKey}\t${result.cluster?.clusterKey ?? "-"}`);
|
|
303
|
+
}), ["numa cluster candidate adopt cand_01 --code ack-dev --name 'ACK dev' --expected-version 0 --reason 'Verified platform ownership' --idempotency-key adopt-cand-01 --yes --json"]);
|
|
304
|
+
const link = candidate.command("link <candidate-key>").description("把 candidate 关联到既有集群")
|
|
305
|
+
.requiredOption("--cluster <cluster-key>", "stable clusterKey")
|
|
306
|
+
.requiredOption("--expected-candidate-version <number>", "candidate version")
|
|
307
|
+
.requiredOption("--expected-cluster-version <number>", "cluster version")
|
|
308
|
+
.requiredOption("--reason <text>", "5-1000 字符治理原因")
|
|
309
|
+
.requiredOption("--idempotency-key <key>", "稳定幂等键")
|
|
310
|
+
.option("--yes", "确认 link");
|
|
311
|
+
addExamples(link.action(async (candidateKey, options, command) => {
|
|
312
|
+
requireYes(options.yes);
|
|
313
|
+
const result = await client().linkCandidate(parseClusterIdentifier(candidateKey, "candidate-key"), {
|
|
314
|
+
clusterKey: parseClusterIdentifier(options.cluster, "cluster"),
|
|
315
|
+
expectedCandidateVersion: integer(options.expectedCandidateVersion, "expected-candidate-version"),
|
|
316
|
+
expectedClusterVersion: integer(options.expectedClusterVersion, "expected-cluster-version"),
|
|
317
|
+
reason: reason(options.reason)
|
|
318
|
+
}, parseClusterIdempotencyKey(options.idempotencyKey));
|
|
319
|
+
await output(command, result, `${result.action}\t${result.candidate.candidateKey}\t${result.cluster?.clusterKey ?? "-"}`);
|
|
320
|
+
}), ["numa cluster candidate link cand_01 --cluster clu_0123456789abcdef0123456789abcdef --expected-candidate-version 0 --expected-cluster-version 1 --reason 'Strong UID match verified' --idempotency-key link-cand-01 --yes --json"]);
|
|
321
|
+
const ignore = candidate.command("ignore <candidate-key>").description("忽略 candidate,并保留审计原因")
|
|
322
|
+
.requiredOption("--expected-version <number>", "candidate version")
|
|
323
|
+
.requiredOption("--reason <text>", "5-1000 字符治理原因")
|
|
324
|
+
.requiredOption("--idempotency-key <key>", "稳定幂等键")
|
|
325
|
+
.option("--yes", "确认 ignore");
|
|
326
|
+
addExamples(ignore.action(async (candidateKey, options, command) => {
|
|
327
|
+
requireYes(options.yes);
|
|
328
|
+
const result = await client().ignoreCandidate(parseClusterIdentifier(candidateKey, "candidate-key"), {
|
|
329
|
+
expectedVersion: integer(options.expectedVersion, "expected-version"), reason: reason(options.reason)
|
|
330
|
+
}, parseClusterIdempotencyKey(options.idempotencyKey));
|
|
331
|
+
await output(command, result, `${result.action}\t${result.candidate.candidateKey}`);
|
|
332
|
+
}), ["numa cluster candidate ignore cand_01 --expected-version 0 --reason 'Temporary test cluster' --idempotency-key ignore-cand-01 --yes --json"]);
|
|
333
|
+
const discover = cluster.command("discover").description("本地发现生成 plan,并与显式批量提交分离");
|
|
334
|
+
const kubeconfig = discover.command("kubeconfig").description("只读遍历 kubeconfig context 并输出严格脱敏 JSON plan")
|
|
335
|
+
.option("--context <name>", "只发现一个 context")
|
|
336
|
+
.option("--kubeconfig <file>", "使用指定本地 kubeconfig;路径不会输出或上传")
|
|
337
|
+
.option("--timeout <seconds>", "每次 kubectl 短超时,默认 5,最大 30", "5");
|
|
338
|
+
addExamples(kubeconfig.action(async (options) => {
|
|
339
|
+
const plan = await (dependencies.discoverKubeconfig ?? discoverKubeconfig)({
|
|
340
|
+
...(options.context ? { context: options.context } : {}),
|
|
341
|
+
...(options.kubeconfig ? { kubeconfig: options.kubeconfig } : {}),
|
|
342
|
+
timeoutSeconds: integer(options.timeout, "timeout", 1, 30)
|
|
343
|
+
});
|
|
344
|
+
outputPlan(plan);
|
|
345
|
+
}), ["numa cluster discover kubeconfig --json > cluster-discovery.json", "numa cluster discover kubeconfig --context ack-dev --json"], [
|
|
346
|
+
"始终只输出脱敏 plan;不会调用 DevOps API,也不会输出 endpoint、kube-system UID、CA、user、token、cert、exec 或 auth-provider。"
|
|
347
|
+
]);
|
|
348
|
+
const submit = discover.command("submit").description("把已审阅的本地 discovery plan 原子批量提交")
|
|
349
|
+
.requiredOption("--file <plan.json>", "cluster discover kubeconfig 输出的 JSON plan")
|
|
350
|
+
.requiredOption("--source <source-key>", "KUBECONFIG_CLI source")
|
|
351
|
+
.requiredOption("--idempotency-key <key>", "稳定幂等键;未知结果原值重试")
|
|
352
|
+
.option("--yes", "确认整批提交");
|
|
353
|
+
addExamples(submit.action(async (options, command) => {
|
|
354
|
+
requireYes(options.yes);
|
|
355
|
+
const sourceKey = parseClusterIdentifier(options.source, "source");
|
|
356
|
+
const raw = await (dependencies.readPlan ?? defaultReadPlan)(options.file);
|
|
357
|
+
const plan = LocalKubeconfigDiscoveryPlanSchema.safeParse(raw);
|
|
358
|
+
if (!plan.success)
|
|
359
|
+
throw new ClusterError(`Discovery plan is invalid: ${plan.error.message}`, "CLUSTER_PLAN_INVALID");
|
|
360
|
+
const api = client();
|
|
361
|
+
const sourceView = await api.getSource(sourceKey);
|
|
362
|
+
if (sourceView.sourceType !== "KUBECONFIG_CLI") {
|
|
363
|
+
throw new ClusterError("Local observations can only be submitted to a KUBECONFIG_CLI source.", "CLUSTER_CLI_INPUT_INVALID");
|
|
364
|
+
}
|
|
365
|
+
const batch = observationBatchFromPlan(plan.data, sourceKey);
|
|
366
|
+
const result = await api.submitObservations(sourceKey, batch, parseClusterIdempotencyKey(options.idempotencyKey));
|
|
367
|
+
await output(command, result, `${result.observationCount} observation(s)\t${result.candidateCount} candidate(s)\treplayed=${result.replayed}`);
|
|
368
|
+
}), ["numa cluster discover submit --file cluster-discovery.json --source local-kubeconfig --idempotency-key kube-observe-20260822-01 --yes --json"], [
|
|
369
|
+
"请求体只包含服务端 ObservationBatchRequest allow-list;整批最多 100 条并原子提交。",
|
|
370
|
+
"结果未知时必须复用同一文件、source 和 idempotency-key,CLI 会用相同 observation keys 重放。"
|
|
371
|
+
]);
|
|
372
|
+
}
|
|
373
|
+
//# sourceMappingURL=commands.js.map
|