mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
feat(ql3): add generic worker management entry
This commit is contained in:
@@ -15,6 +15,7 @@ import { TextDecoder } from 'node:util';
|
||||
|
||||
export type ClusterAuthenticatedManagementClientKind =
|
||||
| 'package'
|
||||
| 'worker'
|
||||
| 'worker-credential'
|
||||
| 'automation'
|
||||
| 'approval'
|
||||
@@ -34,6 +35,10 @@ const MANAGEMENT_CLIENT_POLICIES: Readonly<
|
||||
managementPath: '/api/v3/plugin-packages/management',
|
||||
clientCertificate: 'forbidden',
|
||||
}),
|
||||
worker: Object.freeze({
|
||||
managementPath: '/api/v3/workers/management',
|
||||
clientCertificate: 'required',
|
||||
}),
|
||||
'worker-credential': Object.freeze({
|
||||
managementPath: '/api/v3/worker-credentials/management',
|
||||
clientCertificate: 'required',
|
||||
@@ -147,7 +152,9 @@ export function readCanonicalFile(
|
||||
throw configurationFailure();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ClusterPluginPackageManagementClientConfigurationError) {
|
||||
if (
|
||||
error instanceof ClusterPluginPackageManagementClientConfigurationError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw configurationFailure();
|
||||
@@ -209,7 +216,9 @@ export function readCanonicalFile(
|
||||
return bytes;
|
||||
} catch (error) {
|
||||
bytes?.fill(0);
|
||||
if (error instanceof ClusterPluginPackageManagementClientConfigurationError) {
|
||||
if (
|
||||
error instanceof ClusterPluginPackageManagementClientConfigurationError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw configurationFailure();
|
||||
@@ -349,7 +358,10 @@ export function prepareClusterAuthenticatedManagementClientConfiguration(
|
||||
throw configurationFailure();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ClusterPluginPackageManagementClientConfigurationError) {
|
||||
if (
|
||||
error instanceof
|
||||
ClusterPluginPackageManagementClientConfigurationError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw configurationFailure();
|
||||
@@ -380,7 +392,9 @@ export function prepareClusterAuthenticatedManagementClientConfiguration(
|
||||
caBytes?.fill(0);
|
||||
clientCertificateBytes?.fill(0);
|
||||
clientPrivateKeyBytes?.fill(0);
|
||||
if (error instanceof ClusterPluginPackageManagementClientConfigurationError) {
|
||||
if (
|
||||
error instanceof ClusterPluginPackageManagementClientConfigurationError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw configurationFailure();
|
||||
@@ -408,11 +422,10 @@ export function validateClusterAuthenticatedManagementClientConfiguration(
|
||||
): Readonly<ClusterAuthenticatedManagementClientConfigurationSummary> {
|
||||
const policy = MANAGEMENT_CLIENT_POLICIES[kind];
|
||||
if (policy === undefined) throw configurationFailure();
|
||||
const prepared =
|
||||
prepareClusterAuthenticatedManagementClientKindConfiguration(
|
||||
configFile,
|
||||
kind,
|
||||
);
|
||||
const prepared = prepareClusterAuthenticatedManagementClientKindConfiguration(
|
||||
configFile,
|
||||
kind,
|
||||
);
|
||||
try {
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
|
||||
@@ -85,6 +85,7 @@ export const CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH =
|
||||
'/api/v3/plugin-packages/management';
|
||||
export const CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH =
|
||||
'/api/v3/worker-credentials/management';
|
||||
export const CLUSTER_WORKER_MANAGEMENT_PATH = '/api/v3/workers/management';
|
||||
export const CLUSTER_AUTOMATION_MANAGEMENT_PATH =
|
||||
'/api/v3/automations/management';
|
||||
export const CLUSTER_APPROVAL_MANAGEMENT_PATH = '/api/v3/approvals/management';
|
||||
@@ -94,6 +95,7 @@ export const CLUSTER_RUN_MANAGEMENT_PATH = '/api/v3/runs/management';
|
||||
export type ClusterAuthenticatedManagementPath =
|
||||
| typeof CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH
|
||||
| typeof CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH
|
||||
| typeof CLUSTER_WORKER_MANAGEMENT_PATH
|
||||
| typeof CLUSTER_AUTOMATION_MANAGEMENT_PATH
|
||||
| typeof CLUSTER_APPROVAL_MANAGEMENT_PATH
|
||||
| typeof CLUSTER_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_PATH
|
||||
@@ -101,6 +103,7 @@ export type ClusterAuthenticatedManagementPath =
|
||||
const MANAGEMENT_PATHS = new Set<ClusterAuthenticatedManagementPath>([
|
||||
CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH,
|
||||
CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH,
|
||||
CLUSTER_WORKER_MANAGEMENT_PATH,
|
||||
CLUSTER_AUTOMATION_MANAGEMENT_PATH,
|
||||
CLUSTER_APPROVAL_MANAGEMENT_PATH,
|
||||
CLUSTER_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_PATH,
|
||||
@@ -143,6 +146,7 @@ export interface StartClusterPluginPackageManagementHttpOptions {
|
||||
readonly transport: ClusterAuthenticatedManagementTransport;
|
||||
readonly identities: ClusterPluginPackageIdentityKeysetFile;
|
||||
readonly managementPath?: ClusterAuthenticatedManagementPath;
|
||||
readonly compatibleManagementPaths?: readonly ClusterAuthenticatedManagementPath[];
|
||||
readonly limits?: ClusterPluginPackageManagementHttpLimits;
|
||||
readonly now?: () => number;
|
||||
readonly createRequestId?: () => string;
|
||||
@@ -547,7 +551,8 @@ function responseError(error: unknown): HttpRequestError {
|
||||
error instanceof ClusterApprovalManagementTransportAuthenticationError ||
|
||||
error instanceof
|
||||
ClusterModelProviderCredentialManagementTransportAuthenticationError ||
|
||||
error instanceof ClusterModelProviderCredentialManagementAuthenticationError ||
|
||||
error instanceof
|
||||
ClusterModelProviderCredentialManagementAuthenticationError ||
|
||||
error instanceof ClusterRunManagementTransportAuthenticationError
|
||||
) {
|
||||
return new HttpRequestError(401, 'authentication_required');
|
||||
@@ -573,7 +578,8 @@ function responseError(error: unknown): HttpRequestError {
|
||||
error instanceof WorkerCredentialManagementAuthorizationError ||
|
||||
error instanceof ClusterAutomationManagementAuthorizationError ||
|
||||
error instanceof ClusterApprovalManagementTransportAuthorizationError ||
|
||||
error instanceof ClusterModelProviderCredentialManagementAuthorizationError ||
|
||||
error instanceof
|
||||
ClusterModelProviderCredentialManagementAuthorizationError ||
|
||||
error instanceof ClusterRunManagementAuthorizationError
|
||||
) {
|
||||
return new HttpRequestError(403, 'forbidden');
|
||||
@@ -665,6 +671,7 @@ export async function startClusterPluginPackageManagementHttp(
|
||||
'transport',
|
||||
'identities',
|
||||
'managementPath',
|
||||
'compatibleManagementPaths',
|
||||
'limits',
|
||||
'now',
|
||||
'createRequestId',
|
||||
@@ -711,6 +718,12 @@ export async function startClusterPluginPackageManagementHttp(
|
||||
typeof options.identities.reload !== 'function' ||
|
||||
(options.managementPath !== undefined &&
|
||||
!MANAGEMENT_PATHS.has(options.managementPath)) ||
|
||||
(options.compatibleManagementPaths !== undefined &&
|
||||
(!Array.isArray(options.compatibleManagementPaths) ||
|
||||
options.managementPath !== CLUSTER_WORKER_MANAGEMENT_PATH ||
|
||||
options.compatibleManagementPaths.length !== 1 ||
|
||||
options.compatibleManagementPaths[0] !==
|
||||
CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH)) ||
|
||||
(options.now !== undefined && typeof options.now !== 'function') ||
|
||||
(options.createRequestId !== undefined &&
|
||||
typeof options.createRequestId !== 'function') ||
|
||||
@@ -721,6 +734,10 @@ export async function startClusterPluginPackageManagementHttp(
|
||||
const limits = reviewedLimits(options.limits);
|
||||
const managementPath =
|
||||
options.managementPath ?? CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH;
|
||||
const managementPaths = new Set<ClusterAuthenticatedManagementPath>([
|
||||
managementPath,
|
||||
...(options.compatibleManagementPaths ?? []),
|
||||
]);
|
||||
const now = options.now ?? Date.now;
|
||||
const createRequestId = options.createRequestId ?? randomUUID;
|
||||
const clientCertificateRequired =
|
||||
@@ -824,7 +841,10 @@ export async function startClusterPluginPackageManagementHttp(
|
||||
) {
|
||||
throw new HttpRequestError(401, 'client_certificate_required');
|
||||
}
|
||||
if (request.method !== 'POST' || url !== managementPath) {
|
||||
if (
|
||||
request.method !== 'POST' ||
|
||||
!managementPaths.has(url as ClusterAuthenticatedManagementPath)
|
||||
) {
|
||||
throw new HttpRequestError(404, 'not_found');
|
||||
}
|
||||
if (availability !== 'ready') {
|
||||
|
||||
@@ -71,6 +71,12 @@ export const QINGLONG3_CLUSTER_PRODUCT_COMMANDS: readonly QingLong3ClusterProduc
|
||||
'plugin-package/management/pluginPackageManagementKubernetesClientCli.js',
|
||||
description: 'manage Plugin Packages through a bounded Kubernetes tunnel',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'worker',
|
||||
binary: 'ql3-worker-client',
|
||||
target: 'worker-management/workerManagementClientCli.js',
|
||||
description: 'inspect bounded Worker session state',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'worker-credential',
|
||||
binary: 'ql3-worker-credential-client',
|
||||
|
||||
@@ -14,9 +14,7 @@ import {
|
||||
probeClusterCopilotClientReadiness,
|
||||
validateClusterCopilotClientConfiguration,
|
||||
} from '../copilot-client/client';
|
||||
import {
|
||||
validateClusterAuthenticatedManagementClientConfiguration,
|
||||
} from '../management-support/pluginPackageManagementClient';
|
||||
import { validateClusterAuthenticatedManagementClientConfiguration } from '../management-support/pluginPackageManagementClient';
|
||||
import type { ClusterAuthenticatedManagementClientKind } from '../management-support/pluginPackageManagementClient';
|
||||
import { probeClusterAuthenticatedManagementClientReadiness } from '../management-support/managementReadinessProbe';
|
||||
import {
|
||||
@@ -31,6 +29,7 @@ const CONTEXT_COMMANDS = Object.freeze([
|
||||
'copilot',
|
||||
'package',
|
||||
'package-kubernetes',
|
||||
'worker',
|
||||
'worker-credential',
|
||||
'approval',
|
||||
'run',
|
||||
@@ -92,6 +91,7 @@ const CONTEXT_COMMAND_CLIENT_KINDS: Readonly<
|
||||
> = Object.freeze({
|
||||
package: 'package',
|
||||
'package-kubernetes': 'package',
|
||||
worker: 'worker',
|
||||
'worker-credential': 'worker-credential',
|
||||
approval: 'approval',
|
||||
run: 'run',
|
||||
@@ -337,11 +337,10 @@ export async function validateQingLong3ClusterProductContext(
|
||||
}),
|
||||
);
|
||||
} else if (name === 'package-kubernetes') {
|
||||
const https =
|
||||
validateClusterAuthenticatedManagementClientConfiguration(
|
||||
command.configFile,
|
||||
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
||||
);
|
||||
const https = validateClusterAuthenticatedManagementClientConfiguration(
|
||||
command.configFile,
|
||||
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
||||
);
|
||||
const kubernetes =
|
||||
await validateClusterPluginPackageManagementKubernetesConfiguration(
|
||||
command.kubernetesFile!,
|
||||
@@ -355,11 +354,10 @@ export async function validateQingLong3ClusterProductContext(
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const https =
|
||||
validateClusterAuthenticatedManagementClientConfiguration(
|
||||
command.configFile,
|
||||
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
||||
);
|
||||
const https = validateClusterAuthenticatedManagementClientConfiguration(
|
||||
command.configFile,
|
||||
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
||||
);
|
||||
commands.push(
|
||||
Object.freeze({
|
||||
name,
|
||||
@@ -403,7 +401,7 @@ export async function probeQingLong3ClusterProductContext(
|
||||
command.kubernetesFile!,
|
||||
)
|
||||
: name === 'copilot'
|
||||
? await probeClusterCopilotClientReadiness(command.configFile)
|
||||
? await probeClusterCopilotClientReadiness(command.configFile)
|
||||
: await probeClusterAuthenticatedManagementClientReadiness(
|
||||
command.configFile,
|
||||
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
||||
|
||||
+7
-4
@@ -1,6 +1,7 @@
|
||||
/** TLS 1.3 Worker credential management HTTP adapter boundary. */
|
||||
import {
|
||||
CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH,
|
||||
CLUSTER_WORKER_MANAGEMENT_PATH,
|
||||
startClusterPluginPackageManagementHttp,
|
||||
type ClusterPluginPackageManagementHttpApplication,
|
||||
type ClusterPluginPackageManagementHttpLimits,
|
||||
@@ -32,15 +33,17 @@ export interface StartClusterWorkerCredentialManagementHttpOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the Worker credential management endpoint on the shared Cluster Admin
|
||||
* TLS 1.3/OIDC boundary. The public manager process never receives credential
|
||||
* delivery or Kubernetes execution capabilities.
|
||||
* Starts the Worker management endpoint on the shared Cluster Admin TLS
|
||||
* 1.3/OIDC boundary. The former credential-scoped path remains an exact
|
||||
* compatibility alias on this listener. The public manager process never
|
||||
* receives credential delivery or Kubernetes execution capabilities.
|
||||
*/
|
||||
export async function startClusterWorkerCredentialManagementHttp(
|
||||
options: StartClusterWorkerCredentialManagementHttpOptions,
|
||||
): Promise<Readonly<ClusterWorkerCredentialManagementHttpApplication>> {
|
||||
return startClusterPluginPackageManagementHttp({
|
||||
...options,
|
||||
managementPath: CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH,
|
||||
managementPath: CLUSTER_WORKER_MANAGEMENT_PATH,
|
||||
compatibleManagementPaths: [CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/** Read-only product client boundary for bounded Worker session observation. */
|
||||
import {
|
||||
executeClusterAuthenticatedManagementClient,
|
||||
type ClusterAuthenticatedManagementClientResult,
|
||||
type ClusterAuthenticatedManagementCommandExecution,
|
||||
type ClusterPluginPackageManagementClientConnectionOptions,
|
||||
} from '../management-support/pluginPackageManagementClient';
|
||||
import {
|
||||
ClusterWorkerCredentialManagementTransportRequestError,
|
||||
normalizeClusterWorkerCredentialManagementCommand,
|
||||
type ClusterWorkerCredentialManagementCommand,
|
||||
type ClusterWorkerCredentialManagementTransportResult,
|
||||
} from '../worker-credential/management-server/workerCredentialManagementTransport';
|
||||
import { validateClusterWorkerCredentialManagementClientResult } from '../worker-credential/workerCredentialManagementClient';
|
||||
|
||||
const MANAGEMENT_PATH = '/api/v3/workers/management';
|
||||
|
||||
export type ClusterWorkerManagementCommand = Extract<
|
||||
ClusterWorkerCredentialManagementCommand,
|
||||
{ readonly operation: 'worker-session.inspect' | 'worker-session.list' }
|
||||
>;
|
||||
export type ClusterWorkerManagementTransportResult = Extract<
|
||||
ClusterWorkerCredentialManagementTransportResult,
|
||||
{ readonly operation: 'worker-session.inspect' | 'worker-session.list' }
|
||||
>;
|
||||
export type ClusterWorkerManagementClientExecution =
|
||||
ClusterAuthenticatedManagementCommandExecution<ClusterWorkerManagementCommand>;
|
||||
export type ClusterWorkerManagementClientConnectionOptions =
|
||||
ClusterPluginPackageManagementClientConnectionOptions;
|
||||
export type ClusterWorkerManagementClientResult =
|
||||
ClusterAuthenticatedManagementClientResult<ClusterWorkerManagementTransportResult>;
|
||||
|
||||
export function normalizeClusterWorkerManagementCommand(
|
||||
value: unknown,
|
||||
): Readonly<ClusterWorkerManagementCommand> {
|
||||
const command = normalizeClusterWorkerCredentialManagementCommand(value);
|
||||
if (
|
||||
command.operation !== 'worker-session.inspect' &&
|
||||
command.operation !== 'worker-session.list'
|
||||
) {
|
||||
throw new ClusterWorkerCredentialManagementTransportRequestError(
|
||||
'operation is not available through the read-only Worker client',
|
||||
);
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
function validateClusterWorkerManagementResult(
|
||||
value: unknown,
|
||||
command: Readonly<ClusterWorkerManagementCommand>,
|
||||
): Readonly<ClusterWorkerManagementTransportResult> {
|
||||
const result = validateClusterWorkerCredentialManagementClientResult(
|
||||
value,
|
||||
command,
|
||||
);
|
||||
if (
|
||||
result.operation !== 'worker-session.inspect' &&
|
||||
result.operation !== 'worker-session.list'
|
||||
) {
|
||||
throw new Error('Worker management response is invalid');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const PROTOCOL = Object.freeze({
|
||||
managementPath: MANAGEMENT_PATH,
|
||||
clientCertificate: 'required' as const,
|
||||
normalizeCommand: normalizeClusterWorkerManagementCommand,
|
||||
validateResult: validateClusterWorkerManagementResult,
|
||||
});
|
||||
|
||||
export async function executeClusterWorkerManagementClient(
|
||||
execution: ClusterWorkerManagementClientExecution,
|
||||
connectionOptions?: ClusterWorkerManagementClientConnectionOptions,
|
||||
): Promise<Readonly<ClusterWorkerManagementClientResult>> {
|
||||
return executeClusterAuthenticatedManagementClient(
|
||||
execution,
|
||||
PROTOCOL,
|
||||
connectionOptions,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { ClusterPluginPackageManagementClientRemoteError } from '../management-support/pluginPackageManagementClient';
|
||||
import { executeClusterWorkerManagementClient } from './workerManagementClient';
|
||||
import {
|
||||
createWorkerSessionInspectionCommand,
|
||||
createWorkerSessionListCommand,
|
||||
formatWorkerSessionInspectionCard,
|
||||
formatWorkerSessionListCard,
|
||||
projectWorkerSessionInspection,
|
||||
projectWorkerSessionList,
|
||||
} from './workerManagementProduct';
|
||||
|
||||
const USAGE = [
|
||||
'Usage: ql3-worker-client inspect --config=/absolute/client.json --assertion=/absolute/assertion.jwt --project=PROJECT --worker=WORKER [--format=text|json]',
|
||||
' ql3-worker-client list --config=/absolute/client.json --assertion=/absolute/assertion.jwt --project=PROJECT [--after=WORKER] [--format=text|json]',
|
||||
'',
|
||||
'One invocation performs one bounded read; it never retries, polls or auto-pages.',
|
||||
].join('\n');
|
||||
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
type WorkerClientArguments = Readonly<{
|
||||
kind: 'inspect' | 'list';
|
||||
configFile: string;
|
||||
assertionFile: string;
|
||||
projectId: string;
|
||||
workerId?: string;
|
||||
afterWorkerId?: string;
|
||||
format: 'text' | 'json';
|
||||
}>;
|
||||
|
||||
function argumentsFrom(argv: readonly string[]): WorkerClientArguments | null {
|
||||
const modes = argv.filter(
|
||||
(argument) => argument === 'inspect' || argument === 'list',
|
||||
);
|
||||
if (modes.length !== 1 || argv.length < 4 || argv.length > 6) return null;
|
||||
const kind = modes[0] as 'inspect' | 'list';
|
||||
const values = new Map<string, string>();
|
||||
for (const argument of argv) {
|
||||
if (argument === kind) continue;
|
||||
const match =
|
||||
/^--(config|assertion|project|worker|after|format)=(.+)$/.exec(argument);
|
||||
if (!match || values.has(match[1]!)) return null;
|
||||
values.set(match[1]!, match[2]!);
|
||||
}
|
||||
if (
|
||||
!values.get('config')?.startsWith('/') ||
|
||||
!values.get('assertion')?.startsWith('/') ||
|
||||
!IDENTIFIER.test(values.get('project') ?? '') ||
|
||||
(kind === 'inspect' &&
|
||||
(!IDENTIFIER.test(values.get('worker') ?? '') || values.has('after'))) ||
|
||||
(kind === 'list' &&
|
||||
(values.has('worker') ||
|
||||
(values.has('after') && !IDENTIFIER.test(values.get('after')!)))) ||
|
||||
(values.has('format') &&
|
||||
values.get('format') !== 'text' &&
|
||||
values.get('format') !== 'json')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return Object.freeze({
|
||||
kind,
|
||||
configFile: values.get('config')!,
|
||||
assertionFile: values.get('assertion')!,
|
||||
projectId: values.get('project')!,
|
||||
...(kind === 'inspect' ? { workerId: values.get('worker')! } : {}),
|
||||
...(kind === 'list' && values.has('after')
|
||||
? { afterWorkerId: values.get('after')! }
|
||||
: {}),
|
||||
format: (values.get('format') ?? 'text') as 'text' | 'json',
|
||||
});
|
||||
}
|
||||
|
||||
function failureFact(error: unknown): Readonly<Record<string, unknown>> {
|
||||
const candidate = error as { readonly code?: unknown };
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-worker-management-client',
|
||||
event: 'inspection_failed',
|
||||
code:
|
||||
typeof candidate?.code === 'string'
|
||||
? candidate.code
|
||||
: 'QL3_WORKER_MANAGEMENT_CLIENT_FAILED',
|
||||
...(error instanceof ClusterPluginPackageManagementClientRemoteError
|
||||
? {
|
||||
statusCode: error.statusCode,
|
||||
responseCode: error.responseCode,
|
||||
requestId: error.requestId,
|
||||
...(error.retryAfterSeconds === null
|
||||
? {}
|
||||
: { retryAfterSeconds: error.retryAfterSeconds }),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
async function run(argv: readonly string[]): Promise<void> {
|
||||
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
||||
process.stdout.write(`${USAGE}\n`);
|
||||
return;
|
||||
}
|
||||
const argumentsValue = argumentsFrom(argv);
|
||||
if (argumentsValue === null) {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-worker-management-client',
|
||||
event: 'usage_invalid',
|
||||
code: 'QL3_WORKER_MANAGEMENT_CLIENT_USAGE_INVALID',
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 64;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const command =
|
||||
argumentsValue.kind === 'inspect'
|
||||
? createWorkerSessionInspectionCommand(
|
||||
argumentsValue.projectId,
|
||||
argumentsValue.workerId!,
|
||||
)
|
||||
: createWorkerSessionListCommand(
|
||||
argumentsValue.projectId,
|
||||
argumentsValue.afterWorkerId,
|
||||
);
|
||||
const response = await executeClusterWorkerManagementClient({
|
||||
configFile: argumentsValue.configFile,
|
||||
assertionFile: argumentsValue.assertionFile,
|
||||
command,
|
||||
});
|
||||
const projection =
|
||||
argumentsValue.kind === 'inspect'
|
||||
? projectWorkerSessionInspection(argumentsValue.projectId, response)
|
||||
: projectWorkerSessionList(argumentsValue.projectId, response);
|
||||
process.stdout.write(
|
||||
argumentsValue.format === 'json'
|
||||
? `${JSON.stringify(projection)}\n`
|
||||
: `${
|
||||
projection.schema === 'qinglong/worker-session-inspection@v1'
|
||||
? formatWorkerSessionInspectionCard(projection)
|
||||
: formatWorkerSessionListCard(projection)
|
||||
}\n`,
|
||||
);
|
||||
} catch (error) {
|
||||
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void run(process.argv.slice(2));
|
||||
@@ -0,0 +1,192 @@
|
||||
/** Bounded commands and low-sensitive product projections for Worker sessions. */
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import type {
|
||||
ClusterWorkerManagementClientResult,
|
||||
ClusterWorkerManagementCommand,
|
||||
ClusterWorkerManagementTransportResult,
|
||||
} from './workerManagementClient';
|
||||
|
||||
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
type InspectCommand = Extract<
|
||||
ClusterWorkerManagementCommand,
|
||||
{ readonly operation: 'worker-session.inspect' }
|
||||
>;
|
||||
type ListCommand = Extract<
|
||||
ClusterWorkerManagementCommand,
|
||||
{ readonly operation: 'worker-session.list' }
|
||||
>;
|
||||
type InspectResult = Extract<
|
||||
ClusterWorkerManagementTransportResult,
|
||||
{ readonly operation: 'worker-session.inspect' }
|
||||
>;
|
||||
type ListResult = Extract<
|
||||
ClusterWorkerManagementTransportResult,
|
||||
{ readonly operation: 'worker-session.list' }
|
||||
>;
|
||||
|
||||
export interface WorkerSessionInspection {
|
||||
readonly schema: 'qinglong/worker-session-inspection@v1';
|
||||
readonly projectId: string;
|
||||
readonly observedAtMs: number;
|
||||
readonly found: boolean;
|
||||
readonly worker: InspectResult['worker'];
|
||||
}
|
||||
|
||||
export interface WorkerSessionList {
|
||||
readonly schema: 'qinglong/worker-session-list@v1';
|
||||
readonly projectId: string;
|
||||
readonly observedAtMs: number;
|
||||
readonly count: number;
|
||||
readonly workers: ListResult['workers'];
|
||||
readonly nextAfterWorkerId: string | null;
|
||||
}
|
||||
|
||||
export class ClusterWorkerManagementProductError extends TypeError {
|
||||
readonly code = 'QL3_WORKER_MANAGEMENT_PRODUCT_INPUT_INVALID';
|
||||
|
||||
constructor() {
|
||||
super('Worker management product input is invalid');
|
||||
this.name = 'ClusterWorkerManagementProductError';
|
||||
}
|
||||
}
|
||||
|
||||
function identifier(value: string): string {
|
||||
if (!IDENTIFIER.test(value)) throw new ClusterWorkerManagementProductError();
|
||||
return value;
|
||||
}
|
||||
|
||||
function inspectionId(createId: () => string): string {
|
||||
const value = createId();
|
||||
return identifier(value);
|
||||
}
|
||||
|
||||
export function createWorkerSessionInspectionCommand(
|
||||
projectId: string,
|
||||
workerId: string,
|
||||
createId: () => string = randomUUID,
|
||||
): Readonly<InspectCommand> {
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: 'worker-session.inspect',
|
||||
request: Object.freeze({
|
||||
authorityProjectId: identifier(projectId),
|
||||
workerId: identifier(workerId),
|
||||
inspectionId: inspectionId(createId),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function createWorkerSessionListCommand(
|
||||
projectId: string,
|
||||
afterWorkerId?: string,
|
||||
createId: () => string = randomUUID,
|
||||
): Readonly<ListCommand> {
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: 'worker-session.list',
|
||||
request: Object.freeze({
|
||||
authorityProjectId: identifier(projectId),
|
||||
afterWorkerId:
|
||||
afterWorkerId === undefined ? null : identifier(afterWorkerId),
|
||||
inspectionId: inspectionId(createId),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function cloneWorker<
|
||||
Worker extends
|
||||
| NonNullable<InspectResult['worker']>
|
||||
| ListResult['workers'][number],
|
||||
>(worker: Worker): Worker {
|
||||
return Object.freeze({
|
||||
...worker,
|
||||
...('runtimes' in worker
|
||||
? {
|
||||
runtimes: Object.freeze(
|
||||
worker.runtimes.map((runtime) => Object.freeze({ ...runtime })),
|
||||
),
|
||||
declaredCapacity: Object.freeze({ ...worker.declaredCapacity }),
|
||||
}
|
||||
: {}),
|
||||
}) as Worker;
|
||||
}
|
||||
|
||||
export function projectWorkerSessionInspection(
|
||||
projectId: string,
|
||||
response: Readonly<ClusterWorkerManagementClientResult>,
|
||||
): Readonly<WorkerSessionInspection> {
|
||||
if (response.result.operation !== 'worker-session.inspect') {
|
||||
throw new ClusterWorkerManagementProductError();
|
||||
}
|
||||
return Object.freeze({
|
||||
schema: 'qinglong/worker-session-inspection@v1',
|
||||
projectId: identifier(projectId),
|
||||
observedAtMs: response.result.observedAtMs,
|
||||
found: response.result.worker !== null,
|
||||
worker:
|
||||
response.result.worker === null
|
||||
? null
|
||||
: cloneWorker(response.result.worker),
|
||||
});
|
||||
}
|
||||
|
||||
export function projectWorkerSessionList(
|
||||
projectId: string,
|
||||
response: Readonly<ClusterWorkerManagementClientResult>,
|
||||
): Readonly<WorkerSessionList> {
|
||||
if (response.result.operation !== 'worker-session.list') {
|
||||
throw new ClusterWorkerManagementProductError();
|
||||
}
|
||||
const workers = Object.freeze(response.result.workers.map(cloneWorker));
|
||||
return Object.freeze({
|
||||
schema: 'qinglong/worker-session-list@v1',
|
||||
projectId: identifier(projectId),
|
||||
observedAtMs: response.result.observedAtMs,
|
||||
count: workers.length,
|
||||
workers,
|
||||
nextAfterWorkerId: response.result.nextCursor,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatWorkerSessionInspectionCard(
|
||||
inspection: Readonly<WorkerSessionInspection>,
|
||||
): string {
|
||||
if (inspection.worker === null) {
|
||||
return [
|
||||
`Worker session: not found`,
|
||||
`Project: ${inspection.projectId}`,
|
||||
`Observed: ${inspection.observedAtMs}`,
|
||||
].join('\n');
|
||||
}
|
||||
const worker = inspection.worker;
|
||||
return [
|
||||
`Worker session: ${worker.workerId}`,
|
||||
`Project: ${inspection.projectId}`,
|
||||
`State: ${worker.lifecycle} / ${worker.compatibility} / ${worker.supportTier}`,
|
||||
`Platform: ${worker.operatingSystem ?? 'unknown'} ${
|
||||
worker.architecture
|
||||
} / protocol ${worker.protocolVersion}`,
|
||||
`Capacity: ${worker.availableSlots}/${worker.maxConcurrentRuns} slots available`,
|
||||
`Heartbeat: ${worker.lastHeartbeatAtMs} / lease ${worker.leaseExpiresAtMs}`,
|
||||
`Observed: ${inspection.observedAtMs}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function formatWorkerSessionListCard(
|
||||
page: Readonly<WorkerSessionList>,
|
||||
): string {
|
||||
const lines = [
|
||||
`Worker sessions: ${page.count}`,
|
||||
`Project: ${page.projectId}`,
|
||||
`Observed: ${page.observedAtMs}`,
|
||||
];
|
||||
for (const worker of page.workers) {
|
||||
lines.push(
|
||||
`${worker.workerId} ${worker.lifecycle} ${worker.supportTier} ${worker.architecture} slots ${worker.availableSlots}/${worker.maxConcurrentRuns}`,
|
||||
);
|
||||
}
|
||||
lines.push(`Next after: ${page.nextAfterWorkerId ?? '-'}`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
Reference in New Issue
Block a user