mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 10:32:40 +08:00
feat(ql3): page blocked cancellations
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import type { ClusterRunManagementClientResult } from './runManagementClient';
|
||||
import {
|
||||
RUN_CANCELLATION_DISPATCH_BLOCKED_LIST_REQUEST_SCHEMA,
|
||||
normalizeClusterRunManagementCommand,
|
||||
type ClusterRunManagementCancellationBlockedListCommand,
|
||||
type ClusterRunManagementCancellationBlockedListTransportResult,
|
||||
} from './runManagementTransport';
|
||||
|
||||
export const RUN_CANCELLATION_BLOCKED_LIST_SCHEMA =
|
||||
'qinglong/run-cancellation-blocked-list@v1' as const;
|
||||
|
||||
const CURSOR_PREFIX = 'v1.';
|
||||
const CURSOR_PAYLOAD_PATTERN = /^[A-Za-z0-9_-]{1,512}$/;
|
||||
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
type BlockedCursor = NonNullable<
|
||||
ClusterRunManagementCancellationBlockedListCommand['request']['body']['after']
|
||||
>;
|
||||
|
||||
export interface RunCancellationBlockedListObservation {
|
||||
readonly schemaVersion: 1;
|
||||
readonly schema: typeof RUN_CANCELLATION_BLOCKED_LIST_SCHEMA;
|
||||
readonly component: 'qinglong3-run-management-client';
|
||||
readonly event: 'cancellation_blocked_list_observed';
|
||||
readonly requestId: string;
|
||||
readonly projectId: string;
|
||||
readonly snapshotAtMs: number;
|
||||
readonly observedAtMs: number;
|
||||
readonly items: readonly Readonly<{
|
||||
runId: string;
|
||||
blockedAtMs: number;
|
||||
}>[];
|
||||
readonly truncated: boolean;
|
||||
readonly nextCursor?: string;
|
||||
}
|
||||
|
||||
function invalidCursor(): never {
|
||||
throw new TypeError('Run cancellation blocked cursor is invalid');
|
||||
}
|
||||
|
||||
function exact(value: unknown, keys: readonly string[]): Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
invalidCursor();
|
||||
}
|
||||
const actual = Object.keys(value as object).sort();
|
||||
const expected = [...keys].sort();
|
||||
if (
|
||||
actual.length !== expected.length ||
|
||||
actual.some((key, index) => key !== expected[index])
|
||||
) {
|
||||
invalidCursor();
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function normalizeCursor(value: unknown): Readonly<BlockedCursor> {
|
||||
const cursor = exact(value, ['snapshotAtMs', 'blockedAtMs', 'runId']);
|
||||
if (
|
||||
typeof cursor.snapshotAtMs !== 'number' ||
|
||||
!Number.isSafeInteger(cursor.snapshotAtMs) ||
|
||||
cursor.snapshotAtMs < 0 ||
|
||||
typeof cursor.blockedAtMs !== 'number' ||
|
||||
!Number.isSafeInteger(cursor.blockedAtMs) ||
|
||||
cursor.blockedAtMs < 0 ||
|
||||
cursor.blockedAtMs > cursor.snapshotAtMs ||
|
||||
typeof cursor.runId !== 'string' ||
|
||||
!IDENTIFIER_PATTERN.test(cursor.runId)
|
||||
) {
|
||||
invalidCursor();
|
||||
}
|
||||
return Object.freeze({
|
||||
snapshotAtMs: cursor.snapshotAtMs,
|
||||
blockedAtMs: cursor.blockedAtMs,
|
||||
runId: cursor.runId,
|
||||
});
|
||||
}
|
||||
|
||||
export function decodeRunCancellationBlockedCursor(
|
||||
token: string,
|
||||
): Readonly<BlockedCursor> {
|
||||
if (typeof token !== 'string' || !token.startsWith(CURSOR_PREFIX)) {
|
||||
invalidCursor();
|
||||
}
|
||||
const encoded = token.slice(CURSOR_PREFIX.length);
|
||||
if (!CURSOR_PAYLOAD_PATTERN.test(encoded)) invalidCursor();
|
||||
let bytes: Buffer;
|
||||
let parsed: unknown;
|
||||
try {
|
||||
bytes = Buffer.from(encoded, 'base64url');
|
||||
if (
|
||||
bytes.length < 2 ||
|
||||
bytes.length > 384 ||
|
||||
bytes.toString('base64url') !== encoded
|
||||
) {
|
||||
invalidCursor();
|
||||
}
|
||||
parsed = JSON.parse(bytes.toString('utf8'));
|
||||
} catch {
|
||||
invalidCursor();
|
||||
}
|
||||
return normalizeCursor(parsed);
|
||||
}
|
||||
|
||||
export function encodeRunCancellationBlockedCursor(
|
||||
value: Readonly<BlockedCursor>,
|
||||
): string {
|
||||
const cursor = normalizeCursor(value);
|
||||
return `${CURSOR_PREFIX}${Buffer.from(JSON.stringify(cursor)).toString(
|
||||
'base64url',
|
||||
)}`;
|
||||
}
|
||||
|
||||
export function createRunCancellationBlockedListCommand(
|
||||
projectId: string,
|
||||
cursorToken?: string,
|
||||
createUuid: () => string = randomUUID,
|
||||
): Readonly<ClusterRunManagementCancellationBlockedListCommand> {
|
||||
const requestId = createUuid();
|
||||
const auditEventId = createUuid();
|
||||
let failureAuditEventId = createUuid();
|
||||
for (
|
||||
let attempts = 0;
|
||||
failureAuditEventId === auditEventId && attempts < 3;
|
||||
attempts += 1
|
||||
) {
|
||||
failureAuditEventId = createUuid();
|
||||
}
|
||||
return normalizeClusterRunManagementCommand({
|
||||
schemaVersion: 1,
|
||||
operation: 'run.cancellation.blocked.list',
|
||||
request: {
|
||||
projectId,
|
||||
requestId,
|
||||
auditEventId,
|
||||
failureAuditEventId,
|
||||
body: {
|
||||
schema: RUN_CANCELLATION_DISPATCH_BLOCKED_LIST_REQUEST_SCHEMA,
|
||||
after:
|
||||
cursorToken === undefined
|
||||
? null
|
||||
: decodeRunCancellationBlockedCursor(cursorToken),
|
||||
},
|
||||
},
|
||||
}) as Readonly<ClusterRunManagementCancellationBlockedListCommand>;
|
||||
}
|
||||
|
||||
export function projectRunCancellationBlockedList(
|
||||
result: Readonly<ClusterRunManagementClientResult>,
|
||||
): Readonly<RunCancellationBlockedListObservation> {
|
||||
if (result.result.operation !== 'run.cancellation.blocked.list') {
|
||||
throw new TypeError('Run cancellation blocked list requires a list result');
|
||||
}
|
||||
const page = (
|
||||
result.result as ClusterRunManagementCancellationBlockedListTransportResult
|
||||
).page;
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
schema: RUN_CANCELLATION_BLOCKED_LIST_SCHEMA,
|
||||
component: 'qinglong3-run-management-client',
|
||||
event: 'cancellation_blocked_list_observed',
|
||||
requestId: result.requestId,
|
||||
projectId: page.projectId,
|
||||
snapshotAtMs: page.snapshotAtMs,
|
||||
observedAtMs: page.observedAtMs,
|
||||
items: page.items,
|
||||
truncated: page.truncated,
|
||||
...(page.nextCursor === undefined
|
||||
? {}
|
||||
: { nextCursor: encodeRunCancellationBlockedCursor(page.nextCursor) }),
|
||||
});
|
||||
}
|
||||
|
||||
export function formatRunCancellationBlockedListCard(
|
||||
observation: Readonly<RunCancellationBlockedListObservation>,
|
||||
): string {
|
||||
return [
|
||||
'QingLong 3.0 / Blocked Cancellations',
|
||||
`PROJECT ${observation.projectId}`,
|
||||
`SNAPSHOT ${new Date(observation.snapshotAtMs).toISOString()}`,
|
||||
`OBSERVED ${new Date(observation.observedAtMs).toISOString()}`,
|
||||
`ITEMS ${observation.items.length}`,
|
||||
...observation.items.map(
|
||||
(item) =>
|
||||
`BLOCKED ${new Date(item.blockedAtMs).toISOString()} ${item.runId}`,
|
||||
),
|
||||
`NEXT_CURSOR ${observation.nextCursor ?? '-'}`,
|
||||
`REQUEST ${observation.requestId}`,
|
||||
].join('\n');
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
RunCancellationDispatchManagementNotFoundError,
|
||||
RunCancellationDispatchManagementUnavailableError,
|
||||
type BlockingCancellationDispatchResult,
|
||||
type RunCancellationDispatchBlockedCursor,
|
||||
type RunCancellationDispatchBlockedPage,
|
||||
type RunCancellationDispatchDiagnostic,
|
||||
type RunCancellationDispatchRearmReceipt,
|
||||
type RunCancellationDispatchSummary,
|
||||
@@ -84,6 +86,11 @@ export interface ClusterRunManagementCancellationSummaryRequest {
|
||||
readonly principal: Readonly<SecurityPrincipal>;
|
||||
}
|
||||
|
||||
export interface ClusterRunManagementCancellationBlockedListRequest
|
||||
extends ClusterRunManagementCancellationSummaryRequest {
|
||||
readonly after?: Readonly<RunCancellationDispatchBlockedCursor>;
|
||||
}
|
||||
|
||||
export interface ClusterRunManagementCancellationRearmRequest
|
||||
extends ClusterRunManagementCancellationInspectRequest {
|
||||
readonly mutationId: string;
|
||||
@@ -102,6 +109,9 @@ export interface ClusterRunManagementService {
|
||||
summarizeCancellation(
|
||||
request: Readonly<ClusterRunManagementCancellationSummaryRequest>,
|
||||
): Promise<Readonly<RunCancellationDispatchSummary>>;
|
||||
listBlockedCancellations(
|
||||
request: Readonly<ClusterRunManagementCancellationBlockedListRequest>,
|
||||
): Promise<Readonly<RunCancellationDispatchBlockedPage>>;
|
||||
inspectCancellation(
|
||||
request: Readonly<ClusterRunManagementCancellationInspectRequest>,
|
||||
): Promise<Readonly<RunCancellationDispatchDiagnostic>>;
|
||||
@@ -267,6 +277,34 @@ function exactCancellationSummaryRequest(
|
||||
}
|
||||
}
|
||||
|
||||
function exactCancellationBlockedListRequest(
|
||||
value: unknown,
|
||||
): asserts value is Readonly<ClusterRunManagementCancellationBlockedListRequest> {
|
||||
const hasAfter =
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
Object.hasOwn(value, 'after');
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).sort().join('\0') !==
|
||||
[
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'principal',
|
||||
'projectId',
|
||||
'requestId',
|
||||
...(hasAfter ? ['after'] : []),
|
||||
]
|
||||
.sort()
|
||||
.join('\0')
|
||||
) {
|
||||
throw new ClusterRunManagementRequestError();
|
||||
}
|
||||
}
|
||||
|
||||
function exactCancellationRearmRequest(
|
||||
value: unknown,
|
||||
): asserts value is Readonly<ClusterRunManagementCancellationRearmRequest> {
|
||||
@@ -615,6 +653,99 @@ export function createClusterRunManagementService(
|
||||
throw new ClusterRunManagementUnavailableError({ cause: error });
|
||||
}
|
||||
},
|
||||
async listBlockedCancellations(
|
||||
requestValue: Readonly<ClusterRunManagementCancellationBlockedListRequest>,
|
||||
) {
|
||||
exactCancellationBlockedListRequest(requestValue);
|
||||
const observedAtMs = now();
|
||||
let principal: Readonly<SecurityPrincipal>;
|
||||
const after = requestValue.after;
|
||||
if (
|
||||
!Number.isSafeInteger(observedAtMs) ||
|
||||
observedAtMs < 0 ||
|
||||
!IDENTIFIER_PATTERN.test(requestValue.projectId) ||
|
||||
!IDENTIFIER_PATTERN.test(requestValue.requestId) ||
|
||||
!validUuid(requestValue.auditEventId) ||
|
||||
!validUuid(requestValue.failureAuditEventId) ||
|
||||
requestValue.auditEventId === requestValue.failureAuditEventId ||
|
||||
(after !== undefined &&
|
||||
(!after ||
|
||||
typeof after !== 'object' ||
|
||||
Array.isArray(after) ||
|
||||
Object.keys(after).sort().join('\0') !==
|
||||
['blockedAtMs', 'runId', 'snapshotAtMs'].join('\0') ||
|
||||
!Number.isSafeInteger(after.snapshotAtMs) ||
|
||||
after.snapshotAtMs < 0 ||
|
||||
!Number.isSafeInteger(after.blockedAtMs) ||
|
||||
after.blockedAtMs < 0 ||
|
||||
after.blockedAtMs > after.snapshotAtMs ||
|
||||
!IDENTIFIER_PATTERN.test(after.runId)))
|
||||
) {
|
||||
throw new ClusterRunManagementRequestError();
|
||||
}
|
||||
try {
|
||||
principal = normalizeSecurityPrincipal(
|
||||
requestValue.principal,
|
||||
observedAtMs,
|
||||
);
|
||||
} catch {
|
||||
throw new ClusterRunManagementRequestError();
|
||||
}
|
||||
let fence: Readonly<SecurityPolicyFence> | null = null;
|
||||
try {
|
||||
const decision = await policy.authorize(
|
||||
principal,
|
||||
requestValue.projectId,
|
||||
'run.read',
|
||||
);
|
||||
fence = decision.fence;
|
||||
if (
|
||||
decision.effect !== 'allow' ||
|
||||
!fence ||
|
||||
fence.bindingVersion === null
|
||||
) {
|
||||
throw new ClusterRunManagementAuthorizationError();
|
||||
}
|
||||
return await cancellationDispatches.listBlocked({
|
||||
projectId: requestValue.projectId,
|
||||
requestId: requestValue.requestId,
|
||||
auditEventId: requestValue.auditEventId,
|
||||
principal,
|
||||
policyFence: fence,
|
||||
...(after === undefined ? {} : { after }),
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
await audit.record(
|
||||
normalizeSecurityAuditRecord({
|
||||
eventId: requestValue.failureAuditEventId,
|
||||
requestId: requestValue.requestId,
|
||||
operationId: 'run.cancellation.blocked.list',
|
||||
projectId: requestValue.projectId,
|
||||
subject: principal.subject,
|
||||
authenticationId: principal.authenticationId,
|
||||
outcome: 'denied',
|
||||
reasons: [failureReason(error)],
|
||||
fence,
|
||||
occurredAtMs: observedAtMs,
|
||||
}),
|
||||
);
|
||||
} catch (auditError) {
|
||||
throw new ClusterRunManagementUnavailableError({ cause: auditError });
|
||||
}
|
||||
if (error instanceof ClusterRunManagementAuthorizationError) throw error;
|
||||
if (error instanceof InvalidRunCancellationDispatchManagementError) {
|
||||
throw new ClusterRunManagementRequestError();
|
||||
}
|
||||
if (error instanceof RunCancellationDispatchManagementConflictError) {
|
||||
throw new ClusterRunManagementConflictError();
|
||||
}
|
||||
if (error instanceof RunCancellationDispatchManagementUnavailableError) {
|
||||
throw new ClusterRunManagementUnavailableError({ cause: error });
|
||||
}
|
||||
throw new ClusterRunManagementUnavailableError({ cause: error });
|
||||
}
|
||||
},
|
||||
async inspectCancellation(
|
||||
requestValue: Readonly<ClusterRunManagementCancellationInspectRequest>,
|
||||
) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@qinglong/runtime-core/run-cancellation';
|
||||
import { RUN_STATUSES } from '@qinglong/runtime-core/run';
|
||||
import {
|
||||
CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT,
|
||||
CANCELLATION_DISPATCH_RESULTS,
|
||||
CANCELLATION_DISPATCH_STATUSES,
|
||||
} from '@qinglong/runtime-core/cancellation-dispatch';
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
} from '../management-support/pluginPackageManagementClient';
|
||||
import {
|
||||
RUN_CANCELLATION_DISPATCH_DIAGNOSTIC_SCHEMA,
|
||||
RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_SCHEMA,
|
||||
RUN_CANCELLATION_DISPATCH_REARM_RECEIPT_SCHEMA,
|
||||
RUN_CANCELLATION_DISPATCH_SUMMARY_SCHEMA,
|
||||
normalizeClusterRunManagementCommand,
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
} from './runManagementTransport';
|
||||
|
||||
const MANAGEMENT_PATH = '/api/v3/runs/management';
|
||||
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
export type ClusterRunManagementClientPaths =
|
||||
ClusterPluginPackageManagementClientPaths;
|
||||
@@ -231,6 +234,86 @@ export function validateClusterRunManagementClientResult(
|
||||
envelope as unknown as ClusterRunManagementTransportResult,
|
||||
);
|
||||
}
|
||||
if (command.operation === 'run.cancellation.blocked.list') {
|
||||
const envelope = exact(value, ['schemaVersion', 'operation', 'page']);
|
||||
if (
|
||||
envelope.schemaVersion !== 1 ||
|
||||
envelope.operation !== command.operation
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const page = exact(envelope.page, [
|
||||
'schema',
|
||||
'projectId',
|
||||
'snapshotAtMs',
|
||||
'observedAtMs',
|
||||
'items',
|
||||
'truncated',
|
||||
...(Object.hasOwn(envelope.page as object, 'nextCursor')
|
||||
? ['nextCursor']
|
||||
: []),
|
||||
]);
|
||||
if (
|
||||
page.schema !== RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_SCHEMA ||
|
||||
page.projectId !== command.request.projectId ||
|
||||
!safeInteger(page.snapshotAtMs) ||
|
||||
!safeInteger(page.observedAtMs) ||
|
||||
(page.snapshotAtMs as number) > (page.observedAtMs as number) ||
|
||||
(command.request.body.after === null &&
|
||||
page.snapshotAtMs !== page.observedAtMs) ||
|
||||
(command.request.body.after !== null &&
|
||||
page.snapshotAtMs !== command.request.body.after.snapshotAtMs) ||
|
||||
!Array.isArray(page.items) ||
|
||||
page.items.length > CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT ||
|
||||
typeof page.truncated !== 'boolean' ||
|
||||
page.truncated !== Object.hasOwn(page, 'nextCursor') ||
|
||||
(page.truncated &&
|
||||
page.items.length !== CANCELLATION_DISPATCH_BLOCKED_PAGE_LIMIT)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const items = page.items as unknown[];
|
||||
let previous = command.request.body.after;
|
||||
for (const value of items) {
|
||||
const item = exact(value, ['runId', 'blockedAtMs']);
|
||||
if (
|
||||
typeof item.runId !== 'string' ||
|
||||
!IDENTIFIER_PATTERN.test(item.runId) ||
|
||||
!safeInteger(item.blockedAtMs) ||
|
||||
(item.blockedAtMs as number) > (page.snapshotAtMs as number) ||
|
||||
(previous !== null &&
|
||||
((item.blockedAtMs as number) < previous.blockedAtMs ||
|
||||
((item.blockedAtMs as number) === previous.blockedAtMs &&
|
||||
item.runId <= previous.runId)))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
previous = {
|
||||
snapshotAtMs: page.snapshotAtMs as number,
|
||||
blockedAtMs: item.blockedAtMs as number,
|
||||
runId: item.runId,
|
||||
};
|
||||
}
|
||||
if (page.truncated) {
|
||||
const cursor = exact(page.nextCursor, [
|
||||
'snapshotAtMs',
|
||||
'blockedAtMs',
|
||||
'runId',
|
||||
]);
|
||||
const last = previous;
|
||||
if (
|
||||
last === null ||
|
||||
cursor.snapshotAtMs !== page.snapshotAtMs ||
|
||||
cursor.blockedAtMs !== last.blockedAtMs ||
|
||||
cursor.runId !== last.runId
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
return Object.freeze(
|
||||
envelope as unknown as ClusterRunManagementTransportResult,
|
||||
);
|
||||
}
|
||||
if (command.operation === 'run.cancellation.inspect') {
|
||||
const envelope = exact(value, [
|
||||
'schemaVersion',
|
||||
|
||||
@@ -5,6 +5,12 @@ import {
|
||||
executeClusterRunManagementClient,
|
||||
executeClusterRunManagementCommand,
|
||||
} from './runManagementClient';
|
||||
import {
|
||||
createRunCancellationBlockedListCommand,
|
||||
decodeRunCancellationBlockedCursor,
|
||||
formatRunCancellationBlockedListCard,
|
||||
projectRunCancellationBlockedList,
|
||||
} from './runCancellationBlockedList';
|
||||
import {
|
||||
createRunCancellationStatusCommand,
|
||||
formatRunCancellationStatusCard,
|
||||
@@ -14,6 +20,7 @@ import {
|
||||
const USAGE = [
|
||||
'Usage: ql3-run-client --config=/absolute/client.json --command=/absolute/command.json --assertion=/absolute/assertion.jwt',
|
||||
' ql3-run-client status --config=/absolute/client.json --assertion=/absolute/assertion.jwt --project=PROJECT [--format=text|json]',
|
||||
' ql3-run-client blocked --config=/absolute/client.json --assertion=/absolute/assertion.jwt --project=PROJECT [--cursor=CURSOR] [--format=text|json]',
|
||||
'',
|
||||
'Status exit codes: 0=clear, 10=converging, 20=attention_required.',
|
||||
].join('\n');
|
||||
@@ -32,18 +39,31 @@ type RunManagementClientArguments =
|
||||
assertionFile: string;
|
||||
projectId: string;
|
||||
format: 'text' | 'json';
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: 'blocked';
|
||||
configFile: string;
|
||||
assertionFile: string;
|
||||
projectId: string;
|
||||
cursor?: string;
|
||||
format: 'text' | 'json';
|
||||
}>;
|
||||
|
||||
function argumentsFrom(
|
||||
argv: readonly string[],
|
||||
): Readonly<RunManagementClientArguments> | null {
|
||||
const statusCount = argv.filter((argument) => argument === 'status').length;
|
||||
if (statusCount > 0) {
|
||||
if (statusCount !== 1 || argv.length < 4 || argv.length > 5) return null;
|
||||
const modes = argv.filter(
|
||||
(argument) => argument === 'status' || argument === 'blocked',
|
||||
);
|
||||
if (modes.length > 0) {
|
||||
if (modes.length !== 1 || argv.length < 4 || argv.length > 6) return null;
|
||||
const kind = modes[0] as 'status' | 'blocked';
|
||||
const values = new Map<string, string>();
|
||||
for (const argument of argv) {
|
||||
if (argument === 'status') continue;
|
||||
const match = /^--(config|assertion|project|format)=(.+)$/.exec(argument);
|
||||
if (argument === kind) continue;
|
||||
const match = /^--(config|assertion|project|cursor|format)=(.+)$/.exec(
|
||||
argument,
|
||||
);
|
||||
if (!match || values.has(match[1]!)) return null;
|
||||
values.set(match[1]!, match[2]!);
|
||||
}
|
||||
@@ -54,17 +74,28 @@ function argumentsFrom(
|
||||
!values.get('assertion')!.startsWith('/') ||
|
||||
!values.has('project') ||
|
||||
!PROJECT_ID.test(values.get('project')!) ||
|
||||
(kind === 'status' && values.has('cursor')) ||
|
||||
(values.has('format') &&
|
||||
values.get('format') !== 'text' &&
|
||||
values.get('format') !== 'json')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (kind === 'blocked' && values.has('cursor')) {
|
||||
try {
|
||||
decodeRunCancellationBlockedCursor(values.get('cursor')!);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return Object.freeze({
|
||||
kind: 'status',
|
||||
kind,
|
||||
configFile: values.get('config')!,
|
||||
assertionFile: values.get('assertion')!,
|
||||
projectId: values.get('project')!,
|
||||
...(kind === 'blocked' && values.has('cursor')
|
||||
? { cursor: values.get('cursor')! }
|
||||
: {}),
|
||||
format: (values.get('format') ?? 'text') as 'text' | 'json',
|
||||
});
|
||||
}
|
||||
@@ -131,6 +162,23 @@ async function run(argv: readonly string[]): Promise<void> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (paths.kind === 'blocked') {
|
||||
const result = await executeClusterRunManagementCommand({
|
||||
configFile: paths.configFile,
|
||||
assertionFile: paths.assertionFile,
|
||||
command: createRunCancellationBlockedListCommand(
|
||||
paths.projectId,
|
||||
paths.cursor,
|
||||
),
|
||||
});
|
||||
const page = projectRunCancellationBlockedList(result);
|
||||
process.stdout.write(
|
||||
paths.format === 'json'
|
||||
? `${JSON.stringify(page)}\n`
|
||||
: `${formatRunCancellationBlockedListCard(page)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (paths.kind === 'status') {
|
||||
const result = await executeClusterRunManagementCommand({
|
||||
configFile: paths.configFile,
|
||||
|
||||
@@ -26,6 +26,10 @@ export const RUN_CANCELLATION_DISPATCH_SUMMARY_REQUEST_SCHEMA =
|
||||
'qinglong/run-cancellation-dispatch-summary-request@v1';
|
||||
export const RUN_CANCELLATION_DISPATCH_SUMMARY_SCHEMA =
|
||||
'qinglong/run-cancellation-dispatch-summary@v1';
|
||||
export const RUN_CANCELLATION_DISPATCH_BLOCKED_LIST_REQUEST_SCHEMA =
|
||||
'qinglong/run-cancellation-dispatch-blocked-list-request@v1';
|
||||
export const RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_SCHEMA =
|
||||
'qinglong/run-cancellation-dispatch-blocked-page@v1';
|
||||
export const RUN_CANCELLATION_DISPATCH_DIAGNOSTIC_SCHEMA =
|
||||
'qinglong/run-cancellation-dispatch-diagnostic@v1';
|
||||
export const RUN_CANCELLATION_DISPATCH_REARM_REQUEST_SCHEMA =
|
||||
@@ -96,6 +100,25 @@ export type ClusterRunManagementCancellationSummaryCommand = Readonly<{
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export type ClusterRunManagementCancellationBlockedListCommand = Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'run.cancellation.blocked.list';
|
||||
request: Readonly<{
|
||||
projectId: string;
|
||||
requestId: string;
|
||||
auditEventId: string;
|
||||
failureAuditEventId: string;
|
||||
body: Readonly<{
|
||||
schema: typeof RUN_CANCELLATION_DISPATCH_BLOCKED_LIST_REQUEST_SCHEMA;
|
||||
after: Readonly<{
|
||||
snapshotAtMs: number;
|
||||
blockedAtMs: number;
|
||||
runId: string;
|
||||
}> | null;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export type ClusterRunManagementCancellationRearmCommand = Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'run.cancellation.rearm';
|
||||
@@ -123,6 +146,7 @@ export type ClusterRunManagementCommand =
|
||||
| ClusterRunManagementRetryCommand
|
||||
| ClusterRunManagementStopCommand
|
||||
| ClusterRunManagementCancellationSummaryCommand
|
||||
| ClusterRunManagementCancellationBlockedListCommand
|
||||
| ClusterRunManagementCancellationInspectCommand
|
||||
| ClusterRunManagementCancellationRearmCommand;
|
||||
|
||||
@@ -158,6 +182,19 @@ export type ClusterRunManagementCancellationSummaryTransportResult = Readonly<{
|
||||
>;
|
||||
}>;
|
||||
|
||||
export type ClusterRunManagementCancellationBlockedListTransportResult =
|
||||
Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'run.cancellation.blocked.list';
|
||||
page: Readonly<
|
||||
Awaited<
|
||||
ReturnType<ClusterRunManagementService['listBlockedCancellations']>
|
||||
> & {
|
||||
schema: typeof RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_SCHEMA;
|
||||
}
|
||||
>;
|
||||
}>;
|
||||
|
||||
export type ClusterRunManagementCancellationRearmTransportResult = Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'run.cancellation.rearm';
|
||||
@@ -172,6 +209,7 @@ export type ClusterRunManagementTransportResult =
|
||||
| ClusterRunManagementRetryTransportResult
|
||||
| ClusterRunManagementStopTransportResult
|
||||
| ClusterRunManagementCancellationSummaryTransportResult
|
||||
| ClusterRunManagementCancellationBlockedListTransportResult
|
||||
| ClusterRunManagementCancellationInspectTransportResult
|
||||
| ClusterRunManagementCancellationRearmTransportResult;
|
||||
|
||||
@@ -258,6 +296,7 @@ export function normalizeClusterRunManagementCommand(
|
||||
operation !== 'run.retry' &&
|
||||
operation !== 'run.stop' &&
|
||||
operation !== 'run.cancellation.summary' &&
|
||||
operation !== 'run.cancellation.blocked.list' &&
|
||||
operation !== 'run.cancellation.inspect' &&
|
||||
operation !== 'run.cancellation.rearm'
|
||||
) {
|
||||
@@ -274,7 +313,8 @@ export function normalizeClusterRunManagementCommand(
|
||||
'failureAuditEventId',
|
||||
'body',
|
||||
]
|
||||
: operation === 'run.cancellation.summary'
|
||||
: operation === 'run.cancellation.summary' ||
|
||||
operation === 'run.cancellation.blocked.list'
|
||||
? [
|
||||
'projectId',
|
||||
'requestId',
|
||||
@@ -333,6 +373,51 @@ export function normalizeClusterRunManagementCommand(
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (operation === 'run.cancellation.blocked.list') {
|
||||
const body = exact(request.body, ['schema', 'after']);
|
||||
if (body.schema !== RUN_CANCELLATION_DISPATCH_BLOCKED_LIST_REQUEST_SCHEMA) {
|
||||
invalid();
|
||||
}
|
||||
let after: ClusterRunManagementCancellationBlockedListCommand['request']['body']['after'] =
|
||||
null;
|
||||
if (body.after !== null) {
|
||||
const cursor = exact(body.after, [
|
||||
'snapshotAtMs',
|
||||
'blockedAtMs',
|
||||
'runId',
|
||||
]);
|
||||
if (
|
||||
typeof cursor.snapshotAtMs !== 'number' ||
|
||||
!Number.isSafeInteger(cursor.snapshotAtMs) ||
|
||||
cursor.snapshotAtMs < 0 ||
|
||||
typeof cursor.blockedAtMs !== 'number' ||
|
||||
!Number.isSafeInteger(cursor.blockedAtMs) ||
|
||||
cursor.blockedAtMs < 0 ||
|
||||
cursor.blockedAtMs > cursor.snapshotAtMs
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
after = Object.freeze({
|
||||
snapshotAtMs: cursor.snapshotAtMs,
|
||||
blockedAtMs: cursor.blockedAtMs,
|
||||
runId: identifier(cursor.runId),
|
||||
});
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation,
|
||||
request: Object.freeze({
|
||||
projectId: identifier(request.projectId),
|
||||
requestId: identifier(request.requestId),
|
||||
auditEventId,
|
||||
failureAuditEventId,
|
||||
body: Object.freeze({
|
||||
schema: RUN_CANCELLATION_DISPATCH_BLOCKED_LIST_REQUEST_SCHEMA,
|
||||
after,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (operation === 'run.cancellation.inspect') {
|
||||
const body = exact(request.body, ['schema']);
|
||||
if (body.schema !== RUN_CANCELLATION_DISPATCH_INSPECT_REQUEST_SCHEMA) {
|
||||
@@ -433,6 +518,7 @@ export function createClusterRunManagementTransport(
|
||||
typeof options.service.retry !== 'function' ||
|
||||
typeof options.service.stop !== 'function' ||
|
||||
typeof options.service.summarizeCancellation !== 'function' ||
|
||||
typeof options.service.listBlockedCancellations !== 'function' ||
|
||||
typeof options.service.inspectCancellation !== 'function' ||
|
||||
typeof options.service.rearmCancellation !== 'function' ||
|
||||
(options.now !== undefined && typeof options.now !== 'function')
|
||||
@@ -511,6 +597,26 @@ export function createClusterRunManagementTransport(
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (command.operation === 'run.cancellation.blocked.list') {
|
||||
const result = await options.service.listBlockedCancellations({
|
||||
projectId: command.request.projectId,
|
||||
requestId: command.request.requestId,
|
||||
auditEventId: command.request.auditEventId,
|
||||
failureAuditEventId: command.request.failureAuditEventId,
|
||||
principal,
|
||||
...(command.request.body.after === null
|
||||
? {}
|
||||
: { after: command.request.body.after }),
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
page: Object.freeze({
|
||||
schema: RUN_CANCELLATION_DISPATCH_BLOCKED_PAGE_SCHEMA,
|
||||
...result,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (command.operation === 'run.cancellation.inspect') {
|
||||
const result = await options.service.inspectCancellation({
|
||||
projectId: command.request.projectId,
|
||||
|
||||
Reference in New Issue
Block a user