Files
qinglong/packages/ql3-cluster-admin/src/copilot-console/contracts.ts
T

628 lines
19 KiB
TypeScript

import {
CLUSTER_COPILOT_CLIENT_COMMAND_SCHEMA,
type ClusterCopilotClientCommand,
} from '../copilot-client/contracts';
export const CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA =
'qinglong/cluster-copilot-console-read-request@v1' as const;
export const CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA =
'qinglong/cluster-copilot-console-read-response@v1' as const;
export const CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS = Object.freeze([
'inspect',
'output',
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'package_list',
'package_inspect',
'run_list',
'run_read',
'run_event_list',
'run_step_list',
'task_list',
'task_read',
'workflow_list',
'workflow_run_list',
'workflow_run_read',
'workflow_event_list',
'workflow_step_list',
] as const);
export type ClusterCopilotConsoleReadOperation =
(typeof CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS)[number];
interface BaseReadRequest<
Operation extends ClusterCopilotConsoleReadOperation,
> {
readonly schema: typeof CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA;
readonly operation: Operation;
readonly projectId: string;
readonly requestId: string;
}
export type ClusterCopilotConsoleReadRequest =
| (BaseReadRequest<'inspect'> & Readonly<{ sourceRunId: string }>)
| (BaseReadRequest<'output'> & Readonly<{ sourceRunId: string }>)
| BaseReadRequest<'run_cancellation_status'>
| (BaseReadRequest<'run_cancellation_blocked_list'> &
Readonly<{ cursor: string | null }>)
| (BaseReadRequest<'run_cancellation_inspect'> & Readonly<{ runId: string }>)
| (BaseReadRequest<'worker_list'> &
Readonly<{ afterWorkerId: string | null }>)
| (BaseReadRequest<'worker_inspect'> & Readonly<{ workerId: string }>)
| (BaseReadRequest<'package_list'> &
Readonly<{ afterPackageName: string | null }>)
| (BaseReadRequest<'package_inspect'> & Readonly<{ packageName: string }>)
| (BaseReadRequest<'run_list'> &
Readonly<{
afterCreatedAtMs: number | null;
afterRunId: string | null;
limit: number;
}>)
| (BaseReadRequest<'run_read'> & Readonly<{ runId: string }>)
| (BaseReadRequest<'run_event_list'> &
Readonly<{ runId: string; afterSequence: number; limit: number }>)
| (BaseReadRequest<'run_step_list'> &
Readonly<{
runId: string;
afterStepKey: string | null;
afterStepRunId: string | null;
limit: number;
}>)
| (BaseReadRequest<'task_list'> &
Readonly<{ afterTaskId: string | null; limit: number }>)
| (BaseReadRequest<'task_read'> & Readonly<{ taskId: string }>)
| (BaseReadRequest<'workflow_list'> & Readonly<{ packageName: string }>)
| (BaseReadRequest<'workflow_run_list'> &
Readonly<{
packageName: string;
workflowId: string;
afterAdmittedAtMs: number | null;
afterRunId: string | null;
limit: number;
}>)
| (BaseReadRequest<'workflow_run_read'> &
Readonly<{ packageName: string; workflowId: string; runId: string }>)
| (BaseReadRequest<'workflow_event_list'> &
Readonly<{
packageName: string;
workflowId: string;
runId: string;
afterSequence: number;
limit: number;
}>)
| (BaseReadRequest<'workflow_step_list'> &
Readonly<{
packageName: string;
workflowId: string;
runId: string;
afterStepKey: string | null;
afterStepRunId: string | null;
limit: number;
}>);
export class InvalidClusterCopilotConsoleReadRequestError extends TypeError {
readonly code = 'QL3_CLUSTER_COPILOT_CONSOLE_READ_REQUEST_INVALID';
constructor() {
super('Cluster Copilot Console read request is invalid');
this.name = 'InvalidClusterCopilotConsoleReadRequestError';
}
}
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const COPILOT_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$/;
const RUN_CANCELLATION_CURSOR = /^v1\.[A-Za-z0-9_-]{1,512}$/;
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const WORKFLOW_ID = /^[a-z][a-z0-9-]{0,62}$/;
const UUID_V4 =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
function invalid(): never {
throw new InvalidClusterCopilotConsoleReadRequestError();
}
function exact(
record: Record<string, unknown>,
operation: ClusterCopilotConsoleReadOperation,
fields: readonly string[],
): void {
const actual = Object.keys(record).sort();
const expected = [
'operation',
'projectId',
'requestId',
'schema',
...fields,
].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index]) ||
record.schema !== CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA ||
record.operation !== operation ||
typeof record.projectId !== 'string' ||
!IDENTITY.test(record.projectId) ||
typeof record.requestId !== 'string' ||
!IDENTITY.test(record.requestId)
)
invalid();
}
function identifier(value: unknown): value is string {
return typeof value === 'string' && IDENTITY.test(value);
}
function limit(value: unknown): value is number {
return (
Number.isSafeInteger(value) && Number(value) >= 1 && Number(value) <= 64
);
}
function sequence(value: unknown): value is number {
return (
Number.isSafeInteger(value) &&
Number(value) >= 0 &&
Number(value) <= 2_147_483_647
);
}
function timestamp(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) >= 0;
}
function common(record: Record<string, unknown>) {
return {
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
projectId: record.projectId as string,
requestId: record.requestId as string,
} as const;
}
function workflowTarget(
record: Record<string, unknown>,
requireRun: boolean,
): boolean {
return (
typeof record.packageName === 'string' &&
PACKAGE_NAME.test(record.packageName) &&
typeof record.workflowId === 'string' &&
WORKFLOW_ID.test(record.workflowId) &&
(!requireRun ||
(typeof record.runId === 'string' && UUID_V4.test(record.runId)))
);
}
export function normalizeClusterCopilotConsoleReadRequest(
value: unknown,
): Readonly<ClusterCopilotConsoleReadRequest> {
if (!value || typeof value !== 'object' || Array.isArray(value)) invalid();
const record = value as Record<string, unknown>;
const operation = record.operation;
if (
typeof operation !== 'string' ||
!CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.includes(
operation as ClusterCopilotConsoleReadOperation,
)
)
invalid();
const op = operation as ClusterCopilotConsoleReadOperation;
if (op === 'inspect' || op === 'output') {
exact(record, op, ['sourceRunId']);
if (
typeof record.sourceRunId !== 'string' ||
!COPILOT_RUN_ID.test(record.sourceRunId)
)
invalid();
return Object.freeze({
...common(record),
operation: op,
sourceRunId: record.sourceRunId,
});
}
if (op === 'run_cancellation_status') {
exact(record, op, []);
return Object.freeze({
...common(record),
operation: op,
});
}
if (op === 'run_cancellation_blocked_list') {
exact(record, op, ['cursor']);
if (
record.cursor !== null &&
(typeof record.cursor !== 'string' ||
!RUN_CANCELLATION_CURSOR.test(record.cursor))
)
invalid();
return Object.freeze({
...common(record),
operation: op,
cursor: record.cursor as string | null,
});
}
if (op === 'run_cancellation_inspect') {
exact(record, op, ['runId']);
if (!identifier(record.runId)) invalid();
return Object.freeze({
...common(record),
operation: op,
runId: record.runId,
});
}
if (op === 'worker_list') {
exact(record, op, ['afterWorkerId']);
if (record.afterWorkerId !== null && !identifier(record.afterWorkerId))
invalid();
return Object.freeze({
...common(record),
operation: op,
afterWorkerId: record.afterWorkerId as string | null,
});
}
if (op === 'worker_inspect') {
exact(record, op, ['workerId']);
if (!identifier(record.workerId)) invalid();
return Object.freeze({
...common(record),
operation: op,
workerId: record.workerId,
});
}
if (op === 'package_list') {
exact(record, op, ['afterPackageName']);
if (
record.afterPackageName !== null &&
(typeof record.afterPackageName !== 'string' ||
!PACKAGE_NAME.test(record.afterPackageName))
)
invalid();
return Object.freeze({
...common(record),
operation: op,
afterPackageName: record.afterPackageName as string | null,
});
}
if (op === 'package_inspect') {
exact(record, op, ['packageName']);
if (
typeof record.packageName !== 'string' ||
!PACKAGE_NAME.test(record.packageName)
)
invalid();
return Object.freeze({
...common(record),
operation: op,
packageName: record.packageName,
});
}
if (op === 'run_list') {
exact(record, op, ['afterCreatedAtMs', 'afterRunId', 'limit']);
if (
(record.afterCreatedAtMs === null) !== (record.afterRunId === null) ||
(record.afterCreatedAtMs !== null &&
!timestamp(record.afterCreatedAtMs)) ||
(record.afterRunId !== null && !identifier(record.afterRunId)) ||
!limit(record.limit)
)
invalid();
return Object.freeze({
...common(record),
operation: op,
afterCreatedAtMs: record.afterCreatedAtMs as number | null,
afterRunId: record.afterRunId as string | null,
limit: record.limit as number,
});
}
if (op === 'run_read') {
exact(record, op, ['runId']);
if (!identifier(record.runId)) invalid();
return Object.freeze({
...common(record),
operation: op,
runId: record.runId,
});
}
if (op === 'run_event_list') {
exact(record, op, ['afterSequence', 'limit', 'runId']);
if (
!identifier(record.runId) ||
!sequence(record.afterSequence) ||
!limit(record.limit)
)
invalid();
return Object.freeze({
...common(record),
operation: op,
runId: record.runId,
afterSequence: record.afterSequence as number,
limit: record.limit as number,
});
}
if (op === 'run_step_list') {
exact(record, op, ['afterStepKey', 'afterStepRunId', 'limit', 'runId']);
if (
!identifier(record.runId) ||
(record.afterStepKey === null) !== (record.afterStepRunId === null) ||
(record.afterStepKey !== null && !identifier(record.afterStepKey)) ||
(record.afterStepRunId !== null && !identifier(record.afterStepRunId)) ||
!limit(record.limit)
)
invalid();
return Object.freeze({
...common(record),
operation: op,
runId: record.runId,
afterStepKey: record.afterStepKey as string | null,
afterStepRunId: record.afterStepRunId as string | null,
limit: record.limit as number,
});
}
if (op === 'task_list') {
exact(record, op, ['afterTaskId', 'limit']);
if (
(record.afterTaskId !== null && !identifier(record.afterTaskId)) ||
!limit(record.limit)
)
invalid();
return Object.freeze({
...common(record),
operation: op,
afterTaskId: record.afterTaskId as string | null,
limit: record.limit as number,
});
}
if (op === 'task_read') {
exact(record, op, ['taskId']);
if (!identifier(record.taskId)) invalid();
return Object.freeze({
...common(record),
operation: op,
taskId: record.taskId,
});
}
if (op === 'workflow_list') {
exact(record, op, ['packageName']);
if (
typeof record.packageName !== 'string' ||
!PACKAGE_NAME.test(record.packageName)
)
invalid();
return Object.freeze({
...common(record),
operation: op,
packageName: record.packageName,
});
}
if (op === 'workflow_run_list') {
exact(record, op, [
'afterAdmittedAtMs',
'afterRunId',
'limit',
'packageName',
'workflowId',
]);
if (
!workflowTarget(record, false) ||
(record.afterAdmittedAtMs === null) !== (record.afterRunId === null) ||
(record.afterAdmittedAtMs !== null &&
!timestamp(record.afterAdmittedAtMs)) ||
(record.afterRunId !== null &&
(typeof record.afterRunId !== 'string' ||
!UUID_V4.test(record.afterRunId))) ||
!limit(record.limit)
)
invalid();
return Object.freeze({
...common(record),
operation: op,
packageName: record.packageName as string,
workflowId: record.workflowId as string,
afterAdmittedAtMs: record.afterAdmittedAtMs as number | null,
afterRunId: record.afterRunId as string | null,
limit: record.limit as number,
});
}
if (op === 'workflow_run_read') {
exact(record, op, ['packageName', 'runId', 'workflowId']);
if (!workflowTarget(record, true)) invalid();
return Object.freeze({
...common(record),
operation: op,
packageName: record.packageName as string,
workflowId: record.workflowId as string,
runId: record.runId as string,
});
}
if (op === 'workflow_event_list') {
exact(record, op, [
'afterSequence',
'limit',
'packageName',
'runId',
'workflowId',
]);
if (
!workflowTarget(record, true) ||
!sequence(record.afterSequence) ||
!limit(record.limit)
)
invalid();
return Object.freeze({
...common(record),
operation: op,
packageName: record.packageName as string,
workflowId: record.workflowId as string,
runId: record.runId as string,
afterSequence: record.afterSequence as number,
limit: record.limit as number,
});
}
exact(record, 'workflow_step_list', [
'afterStepKey',
'afterStepRunId',
'limit',
'packageName',
'runId',
'workflowId',
]);
if (
!workflowTarget(record, true) ||
(record.afterStepKey === null) !== (record.afterStepRunId === null) ||
(record.afterStepKey !== null &&
(typeof record.afterStepKey !== 'string' ||
!WORKFLOW_ID.test(record.afterStepKey))) ||
(record.afterStepRunId !== null &&
(typeof record.afterStepRunId !== 'string' ||
!UUID_V4.test(record.afterStepRunId))) ||
!limit(record.limit)
)
invalid();
return Object.freeze({
...common(record),
operation: 'workflow_step_list',
packageName: record.packageName as string,
workflowId: record.workflowId as string,
runId: record.runId as string,
afterStepKey: record.afterStepKey as string | null,
afterStepRunId: record.afterStepRunId as string | null,
limit: record.limit as number,
});
}
export function clusterCopilotConsoleClientCommand(
request: Readonly<ClusterCopilotConsoleReadRequest>,
): Readonly<ClusterCopilotClientCommand> {
const normalized = normalizeClusterCopilotConsoleReadRequest(request);
if (normalized.operation !== 'inspect' && normalized.operation !== 'output')
invalid();
return Object.freeze({
schema: CLUSTER_COPILOT_CLIENT_COMMAND_SCHEMA,
operation: normalized.operation,
projectId: normalized.projectId,
sourceRunId: normalized.sourceRunId,
requestId: normalized.requestId,
});
}
function encoded(value: string): string {
return encodeURIComponent(value);
}
function query(
entries: readonly (readonly [string, string | number])[],
): string {
return entries.length === 0
? ''
: '?' +
entries
.map(([key, value]) => encoded(key) + '=' + encoded(String(value)))
.join('&');
}
export function clusterCopilotConsoleProjectReadPath(
request: Readonly<ClusterCopilotConsoleReadRequest>,
): string {
const normalized = normalizeClusterCopilotConsoleReadRequest(request);
if (
normalized.operation === 'inspect' ||
normalized.operation === 'output' ||
normalized.operation === 'run_cancellation_status' ||
normalized.operation === 'run_cancellation_blocked_list' ||
normalized.operation === 'run_cancellation_inspect' ||
normalized.operation === 'worker_list' ||
normalized.operation === 'worker_inspect' ||
normalized.operation === 'package_list' ||
normalized.operation === 'package_inspect'
)
invalid();
const project = '/api/v3/projects/' + encoded(normalized.projectId);
if (normalized.operation === 'run_list') {
const cursor: (readonly [string, string | number])[] =
normalized.afterCreatedAtMs === null
? []
: [
['after_created_at_ms', normalized.afterCreatedAtMs],
['after_run_id', normalized.afterRunId!],
];
return project + '/runs' + query([...cursor, ['limit', normalized.limit]]);
}
if (normalized.operation === 'run_read')
return project + '/runs/' + encoded(normalized.runId);
if (normalized.operation === 'run_event_list')
return (
project +
'/runs/' +
encoded(normalized.runId) +
'/events' +
query([
['after_sequence', normalized.afterSequence],
['limit', normalized.limit],
])
);
if (normalized.operation === 'run_step_list') {
const cursor: (readonly [string, string | number])[] =
normalized.afterStepKey === null
? []
: [
['after_step_key', normalized.afterStepKey],
['after_step_run_id', normalized.afterStepRunId!],
];
return (
project +
'/runs/' +
encoded(normalized.runId) +
'/steps' +
query([...cursor, ['limit', normalized.limit]])
);
}
if (normalized.operation === 'task_list') {
const cursor: (readonly [string, string | number])[] =
normalized.afterTaskId === null
? []
: [['after_task_id', normalized.afterTaskId]];
return project + '/tasks' + query([...cursor, ['limit', normalized.limit]]);
}
if (normalized.operation === 'task_read')
return project + '/tasks/' + encoded(normalized.taskId);
const packageRoot = project + '/packages/' + encoded(normalized.packageName);
if (normalized.operation === 'workflow_list')
return packageRoot + '/workflows';
const workflowRoot =
packageRoot + '/workflows/' + encoded(normalized.workflowId);
if (normalized.operation === 'workflow_run_list') {
const cursor: (readonly [string, string | number])[] =
normalized.afterAdmittedAtMs === null
? []
: [
['after_admitted_at_ms', normalized.afterAdmittedAtMs],
['after_run_id', normalized.afterRunId!],
];
return (
workflowRoot + '/runs' + query([...cursor, ['limit', normalized.limit]])
);
}
const runRoot = workflowRoot + '/runs/' + encoded(normalized.runId);
if (normalized.operation === 'workflow_run_read') return runRoot;
if (normalized.operation === 'workflow_event_list')
return (
runRoot +
'/events' +
query([
['after_sequence', normalized.afterSequence],
['limit', normalized.limit],
])
);
const cursor: (readonly [string, string | number])[] =
normalized.afterStepKey === null
? []
: [
['after_step_key', normalized.afterStepKey],
['after_step_run_id', normalized.afterStepRunId!],
];
return runRoot + '/steps' + query([...cursor, ['limit', normalized.limit]]);
}