feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,535 @@
// Plugin Package Workflow owns inspection and durable authorized admission.
import type {
SecurityPolicyFence,
SecurityPrincipal,
} from '@qinglong/runtime-core/security';
import { normalizeSecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
import type {
PluginPackageAutomationPublication,
PluginPackageAutomationPublicationRepository,
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import type {
PluginPackageMaterializedRevision,
PluginPackageMaterializedRevisionRepository,
PluginPackageWorkflowResource,
} from '@qinglong/runtime-core/plugin-package-resource-materialization';
import type {
PluginPackageWorkflowAdministrationRepository,
PluginPackageWorkflowRunEventListRepository,
PluginPackageWorkflowRunEventListResult,
PluginPackageWorkflowRunInspectionRepository,
PluginPackageWorkflowRunInspectionResult,
PluginPackageWorkflowRunListRepository,
PluginPackageWorkflowRunListResult,
PluginPackageWorkflowStepRunListRepository,
PluginPackageWorkflowStepRunListResult,
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
import type {
ClusterRunCancellationRepository,
ClusterRunCancellationResult,
} from '@qinglong/runtime-core/cluster-run-cancellation';
import {
createPluginPackageWorkflowExecutionPlan,
type PluginPackageWorkflowAdmissionReceipt,
type PluginPackageWorkflowExecutionPlan,
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import {
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
export interface ClusterPluginPackageWorkflowSummary {
readonly id: string;
readonly name: string;
readonly enabled: boolean;
readonly steps: readonly Readonly<{
id: string;
task: string;
needs: readonly string[];
}>[];
}
export interface StartClusterPluginPackageWorkflowCommand {
readonly projectId: string;
readonly packageName: string;
readonly workflowId: string;
readonly planId: string;
readonly runId: string;
readonly stepRunIds: Readonly<Record<string, string>>;
readonly principal: Readonly<SecurityPrincipal>;
readonly policyFence: Readonly<SecurityPolicyFence>;
readonly plannedAtMs: number;
}
export interface CancelClusterPluginPackageWorkflowCommand {
readonly projectId: string;
readonly packageName: string;
readonly workflowId: string;
readonly runId: string;
readonly mutationId: string;
readonly eventId: string;
readonly principal: Readonly<SecurityPrincipal>;
readonly policyFence: Readonly<SecurityPolicyFence>;
}
export interface InspectClusterPluginPackageWorkflowRunCommand {
readonly projectId: string;
readonly packageName: string;
readonly workflowId: string;
readonly runId: string;
readonly requestId: string;
readonly auditEventId: string;
readonly principal: Readonly<SecurityPrincipal>;
readonly policyFence: Readonly<SecurityPolicyFence>;
readonly observedAtMs: number;
}
export interface ListClusterPluginPackageWorkflowRunsCommand {
readonly projectId: string;
readonly packageName: string;
readonly workflowId: string;
readonly limit: number;
readonly after: Readonly<{ admittedAtMs: number; runId: string }> | null;
readonly requestId: string;
readonly auditEventId: string;
readonly principal: Readonly<SecurityPrincipal>;
readonly policyFence: Readonly<SecurityPolicyFence>;
readonly observedAtMs: number;
}
export interface ListClusterPluginPackageWorkflowStepRunsCommand {
readonly projectId: string;
readonly packageName: string;
readonly workflowId: string;
readonly runId: string;
readonly limit: number;
readonly after: Readonly<{ stepKey: string; id: string }> | null;
readonly requestId: string;
readonly auditEventId: string;
readonly principal: Readonly<SecurityPrincipal>;
readonly policyFence: Readonly<SecurityPolicyFence>;
readonly observedAtMs: number;
}
export interface ListClusterPluginPackageWorkflowRunEventsCommand {
readonly projectId: string;
readonly packageName: string;
readonly workflowId: string;
readonly runId: string;
readonly limit: number;
readonly afterSequence: number;
readonly requestId: string;
readonly auditEventId: string;
readonly principal: Readonly<SecurityPrincipal>;
readonly policyFence: Readonly<SecurityPolicyFence>;
readonly observedAtMs: number;
}
export interface ClusterPluginPackageWorkflowAdministrationCapability {
inspect(
projectId: string,
packageName: string,
): Promise<
Readonly<{
found: boolean;
publicationState: PluginPackageAutomationPublication['state'] | null;
workflows: readonly Readonly<ClusterPluginPackageWorkflowSummary>[];
}>
>;
start(command: Readonly<StartClusterPluginPackageWorkflowCommand>): Promise<
Readonly<{
status: 'created' | 'existing';
plan: Readonly<PluginPackageWorkflowExecutionPlan>;
receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
}>
>;
cancel(
command: Readonly<CancelClusterPluginPackageWorkflowCommand>,
): Promise<Readonly<ClusterRunCancellationResult>>;
inspectRun(
command: Readonly<InspectClusterPluginPackageWorkflowRunCommand>,
): Promise<Readonly<PluginPackageWorkflowRunInspectionResult>>;
listRuns(
command: Readonly<ListClusterPluginPackageWorkflowRunsCommand>,
): Promise<Readonly<PluginPackageWorkflowRunListResult>>;
listStepRuns(
command: Readonly<ListClusterPluginPackageWorkflowStepRunsCommand>,
): Promise<Readonly<PluginPackageWorkflowStepRunListResult>>;
listRunEvents(
command: Readonly<ListClusterPluginPackageWorkflowRunEventsCommand>,
): Promise<Readonly<PluginPackageWorkflowRunEventListResult>>;
}
export class ClusterPluginPackageWorkflowNotFoundError extends Error {
readonly code = 'CLUSTER_PLUGIN_PACKAGE_WORKFLOW_NOT_FOUND';
constructor() {
super('Active Plugin Package Workflow is not available');
this.name = 'ClusterPluginPackageWorkflowNotFoundError';
}
}
export class ClusterPluginPackageWorkflowConflictError extends Error {
readonly code = 'CLUSTER_PLUGIN_PACKAGE_WORKFLOW_CONFLICT';
constructor() {
super('Plugin Package Workflow request conflicts with durable identity');
this.name = 'ClusterPluginPackageWorkflowConflictError';
}
}
export class ClusterPluginPackageWorkflowUnavailableError extends Error {
readonly code = 'CLUSTER_PLUGIN_PACKAGE_WORKFLOW_UNAVAILABLE';
constructor() {
super('Plugin Package Workflow administration is unavailable');
this.name = 'ClusterPluginPackageWorkflowUnavailableError';
}
}
function summary(
workflow: Readonly<PluginPackageWorkflowResource>,
): Readonly<ClusterPluginPackageWorkflowSummary> {
return Object.freeze({
id: workflow.id,
name: workflow.name,
enabled: workflow.enabled,
steps: Object.freeze(
workflow.steps.map((step) =>
Object.freeze({
id: step.id,
task: step.task,
needs: Object.freeze([...step.needs]),
}),
),
),
});
}
function sameReplay(
plan: Readonly<PluginPackageWorkflowExecutionPlan>,
command: Readonly<StartClusterPluginPackageWorkflowCommand>,
): boolean {
const requested = Object.entries(command.stepRunIds).sort(([a], [b]) =>
a.localeCompare(b),
);
const stored = plan.steps
.map((step) => [step.stepKey, step.stepRunId] as const)
.sort(([a], [b]) => a.localeCompare(b));
return (
plan.planId === command.planId &&
plan.runId === command.runId &&
plan.target.projectId === command.projectId &&
plan.target.packageName === command.packageName &&
plan.target.workflowId === command.workflowId &&
stored.length === requested.length &&
stored.every(
([key, id], index) =>
key === requested[index]?.[0] && id === requested[index]?.[1],
)
);
}
export function createClusterPluginPackageWorkflowAdministrationCapability(
publications: Pick<
PluginPackageAutomationPublicationRepository,
'findCurrent'
>,
revisions: Pick<PluginPackageMaterializedRevisionRepository, 'find'>,
admissions: PluginPackageWorkflowAdministrationRepository,
runInspections: PluginPackageWorkflowRunInspectionRepository,
runLists: PluginPackageWorkflowRunListRepository,
stepRunLists: PluginPackageWorkflowStepRunListRepository,
runEventLists: PluginPackageWorkflowRunEventListRepository,
cancellations: ClusterRunCancellationRepository,
taskSpecSemanticRegistry: TaskSpecSemanticRegistry = createBuiltInTaskSpecSemanticRegistry(),
): ClusterPluginPackageWorkflowAdministrationCapability {
if (
!publications ||
typeof publications.findCurrent !== 'function' ||
!revisions ||
typeof revisions.find !== 'function' ||
!admissions ||
typeof admissions.findPlanByPlanId !== 'function' ||
typeof admissions.admitAuthorized !== 'function' ||
!runInspections ||
typeof runInspections.inspectRunAuthorized !== 'function' ||
!runLists ||
typeof runLists.listRunsAuthorized !== 'function' ||
!stepRunLists ||
typeof stepRunLists.listStepRunsAuthorized !== 'function' ||
!runEventLists ||
typeof runEventLists.listRunEventsAuthorized !== 'function' ||
!cancellations ||
typeof cancellations.requestUserCancellation !== 'function' ||
!(taskSpecSemanticRegistry instanceof TaskSpecSemanticRegistry)
) {
throw new TypeError(
'Cluster Plugin Package Workflow administration dependencies are invalid',
);
}
async function currentTarget(
projectId: string,
packageName: string,
): Promise<Readonly<{
publication: Readonly<PluginPackageAutomationPublication>;
revision: Readonly<PluginPackageMaterializedRevision>;
}> | null> {
try {
const publication = await publications.findCurrent(
projectId,
packageName,
);
if (!publication) return null;
const revision = await revisions.find(
publication.target.generationDigest,
);
if (
!revision ||
revision.revisionDigest !==
publication.target.materializedRevisionDigest
) {
throw new ClusterPluginPackageWorkflowUnavailableError();
}
return Object.freeze({ publication, revision });
} catch (error) {
if (error instanceof ClusterPluginPackageWorkflowUnavailableError) {
throw error;
}
throw new ClusterPluginPackageWorkflowUnavailableError();
}
}
return Object.freeze({
async inspect(projectId: string, packageName: string) {
const target = await currentTarget(projectId, packageName);
return target
? Object.freeze({
found: true,
publicationState: target.publication.state,
workflows: Object.freeze(
target.publication.definitions.workflows.map(summary),
),
})
: Object.freeze({
found: false,
publicationState: null,
workflows: Object.freeze([]),
});
},
async start(command: Readonly<StartClusterPluginPackageWorkflowCommand>) {
let plan = await admissions.findPlanByPlanId(command.planId);
if (plan) {
if (!sameReplay(plan, command)) {
throw new ClusterPluginPackageWorkflowConflictError();
}
} else {
const target = await currentTarget(
command.projectId,
command.packageName,
);
const workflow = target?.publication.definitions.workflows.find(
({ id }) => id === command.workflowId,
);
if (
!target ||
target.publication.state !== 'active' ||
!workflow?.enabled
) {
throw new ClusterPluginPackageWorkflowNotFoundError();
}
try {
plan = createPluginPackageWorkflowExecutionPlan({
planId: command.planId,
runId: command.runId,
workflowId: command.workflowId,
stepRunIds: command.stepRunIds,
publication: target.publication,
revision: target.revision,
taskSpecSemanticRegistry,
plannedAtMs: command.plannedAtMs,
});
} catch {
throw new ClusterPluginPackageWorkflowConflictError();
}
}
if (command.policyFence.bindingVersion === null) {
throw new ClusterPluginPackageWorkflowUnavailableError();
}
const admitted = await admissions.admitAuthorized({
plan,
actor: command.principal.subject,
fence: {
projectVersion: command.policyFence.projectVersion,
bindingVersion: command.policyFence.bindingVersion,
},
audit: normalizeSecurityAuditRecord({
eventId: command.planId,
requestId: command.planId,
operationId: 'workflow.start',
projectId: command.projectId,
subject: command.principal.subject,
authenticationId: command.principal.authenticationId,
outcome: 'allowed',
reasons: ['project_policy_allowed'],
fence: command.policyFence,
occurredAtMs: plan.plannedAtMs,
}),
});
return Object.freeze({
status: admitted.status,
plan,
receipt: admitted.receipt,
});
},
async cancel(command: Readonly<CancelClusterPluginPackageWorkflowCommand>) {
if (command.policyFence.bindingVersion === null) {
throw new ClusterPluginPackageWorkflowUnavailableError();
}
return cancellations.requestUserCancellation({
projectId: command.projectId,
runId: command.runId,
mutationId: command.mutationId,
eventId: command.eventId,
subject: command.principal.subject,
policyFence: command.policyFence,
workflowTarget: {
packageName: command.packageName,
workflowId: command.workflowId,
},
});
},
async inspectRun(
command: Readonly<InspectClusterPluginPackageWorkflowRunCommand>,
) {
if (command.policyFence.bindingVersion === null) {
throw new ClusterPluginPackageWorkflowUnavailableError();
}
return runInspections.inspectRunAuthorized({
projectId: command.projectId,
packageName: command.packageName,
workflowId: command.workflowId,
runId: command.runId,
actor: command.principal.subject,
fence: {
projectVersion: command.policyFence.projectVersion,
bindingVersion: command.policyFence.bindingVersion,
},
audit: normalizeSecurityAuditRecord({
eventId: command.auditEventId,
requestId: command.requestId,
operationId: 'workflow.run.read',
projectId: command.projectId,
subject: command.principal.subject,
authenticationId: command.principal.authenticationId,
outcome: 'allowed',
reasons: ['project_policy_allowed'],
fence: command.policyFence,
occurredAtMs: command.observedAtMs,
}),
});
},
async listRuns(
command: Readonly<ListClusterPluginPackageWorkflowRunsCommand>,
) {
if (command.policyFence.bindingVersion === null) {
throw new ClusterPluginPackageWorkflowUnavailableError();
}
return runLists.listRunsAuthorized({
projectId: command.projectId,
packageName: command.packageName,
workflowId: command.workflowId,
limit: command.limit,
after: command.after,
actor: command.principal.subject,
fence: {
projectVersion: command.policyFence.projectVersion,
bindingVersion: command.policyFence.bindingVersion,
},
audit: normalizeSecurityAuditRecord({
eventId: command.auditEventId,
requestId: command.requestId,
operationId: 'workflow.run.list',
projectId: command.projectId,
subject: command.principal.subject,
authenticationId: command.principal.authenticationId,
outcome: 'allowed',
reasons: ['project_policy_allowed'],
fence: command.policyFence,
occurredAtMs: command.observedAtMs,
}),
});
},
async listStepRuns(
command: Readonly<ListClusterPluginPackageWorkflowStepRunsCommand>,
) {
if (command.policyFence.bindingVersion === null) {
throw new ClusterPluginPackageWorkflowUnavailableError();
}
return stepRunLists.listStepRunsAuthorized({
projectId: command.projectId,
packageName: command.packageName,
workflowId: command.workflowId,
runId: command.runId,
limit: command.limit,
after: command.after,
actor: command.principal.subject,
fence: {
projectVersion: command.policyFence.projectVersion,
bindingVersion: command.policyFence.bindingVersion,
},
audit: normalizeSecurityAuditRecord({
eventId: command.auditEventId,
requestId: command.requestId,
operationId: 'workflow.step.list',
projectId: command.projectId,
subject: command.principal.subject,
authenticationId: command.principal.authenticationId,
outcome: 'allowed',
reasons: ['project_policy_allowed'],
fence: command.policyFence,
occurredAtMs: command.observedAtMs,
}),
});
},
async listRunEvents(
command: Readonly<ListClusterPluginPackageWorkflowRunEventsCommand>,
) {
if (command.policyFence.bindingVersion === null) {
throw new ClusterPluginPackageWorkflowUnavailableError();
}
return runEventLists.listRunEventsAuthorized({
projectId: command.projectId,
packageName: command.packageName,
workflowId: command.workflowId,
runId: command.runId,
limit: command.limit,
afterSequence: command.afterSequence,
actor: command.principal.subject,
fence: {
projectVersion: command.policyFence.projectVersion,
bindingVersion: command.policyFence.bindingVersion,
},
audit: normalizeSecurityAuditRecord({
eventId: command.auditEventId,
requestId: command.requestId,
operationId: 'workflow.event.list',
projectId: command.projectId,
subject: command.principal.subject,
authenticationId: command.principal.authenticationId,
outcome: 'allowed',
reasons: ['project_policy_allowed'],
fence: command.policyFence,
occurredAtMs: command.observedAtMs,
}),
});
},
});
}
@@ -0,0 +1,707 @@
// Plugin Package Workflow owns its bounded inspect/start/cancel transport adapter.
import { randomUUID } from 'node:crypto';
import {
CLUSTER_RUN_CANCELLATION_SCHEMA,
createClusterRunCancellationResponseBody,
parseClusterRunCancellationRequestBody,
} from '@qinglong/runtime-core/cluster-run-cancellation';
import {
DEFAULT_PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_PAGE_SIZE,
DEFAULT_PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_PAGE_SIZE,
DEFAULT_PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_PAGE_SIZE,
MAX_PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_PAGE_SIZE,
MAX_PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_PAGE_SIZE,
MAX_PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_PAGE_SIZE,
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
import type { ClusterControlAdmissionResponse } from '../../transport/httpSurface';
import type { ClusterPluginPackageWorkflowAdministrationCapability } from './pluginPackageWorkflowAdministration';
import type {
ClusterControlAuthorizedOperationRequest,
ClusterControlRouteDefinition,
ClusterControlRouteParameters,
} from '../../transport/routeRegistry';
export const CLUSTER_PLUGIN_PACKAGE_WORKFLOW_LIST_RESPONSE_SCHEMA =
'qinglong/cluster-plugin-package-workflow-list@v1' as const;
export const CLUSTER_PLUGIN_PACKAGE_WORKFLOW_START_REQUEST_SCHEMA =
'qinglong/cluster-plugin-package-workflow-start-request@v1' as const;
export const CLUSTER_PLUGIN_PACKAGE_WORKFLOW_START_RESPONSE_SCHEMA =
'qinglong/cluster-plugin-package-workflow-start-response@v1' as const;
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}$/;
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const RESOURCE_ID = /^[a-z][a-z0-9-]{0,62}$/;
function parseRunListQuery(
query: Readonly<Record<string, readonly string[]>>,
): Readonly<{
limit: number;
after: Readonly<{ admittedAtMs: number; runId: string }> | null;
}> {
const limitValues = query.limit;
const admittedAtValues = query.after_admitted_at_ms;
const runIdValues = query.after_run_id;
if (
(limitValues !== undefined && limitValues.length !== 1) ||
(admittedAtValues !== undefined && admittedAtValues.length !== 1) ||
(runIdValues !== undefined && runIdValues.length !== 1) ||
(admittedAtValues === undefined) !== (runIdValues === undefined)
) {
throw new TypeError();
}
const limit =
limitValues === undefined
? DEFAULT_PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_PAGE_SIZE
: Number(limitValues[0]);
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > MAX_PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_PAGE_SIZE ||
(limitValues !== undefined && String(limit) !== limitValues[0])
) {
throw new TypeError();
}
if (admittedAtValues === undefined || runIdValues === undefined) {
return Object.freeze({ limit, after: null });
}
const admittedAtMs = Number(admittedAtValues[0]);
const runId = runIdValues[0]!;
if (
!Number.isSafeInteger(admittedAtMs) ||
admittedAtMs < 0 ||
String(admittedAtMs) !== admittedAtValues[0] ||
!UUID_V4.test(runId)
) {
throw new TypeError();
}
return Object.freeze({
limit,
after: Object.freeze({ admittedAtMs, runId }),
});
}
function parseStepRunListQuery(
query: Readonly<Record<string, readonly string[]>>,
): Readonly<{
limit: number;
after: Readonly<{ stepKey: string; id: string }> | null;
}> {
const limitValues = query.limit;
const stepKeyValues = query.after_step_key;
const stepRunIdValues = query.after_step_run_id;
if (
(limitValues !== undefined && limitValues.length !== 1) ||
(stepKeyValues !== undefined && stepKeyValues.length !== 1) ||
(stepRunIdValues !== undefined && stepRunIdValues.length !== 1) ||
(stepKeyValues === undefined) !== (stepRunIdValues === undefined)
) {
throw new TypeError();
}
const limit =
limitValues === undefined
? DEFAULT_PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_PAGE_SIZE
: Number(limitValues[0]);
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > MAX_PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_PAGE_SIZE ||
(limitValues !== undefined && String(limit) !== limitValues[0])
) {
throw new TypeError();
}
if (stepKeyValues === undefined || stepRunIdValues === undefined) {
return Object.freeze({ limit, after: null });
}
const stepKey = stepKeyValues[0]!;
const id = stepRunIdValues[0]!;
if (!RESOURCE_ID.test(stepKey) || !UUID_V4.test(id)) {
throw new TypeError();
}
return Object.freeze({
limit,
after: Object.freeze({ stepKey, id }),
});
}
function parseRunEventListQuery(
query: Readonly<Record<string, readonly string[]>>,
): Readonly<{ limit: number; afterSequence: number }> {
const limitValues = query.limit;
const afterValues = query.after_sequence;
if (
(limitValues !== undefined && limitValues.length !== 1) ||
(afterValues !== undefined && afterValues.length !== 1)
) {
throw new TypeError();
}
const limit =
limitValues === undefined
? DEFAULT_PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_PAGE_SIZE
: Number(limitValues[0]);
const afterSequence = afterValues === undefined ? 0 : Number(afterValues[0]);
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > MAX_PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_PAGE_SIZE ||
(limitValues !== undefined && String(limit) !== limitValues[0]) ||
!Number.isSafeInteger(afterSequence) ||
afterSequence < 0 ||
(afterValues !== undefined && String(afterSequence) !== afterValues[0])
) {
throw new TypeError();
}
return Object.freeze({ limit, afterSequence });
}
function response(
statusCode: number,
body: Readonly<Record<string, unknown>>,
): ClusterControlAdmissionResponse {
return Object.freeze({ statusCode, body: Object.freeze(body) });
}
function parseBody(value: unknown): Readonly<{
planId: string;
runId: string;
stepRunIds: Readonly<Record<string, string>>;
}> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError();
}
const body = value as Record<string, unknown>;
if (
Object.keys(body).sort().join(',') !== 'planId,runId,schema,stepRunIds' ||
body.schema !== CLUSTER_PLUGIN_PACKAGE_WORKFLOW_START_REQUEST_SCHEMA ||
typeof body.planId !== 'string' ||
!UUID_V4.test(body.planId) ||
typeof body.runId !== 'string' ||
!UUID_V4.test(body.runId) ||
!body.stepRunIds ||
typeof body.stepRunIds !== 'object' ||
Array.isArray(body.stepRunIds) ||
Object.getPrototypeOf(body.stepRunIds) !== Object.prototype
) {
throw new TypeError();
}
const entries = Object.entries(body.stepRunIds as Record<string, unknown>);
if (
entries.length < 1 ||
entries.length > 128 ||
entries.some(
([key, id]) =>
!RESOURCE_ID.test(key) || typeof id !== 'string' || !UUID_V4.test(id),
) ||
new Set(entries.map(([, id]) => id)).size !== entries.length
) {
throw new TypeError();
}
return Object.freeze({
planId: body.planId,
runId: body.runId,
stepRunIds: Object.freeze(
Object.fromEntries(entries) as Record<string, string>,
),
});
}
function errorResponse(error: unknown): ClusterControlAdmissionResponse {
const code =
error && typeof error === 'object' && 'code' in error
? (error as { code?: unknown }).code
: null;
if (code === 'CLUSTER_PLUGIN_PACKAGE_WORKFLOW_NOT_FOUND') {
return response(404, { code: 'workflow_not_found' });
}
if (
code === 'CLUSTER_PLUGIN_PACKAGE_WORKFLOW_CONFLICT' ||
code === 'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_MUTATION_CONFLICT' ||
code === 'PLUGIN_PACKAGE_WORKFLOW_ADMISSION_CONFLICT'
) {
return response(409, { code: 'workflow_start_conflict' });
}
if (
code ===
'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT'
) {
return response(409, { code: 'authorization_fence_changed' });
}
if (code === 'CLUSTER_RUN_CANCELLATION_NOT_FOUND') {
return response(404, { code: 'workflow_run_not_found' });
}
if (code === 'CLUSTER_RUN_CANCELLATION_FENCE_REJECTED') {
const candidateReason =
error && typeof error === 'object' && 'reason' in error
? (error as { reason?: unknown }).reason
: null;
const reason =
candidateReason === 'authorization_changed' ||
candidateReason === 'project_mismatch' ||
candidateReason === 'state_mismatch'
? candidateReason
: 'state_mismatch';
return response(409, {
code: 'workflow_cancellation_fence_rejected',
reason,
});
}
return response(503, { code: 'workflow_administration_unavailable' });
}
function runInspectionErrorResponse(
error: unknown,
): ClusterControlAdmissionResponse {
const code =
error && typeof error === 'object' && 'code' in error
? (error as { code?: unknown }).code
: null;
if (
code ===
'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT'
) {
return response(409, { code: 'authorization_fence_changed' });
}
return response(503, { code: 'workflow_run_query_unavailable' });
}
function runListErrorResponse(error: unknown): ClusterControlAdmissionResponse {
const code =
error && typeof error === 'object' && 'code' in error
? (error as { code?: unknown }).code
: null;
if (
code ===
'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT'
) {
return response(409, { code: 'authorization_fence_changed' });
}
return response(503, { code: 'workflow_run_list_unavailable' });
}
function stepRunListErrorResponse(
error: unknown,
): ClusterControlAdmissionResponse {
const code =
error && typeof error === 'object' && 'code' in error
? (error as { code?: unknown }).code
: null;
if (
code ===
'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT'
) {
return response(409, { code: 'authorization_fence_changed' });
}
return response(503, { code: 'workflow_step_run_query_unavailable' });
}
function runEventListErrorResponse(
error: unknown,
): ClusterControlAdmissionResponse {
const code =
error && typeof error === 'object' && 'code' in error
? (error as { code?: unknown }).code
: null;
if (
code ===
'PLUGIN_PACKAGE_WORKFLOW_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT'
) {
return response(409, { code: 'authorization_fence_changed' });
}
return response(503, { code: 'workflow_run_event_query_unavailable' });
}
export function createClusterControlPluginPackageWorkflowRoutes(
capability: ClusterPluginPackageWorkflowAdministrationCapability,
now: () => number = Date.now,
createEventId: () => string = randomUUID,
): readonly Readonly<ClusterControlRouteDefinition>[] {
if (
!capability ||
typeof capability.inspect !== 'function' ||
typeof capability.inspectRun !== 'function' ||
typeof capability.listRuns !== 'function' ||
typeof capability.listStepRuns !== 'function' ||
typeof capability.listRunEvents !== 'function' ||
typeof capability.start !== 'function' ||
typeof capability.cancel !== 'function' ||
typeof now !== 'function' ||
typeof createEventId !== 'function'
) {
throw new TypeError('Cluster-control Workflow capability is invalid');
}
const common = {
projectParameter: 'projectId' as const,
};
return Object.freeze([
Object.freeze({
...common,
method: 'GET' as const,
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows',
operationId: 'workflow.read',
permission: 'run.read',
async handle(
authorized: ClusterControlAuthorizedOperationRequest,
parameters: ClusterControlRouteParameters,
) {
if (
authorized.projectId === null ||
typeof parameters.packageName !== 'string' ||
!PACKAGE_NAME.test(parameters.packageName)
) {
return response(503, { code: 'workflow_administration_unavailable' });
}
try {
const result = await capability.inspect(
authorized.projectId,
parameters.packageName,
);
return response(200, {
schema: CLUSTER_PLUGIN_PACKAGE_WORKFLOW_LIST_RESPONSE_SCHEMA,
...result,
});
} catch (error) {
return errorResponse(error);
}
},
}),
Object.freeze({
...common,
method: 'GET' as const,
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs',
operationId: 'workflow.run.list',
permission: 'run.read',
allowedQuery: Object.freeze([
'after_admitted_at_ms',
'after_run_id',
'limit',
]),
async handle(
authorized: ClusterControlAuthorizedOperationRequest,
parameters: ClusterControlRouteParameters,
) {
if (authorized.request.body !== null) {
return response(400, { code: 'invalid_request_body' });
}
let page;
try {
page = parseRunListQuery(authorized.request.query);
} catch {
return response(400, { code: 'invalid_workflow_run_query' });
}
const observedAtMs = now();
if (
authorized.projectId === null ||
typeof parameters.packageName !== 'string' ||
!PACKAGE_NAME.test(parameters.packageName) ||
typeof parameters.workflowId !== 'string' ||
!RESOURCE_ID.test(parameters.workflowId) ||
!authorized.policyFence ||
authorized.policyFence.bindingVersion === null ||
!Number.isSafeInteger(observedAtMs) ||
observedAtMs < 0
) {
return response(503, { code: 'workflow_run_list_unavailable' });
}
try {
const result = await capability.listRuns({
projectId: authorized.projectId,
packageName: parameters.packageName,
workflowId: parameters.workflowId,
limit: page.limit,
after: page.after,
requestId: authorized.request.requestId,
auditEventId: createEventId(),
principal: authorized.principal,
policyFence: authorized.policyFence,
observedAtMs,
});
return response(200, { ...result });
} catch (error) {
return runListErrorResponse(error);
}
},
}),
Object.freeze({
...common,
method: 'GET' as const,
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs/{runId}',
operationId: 'workflow.run.read',
permission: 'run.read',
async handle(
authorized: ClusterControlAuthorizedOperationRequest,
parameters: ClusterControlRouteParameters,
) {
const observedAtMs = now();
if (authorized.request.body !== null) {
return response(400, { code: 'invalid_request_body' });
}
if (
authorized.projectId === null ||
typeof parameters.packageName !== 'string' ||
!PACKAGE_NAME.test(parameters.packageName) ||
typeof parameters.workflowId !== 'string' ||
!RESOURCE_ID.test(parameters.workflowId) ||
typeof parameters.runId !== 'string' ||
!UUID_V4.test(parameters.runId) ||
!authorized.policyFence ||
authorized.policyFence.bindingVersion === null ||
!Number.isSafeInteger(observedAtMs) ||
observedAtMs < 0
) {
return response(503, { code: 'workflow_run_query_unavailable' });
}
try {
const result = await capability.inspectRun({
projectId: authorized.projectId,
packageName: parameters.packageName,
workflowId: parameters.workflowId,
runId: parameters.runId,
requestId: authorized.request.requestId,
auditEventId: createEventId(),
principal: authorized.principal,
policyFence: authorized.policyFence,
observedAtMs,
});
return result.found
? response(200, { ...result })
: response(404, { code: 'workflow_run_not_found' });
} catch (error) {
return runInspectionErrorResponse(error);
}
},
}),
Object.freeze({
...common,
method: 'GET' as const,
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs/{runId}/steps',
operationId: 'workflow.step.list',
permission: 'run.read',
allowedQuery: Object.freeze([
'after_step_key',
'after_step_run_id',
'limit',
]),
async handle(
authorized: ClusterControlAuthorizedOperationRequest,
parameters: ClusterControlRouteParameters,
) {
if (authorized.request.body !== null) {
return response(400, { code: 'invalid_request_body' });
}
let page;
try {
page = parseStepRunListQuery(authorized.request.query);
} catch {
return response(400, { code: 'invalid_step_run_query' });
}
const observedAtMs = now();
if (
authorized.projectId === null ||
typeof parameters.packageName !== 'string' ||
!PACKAGE_NAME.test(parameters.packageName) ||
typeof parameters.workflowId !== 'string' ||
!RESOURCE_ID.test(parameters.workflowId) ||
typeof parameters.runId !== 'string' ||
!UUID_V4.test(parameters.runId) ||
!authorized.policyFence ||
authorized.policyFence.bindingVersion === null ||
!Number.isSafeInteger(observedAtMs) ||
observedAtMs < 0
) {
return response(503, { code: 'workflow_step_run_query_unavailable' });
}
try {
const result = await capability.listStepRuns({
projectId: authorized.projectId,
packageName: parameters.packageName,
workflowId: parameters.workflowId,
runId: parameters.runId,
limit: page.limit,
after: page.after,
requestId: authorized.request.requestId,
auditEventId: createEventId(),
principal: authorized.principal,
policyFence: authorized.policyFence,
observedAtMs,
});
return result.found
? response(200, { ...result })
: response(404, { code: 'workflow_run_not_found' });
} catch (error) {
return stepRunListErrorResponse(error);
}
},
}),
Object.freeze({
...common,
method: 'GET' as const,
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs/{runId}/events',
operationId: 'workflow.event.list',
permission: 'run.read',
allowedQuery: Object.freeze(['after_sequence', 'limit']),
async handle(
authorized: ClusterControlAuthorizedOperationRequest,
parameters: ClusterControlRouteParameters,
) {
if (authorized.request.body !== null) {
return response(400, { code: 'invalid_request_body' });
}
let page;
try {
page = parseRunEventListQuery(authorized.request.query);
} catch {
return response(400, { code: 'invalid_run_event_query' });
}
const observedAtMs = now();
if (
authorized.projectId === null ||
typeof parameters.packageName !== 'string' ||
!PACKAGE_NAME.test(parameters.packageName) ||
typeof parameters.workflowId !== 'string' ||
!RESOURCE_ID.test(parameters.workflowId) ||
typeof parameters.runId !== 'string' ||
!UUID_V4.test(parameters.runId) ||
!authorized.policyFence ||
authorized.policyFence.bindingVersion === null ||
!Number.isSafeInteger(observedAtMs) ||
observedAtMs < 0
) {
return response(503, {
code: 'workflow_run_event_query_unavailable',
});
}
try {
const result = await capability.listRunEvents({
projectId: authorized.projectId,
packageName: parameters.packageName,
workflowId: parameters.workflowId,
runId: parameters.runId,
limit: page.limit,
afterSequence: page.afterSequence,
requestId: authorized.request.requestId,
auditEventId: createEventId(),
principal: authorized.principal,
policyFence: authorized.policyFence,
observedAtMs,
});
return result.found
? response(200, { ...result })
: response(404, { code: 'workflow_run_not_found' });
} catch (error) {
return runEventListErrorResponse(error);
}
},
}),
Object.freeze({
...common,
method: 'POST' as const,
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs',
operationId: 'workflow.start',
permission: 'run.start',
async handle(
authorized: ClusterControlAuthorizedOperationRequest,
parameters: ClusterControlRouteParameters,
) {
let body;
try {
body = parseBody(authorized.request.body);
} catch {
return response(400, { code: 'invalid_workflow_start_request' });
}
const plannedAtMs = now();
if (
authorized.projectId === null ||
typeof parameters.packageName !== 'string' ||
!PACKAGE_NAME.test(parameters.packageName) ||
typeof parameters.workflowId !== 'string' ||
!RESOURCE_ID.test(parameters.workflowId) ||
!authorized.policyFence ||
authorized.policyFence.bindingVersion === null ||
!Number.isSafeInteger(plannedAtMs) ||
plannedAtMs < 0
) {
return response(503, { code: 'workflow_administration_unavailable' });
}
try {
const result = await capability.start({
projectId: authorized.projectId,
packageName: parameters.packageName,
workflowId: parameters.workflowId,
planId: body.planId,
runId: body.runId,
stepRunIds: body.stepRunIds,
principal: authorized.principal,
policyFence: authorized.policyFence,
plannedAtMs,
});
return response(result.status === 'created' ? 201 : 200, {
schema: CLUSTER_PLUGIN_PACKAGE_WORKFLOW_START_RESPONSE_SCHEMA,
status: result.status,
replayed: result.status === 'existing',
planId: result.plan.planId,
runId: result.plan.runId,
receiptDigest: result.receipt.receiptDigest,
});
} catch (error) {
return errorResponse(error);
}
},
}),
Object.freeze({
...common,
method: 'POST' as const,
path: '/api/v3/projects/{projectId}/packages/{packageName}/workflows/{workflowId}/runs/{runId}/cancellation',
operationId: 'workflow.cancel',
permission: 'run.stop',
async handle(
authorized: ClusterControlAuthorizedOperationRequest,
parameters: ClusterControlRouteParameters,
) {
let body;
try {
body = parseClusterRunCancellationRequestBody(
authorized.request.body,
);
} catch {
return response(400, {
code: 'invalid_workflow_cancellation_request',
schema: CLUSTER_RUN_CANCELLATION_SCHEMA,
});
}
if (
authorized.projectId === null ||
typeof parameters.packageName !== 'string' ||
!PACKAGE_NAME.test(parameters.packageName) ||
typeof parameters.workflowId !== 'string' ||
!RESOURCE_ID.test(parameters.workflowId) ||
typeof parameters.runId !== 'string' ||
!UUID_V4.test(parameters.runId) ||
!authorized.policyFence ||
authorized.policyFence.bindingVersion === null
) {
return response(503, {
code: 'workflow_administration_unavailable',
});
}
try {
const result = await capability.cancel({
projectId: authorized.projectId,
packageName: parameters.packageName,
workflowId: parameters.workflowId,
runId: parameters.runId,
mutationId: body.mutationId,
eventId: createEventId(),
principal: authorized.principal,
policyFence: authorized.policyFence,
});
return response(
result.status === 'accepted' ? 202 : 200,
createClusterRunCancellationResponseBody(result),
);
} catch (error) {
return errorResponse(error);
}
},
}),
]);
}