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,222 @@
// PostgreSQL post-install resource materialization and recovery authority.
import type { PostgresPool } from '@qinglong/runtime-core';
import {
InvalidPluginPackageResourceMaterializationError,
MAX_PLUGIN_PACKAGE_MATERIALIZED_REVISION_JSON_BYTES,
PluginPackageResourceMaterializationConflictError,
PluginPackageResourceMaterializationUnavailableError,
normalizePluginPackageMaterializedRevision,
type PluginPackageMaterializedRevision,
type PluginPackageMaterializedRevisionRepository,
} from '@qinglong/runtime-core/plugin-package-resource-materialization';
import {
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import {
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const DIGEST = /^[0-9a-f]{64}$/;
function invalid(message: string): never {
throw new InvalidPluginPackageResourceMaterializationError(message);
}
function unavailable(): PluginPackageResourceMaterializationUnavailableError {
return new PluginPackageResourceMaterializationUnavailableError();
}
function generationDigest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) {
return invalid('generation digest is invalid');
}
return value;
}
function serialize(
revision: Readonly<PluginPackageMaterializedRevision>,
): string {
const value = JSON.stringify(revision);
if (
Buffer.byteLength(value, 'utf8') >
MAX_PLUGIN_PACKAGE_MATERIALIZED_REVISION_JSON_BYTES
) {
return invalid('materialized revision exceeds the durable JSON budget');
}
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageResourceMaterializationError ||
error instanceof PluginPackageResourceMaterializationConflictError ||
error instanceof PluginPackageResourceMaterializationUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackageResourceMaterializationConflictError(
'durable revision identity is already bound',
);
}
return new PluginPackageResourceMaterializationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export class PostgresPluginPackageMaterializedRevisionRepository
implements PluginPackageMaterializedRevisionRepository
{
private readonly taskSpecSemanticRegistry: TaskSpecSemanticRegistry;
constructor(
private readonly pool: Pick<PostgresPool, 'query'>,
taskSpecSemanticRegistry = createBuiltInTaskSpecSemanticRegistry(),
) {
if (
!pool ||
typeof pool.query !== 'function' ||
!(taskSpecSemanticRegistry instanceof TaskSpecSemanticRegistry)
) {
throw new TypeError(
'PostgreSQL materialized revision repository options are invalid',
);
}
this.taskSpecSemanticRegistry = taskSpecSemanticRegistry;
}
private parse(row: Row): Readonly<PluginPackageMaterializedRevision> {
try {
const revision = normalizePluginPackageMaterializedRevision(
postgresRequiredJsonObject(
row.revisionJson,
unavailable,
) as unknown as PluginPackageMaterializedRevision,
this.taskSpecSemanticRegistry,
);
if (
revision.generation.generationDigest !==
postgresRequiredString(row.generationDigest, unavailable) ||
revision.generation.projectId !==
postgresRequiredString(row.projectId, unavailable) ||
revision.generation.packageName !==
postgresRequiredString(row.packageName, unavailable) ||
revision.generation.generation !==
postgresRequiredInteger(row.generation, unavailable) ||
revision.generation.lockDigest !==
postgresRequiredString(row.lockDigest, unavailable) ||
revision.manifestDigest !==
postgresRequiredString(row.manifestDigest, unavailable) ||
revision.revisionDigest !==
postgresRequiredString(row.revisionDigest, unavailable)
) {
throw unavailable();
}
const createdAtMs = postgresRequiredInteger(row.createdAtMs, unavailable);
if (createdAtMs < 0) throw unavailable();
return revision;
} catch (error) {
if (error instanceof PluginPackageResourceMaterializationUnavailableError) {
throw error;
}
throw unavailable();
}
}
private async findStored(
digest: string,
): Promise<Readonly<PluginPackageMaterializedRevision> | null> {
const result = await this.pool.query<Row>(
`SELECT generation_digest AS "generationDigest",
project_id AS "projectId",
package_name AS "packageName",
generation,
lock_digest AS "lockDigest",
manifest_digest AS "manifestDigest",
revision_digest AS "revisionDigest",
revision_json AS "revisionJson",
created_at_ms AS "createdAtMs"
FROM "ql3"."plugin_package_materialized_revisions"
WHERE generation_digest = $1
LIMIT 2`,
[digest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return this.parse(result.rows[0]!);
}
async find(
digest: string,
): Promise<Readonly<PluginPackageMaterializedRevision> | null> {
try {
return await this.findStored(generationDigest(digest));
} catch (error) {
throw mapStorageError(error);
}
}
async publish(
value: Readonly<PluginPackageMaterializedRevision>,
): Promise<
Readonly<{
status: 'created' | 'existing';
revision: Readonly<PluginPackageMaterializedRevision>;
}>
> {
const revision = normalizePluginPackageMaterializedRevision(
value,
this.taskSpecSemanticRegistry,
);
const revisionJson = serialize(revision);
try {
const inserted = await this.pool.query(
`INSERT INTO "ql3"."plugin_package_materialized_revisions" (
generation_digest, project_id, package_name, generation,
lock_digest, manifest_digest, revision_digest, revision_json,
created_at_ms
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb,
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint)
ON CONFLICT (generation_digest) DO NOTHING
RETURNING generation_digest`,
[
revision.generation.generationDigest,
revision.generation.projectId,
revision.generation.packageName,
revision.generation.generation,
revision.generation.lockDigest,
revision.manifestDigest,
revision.revisionDigest,
revisionJson,
],
);
const stored = await this.findStored(
revision.generation.generationDigest,
);
if (!stored) throw unavailable();
if (
stored.revisionDigest !== revision.revisionDigest ||
JSON.stringify(stored) !== revisionJson
) {
throw new PluginPackageResourceMaterializationConflictError(
'generation digest is bound to another semantic revision',
);
}
return Object.freeze({
status: inserted.rows.length === 1 ? 'created' : 'existing',
revision: stored,
});
} catch (error) {
throw mapStorageError(error);
}
}
}
@@ -0,0 +1,341 @@
// PostgreSQL review proposal authority for Plugin Package installation.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
PluginPackageInstallProposalConflictError,
PluginPackageInstallProposalUnavailableError,
normalizePluginPackageInstallProposal,
type CreatePluginPackageInstallProposalCommand,
type CreatePluginPackageInstallProposalResult,
type PluginPackageInstallProposal,
type PluginPackageInstallProposalRepository,
} from '@qinglong/runtime-core/plugin-package-proposal';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
function unavailable(): PluginPackageInstallProposalUnavailableError {
return new PluginPackageInstallProposalUnavailableError();
}
function nullableString(value: unknown): string | null {
return value === null ? null : postgresRequiredString(value, unavailable);
}
function nullableInteger(value: unknown): number | null {
return value === null ? null : postgresRequiredInteger(value, unavailable);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function parseProposal(row: Row): Readonly<PluginPackageInstallProposal> {
try {
const proposal = normalizePluginPackageInstallProposal(
postgresRequiredJsonObject(
row.proposalJson,
unavailable,
) as unknown as PluginPackageInstallProposal,
);
if (
proposal.proposalDigest !==
postgresRequiredString(row.proposalDigest, unavailable)
) {
throw unavailable();
}
return proposal;
} catch (error) {
if (error instanceof PluginPackageInstallProposalUnavailableError) {
throw error;
}
throw unavailable();
}
}
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
try {
const subjectType = nullableString(row.subjectType);
const subjectId = nullableString(row.subjectId);
const projectVersion = nullableInteger(row.projectVersion);
if (!Array.isArray(row.reasons)) throw unavailable();
return normalizeSecurityAuditRecord({
eventId: postgresRequiredString(row.eventId, unavailable),
requestId: postgresRequiredString(row.requestId, unavailable),
operationId: postgresRequiredString(row.operationId, unavailable),
projectId: nullableString(row.projectId),
subject:
subjectType === null || subjectId === null
? null
: { type: subjectType, id: subjectId },
authenticationId: nullableString(row.authenticationId),
outcome: postgresRequiredString(row.outcome, unavailable),
reasons: row.reasons,
fence:
projectVersion === null
? null
: {
projectVersion,
bindingVersion: nullableInteger(row.bindingVersion),
},
occurredAtMs: postgresRequiredInteger(row.occurredAtMs, unavailable),
} as SecurityAuditRecord);
} catch (error) {
if (error instanceof PluginPackageInstallProposalUnavailableError) {
throw error;
}
throw unavailable();
}
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackageInstallProposalConflictError ||
error instanceof PluginPackageInstallProposalUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackageInstallProposalConflictError();
}
return new PluginPackageInstallProposalUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
async function proposalByActionRef(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackageInstallProposal> | null> {
const result = await queryable.query<Row>(
`SELECT proposal_json AS "proposalJson",
proposal_digest AS "proposalDigest"
FROM "ql3"."plugin_package_install_proposals"
WHERE action_ref = $1
LIMIT 2`,
[actionRef],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseProposal(result.rows[0]!);
}
export function findPostgresPluginPackageInstallProposal(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackageInstallProposal> | null> {
return proposalByActionRef(queryable, actionRef);
}
async function auditById(
queryable: Queryable,
eventId: string,
): Promise<Readonly<SecurityAuditRecord> | null> {
const result = await queryable.query<Row>(
`SELECT event_id AS "eventId", request_id AS "requestId",
operation_id AS "operationId", project_id AS "projectId",
subject_type AS "subjectType", subject_id AS "subjectId",
authentication_id AS "authenticationId", outcome,
reasons, project_version AS "projectVersion",
binding_version AS "bindingVersion",
occurred_at_ms AS "occurredAtMs"
FROM "ql3"."security_audit_events"
WHERE event_id = $1
LIMIT 2`,
[eventId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseAudit(result.rows[0]!);
}
export class PostgresPluginPackageInstallProposalRepository
implements PluginPackageInstallProposalRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL Package proposal pool is invalid');
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async findProposalByActionRef(
actionRef: string,
): Promise<Readonly<PluginPackageInstallProposal> | null> {
try {
return await proposalByActionRef(this.pool, actionRef);
} catch (error) {
throw mappedError(error);
}
}
createProposal(
command: CreatePluginPackageInstallProposalCommand,
): Promise<Readonly<CreatePluginPackageInstallProposalResult>> {
const proposal = normalizePluginPackageInstallProposal(command.proposal);
const audit = normalizeSecurityAuditRecord(command.audit);
if (
audit.requestId !== proposal.actionRef ||
audit.operationId !== 'plugin_package.propose' ||
audit.projectId !== proposal.projectId ||
audit.subject?.type !== proposal.proposedBy.type ||
audit.subject.id !== proposal.proposedBy.id ||
audit.authenticationId === null ||
audit.outcome !== 'allowed' ||
!same(audit.reasons, ['package_proposal']) ||
audit.fence?.projectVersion !== proposal.proposalFence.projectVersion ||
audit.fence.bindingVersion !== proposal.proposalFence.bindingVersion ||
audit.occurredAtMs !== proposal.createdAtMs
) {
throw new PluginPackageInstallProposalConflictError();
}
return this.#transaction(async (client) => {
const existing = await proposalByActionRef(client, proposal.actionRef);
if (existing) {
const existingAudit = await auditById(client, audit.eventId);
if (
!same(existing, proposal) ||
!existingAudit ||
!same(existingAudit, audit)
) {
throw new PluginPackageInstallProposalConflictError();
}
return Object.freeze({
status: 'existing' as const,
proposal,
});
}
const fence = await client.query<Row>(
`SELECT "ql3"."lock_approval_policy_fence"(
$1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
) AS "matches"`,
[
proposal.projectId,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
],
);
if (
fence.rows.length !== 1 ||
!postgresRequiredBoolean(fence.rows[0]!.matches, unavailable)
) {
throw new PluginPackageInstallProposalConflictError();
}
const inserted = await client.query(
`INSERT INTO "ql3"."plugin_package_install_proposals" (
action_ref, project_id, action_type, permission, action_digest,
preview_digest, proposed_by_type, proposed_by_id,
fence_project_version, fence_binding_version, created_at_ms,
proposal_json, proposal_digest
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb, $13
)`,
[
proposal.actionRef,
proposal.projectId,
proposal.actionType,
proposal.permission,
proposal.actionDigest,
proposal.previewDigest,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
proposal.createdAtMs,
JSON.stringify(proposal),
proposal.proposalDigest,
],
);
if (inserted.rowCount !== 1) throw unavailable();
const auditInserted = await client.query(
`INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type,
subject_id, authentication_id, outcome, reasons,
project_version, binding_version, occurred_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
)`,
[
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
],
);
if (auditInserted.rowCount !== 1) throw unavailable();
return Object.freeze({ status: 'created' as const, proposal });
});
}
}
@@ -0,0 +1,159 @@
// PostgreSQL adapter owned by the Plugin Package lifecycle capability.
import type { PostgresPool } from '@qinglong/runtime-core';
import {
InvalidPluginPackageLifecyclePlanError,
PluginPackageLifecyclePlanConflictError,
PluginPackageLifecyclePlanUnavailableError,
normalizePluginPackageLifecyclePlan,
type CreatePluginPackageLifecyclePlanResult,
type PluginPackageLifecyclePlan,
type PluginPackageLifecyclePlanRepository,
} from '@qinglong/runtime-core/plugin-package-lifecycle-plan';
import {
postgresRequiredJsonObject,
postgresSqlState,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const ACTION_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
function unavailable(
cause?: unknown,
): PluginPackageLifecyclePlanUnavailableError {
return new PluginPackageLifecyclePlanUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageLifecyclePlanError ||
error instanceof PluginPackageLifecyclePlanConflictError ||
error instanceof PluginPackageLifecyclePlanUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackageLifecyclePlanConflictError(
'plan identity or target is already bound',
);
}
return unavailable(error);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function normalizeRow(row: Row): Readonly<PluginPackageLifecyclePlan> {
try {
return normalizePluginPackageLifecyclePlan(
postgresRequiredJsonObject(
row.planJson,
unavailable,
) as unknown as PluginPackageLifecyclePlan,
);
} catch (error) {
throw unavailable(error);
}
}
function validateActionRef(value: string): string {
if (typeof value !== 'string' || !ACTION_REF_PATTERN.test(value)) {
throw new InvalidPluginPackageLifecyclePlanError(
'actionRef is invalid',
);
}
return value;
}
export class PostgresPluginPackageLifecyclePlanReader {
constructor(protected readonly pool: PostgresPool) {
if (
!pool ||
typeof pool !== 'object' ||
typeof pool.query !== 'function'
) {
throw new TypeError(
'PostgreSQL Plugin Package lifecycle plan reader is invalid',
);
}
}
async findByActionRef(
actionRef: string,
): Promise<Readonly<PluginPackageLifecyclePlan> | null> {
validateActionRef(actionRef);
try {
const result = await this.pool.query<Row>(
`SELECT plan_json AS "planJson"
FROM "ql3"."plugin_package_lifecycle_plans"
WHERE action_ref = $1
LIMIT 2`,
[actionRef],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
const plan = normalizeRow(result.rows[0]!);
if (plan.actionRef !== actionRef) throw unavailable();
return plan;
} catch (error) {
throw mapStorageError(error);
}
}
}
export class PostgresPluginPackageLifecyclePlanRepository
extends PostgresPluginPackageLifecyclePlanReader
implements PluginPackageLifecyclePlanRepository
{
async create(
planValue: Readonly<PluginPackageLifecyclePlan>,
): Promise<Readonly<CreatePluginPackageLifecyclePlanResult>> {
const plan = normalizePluginPackageLifecyclePlan(planValue);
try {
const inserted = await this.pool.query<Row>(
`INSERT INTO "ql3"."plugin_package_lifecycle_plans" (
action_ref, plan_digest, action, project_id, package_name,
installation_id, lock_digest, impact_digest, requested_by_type,
requested_by_id, planned_at_ms, expires_at_ms, plan_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb
)
ON CONFLICT DO NOTHING
RETURNING action_ref AS "actionRef"`,
[
plan.actionRef,
plan.planDigest,
plan.impact.action,
plan.impact.target.projectId,
plan.impact.target.packageName,
plan.impact.target.installationId,
plan.impact.target.lockDigest,
plan.impact.impactDigest,
plan.requestedBy.type,
plan.requestedBy.id,
plan.plannedAtMs,
plan.expiresAtMs,
JSON.stringify(plan),
],
);
const stored = await this.findByActionRef(plan.actionRef);
if (!stored || !same(stored, plan)) {
throw new PluginPackageLifecyclePlanConflictError(
'plan identity is already bound to another impact',
);
}
return Object.freeze({
status:
inserted.rows.length === 1 ? ('created' as const) : ('existing' as const),
plan: stored,
});
} catch (error) {
throw mapStorageError(error);
}
}
}
@@ -0,0 +1,787 @@
// PostgreSQL adapter owned by the Plugin Package lifecycle capability.
import type {
PostgresClient,
PostgresPool,
PostgresQueryable,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageQuarantineError,
MAX_PLUGIN_PACKAGE_QUARANTINE_RETAINED_SOURCES,
MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS,
PluginPackageQuarantineConflictError,
PluginPackageQuarantineUnavailableError,
assertPluginPackageWithdrawalMatchesEvent,
createPluginPackageWithdrawalReceipt,
normalizePluginPackageQuarantineEvent,
normalizePluginPackageWithdrawalReceipt,
pluginPackageQuarantineTaskMutationId,
type PluginPackageQuarantineEvent,
type PluginPackageQuarantineRepository,
type PluginPackageQuarantineTarget,
type PluginPackageQuarantineTaskWithdrawal,
type PluginPackageWithdrawalReceipt,
} from '@qinglong/runtime-core/plugin-package-quarantine';
import {
normalizePluginPackageInstallRecord,
type PluginPackageInstallRecord,
} from '@qinglong/runtime-core/plugin-package-install';
import {
createProjectToolDefinitionSnapshot,
normalizeProjectToolDefinitionSnapshot,
projectToolDefinitionActiveVectorDigest,
projectToolDefinitionSnapshotContribution,
type ProjectToolDefinitionSnapshot,
type ProjectToolDefinitionSnapshotContribution,
type ProjectToolDefinitionSnapshotSource,
} from '@qinglong/runtime-core/project-tool-definition-snapshot';
import {
createTaskDefinitionRecord,
normalizeTaskDefinitionRecord,
type TaskDefinitionRecord,
} from '@qinglong/runtime-core/task-definition';
import {
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
import { isPostgresAvailabilityError } from '../../connection/pool';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresQueryable, 'query'>;
export const CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT = 128;
function unavailable(
cause?: unknown,
): PluginPackageQuarantineUnavailableError {
return new PluginPackageQuarantineUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageQuarantineError ||
error instanceof PluginPackageQuarantineConflictError ||
error instanceof PluginPackageQuarantineUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackageQuarantineConflictError(
'durable quarantine identity or target state conflicts',
);
}
return unavailable(error);
}
function recordJson(row: Row, key: string): Readonly<Record<string, unknown>> {
return postgresRequiredJsonObject(row[key], unavailable);
}
function integer(row: Row, key: string): number {
const value = postgresRequiredInteger(row[key], unavailable);
if (value < 0) throw unavailable();
return value;
}
function text(row: Row, key: string): string {
return postgresRequiredString(row[key], unavailable);
}
function taskRecord(row: Row): Readonly<TaskDefinitionRecord> {
try {
const description = row.description;
if (description !== null && typeof description !== 'string') {
throw unavailable();
}
return normalizeTaskDefinitionRecord({
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
revision: integer(row, 'revision'),
mutationId: text(row, 'mutationId'),
name: text(row, 'name'),
...(description === null ? {} : { description }),
kind: text(row, 'kind') as TaskDefinitionRecord['kind'],
spec: recordJson(
row,
'specJson',
) as unknown as TaskDefinitionRecord['spec'],
labels: recordJson(row, 'labelsJson') as TaskDefinitionRecord['labels'],
enabled: postgresRequiredBoolean(row.enabled, unavailable),
contentDigest: text(row, 'contentDigest'),
createdAtMs: integer(row, 'createdAtMs'),
updatedAtMs: integer(row, 'updatedAtMs'),
});
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) throw error;
throw unavailable(error);
}
}
function activeSource(
contribution: Readonly<ProjectToolDefinitionSnapshotContribution>,
): Readonly<ProjectToolDefinitionSnapshotSource> {
return Object.freeze({
installationId: contribution.generation.installationId,
packageName: contribution.generation.packageName,
generation: contribution.generation.generation,
generationDigest: contribution.generation.generationDigest,
lockDigest: contribution.generation.lockDigest,
revisionDigest: contribution.revisionDigest,
});
}
export class PostgresPluginPackageQuarantineRepository
implements PluginPackageQuarantineRepository
{
readonly #registry: TaskSpecSemanticRegistry;
constructor(
private readonly pool: PostgresPool,
options: Readonly<{ registry?: TaskSpecSemanticRegistry }> = {},
) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL Plugin Package quarantine repository is invalid',
);
}
const registry =
options.registry ?? createBuiltInTaskSpecSemanticRegistry();
if (!(registry instanceof TaskSpecSemanticRegistry)) {
throw new TypeError('TaskSpec semantic registry is invalid');
}
this.#registry = registry;
}
async #eventByDigest(
queryable: Queryable,
eventDigest: string,
): Promise<Readonly<PluginPackageQuarantineEvent> | null> {
const result = await queryable.query<Row>(
`SELECT event_json AS "eventJson"
FROM "ql3"."plugin_package_quarantine_events"
WHERE event_digest = $1
LIMIT 2`,
[eventDigest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
try {
return normalizePluginPackageQuarantineEvent(
recordJson(result.rows[0]!, 'eventJson') as unknown as
PluginPackageQuarantineEvent,
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async #receiptByEvent(
queryable: Queryable,
event: Readonly<PluginPackageQuarantineEvent>,
): Promise<Readonly<PluginPackageWithdrawalReceipt> | null> {
const result = await queryable.query<Row>(
`SELECT receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_withdrawal_receipts"
WHERE event_digest = $1
LIMIT 2`,
[event.eventDigest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
try {
const receipt = normalizePluginPackageWithdrawalReceipt(
recordJson(result.rows[0]!, 'receiptJson') as unknown as
PluginPackageWithdrawalReceipt,
);
assertPluginPackageWithdrawalMatchesEvent(event, receipt);
await this.#assertReceiptRelations(queryable, receipt);
return receipt;
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async #assertReceiptRelations(
queryable: Queryable,
receipt: Readonly<PluginPackageWithdrawalReceipt>,
): Promise<void> {
const tasks = await queryable.query<Row>(
`SELECT
item.project_id AS "projectId",
item.task_id AS "taskId",
item.previous_revision AS "previousRevision",
item.disabled_revision AS "disabledRevision",
item.previous_content_digest AS "previousContentDigest",
item.disabled_content_digest AS "disabledContentDigest",
previous.content_digest AS "storedPreviousContentDigest",
disabled.content_digest AS "storedDisabledContentDigest"
FROM "ql3"."plugin_package_withdrawal_tasks" AS item
JOIN "ql3"."task_definition_revisions" AS previous
ON previous.project_id = item.project_id
AND previous.task_id = item.task_id
AND previous.revision = item.previous_revision
JOIN "ql3"."task_definition_revisions" AS disabled
ON disabled.project_id = item.project_id
AND disabled.task_id = item.task_id
AND disabled.revision = item.disabled_revision
WHERE item.event_digest = $1
ORDER BY item.task_id`,
[receipt.eventDigest],
);
const withdrawals = Object.freeze(
tasks.rows.map((row) =>
Object.freeze({
taskId: text(row, 'taskId'),
previousRevision: integer(row, 'previousRevision'),
disabledRevision: integer(row, 'disabledRevision'),
previousContentDigest: text(row, 'previousContentDigest'),
disabledContentDigest: text(row, 'disabledContentDigest'),
}),
),
);
if (
tasks.rows.some(
(row) =>
text(row, 'projectId') !== receipt.target.projectId ||
text(row, 'previousContentDigest') !==
text(row, 'storedPreviousContentDigest') ||
text(row, 'disabledContentDigest') !==
text(row, 'storedDisabledContentDigest'),
) ||
!same(withdrawals, receipt.capability.taskWithdrawals)
) {
throw unavailable();
}
if (receipt.capability.status === 'not_active') return;
const snapshots = await queryable.query<Row>(
`SELECT snapshot_json AS "snapshotJson"
FROM "ql3"."project_tool_definition_snapshots"
WHERE project_id = $1 AND active_vector_digest = $2
AND snapshot_digest = $3
LIMIT 2`,
[
receipt.target.projectId,
receipt.capability.currentActiveVectorDigest,
receipt.capability.currentToolSnapshotDigest,
],
);
if (snapshots.rows.length !== 1) throw unavailable();
try {
const snapshot = normalizeProjectToolDefinitionSnapshot(
recordJson(snapshots.rows[0]!, 'snapshotJson') as unknown as
ProjectToolDefinitionSnapshot,
);
if (
snapshot.sources.length !== receipt.capability.retainedSourceCount ||
snapshot.sources.some(
(source) =>
source.packageName === receipt.target.packageName &&
source.installationId === receipt.target.installationId &&
source.lockDigest === receipt.target.lockDigest,
)
) {
throw unavailable();
}
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async #findStored(
queryable: Queryable,
eventDigest: string,
): Promise<Readonly<PluginPackageWithdrawalReceipt> | null> {
const event = await this.#eventByDigest(queryable, eventDigest);
if (!event) return null;
const receipt = await this.#receiptByEvent(queryable, event);
if (!receipt) throw unavailable();
return receipt;
}
async findTargetsByLockDigest(
lockDigest: string,
): Promise<readonly Readonly<PluginPackageQuarantineTarget>[]> {
if (typeof lockDigest !== 'string' || !/^[0-9a-f]{64}$/.test(lockDigest)) {
throw new InvalidPluginPackageQuarantineError('lockDigest is invalid');
}
try {
const result = await this.pool.query<Row>(
`SELECT record_json AS "recordJson"
FROM "ql3"."plugin_package_installs"
WHERE lock_digest = $1
AND state IN ('queued', 'staged', 'activating', 'active')
ORDER BY project_id, package_name, installation_id
LIMIT $2`,
[lockDigest, CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT + 1],
);
if (
result.rows.length > CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT
) {
throw new PluginPackageQuarantineConflictError(
'matching install targets exceed the Cluster limit',
);
}
return Object.freeze(
result.rows.map((row) => {
const record = normalizePluginPackageInstallRecord(
recordJson(row, 'recordJson') as unknown as
PluginPackageInstallRecord,
);
return Object.freeze({
projectId: record.projectId,
packageName: record.packageName,
installationId: record.installationId,
lockDigest: record.lockDigest,
installState:
record.state as PluginPackageQuarantineTarget['installState'],
installVersion: record.version,
installRecordDigest: record.recordDigest,
activeLockDigest: record.activeLockDigest,
});
}),
);
} catch (error) {
throw mapStorageError(error);
}
}
async findByEventDigest(
eventDigest: string,
): Promise<Readonly<PluginPackageWithdrawalReceipt> | null> {
if (
typeof eventDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(eventDigest)
) {
throw new InvalidPluginPackageQuarantineError('eventDigest is invalid');
}
try {
return await this.#findStored(this.pool, eventDigest);
} catch (error) {
throw mapStorageError(error);
}
}
async #install(
queryable: Queryable,
event: Readonly<PluginPackageQuarantineEvent>,
): Promise<Readonly<PluginPackageInstallRecord>> {
const result = await queryable.query<Row>(
`SELECT record_json AS "recordJson"
FROM "ql3"."plugin_package_installs"
WHERE installation_id = $1
LIMIT 2`,
[event.target.installationId],
);
if (result.rows.length !== 1) {
throw new PluginPackageQuarantineConflictError(
'target install is absent',
);
}
let record: Readonly<PluginPackageInstallRecord>;
try {
record = normalizePluginPackageInstallRecord(
recordJson(result.rows[0]!, 'recordJson') as unknown as
PluginPackageInstallRecord,
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
throw error;
}
throw unavailable(error);
}
if (
record.projectId !== event.target.projectId ||
record.packageName !== event.target.packageName ||
record.lockDigest !== event.target.lockDigest ||
record.state !== event.target.installState ||
record.version !== event.target.installVersion ||
record.recordDigest !== event.target.installRecordDigest ||
record.activeLockDigest !== event.target.activeLockDigest
) {
throw new PluginPackageQuarantineConflictError(
'target install advanced or drifted',
);
}
return record;
}
async #activeContributions(
queryable: Queryable,
projectId: string,
): Promise<
readonly Readonly<ProjectToolDefinitionSnapshotContribution>[]
> {
const result = await queryable.query<Row>(
`SELECT revision.revision_json AS "revisionJson"
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS head_install
ON head_install.installation_id = head.installation_id
JOIN "ql3"."plugin_package_installs" AS active_install
ON active_install.project_id = head.project_id
AND active_install.package_name = head.package_name
AND active_install.lock_digest = head_install.active_lock_digest
JOIN "ql3"."plugin_package_materialized_revisions" AS revision
ON revision.project_id = active_install.project_id
AND revision.package_name = active_install.package_name
AND revision.generation = active_install.target_generation
AND revision.lock_digest = active_install.lock_digest
LEFT JOIN "ql3"."plugin_package_quarantine_events" AS quarantine
ON quarantine.project_id = active_install.project_id
AND quarantine.package_name = active_install.package_name
AND quarantine.installation_id = active_install.installation_id
AND quarantine.lock_digest = active_install.lock_digest
WHERE head.project_id = $1
AND head_install.active_lock_digest IS NOT NULL
AND active_install.state = 'active'
AND quarantine.event_digest IS NULL
ORDER BY head.package_name
LIMIT $2`,
[projectId, MAX_PLUGIN_PACKAGE_QUARANTINE_RETAINED_SOURCES + 2],
);
if (
result.rows.length >
MAX_PLUGIN_PACKAGE_QUARANTINE_RETAINED_SOURCES + 1
) {
throw new PluginPackageQuarantineConflictError(
'active Package sources exceed the Cluster quarantine limit',
);
}
try {
return Object.freeze(
result.rows.map((row) =>
projectToolDefinitionSnapshotContribution(
recordJson(row, 'revisionJson') as never,
this.#registry,
),
),
);
} catch (error) {
if (error instanceof PluginPackageQuarantineUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async #enabledOwnedTasks(
queryable: Queryable,
event: Readonly<PluginPackageQuarantineEvent>,
): Promise<readonly Readonly<TaskDefinitionRecord>[]> {
const result = await queryable.query<Row>(
`SELECT
head.project_id AS "projectId",
head.task_id AS "taskId",
revision.revision,
revision.mutation_id::text AS "mutationId",
revision.name,
revision.description,
revision.kind,
revision.spec_json AS "specJson",
revision.labels_json AS "labelsJson",
revision.enabled,
revision.content_digest AS "contentDigest",
head.created_at_ms AS "createdAtMs",
revision.created_at_ms AS "updatedAtMs"
FROM "ql3"."plugin_package_task_ownerships" AS ownership
JOIN "ql3"."task_definitions" AS head
ON head.project_id = ownership.project_id
AND head.task_id = ownership.task_id
JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE ownership.project_id = $1
AND ownership.package_name = $2
AND revision.enabled = true
ORDER BY ownership.task_id
LIMIT $3`,
[
event.target.projectId,
event.target.packageName,
MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS + 1,
],
);
if (
result.rows.length > MAX_PLUGIN_PACKAGE_QUARANTINE_TASK_WITHDRAWALS
) {
throw new PluginPackageQuarantineConflictError(
'owned Tasks exceed the quarantine withdrawal limit',
);
}
return Object.freeze(result.rows.map(taskRecord));
}
#disabledTask(
event: Readonly<PluginPackageQuarantineEvent>,
current: Readonly<TaskDefinitionRecord>,
committedAtMs: number,
): Readonly<{
disabled: Readonly<TaskDefinitionRecord>;
withdrawal: Readonly<PluginPackageQuarantineTaskWithdrawal>;
}> {
const disabled = createTaskDefinitionRecord(
{
projectId: current.projectId,
taskId: current.taskId,
expectedRevision: current.revision,
mutationId: pluginPackageQuarantineTaskMutationId(
event.eventDigest,
current.taskId,
),
name: current.name,
...(current.description === undefined
? {}
: { description: current.description }),
kind: current.kind,
spec: current.spec,
labels: current.labels,
enabled: false,
occurredAtMs: committedAtMs,
},
current.createdAtMs,
);
return Object.freeze({
disabled,
withdrawal: Object.freeze({
taskId: current.taskId,
previousRevision: current.revision,
disabledRevision: disabled.revision,
previousContentDigest: current.contentDigest,
disabledContentDigest: disabled.contentDigest,
}),
});
}
async #databaseNowMs(queryable: Queryable): Promise<number> {
const result = await queryable.query<Row>(
`SELECT floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
AS "nowMs"`,
);
if (result.rows.length !== 1) throw unavailable();
return integer(result.rows[0]!, 'nowMs');
}
async #commit(
client: PostgresClient,
event: Readonly<PluginPackageQuarantineEvent>,
): Promise<
Readonly<{
created: boolean;
receipt: Readonly<PluginPackageWithdrawalReceipt>;
}>
> {
const existingEvent = await this.#eventByDigest(client, event.eventDigest);
if (existingEvent) {
if (!same(existingEvent, event)) {
throw new PluginPackageQuarantineConflictError(
'event digest is bound to another quarantine',
);
}
const existingReceipt = await this.#receiptByEvent(
client,
existingEvent,
);
if (!existingReceipt) throw unavailable();
return Object.freeze({ created: false, receipt: existingReceipt });
}
await this.#install(client, event);
const committedAtMs = Math.max(
await this.#databaseNowMs(client),
event.occurredAtMs,
);
let snapshot: Readonly<ProjectToolDefinitionSnapshot> | null = null;
let taskWrites: readonly Readonly<{
disabled: Readonly<TaskDefinitionRecord>;
withdrawal: Readonly<PluginPackageQuarantineTaskWithdrawal>;
}>[] = Object.freeze([]);
let receipt: Readonly<PluginPackageWithdrawalReceipt>;
if (event.target.installState !== 'active') {
receipt = createPluginPackageWithdrawalReceipt({
eventDigest: event.eventDigest,
target: event.target,
capability: {
status: 'not_active',
taskWithdrawals: [],
previousActiveVectorDigest: null,
currentActiveVectorDigest: null,
currentToolSnapshotDigest: null,
retainedSourceCount: 0,
},
committedAtMs,
});
} else {
const previousContributions = await this.#activeContributions(
client,
event.target.projectId,
);
const targetIndex = previousContributions.findIndex(
({ generation }) =>
generation.packageName === event.target.packageName &&
generation.installationId === event.target.installationId &&
generation.lockDigest === event.target.lockDigest,
);
if (targetIndex < 0) {
throw new PluginPackageQuarantineConflictError(
'active target is not a complete Tool source',
);
}
const previousSources = Object.freeze(
previousContributions.map(activeSource),
);
const retainedContributions = Object.freeze(
previousContributions.filter((_, index) => index !== targetIndex),
);
snapshot = createProjectToolDefinitionSnapshot({
projectId: event.target.projectId,
contributions: retainedContributions,
});
const tasks = await this.#enabledOwnedTasks(client, event);
taskWrites = Object.freeze(
tasks.map((task) => this.#disabledTask(event, task, committedAtMs)),
);
receipt = createPluginPackageWithdrawalReceipt({
eventDigest: event.eventDigest,
target: event.target,
capability: {
status: 'withdrawn',
taskWithdrawals: taskWrites.map(({ withdrawal }) => withdrawal),
previousActiveVectorDigest: projectToolDefinitionActiveVectorDigest(
event.target.projectId,
previousSources,
),
currentActiveVectorDigest: snapshot.activeVectorDigest,
currentToolSnapshotDigest: snapshot.snapshotDigest,
retainedSourceCount: snapshot.sources.length,
},
committedAtMs,
});
}
const committed = await client.query<Row>(
`SELECT "ql3"."commit_plugin_package_quarantine"(
$1::jsonb, $2::jsonb, $3::jsonb, $4::jsonb
) AS "created"`,
[
JSON.stringify(event),
JSON.stringify(receipt),
JSON.stringify(taskWrites),
JSON.stringify(snapshot),
],
);
if (
committed.rows.length !== 1 ||
typeof committed.rows[0]?.created !== 'boolean'
) {
throw unavailable();
}
const stored = await this.#findStored(client, event.eventDigest);
if (!stored || !same(stored, receipt)) throw unavailable();
return Object.freeze({
created: committed.rows[0].created,
receipt: stored,
});
}
async quarantine(
eventValue: Readonly<PluginPackageQuarantineEvent>,
confirmAuthorization: () => void | Promise<void>,
): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<PluginPackageWithdrawalReceipt>;
}>
> {
const event = normalizePluginPackageQuarantineEvent(eventValue);
if (typeof confirmAuthorization !== 'function') {
throw new InvalidPluginPackageQuarantineError(
'confirmAuthorization is invalid',
);
}
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
if (attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS) continue;
throw unavailable(error);
}
let began = false;
let authorizationFailure = false;
const authorize = async (): Promise<void> => {
try {
await confirmAuthorization();
} catch (error) {
authorizationFailure = true;
throw error;
}
};
try {
await configurePostgresDefinitionTransaction(client);
began = true;
await authorize();
const result = await this.#commit(client, event);
await authorize();
await client.query('COMMIT');
began = false;
return Object.freeze({
status: result.created ? 'created' : 'existing',
receipt: result.receipt,
});
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
if (authorizationFailure) throw error;
const state = postgresSqlState(error);
if (
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS &&
(isPostgresAvailabilityError(error) ||
!state ||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) ||
state.startsWith('08'))
) {
continue;
}
throw mapStorageError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}
@@ -0,0 +1,650 @@
// PostgreSQL adapter owned by Plugin Package publication and recovery.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageAutomationPublicationError,
MAX_PLUGIN_PACKAGE_AUTOMATION_PUBLICATION_BYTES,
PluginPackageAutomationPublicationConflictError,
PluginPackageAutomationPublicationUnavailableError,
assertPluginPackageAutomationPublicationRecoveryPageSize,
assertPluginPackageAutomationPublicationSuccessor,
normalizePluginPackageAutomationPublication,
normalizePluginPackageAutomationPublicationRecoveryCursor,
type PluginPackageAutomationPublication,
type PluginPackageAutomationPublicationRecoveryPage,
type PluginPackageAutomationPublicationRepository,
type PluginPackageAutomationPublicationRecoverySource,
type PluginPackageAutomationPublicationStartGuard,
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const DIGEST = /^[0-9a-f]{64}$/;
function invalid(message: string): never {
throw new InvalidPluginPackageAutomationPublicationError(message);
}
function unavailable(
error?: unknown,
): PluginPackageAutomationPublicationUnavailableError {
return new PluginPackageAutomationPublicationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
function targetIdentity(projectId: unknown, packageName: unknown): {
readonly projectId: string;
readonly packageName: string;
} {
if (typeof projectId !== 'string' || !IDENTIFIER.test(projectId)) {
return invalid('projectId is invalid');
}
if (typeof packageName !== 'string' || !PACKAGE_NAME.test(packageName)) {
return invalid('packageName is invalid');
}
return { projectId, packageName };
}
function publicationDigest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) {
return invalid('publicationDigest is invalid');
}
return value;
}
function serialize(
publication: Readonly<PluginPackageAutomationPublication>,
): string {
const value = JSON.stringify(publication);
if (
Buffer.byteLength(value, 'utf8') >
MAX_PLUGIN_PACKAGE_AUTOMATION_PUBLICATION_BYTES
) {
return invalid('publication exceeds the durable JSON budget');
}
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageAutomationPublicationError ||
error instanceof PluginPackageAutomationPublicationConflictError ||
error instanceof PluginPackageAutomationPublicationUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (
state === '23503' ||
state === '23505' ||
state === '23514' ||
state === '40001'
) {
return new PluginPackageAutomationPublicationConflictError(
'durable publication chain changed',
);
}
return unavailable(error);
}
export class PostgresPluginPackageAutomationPublicationRepository
implements
PluginPackageAutomationPublicationRepository,
PluginPackageAutomationPublicationRecoverySource,
PluginPackageAutomationPublicationStartGuard
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL automation publication repository is invalid',
);
}
}
#parse(row: Row): Readonly<PluginPackageAutomationPublication> {
try {
const publication = normalizePluginPackageAutomationPublication(
postgresRequiredJsonObject(
row.publicationJson,
unavailable,
) as unknown as PluginPackageAutomationPublication,
);
if (
publication.publicationDigest !==
postgresRequiredString(row.publicationDigest, unavailable) ||
publication.target.projectId !==
postgresRequiredString(row.projectId, unavailable) ||
publication.target.packageName !==
postgresRequiredString(row.packageName, unavailable) ||
publication.target.installationId !==
postgresRequiredString(row.installationId, unavailable) ||
publication.target.lockDigest !==
postgresRequiredString(row.lockDigest, unavailable) ||
publication.target.generation !==
postgresRequiredInteger(row.generation, unavailable) ||
publication.target.generationDigest !==
postgresRequiredString(row.generationDigest, unavailable) ||
publication.target.materializedRevisionDigest !==
postgresRequiredString(row.materializedRevisionDigest, unavailable) ||
publication.state !==
postgresRequiredString(row.state, unavailable) ||
publication.version !==
postgresRequiredInteger(row.version, unavailable) ||
publication.publishedAtMs !==
postgresRequiredInteger(row.publishedAtMs, unavailable)
) {
throw unavailable();
}
return publication;
} catch (error) {
if (
error instanceof PluginPackageAutomationPublicationUnavailableError
) {
throw error;
}
throw unavailable(error);
}
}
async #findByDigest(
queryable: Queryable,
digest: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const result = await queryable.query<Row>(
`SELECT publication_digest AS "publicationDigest",
project_id AS "projectId",
package_name AS "packageName",
installation_id AS "installationId",
lock_digest AS "lockDigest",
generation,
generation_digest AS "generationDigest",
materialized_revision_digest AS "materializedRevisionDigest",
state,
version,
published_at_ms AS "publishedAtMs",
publication_json AS "publicationJson"
FROM "ql3"."plugin_package_automation_publications"
WHERE publication_digest = $1
LIMIT 2`,
[digest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return this.#parse(result.rows[0]!);
}
async #findCurrent(
queryable: Queryable,
projectId: string,
packageName: string,
lock = false,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const result = await queryable.query<Row>(
`SELECT publication.publication_digest AS "publicationDigest",
publication.project_id AS "projectId",
publication.package_name AS "packageName",
publication.installation_id AS "installationId",
publication.lock_digest AS "lockDigest",
publication.generation,
publication.generation_digest AS "generationDigest",
publication.materialized_revision_digest
AS "materializedRevisionDigest",
publication.state,
publication.version,
publication.published_at_ms AS "publishedAtMs",
publication.publication_json AS "publicationJson"
FROM "ql3"."plugin_package_automation_publication_heads" AS head
JOIN "ql3"."plugin_package_automation_publications" AS publication
ON publication.publication_digest = head.publication_digest
WHERE head.project_id = $1 AND head.package_name = $2
LIMIT 2${lock ? ' FOR UPDATE OF head' : ''}`,
[projectId, packageName],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return this.#parse(result.rows[0]!);
}
async findCurrent(
projectIdValue: string,
packageNameValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
try {
return await this.#findCurrent(this.pool, projectId, packageName);
} catch (error) {
throw mapStorageError(error);
}
}
async findByDigest(
publicationDigestValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const digest = publicationDigest(publicationDigestValue);
try {
return await this.#findByDigest(this.pool, digest);
} catch (error) {
throw mapStorageError(error);
}
}
async isStartAllowed(
projectIdValue: string,
packageNameValue: string,
publicationDigestValue: string,
): Promise<boolean> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
const digest = publicationDigest(publicationDigestValue);
try {
const result = await this.pool.query<Row>(
`SELECT "ql3"."plugin_package_automation_start_allowed"(
$1::varchar, $2::varchar, $3::char(64)
) AS "allowed"`,
[projectId, packageName, digest],
);
if (
result.rows.length !== 1 ||
typeof result.rows[0]?.allowed !== 'boolean'
) {
throw unavailable();
}
return result.rows[0].allowed;
} catch (error) {
throw mapStorageError(error);
}
}
async listPendingPage(options: {
readonly limit: number;
readonly after?: Readonly<{
readonly projectId: string;
readonly packageName: string;
}>;
}): Promise<Readonly<PluginPackageAutomationPublicationRecoveryPage>> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new InvalidPluginPackageAutomationPublicationError(
'pending page options are invalid',
);
}
assertPluginPackageAutomationPublicationRecoveryPageSize(options.limit);
const after =
options.after === undefined
? undefined
: normalizePluginPackageAutomationPublicationRecoveryCursor(
options.after,
);
try {
const result = await this.pool.query<Row>(
`SELECT install.project_id AS "projectId",
install.package_name AS "packageName"
FROM "ql3"."plugin_package_install_heads" AS install_head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = install_head.installation_id
JOIN "ql3"."plugin_package_materialized_revisions" AS revision
ON revision.project_id = install.project_id
AND revision.package_name = install.package_name
AND revision.generation = install.target_generation
AND revision.lock_digest = install.lock_digest
LEFT JOIN
"ql3"."plugin_package_automation_publication_heads" AS publication
ON publication.project_id = install.project_id
AND publication.package_name = install.package_name
WHERE install.state = 'active'
AND install.active_lock_digest = install.lock_digest
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
WHERE quarantine.project_id = install.project_id
AND quarantine.package_name = install.package_name
AND quarantine.installation_id = install.installation_id
AND quarantine.lock_digest = install.lock_digest
)
AND (
publication.generation_digest IS NULL OR
publication.generation_digest <> revision.generation_digest
)
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
ON revoked.publisher = provenance.publisher
AND revoked.key_id = provenance.key_id
WHERE provenance.installation_id = install.installation_id
AND provenance.lock_digest = install.lock_digest
)
AND (
$1::varchar IS NULL OR install.project_id > $1 OR
(install.project_id = $1 AND install.package_name > $2)
)
ORDER BY install.project_id, install.package_name
LIMIT $3`,
[
after?.projectId ?? null,
after?.packageName ?? null,
options.limit + 1,
],
);
const truncated = result.rows.length > options.limit;
const candidates = result.rows.slice(0, options.limit).map((row) =>
Object.freeze({
projectId: postgresRequiredString(row.projectId, unavailable),
packageName: postgresRequiredString(row.packageName, unavailable),
}),
);
const last = candidates.at(-1);
return Object.freeze({
candidates: Object.freeze(candidates),
truncated,
...(truncated && last
? {
next: Object.freeze({
projectId: last.projectId,
packageName: last.packageName,
}),
}
: {}),
});
} catch (error) {
throw mapStorageError(error);
}
}
async findCurrentInTransaction(
client: PostgresClient,
projectIdValue: string,
packageNameValue: string,
): Promise<Readonly<PluginPackageAutomationPublication> | null> {
const { projectId, packageName } = targetIdentity(
projectIdValue,
packageNameValue,
);
if (!client || typeof client.query !== 'function') {
throw new TypeError(
'PostgreSQL automation publication transaction is invalid',
);
}
return this.#findCurrent(client, projectId, packageName, true);
}
async publishInTransaction(
client: PostgresClient,
value: Readonly<PluginPackageAutomationPublication>,
): Promise<
Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}>
> {
if (!client || typeof client.query !== 'function') {
throw new TypeError(
'PostgreSQL automation publication transaction is invalid',
);
}
const publication = normalizePluginPackageAutomationPublication(value);
const publicationJson = serialize(publication);
const existing = await this.#findByDigest(
client,
publication.publicationDigest,
);
if (existing) {
if (serialize(existing) !== publicationJson) {
throw new PluginPackageAutomationPublicationConflictError(
'publication digest is bound to another semantic publication',
);
}
return Object.freeze({
status: 'existing' as const,
publication: existing,
});
}
const current = await this.#findCurrent(
client,
publication.target.projectId,
publication.target.packageName,
true,
);
if (publication.version === 1) {
if (current) {
throw new PluginPackageAutomationPublicationConflictError(
'Package already has an automation publication head',
);
}
} else {
if (!current) {
throw new PluginPackageAutomationPublicationConflictError(
'previous automation publication head is absent',
);
}
try {
assertPluginPackageAutomationPublicationSuccessor(
current,
publication,
);
} catch (error) {
if (error instanceof InvalidPluginPackageAutomationPublicationError) {
throw new PluginPackageAutomationPublicationConflictError(
'automation publication does not succeed the current head',
);
}
throw error;
}
}
const revision = await client.query<Row>(
`SELECT revision_digest AS "revisionDigest",
project_id AS "projectId",
package_name AS "packageName",
generation,
lock_digest AS "lockDigest"
FROM "ql3"."plugin_package_materialized_revisions"
WHERE generation_digest = $1
LIMIT 2`,
[publication.target.generationDigest],
);
if (
revision.rows.length !== 1 ||
postgresRequiredString(
revision.rows[0]!.revisionDigest,
unavailable,
) !== publication.target.materializedRevisionDigest ||
postgresRequiredString(revision.rows[0]!.projectId, unavailable) !==
publication.target.projectId ||
postgresRequiredString(
revision.rows[0]!.packageName,
unavailable,
) !== publication.target.packageName ||
postgresRequiredInteger(
revision.rows[0]!.generation,
unavailable,
) !== publication.target.generation ||
postgresRequiredString(revision.rows[0]!.lockDigest, unavailable) !==
publication.target.lockDigest
) {
throw new PluginPackageAutomationPublicationConflictError(
'materialized revision fence does not match publication target',
);
}
const securityFence = await client.query<Row>(
`SELECT
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
WHERE quarantine.project_id = $1
AND quarantine.package_name = $2
AND quarantine.installation_id = $3
AND quarantine.lock_digest = $4
) OR EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
ON revoked.publisher = provenance.publisher
AND revoked.key_id = provenance.key_id
WHERE provenance.installation_id = $3
AND provenance.lock_digest = $4
) AS "blocked"`,
[
publication.target.projectId,
publication.target.packageName,
publication.target.installationId,
publication.target.lockDigest,
],
);
if (
securityFence.rows.length !== 1 ||
typeof securityFence.rows[0]?.blocked !== 'boolean'
) {
throw unavailable();
}
if (securityFence.rows[0].blocked) {
throw new PluginPackageAutomationPublicationConflictError(
'security-fenced Package generation cannot publish automation',
);
}
await client.query(
`INSERT INTO "ql3"."plugin_package_automation_publications" (
publication_digest, project_id, package_name, installation_id,
lock_digest, generation, generation_digest,
materialized_revision_digest, state, version,
previous_publication_digest, lifecycle_event_digest,
published_at_ms, publication_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14::jsonb
)`,
[
publication.publicationDigest,
publication.target.projectId,
publication.target.packageName,
publication.target.installationId,
publication.target.lockDigest,
publication.target.generation,
publication.target.generationDigest,
publication.target.materializedRevisionDigest,
publication.state,
publication.version,
publication.previousPublicationDigest,
publication.lifecycleEventDigest,
publication.publishedAtMs,
publicationJson,
],
);
if (!current) {
await client.query(
`INSERT INTO "ql3"."plugin_package_automation_publication_heads" (
project_id, package_name, publication_digest,
generation_digest, state, version, updated_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
publication.target.projectId,
publication.target.packageName,
publication.publicationDigest,
publication.target.generationDigest,
publication.state,
publication.version,
publication.publishedAtMs,
],
);
} else {
const updated = await client.query(
`UPDATE "ql3"."plugin_package_automation_publication_heads"
SET publication_digest = $1, generation_digest = $2, state = $3,
version = $4, updated_at_ms = $5
WHERE project_id = $6 AND package_name = $7
AND publication_digest = $8 AND version = $9`,
[
publication.publicationDigest,
publication.target.generationDigest,
publication.state,
publication.version,
publication.publishedAtMs,
publication.target.projectId,
publication.target.packageName,
current.publicationDigest,
current.version,
],
);
if (updated.rowCount !== 1) {
throw new PluginPackageAutomationPublicationConflictError(
'automation publication head changed',
);
}
}
return Object.freeze({
status: 'created' as const,
publication,
});
}
async publish(
value: Readonly<PluginPackageAutomationPublication>,
): Promise<
Readonly<{
status: 'created' | 'existing';
publication: Readonly<PluginPackageAutomationPublication>;
}>
> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
if (attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS) continue;
throw unavailable(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await this.publishInTransaction(client, value);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS &&
(state === undefined ||
state.startsWith('08') ||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state))
) {
continue;
}
throw mapStorageError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}
@@ -0,0 +1,598 @@
// PostgreSQL adapter owned by Plugin Package publication and recovery.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageTaskReconciliationError,
PluginPackageTaskReconciliationConflictError,
PluginPackageTaskReconciliationUnavailableError,
normalizePluginPackageTaskReconciliationReceipt,
planPluginPackageTaskReconciliation,
pluginPackageTaskReconciliationTaskIds,
type PluginPackageTaskOwnershipFact,
type PluginPackageTaskReconciliationReceipt,
type PluginPackageTaskReconciliationRepository,
} from '@qinglong/runtime-core/plugin-package-task-reconciliation';
import {
assertPluginPackageTaskPublicationRecoveryPageSize,
normalizePluginPackageTaskPublicationRecoveryCursor,
type PluginPackageTaskPublicationRecoveryPage,
type PluginPackageTaskPublicationRecoverySource,
} from '@qinglong/runtime-core/plugin-package-task-publication';
import {
normalizePluginPackageMaterializedRevision,
type PluginPackageMaterializedRevision,
} from '@qinglong/runtime-core/plugin-package-resource-materialization';
import {
normalizePluginPackageResourceGeneration,
type PluginPackageResourceGenerationSource,
} from '@qinglong/runtime-core/plugin-package-resource-generation';
import {
normalizeTaskDefinitionRecord,
type TaskDefinitionRecord,
} from '@qinglong/runtime-core/task-definition';
import {
compileClusterCommandTaskDefinition,
type ClusterTaskExecutionRevision,
} from '@qinglong/runtime-core/cluster-execution-revision';
import {
BUILT_IN_COMMAND_TASK_SPEC_SCHEMA,
TaskSpecSemanticRegistry,
createBuiltInTaskSpecSemanticRegistry,
} from '@qinglong/runtime-core/task-spec-semantic';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const SELECT_TASK_FIELDS = `
head.project_id AS "projectId",
head.task_id AS "taskId",
revision.revision,
revision.mutation_id AS "mutationId",
revision.name,
revision.description,
revision.kind,
revision.spec_json AS "specJson",
revision.labels_json AS "labelsJson",
revision.enabled,
revision.content_digest AS "contentDigest",
head.created_at_ms AS "createdAtMs",
revision.created_at_ms AS "updatedAtMs"`;
function unavailable(): PluginPackageTaskReconciliationUnavailableError {
return new PluginPackageTaskReconciliationUnavailableError();
}
function taskRecord(row: Row): Readonly<TaskDefinitionRecord> {
try {
const description = row.description;
if (description !== null && typeof description !== 'string') {
throw unavailable();
}
return normalizeTaskDefinitionRecord({
projectId: postgresRequiredString(row.projectId, unavailable),
taskId: postgresRequiredString(row.taskId, unavailable),
revision: postgresRequiredInteger(row.revision, unavailable),
mutationId: postgresRequiredString(row.mutationId, unavailable),
name: postgresRequiredString(row.name, unavailable),
...(description === null ? {} : { description }),
kind: postgresRequiredString(
row.kind,
unavailable,
) as TaskDefinitionRecord['kind'],
spec: postgresRequiredJsonObject(
row.specJson,
unavailable,
) as unknown as TaskDefinitionRecord['spec'],
labels: postgresRequiredJsonObject(
row.labelsJson,
unavailable,
) as TaskDefinitionRecord['labels'],
enabled: postgresRequiredBoolean(row.enabled, unavailable),
contentDigest: postgresRequiredString(row.contentDigest, unavailable),
createdAtMs: postgresRequiredInteger(row.createdAtMs, unavailable),
updatedAtMs: postgresRequiredInteger(row.updatedAtMs, unavailable),
});
} catch (error) {
if (error instanceof PluginPackageTaskReconciliationUnavailableError) {
throw error;
}
throw unavailable();
}
}
function executionPlanJson(
value: Readonly<ClusterTaskExecutionRevision>,
): Readonly<Record<string, unknown>> {
return Object.freeze({
command: value.command,
environment: value.environment,
...(value.workingDirectory === undefined
? {}
: { workingDirectory: value.workingDirectory }),
...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }),
...(value.placement === undefined
? {}
: { placement: value.placement }),
});
}
function executionJson(
value: Readonly<ClusterTaskExecutionRevision>,
): Readonly<Record<string, unknown>> {
return Object.freeze({
projectId: value.projectId,
taskId: value.taskId,
sourceRevision: value.sourceRevision,
taskRevision: value.taskRevision,
sourceContentDigest: value.sourceContentDigest,
executorType: value.executorType,
planSchema: value.planSchema,
planJson: executionPlanJson(value),
contentDigest: value.contentDigest,
createdAtMs: value.createdAtMs,
});
}
function mappedError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageTaskReconciliationError ||
error instanceof PluginPackageTaskReconciliationConflictError ||
error instanceof PluginPackageTaskReconciliationUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (
state === '23503' ||
state === '23505' ||
state === '23514' ||
state === '40001'
) {
return new PluginPackageTaskReconciliationConflictError(
'durable generation or TaskDefinition fence changed',
);
}
return new PluginPackageTaskReconciliationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export class PostgresPluginPackageTaskReconciliationRepository
implements
PluginPackageTaskReconciliationRepository,
PluginPackageTaskPublicationRecoverySource
{
readonly #registry: TaskSpecSemanticRegistry;
constructor(
private readonly pool: PostgresPool,
registry = createBuiltInTaskSpecSemanticRegistry(),
) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function' ||
!(registry instanceof TaskSpecSemanticRegistry)
) {
throw new TypeError(
'PostgreSQL Package Task reconciliation repository options are invalid',
);
}
this.#registry = registry;
}
async #findStored(
queryable: Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>,
generationDigest: string,
): Promise<Readonly<PluginPackageTaskReconciliationReceipt> | null> {
const result = await queryable.query<Row>(
`SELECT receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_task_reconciliations"
WHERE generation_digest = $1
LIMIT 2`,
[generationDigest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
try {
return normalizePluginPackageTaskReconciliationReceipt(
postgresRequiredJsonObject(
result.rows[0]!.receiptJson,
unavailable,
) as unknown as PluginPackageTaskReconciliationReceipt,
);
} catch (error) {
if (error instanceof PluginPackageTaskReconciliationUnavailableError) {
throw error;
}
throw unavailable();
}
}
async find(
generationDigest: string,
): Promise<Readonly<PluginPackageTaskReconciliationReceipt> | null> {
if (
typeof generationDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(generationDigest)
) {
throw new InvalidPluginPackageTaskReconciliationError(
'generationDigest is invalid',
);
}
try {
return await this.#findStored(this.pool, generationDigest);
} catch (error) {
throw mappedError(error);
}
}
async listPendingPage(options: {
readonly limit: number;
readonly after?: Readonly<{
readonly projectId: string;
readonly packageName: string;
}>;
}): Promise<Readonly<PluginPackageTaskPublicationRecoveryPage>> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new InvalidPluginPackageTaskReconciliationError(
'pending page options are invalid',
);
}
assertPluginPackageTaskPublicationRecoveryPageSize(options.limit);
const after =
options.after === undefined
? undefined
: normalizePluginPackageTaskPublicationRecoveryCursor(options.after);
try {
const result = await this.pool.query<Row>(
`SELECT head.project_id AS "projectId",
head.package_name AS "packageName"
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
LEFT JOIN "ql3"."plugin_package_task_reconciliations" AS receipt
ON receipt.project_id = install.project_id
AND receipt.package_name = install.package_name
AND receipt.generation = install.target_generation
AND receipt.lock_digest = install.lock_digest
WHERE install.state = 'active'
AND install.active_lock_digest = install.lock_digest
AND receipt.generation_digest IS NULL
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
ON revoked.publisher = provenance.publisher
AND revoked.key_id = provenance.key_id
WHERE provenance.installation_id = install.installation_id
AND provenance.lock_digest = install.lock_digest
)
AND (
$1::varchar IS NULL OR head.project_id > $1 OR
(head.project_id = $1 AND head.package_name > $2)
)
ORDER BY head.project_id, head.package_name
LIMIT $3`,
[
after?.projectId ?? null,
after?.packageName ?? null,
options.limit + 1,
],
);
const truncated = result.rows.length > options.limit;
const candidates = result.rows.slice(0, options.limit).map((row) =>
Object.freeze({
projectId: postgresRequiredString(row.projectId, unavailable),
packageName: postgresRequiredString(row.packageName, unavailable),
}),
);
const last = candidates.at(-1);
return Object.freeze({
candidates: Object.freeze(candidates),
truncated,
...(truncated && last
? {
next: Object.freeze({
projectId: last.projectId,
packageName: last.packageName,
}),
}
: {}),
});
} catch (error) {
throw mappedError(error);
}
}
async reconcile(
revisionValue: Readonly<PluginPackageMaterializedRevision>,
activeGenerationSource: PluginPackageResourceGenerationSource,
): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<PluginPackageTaskReconciliationReceipt>;
}>
> {
const revision = normalizePluginPackageMaterializedRevision(
revisionValue,
this.#registry,
);
if (
!activeGenerationSource ||
typeof activeGenerationSource.findActiveResourceGeneration !== 'function'
) {
throw new InvalidPluginPackageTaskReconciliationError(
'active generation source is invalid',
);
}
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch {
throw unavailable();
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const existing = await this.#findStored(
client,
revision.generation.generationDigest,
);
if (existing) {
if (
existing.materializedRevisionDigest !== revision.revisionDigest ||
existing.projectId !== revision.generation.projectId ||
existing.packageName !== revision.generation.packageName
) {
throw new PluginPackageTaskReconciliationConflictError(
'generation is bound to another materialized revision',
);
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'existing',
receipt: existing,
});
}
const materialized = await client.query<Row>(
`SELECT revision_digest AS "revisionDigest"
FROM "ql3"."plugin_package_materialized_revisions"
WHERE generation_digest = $1`,
[revision.generation.generationDigest],
);
if (
materialized.rows.length !== 1 ||
postgresRequiredString(
materialized.rows[0]!.revisionDigest,
unavailable,
) !== revision.revisionDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'materialized revision is not durably published',
);
}
const install = await client.query<Row>(
`SELECT install.installation_id AS "installationId",
install.state,
install.target_generation AS "targetGeneration",
install.lock_digest AS "lockDigest",
install.previous_active_lock_digest AS "previousLockDigest"
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
WHERE head.project_id = $1 AND head.package_name = $2
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
ON revoked.publisher = provenance.publisher
AND revoked.key_id = provenance.key_id
WHERE provenance.installation_id = install.installation_id
AND provenance.lock_digest = install.lock_digest
)
FOR UPDATE OF install`,
[
revision.generation.projectId,
revision.generation.packageName,
],
);
const installRow = install.rows[0];
if (
install.rows.length !== 1 ||
!installRow ||
postgresRequiredString(installRow.installationId, unavailable) !==
revision.generation.installationId ||
postgresRequiredString(installRow.state, unavailable) !== 'active' ||
postgresRequiredInteger(installRow.targetGeneration, unavailable) !==
revision.generation.generation ||
postgresRequiredString(installRow.lockDigest, unavailable) !==
revision.generation.lockDigest ||
(installRow.previousLockDigest === null
? null
: postgresRequiredString(
installRow.previousLockDigest,
unavailable,
)) !== revision.generation.previousActiveLockDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'Package install head is not the materialized generation',
);
}
const previousResult =
revision.generation.generation === 1
? { rows: [] as Row[] }
: await client.query<Row>(
`SELECT receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_task_reconciliations"
WHERE project_id = $1 AND package_name = $2
AND generation = $3`,
[
revision.generation.projectId,
revision.generation.packageName,
revision.generation.generation - 1,
],
);
const previous =
previousResult.rows.length === 0
? null
: normalizePluginPackageTaskReconciliationReceipt(
postgresRequiredJsonObject(
previousResult.rows[0]!.receiptJson,
unavailable,
) as unknown as PluginPackageTaskReconciliationReceipt,
);
if (
previousResult.rows.length > 1 ||
(revision.generation.generation > 1 && previous === null)
) {
throw new PluginPackageTaskReconciliationConflictError(
'previous generation receipt is missing',
);
}
const taskIds = pluginPackageTaskReconciliationTaskIds(
revision,
previous,
this.#registry,
);
const facts: PluginPackageTaskOwnershipFact[] = [];
for (const taskId of taskIds) {
const result = await client.query<Row>(
`SELECT
${SELECT_TASK_FIELDS},
ownership.package_name AS "ownerPackageName"
FROM (SELECT 1) AS seed
LEFT JOIN "ql3"."task_definitions" AS head
ON head.project_id = $1 AND head.task_id = $2
LEFT JOIN "ql3"."task_definition_revisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
LEFT JOIN "ql3"."plugin_package_task_ownerships" AS ownership
ON ownership.project_id = $1 AND ownership.task_id = $2`,
[revision.generation.projectId, taskId],
);
if (result.rows.length !== 1) throw unavailable();
const row = result.rows[0]!;
facts.push(
Object.freeze({
taskId,
packageName:
row.ownerPackageName === null
? null
: postgresRequiredString(
row.ownerPackageName,
unavailable,
),
current: row.revision === null ? null : taskRecord(row),
}),
);
}
const clock = await client.query<Row>(
`SELECT floor(
extract(epoch FROM clock_timestamp()) * 1000
)::bigint AS "nowMs"`,
);
if (clock.rows.length !== 1) throw unavailable();
const plan = planPluginPackageTaskReconciliation({
revision,
previousReceipt: previous,
facts: Object.freeze(facts),
committedAtMs: postgresRequiredInteger(
clock.rows[0]!.nowMs,
unavailable,
),
taskSpecSemanticRegistry: this.#registry,
});
const activeValue =
await activeGenerationSource.findActiveResourceGeneration(
revision.generation.projectId,
revision.generation.packageName,
);
if (
activeValue === null ||
normalizePluginPackageResourceGeneration(activeValue)
.generationDigest !== revision.generation.generationDigest
) {
throw new PluginPackageTaskReconciliationConflictError(
'active generation changed during reconciliation',
);
}
const executions = plan.writes.flatMap(({ definition }) =>
definition.enabled &&
definition.kind === 'command' &&
definition.spec.schema === BUILT_IN_COMMAND_TASK_SPEC_SCHEMA
? [
executionJson(
compileClusterCommandTaskDefinition(
definition,
this.#registry,
),
),
]
: [],
);
const committed = await client.query<Row>(
`SELECT "ql3"."commit_plugin_package_task_reconciliation"(
$1::char(64), $2::char(64), $3::jsonb, $4::jsonb, $5::jsonb
) AS "created"`,
[
revision.generation.generationDigest,
revision.revisionDigest,
JSON.stringify(plan.receipt),
JSON.stringify(plan.writes),
JSON.stringify(executions),
],
);
if (
committed.rows.length !== 1 ||
typeof committed.rows[0]!.created !== 'boolean'
) {
throw unavailable();
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: committed.rows[0]!.created ? 'created' : 'existing',
receipt: plan.receipt,
});
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}
@@ -0,0 +1,484 @@
// PostgreSQL Plugin Package publisher revocation proposal authority.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
approvalRequestDigest,
normalizeApprovalRequestRecord,
type ApprovalRequestRecord,
} from '@qinglong/runtime-core/approved-action';
import {
PluginPackagePublisherRevocationProposalConflictError,
PluginPackagePublisherRevocationProposalUnavailableError,
normalizePluginPackagePublisherRevocationProposal,
type CreatePluginPackagePublisherRevocationProposalCommand,
type CreatePluginPackagePublisherRevocationProposalResult,
type PluginPackagePublisherRevocationProposal,
type PluginPackagePublisherRevocationProposalRepository,
} from '@qinglong/runtime-core/plugin-package-publisher-revocation-proposal';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
// Must stay byte-for-byte compatible with publisher provenance mutations.
const SIGNER_ADVISORY_LOCK_SEED = 774635229;
async function publisherSignerLock(
queryable: Queryable,
publisher: string,
keyId: string,
): Promise<void> {
await queryable.query(
`SELECT pg_advisory_xact_lock(hashtextextended($1, $2))`,
[JSON.stringify([publisher, keyId]), SIGNER_ADVISORY_LOCK_SEED],
);
}
function unavailable(
cause?: unknown,
): PluginPackagePublisherRevocationProposalUnavailableError {
return new PluginPackagePublisherRevocationProposalUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function nullableString(value: unknown): string | null {
return value === null ? null : postgresRequiredString(value, unavailable);
}
function nullableInteger(value: unknown): number | null {
return value === null ? null : postgresRequiredInteger(value, unavailable);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackagePublisherRevocationProposalConflictError ||
error instanceof PluginPackagePublisherRevocationProposalUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackagePublisherRevocationProposalConflictError();
}
return unavailable(error);
}
function parseProposal(
row: Row,
): Readonly<PluginPackagePublisherRevocationProposal> {
try {
const proposal = normalizePluginPackagePublisherRevocationProposal(
postgresRequiredJsonObject(
row.proposalJson,
unavailable,
) as unknown as PluginPackagePublisherRevocationProposal,
);
if (
proposal.proposalDigest !==
postgresRequiredString(row.proposalDigest, unavailable)
) {
throw unavailable();
}
return proposal;
} catch (error) {
if (
error instanceof
PluginPackagePublisherRevocationProposalUnavailableError
) {
throw error;
}
throw unavailable(error);
}
}
function parseApprovalRequest(
row: Row,
): Readonly<ApprovalRequestRecord> {
try {
const request = normalizeApprovalRequestRecord(
postgresRequiredJsonObject(
row.requestJson,
unavailable,
) as unknown as ApprovalRequestRecord,
);
if (
approvalRequestDigest(request) !==
postgresRequiredString(row.requestDigest, unavailable)
) {
throw unavailable();
}
return request;
} catch (error) {
if (
error instanceof
PluginPackagePublisherRevocationProposalUnavailableError
) {
throw error;
}
throw unavailable(error);
}
}
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
try {
const subjectType = nullableString(row.subjectType);
const subjectId = nullableString(row.subjectId);
const projectVersion = nullableInteger(row.projectVersion);
if (!Array.isArray(row.reasons)) throw unavailable();
return normalizeSecurityAuditRecord({
eventId: postgresRequiredString(row.eventId, unavailable),
requestId: postgresRequiredString(row.requestId, unavailable),
operationId: postgresRequiredString(row.operationId, unavailable),
projectId: nullableString(row.projectId),
subject:
subjectType === null || subjectId === null
? null
: { type: subjectType, id: subjectId },
authenticationId: nullableString(row.authenticationId),
outcome: postgresRequiredString(row.outcome, unavailable),
reasons: row.reasons,
fence:
projectVersion === null
? null
: {
projectVersion,
bindingVersion: nullableInteger(row.bindingVersion),
},
occurredAtMs: postgresRequiredInteger(row.occurredAtMs, unavailable),
} as SecurityAuditRecord);
} catch (error) {
if (
error instanceof
PluginPackagePublisherRevocationProposalUnavailableError
) {
throw error;
}
throw unavailable(error);
}
}
async function proposalByActionRef(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackagePublisherRevocationProposal> | null> {
const result = await queryable.query<Row>(
`SELECT proposal_json AS "proposalJson",
proposal_digest AS "proposalDigest"
FROM "ql3"."plugin_package_publisher_revocation_proposals"
WHERE action_ref = $1
LIMIT 2`,
[actionRef],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseProposal(result.rows[0]!);
}
async function auditById(
queryable: Queryable,
eventId: string,
): Promise<Readonly<SecurityAuditRecord> | null> {
const result = await queryable.query<Row>(
`SELECT event_id AS "eventId", request_id AS "requestId",
operation_id AS "operationId", project_id AS "projectId",
subject_type AS "subjectType", subject_id AS "subjectId",
authentication_id AS "authenticationId", outcome,
reasons, project_version AS "projectVersion",
binding_version AS "bindingVersion",
occurred_at_ms AS "occurredAtMs"
FROM "ql3"."security_audit_events"
WHERE event_id = $1
LIMIT 2`,
[eventId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseAudit(result.rows[0]!);
}
export function findPostgresPluginPackagePublisherRevocationProposal(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackagePublisherRevocationProposal> | null> {
return proposalByActionRef(queryable, actionRef);
}
export class PostgresPluginPackagePublisherRevocationProposalRepository
implements PluginPackagePublisherRevocationProposalRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL publisher revocation proposal pool is invalid',
);
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async findProposalByActionRef(
actionRef: string,
): Promise<Readonly<PluginPackagePublisherRevocationProposal> | null> {
try {
return await proposalByActionRef(this.pool, actionRef);
} catch (error) {
throw mappedError(error);
}
}
async listApprovedRequests(
limitValue: number,
): Promise<readonly Readonly<ApprovalRequestRecord>[]> {
if (
!Number.isSafeInteger(limitValue) ||
limitValue < 1 ||
limitValue > 64
) {
throw new TypeError(
'publisher revocation approval page limit is invalid',
);
}
try {
const result = await this.pool.query<Row>(
`SELECT request.request_json AS "requestJson",
request.request_digest AS "requestDigest"
FROM "ql3"."approval_requests" AS request
JOIN "ql3"."plugin_package_publisher_revocation_proposals"
AS proposal
ON proposal.action_ref = request.action_ref
WHERE request.state = 'approved'
AND request.action_type =
'plugin_package.publisher_key.revoke'
ORDER BY request.updated_at_ms, request.request_id
LIMIT $1`,
[limitValue],
);
if (result.rows.length > limitValue) throw unavailable();
return Object.freeze(result.rows.map(parseApprovalRequest));
} catch (error) {
throw mappedError(error);
}
}
createProposal(
command: CreatePluginPackagePublisherRevocationProposalCommand,
): Promise<
Readonly<CreatePluginPackagePublisherRevocationProposalResult>
> {
const proposal = normalizePluginPackagePublisherRevocationProposal(
command.proposal,
);
const audit = normalizeSecurityAuditRecord(command.audit);
if (
audit.requestId !== proposal.actionRef ||
audit.operationId !==
'plugin_package.publisher_revocation.propose' ||
audit.projectId !== proposal.projectId ||
audit.subject?.type !== proposal.proposedBy.type ||
audit.subject.id !== proposal.proposedBy.id ||
audit.authenticationId === null ||
audit.outcome !== 'allowed' ||
!same(audit.reasons, ['publisher_revocation_proposal']) ||
audit.fence?.projectVersion !==
proposal.proposalFence.projectVersion ||
audit.fence.bindingVersion !==
proposal.proposalFence.bindingVersion ||
audit.occurredAtMs !== proposal.createdAtMs
) {
throw new PluginPackagePublisherRevocationProposalConflictError();
}
return this.#transaction(async (client) => {
const existing = await proposalByActionRef(
client,
proposal.actionRef,
);
if (existing) {
const existingAudit = await auditById(client, audit.eventId);
if (
!same(existing, proposal) ||
!existingAudit ||
!same(existingAudit, audit)
) {
throw new PluginPackagePublisherRevocationProposalConflictError();
}
return Object.freeze({
status: 'existing' as const,
proposal,
});
}
await publisherSignerLock(
client,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
);
const trust = await client.query<Row>(
`SELECT generation,
effective_trust_digest AS "effectiveTrustDigest"
FROM "ql3"."plugin_package_publisher_trust_heads"
WHERE authority_id = $1
LIMIT 2`,
[proposal.actionInput.trustAuthorityId],
);
if (
trust.rows.length !== 1 ||
postgresRequiredInteger(
trust.rows[0]!.generation,
unavailable,
) !== proposal.actionInput.trustGeneration ||
postgresRequiredString(
trust.rows[0]!.effectiveTrustDigest,
unavailable,
) !== proposal.actionInput.previousTrustDigest
) {
throw new PluginPackagePublisherRevocationProposalConflictError();
}
const fence = await client.query<Row>(
`SELECT "ql3"."lock_approval_policy_fence"(
$1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
) AS "matches"`,
[
proposal.projectId,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
],
);
if (
fence.rows.length !== 1 ||
!postgresRequiredBoolean(fence.rows[0]!.matches, unavailable)
) {
throw new PluginPackagePublisherRevocationProposalConflictError();
}
const action = proposal.actionInput;
const inserted = await client.query(
`INSERT INTO
"ql3"."plugin_package_publisher_revocation_proposals" (
action_ref, project_id, authority_id, trust_generation,
publisher, key_id, previous_trust_digest,
current_trust_digest, action_type, permission, action_digest,
preview_digest, authorization_mode, reason_code,
proposed_by_type, proposed_by_id, proposer_assurance,
fence_project_version, fence_binding_version, created_at_ms,
proposal_json, proposal_digest
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14, $15, $16, $17, $18, $19, $20, $21::jsonb, $22
)`,
[
proposal.actionRef,
proposal.projectId,
action.trustAuthorityId,
action.trustGeneration,
action.publisher,
action.keyId,
action.previousTrustDigest,
action.currentTrustDigest,
proposal.actionType,
proposal.permission,
proposal.actionDigest,
proposal.previewDigest,
action.authorizationMode,
action.reasonCode,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposerAssurance,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
proposal.createdAtMs,
JSON.stringify(proposal),
proposal.proposalDigest,
],
);
if (inserted.rowCount !== 1) throw unavailable();
const auditInserted = await client.query(
`INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type,
subject_id, authentication_id, outcome, reasons,
project_version, binding_version, occurred_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
)`,
[
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
],
);
if (auditInserted.rowCount !== 1) throw unavailable();
return Object.freeze({ status: 'created' as const, proposal });
});
}
}
@@ -0,0 +1,288 @@
// PostgreSQL Plugin Package publisher trust observation authority.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
PluginPackagePublisherTrustAuthorityConflictError,
PluginPackagePublisherTrustAuthorityUnavailableError,
createPluginPackagePublisherTrustHead,
normalizePluginPackagePublisherTrustHead,
normalizePluginPackagePublisherTrustSnapshot,
type ObservePluginPackagePublisherTrustSnapshotInput,
type ObservePluginPackagePublisherTrustSnapshotResult,
type PluginPackagePublisherTrustAuthorityRepository,
type PluginPackagePublisherTrustAuthorityState,
type PluginPackagePublisherTrustHead,
type PluginPackagePublisherTrustSnapshot,
} from '@qinglong/runtime-core/plugin-package-publisher-trust';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
const AUTHORITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
function unavailable(
cause?: unknown,
): PluginPackagePublisherTrustAuthorityUnavailableError {
return new PluginPackagePublisherTrustAuthorityUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function authorityId(value: unknown, label: string): string {
if (typeof value !== 'string' || !AUTHORITY_PATTERN.test(value)) {
throw new TypeError(`${label} is invalid`);
}
return value;
}
function timestamp(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new TypeError('publisher trust observation time is invalid');
}
return value as number;
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackagePublisherTrustAuthorityConflictError ||
error instanceof PluginPackagePublisherTrustAuthorityUnavailableError ||
error instanceof TypeError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackagePublisherTrustAuthorityConflictError();
}
return unavailable(error);
}
function parseHead(row: Row): Readonly<PluginPackagePublisherTrustHead> {
try {
const head = normalizePluginPackagePublisherTrustHead(
postgresRequiredJsonObject(
row.headJson,
unavailable,
) as unknown as PluginPackagePublisherTrustHead,
);
if (
head.headDigest !==
postgresRequiredString(row.headDigest, unavailable)
) {
throw unavailable();
}
return head;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustAuthorityUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseSnapshot(
row: Row,
): Readonly<PluginPackagePublisherTrustSnapshot> {
try {
const snapshot = normalizePluginPackagePublisherTrustSnapshot(
postgresRequiredJsonObject(
row.snapshotJson,
unavailable,
) as unknown as PluginPackagePublisherTrustSnapshot,
);
if (
snapshot.snapshotDigest !==
postgresRequiredString(row.snapshotDigest, unavailable)
) {
throw unavailable();
}
return snapshot;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustAuthorityUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async function authorityById(
queryable: Queryable,
value: string,
forUpdate = false,
): Promise<Readonly<PluginPackagePublisherTrustAuthorityState> | null> {
const result = await queryable.query<Row>(
`SELECT head.head_json AS "headJson",
head.head_digest AS "headDigest",
snapshot.snapshot_json AS "snapshotJson",
snapshot.snapshot_digest AS "snapshotDigest"
FROM "ql3"."plugin_package_publisher_trust_heads" AS head
JOIN "ql3"."plugin_package_publisher_trust_snapshots" AS snapshot
ON snapshot.snapshot_digest = head.effective_trust_digest
WHERE head.authority_id = $1
LIMIT 2${forUpdate ? ' FOR UPDATE OF head' : ''}`,
[value],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return Object.freeze({
head: parseHead(result.rows[0]!),
effectiveSnapshot: parseSnapshot(result.rows[0]!),
});
}
export class PostgresPluginPackagePublisherTrustAuthorityRepository
implements PluginPackagePublisherTrustAuthorityRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError('PostgreSQL publisher trust pool is invalid');
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async findAuthority(
authorityIdValue: string,
): Promise<Readonly<PluginPackagePublisherTrustAuthorityState> | null> {
const id = authorityId(authorityIdValue, 'publisher trust authorityId');
try {
return await authorityById(this.pool, id);
} catch (error) {
throw mappedError(error);
}
}
observeSnapshot(
input: ObservePluginPackagePublisherTrustSnapshotInput,
): Promise<Readonly<ObservePluginPackagePublisherTrustSnapshotResult>> {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
return Promise.reject(
new TypeError('publisher trust observation is invalid'),
);
}
const id = authorityId(input.authorityId, 'publisher trust authorityId');
const observedBy = authorityId(
input.observedBy,
'publisher trust observer',
);
const observedAtMs = timestamp(input.observedAtMs);
const snapshot = normalizePluginPackagePublisherTrustSnapshot(
input.snapshot,
);
if (snapshot.keys.length < 1) {
return Promise.reject(
new TypeError('publisher base trust snapshot must contain one key'),
);
}
const initialHead = createPluginPackagePublisherTrustHead(
id,
snapshot,
observedAtMs,
);
return this.#transaction(async (client) => {
await client.query(
`INSERT INTO "ql3"."plugin_package_publisher_trust_snapshots" (
snapshot_digest, key_count, observed_by, observed_at_ms,
snapshot_json
) VALUES ($1, $2, $3, $4, $5::jsonb)
ON CONFLICT (snapshot_digest) DO NOTHING`,
[
snapshot.snapshotDigest,
snapshot.keys.length,
observedBy,
observedAtMs,
JSON.stringify(snapshot),
],
);
const inserted = await client.query(
`INSERT INTO "ql3"."plugin_package_publisher_trust_heads" (
authority_id, generation, base_snapshot_digest,
effective_trust_digest, updated_at_ms, head_digest, head_json
) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)
ON CONFLICT (authority_id) DO NOTHING`,
[
initialHead.authorityId,
initialHead.generation,
initialHead.baseSnapshotDigest,
initialHead.effectiveTrustDigest,
initialHead.updatedAtMs,
initialHead.headDigest,
JSON.stringify(initialHead),
],
);
if (inserted.rowCount === 1) {
return Object.freeze({
status: 'created' as const,
head: initialHead,
effectiveSnapshot: snapshot,
});
}
if (inserted.rowCount !== 0) throw unavailable();
const existing = await authorityById(client, id);
if (!existing) {
throw new PluginPackagePublisherTrustAuthorityConflictError();
}
return Object.freeze({
status:
existing.head.baseSnapshotDigest === snapshot.snapshotDigest
? ('existing' as const)
: ('candidate' as const),
...existing,
});
});
}
}
@@ -0,0 +1,594 @@
// PostgreSQL Plugin Package publisher trust-transition proposal authority.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
approvalRequestDigest,
normalizeApprovalRequestRecord,
type ApprovalRequestRecord,
} from '@qinglong/runtime-core/approved-action';
import {
createPluginPackagePublisherTrustOverlapAdditionSnapshot,
createPluginPackagePublisherTrustRetirementSnapshot,
normalizePluginPackagePublisherTrustSnapshot,
type PluginPackagePublisherTrustSnapshot,
} from '@qinglong/runtime-core/plugin-package-publisher-trust';
import {
PluginPackagePublisherTrustTransitionConflictError,
PluginPackagePublisherTrustTransitionUnavailableError,
normalizePluginPackagePublisherTrustTransitionProposal,
type CreatePluginPackagePublisherTrustTransitionProposalCommand,
type CreatePluginPackagePublisherTrustTransitionProposalResult,
type PluginPackagePublisherTrustTransitionProposal,
type PluginPackagePublisherTrustTransitionProposalRepository,
} from '@qinglong/runtime-core/plugin-package-publisher-trust-transition-proposal';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
// Must stay byte-for-byte compatible with provenance and revocation mutations.
const SIGNER_ADVISORY_LOCK_SEED = 774635229;
async function publisherSignerLock(
queryable: Queryable,
publisher: string,
keyId: string,
): Promise<void> {
await queryable.query(
`SELECT pg_advisory_xact_lock(hashtextextended($1, $2))`,
[JSON.stringify([publisher, keyId]), SIGNER_ADVISORY_LOCK_SEED],
);
}
function unavailable(
cause?: unknown,
): PluginPackagePublisherTrustTransitionUnavailableError {
return new PluginPackagePublisherTrustTransitionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function nullableString(value: unknown): string | null {
return value === null ? null : postgresRequiredString(value, unavailable);
}
function nullableInteger(value: unknown): number | null {
return value === null ? null : postgresRequiredInteger(value, unavailable);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackagePublisherTrustTransitionConflictError ||
error instanceof PluginPackagePublisherTrustTransitionUnavailableError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackagePublisherTrustTransitionConflictError();
}
return unavailable(error);
}
function parseProposal(
row: Row,
): Readonly<PluginPackagePublisherTrustTransitionProposal> {
try {
const proposal = normalizePluginPackagePublisherTrustTransitionProposal(
postgresRequiredJsonObject(
row.proposalJson,
unavailable,
) as unknown as PluginPackagePublisherTrustTransitionProposal,
);
if (
proposal.proposalDigest !==
postgresRequiredString(row.proposalDigest, unavailable)
) {
throw unavailable();
}
return proposal;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseSnapshot(
row: Row,
): Readonly<PluginPackagePublisherTrustSnapshot> {
try {
const snapshot = normalizePluginPackagePublisherTrustSnapshot(
postgresRequiredJsonObject(
row.snapshotJson,
unavailable,
) as unknown as PluginPackagePublisherTrustSnapshot,
);
if (
snapshot.snapshotDigest !==
postgresRequiredString(row.snapshotDigest, unavailable)
) {
throw unavailable();
}
return snapshot;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseApprovalRequest(
row: Row,
): Readonly<ApprovalRequestRecord> {
try {
const request = normalizeApprovalRequestRecord(
postgresRequiredJsonObject(
row.requestJson,
unavailable,
) as unknown as ApprovalRequestRecord,
);
if (
approvalRequestDigest(request) !==
postgresRequiredString(row.requestDigest, unavailable)
) {
throw unavailable();
}
return request;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
try {
const subjectType = nullableString(row.subjectType);
const subjectId = nullableString(row.subjectId);
const projectVersion = nullableInteger(row.projectVersion);
if (!Array.isArray(row.reasons)) throw unavailable();
return normalizeSecurityAuditRecord({
eventId: postgresRequiredString(row.eventId, unavailable),
requestId: postgresRequiredString(row.requestId, unavailable),
operationId: postgresRequiredString(row.operationId, unavailable),
projectId: nullableString(row.projectId),
subject:
subjectType === null || subjectId === null
? null
: { type: subjectType, id: subjectId },
authenticationId: nullableString(row.authenticationId),
outcome: postgresRequiredString(row.outcome, unavailable),
reasons: row.reasons,
fence:
projectVersion === null
? null
: {
projectVersion,
bindingVersion: nullableInteger(row.bindingVersion),
},
occurredAtMs: postgresRequiredInteger(row.occurredAtMs, unavailable),
} as SecurityAuditRecord);
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async function proposalByActionRef(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackagePublisherTrustTransitionProposal> | null> {
const result = await queryable.query<Row>(
`SELECT proposal_json AS "proposalJson",
proposal_digest AS "proposalDigest"
FROM "ql3"."plugin_package_publisher_trust_transition_proposals"
WHERE action_ref = $1
LIMIT 2`,
[actionRef],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseProposal(result.rows[0]!);
}
async function snapshotByDigest(
queryable: Queryable,
snapshotDigest: string,
): Promise<Readonly<PluginPackagePublisherTrustSnapshot> | null> {
const result = await queryable.query<Row>(
`SELECT snapshot_json AS "snapshotJson",
snapshot_digest AS "snapshotDigest"
FROM "ql3"."plugin_package_publisher_trust_snapshots"
WHERE snapshot_digest = $1
LIMIT 2`,
[snapshotDigest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseSnapshot(result.rows[0]!);
}
async function auditById(
queryable: Queryable,
eventId: string,
): Promise<Readonly<SecurityAuditRecord> | null> {
const result = await queryable.query<Row>(
`SELECT event_id AS "eventId", request_id AS "requestId",
operation_id AS "operationId", project_id AS "projectId",
subject_type AS "subjectType", subject_id AS "subjectId",
authentication_id AS "authenticationId", outcome,
reasons, project_version AS "projectVersion",
binding_version AS "bindingVersion",
occurred_at_ms AS "occurredAtMs"
FROM "ql3"."security_audit_events"
WHERE event_id = $1
LIMIT 2`,
[eventId],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseAudit(result.rows[0]!);
}
export function findPostgresPluginPackagePublisherTrustTransitionProposal(
queryable: Queryable,
actionRef: string,
): Promise<Readonly<PluginPackagePublisherTrustTransitionProposal> | null> {
return proposalByActionRef(queryable, actionRef);
}
export class PostgresPluginPackagePublisherTrustTransitionProposalRepository
implements PluginPackagePublisherTrustTransitionProposalRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL publisher trust transition proposal pool is invalid',
);
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async findProposalByActionRef(
actionRef: string,
): Promise<Readonly<PluginPackagePublisherTrustTransitionProposal> | null> {
try {
return await proposalByActionRef(this.pool, actionRef);
} catch (error) {
throw mappedError(error);
}
}
async listApprovedRequests(
limitValue: number,
): Promise<readonly Readonly<ApprovalRequestRecord>[]> {
if (
!Number.isSafeInteger(limitValue) ||
limitValue < 1 ||
limitValue > 64
) {
throw new TypeError(
'publisher trust transition approval page limit is invalid',
);
}
try {
const result = await this.pool.query<Row>(
`SELECT request.request_json AS "requestJson",
request.request_digest AS "requestDigest"
FROM "ql3"."approval_requests" AS request
JOIN "ql3"."plugin_package_publisher_trust_transition_proposals"
AS proposal
ON proposal.action_ref = request.action_ref
WHERE request.state = 'approved'
AND request.request_json ->> 'decisionMode' =
'separation_of_duty'
AND request.action_type IN (
'plugin_package.publisher_key.overlap_add',
'plugin_package.publisher_key.safe_retire'
)
ORDER BY request.updated_at_ms, request.request_id
LIMIT $1`,
[limitValue],
);
if (result.rows.length > limitValue) throw unavailable();
return Object.freeze(result.rows.map(parseApprovalRequest));
} catch (error) {
throw mappedError(error);
}
}
createProposal(
command: CreatePluginPackagePublisherTrustTransitionProposalCommand,
): Promise<
Readonly<CreatePluginPackagePublisherTrustTransitionProposalResult>
> {
const proposal =
normalizePluginPackagePublisherTrustTransitionProposal(
command.proposal,
);
const candidateSnapshot =
normalizePluginPackagePublisherTrustSnapshot(
command.candidateSnapshot,
);
const audit = normalizeSecurityAuditRecord(command.audit);
if (
candidateSnapshot.snapshotDigest !==
proposal.actionInput.currentTrustDigest ||
audit.requestId !== proposal.actionRef ||
audit.operationId !==
'plugin_package.publisher_trust_transition.propose' ||
audit.projectId !== proposal.projectId ||
audit.subject?.type !== proposal.proposedBy.type ||
audit.subject.id !== proposal.proposedBy.id ||
audit.authenticationId === null ||
audit.outcome !== 'allowed' ||
!same(audit.reasons, ['publisher_trust_transition_proposal']) ||
audit.fence?.projectVersion !==
proposal.proposalFence.projectVersion ||
audit.fence.bindingVersion !==
proposal.proposalFence.bindingVersion ||
audit.occurredAtMs !== proposal.createdAtMs
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
return this.#transaction(async (client) => {
const existing = await proposalByActionRef(
client,
proposal.actionRef,
);
if (existing) {
const [existingAudit, existingCandidate] = await Promise.all([
auditById(client, audit.eventId),
snapshotByDigest(client, candidateSnapshot.snapshotDigest),
]);
if (
!same(existing, proposal) ||
!existingAudit ||
!same(existingAudit, audit) ||
!existingCandidate ||
!same(existingCandidate, candidateSnapshot)
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
return Object.freeze({
status: 'existing' as const,
proposal,
});
}
await publisherSignerLock(
client,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
);
const trust = await client.query<Row>(
`SELECT head.generation,
head.effective_trust_digest AS "effectiveTrustDigest",
snapshot.snapshot_json AS "snapshotJson",
snapshot.snapshot_digest AS "snapshotDigest"
FROM "ql3"."plugin_package_publisher_trust_heads" AS head
JOIN "ql3"."plugin_package_publisher_trust_snapshots" AS snapshot
ON snapshot.snapshot_digest = head.effective_trust_digest
WHERE head.authority_id = $1
LIMIT 2`,
[proposal.actionInput.trustAuthorityId],
);
if (
trust.rows.length !== 1 ||
postgresRequiredInteger(
trust.rows[0]!.generation,
unavailable,
) !== proposal.actionInput.trustGeneration ||
postgresRequiredString(
trust.rows[0]!.effectiveTrustDigest,
unavailable,
) !== proposal.actionInput.previousTrustDigest
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const effectiveSnapshot = parseSnapshot(trust.rows[0]!);
let expectedCandidate: Readonly<PluginPackagePublisherTrustSnapshot>;
try {
expectedCandidate =
proposal.actionInput.mode === 'overlap_add'
? createPluginPackagePublisherTrustOverlapAdditionSnapshot(
effectiveSnapshot,
candidateSnapshot,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
proposal.createdAtMs,
)
: createPluginPackagePublisherTrustRetirementSnapshot(
effectiveSnapshot,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
proposal.createdAtMs,
);
} catch {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
if (!same(expectedCandidate, candidateSnapshot)) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const fence = await client.query<Row>(
`SELECT "ql3"."lock_approval_policy_fence"(
$1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
) AS "matches"`,
[
proposal.projectId,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
],
);
if (
fence.rows.length !== 1 ||
!postgresRequiredBoolean(fence.rows[0]!.matches, unavailable)
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const snapshotInsert = await client.query(
`INSERT INTO "ql3"."plugin_package_publisher_trust_snapshots" (
snapshot_digest, key_count, observed_by, observed_at_ms,
snapshot_json
) VALUES ($1, $2, $3, $4, $5::jsonb)
ON CONFLICT (snapshot_digest) DO NOTHING`,
[
candidateSnapshot.snapshotDigest,
candidateSnapshot.keys.length,
'cluster-package-manager',
proposal.createdAtMs,
JSON.stringify(candidateSnapshot),
],
);
if (
snapshotInsert.rowCount !== 0 &&
snapshotInsert.rowCount !== 1
) {
throw unavailable();
}
const storedCandidate = await snapshotByDigest(
client,
candidateSnapshot.snapshotDigest,
);
if (!storedCandidate || !same(storedCandidate, candidateSnapshot)) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const action = proposal.actionInput;
const inserted = await client.query(
`INSERT INTO
"ql3"."plugin_package_publisher_trust_transition_proposals" (
action_ref, project_id, authority_id, trust_generation, mode,
publisher, key_id, previous_trust_digest,
current_trust_digest, action_type, permission, action_digest,
preview_digest, proposed_by_type, proposed_by_id,
proposer_assurance, fence_project_version,
fence_binding_version, created_at_ms, proposal_json,
proposal_digest
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14, $15, $16, $17, $18, $19, $20::jsonb, $21
)`,
[
proposal.actionRef,
proposal.projectId,
action.trustAuthorityId,
action.trustGeneration,
action.mode,
action.publisher,
action.keyId,
action.previousTrustDigest,
action.currentTrustDigest,
proposal.actionType,
proposal.permission,
proposal.actionDigest,
proposal.previewDigest,
proposal.proposedBy.type,
proposal.proposedBy.id,
proposal.proposerAssurance,
proposal.proposalFence.projectVersion,
proposal.proposalFence.bindingVersion,
proposal.createdAtMs,
JSON.stringify(proposal),
proposal.proposalDigest,
],
);
if (inserted.rowCount !== 1) throw unavailable();
const auditInserted = await client.query(
`INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type,
subject_id, authentication_id, outcome, reasons,
project_version, binding_version, occurred_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
)`,
[
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
],
);
if (auditInserted.rowCount !== 1) throw unavailable();
return Object.freeze({ status: 'created' as const, proposal });
});
}
}
@@ -0,0 +1,583 @@
// PostgreSQL Plugin Package publisher trust-transition execution authority.
import type {
PostgresClient,
PostgresPool,
} from '@qinglong/runtime-core';
import {
approvalRequestDigest,
approvedActionDispatchDigest,
normalizeApprovalRequestRecord,
normalizeApprovedActionDispatchRecord,
type ApprovalRequestRecord,
type ApprovedActionDispatchRecord,
} from '@qinglong/runtime-core/approved-action';
import {
advancePluginPackagePublisherTrustHead,
createPluginPackagePublisherTrustOverlapAdditionSnapshot,
createPluginPackagePublisherTrustRetirementSnapshot,
normalizePluginPackagePublisherTrustHead,
normalizePluginPackagePublisherTrustSnapshot,
type PluginPackagePublisherTrustHead,
type PluginPackagePublisherTrustSnapshot,
} from '@qinglong/runtime-core/plugin-package-publisher-trust';
import {
PluginPackagePublisherTrustTransitionConflictError,
PluginPackagePublisherTrustTransitionUnavailableError,
normalizePluginPackagePublisherTrustTransitionProposal,
normalizePluginPackagePublisherTrustTransitionReceipt,
resolvePluginPackagePublisherTrustTransitionProposal,
type PluginPackagePublisherTrustTransitionProposal,
type PluginPackagePublisherTrustTransitionReceipt,
} from '@qinglong/runtime-core/plugin-package-publisher-trust-transition-proposal';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
const SIGNER_ADVISORY_LOCK_SEED = 774635229;
export interface ApplyPostgresPluginPackagePublisherTrustTransitionInput {
readonly dispatch: ApprovedActionDispatchRecord;
readonly executedAtMs: number;
}
export interface ApplyPostgresPluginPackagePublisherTrustTransitionResult {
readonly status: 'created' | 'existing';
readonly receipt: Readonly<PluginPackagePublisherTrustTransitionReceipt>;
readonly head: Readonly<PluginPackagePublisherTrustHead>;
}
function unavailable(
cause?: unknown,
): PluginPackagePublisherTrustTransitionUnavailableError {
return new PluginPackagePublisherTrustTransitionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackagePublisherTrustTransitionConflictError ||
error instanceof PluginPackagePublisherTrustTransitionUnavailableError ||
error instanceof TypeError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackagePublisherTrustTransitionConflictError();
}
return unavailable(error);
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function timestamp(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new TypeError('publisher trust transition execution time is invalid');
}
return value as number;
}
function parseProposal(
row: Row,
): Readonly<PluginPackagePublisherTrustTransitionProposal> {
try {
const proposal = normalizePluginPackagePublisherTrustTransitionProposal(
postgresRequiredJsonObject(
row.proposalJson,
unavailable,
) as unknown as PluginPackagePublisherTrustTransitionProposal,
);
if (
proposal.proposalDigest !==
postgresRequiredString(row.proposalDigest, unavailable)
) {
throw unavailable();
}
return proposal;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseDispatch(
row: Row,
): Readonly<ApprovedActionDispatchRecord> {
try {
const dispatch = normalizeApprovedActionDispatchRecord(
postgresRequiredJsonObject(
row.dispatchJson,
unavailable,
) as unknown as ApprovedActionDispatchRecord,
);
if (
approvedActionDispatchDigest(dispatch) !==
postgresRequiredString(row.dispatchDigest, unavailable)
) {
throw unavailable();
}
return dispatch;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseApproval(
row: Row,
): Readonly<ApprovalRequestRecord> {
try {
const approval = normalizeApprovalRequestRecord(
postgresRequiredJsonObject(
row.approvalJson,
unavailable,
) as unknown as ApprovalRequestRecord,
);
if (
approvalRequestDigest(approval) !==
postgresRequiredString(row.approvalDigest, unavailable)
) {
throw unavailable();
}
return approval;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseHead(row: Row): Readonly<PluginPackagePublisherTrustHead> {
try {
const head = normalizePluginPackagePublisherTrustHead(
postgresRequiredJsonObject(
row.headJson,
unavailable,
) as unknown as PluginPackagePublisherTrustHead,
);
if (
head.headDigest !== postgresRequiredString(row.headDigest, unavailable)
) {
throw unavailable();
}
return head;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseSnapshot(
row: Row,
jsonKey: string,
digestKey: string,
): Readonly<PluginPackagePublisherTrustSnapshot> {
try {
const snapshot = normalizePluginPackagePublisherTrustSnapshot(
postgresRequiredJsonObject(
row[jsonKey],
unavailable,
) as unknown as PluginPackagePublisherTrustSnapshot,
);
if (
snapshot.snapshotDigest !==
postgresRequiredString(row[digestKey], unavailable)
) {
throw unavailable();
}
return snapshot;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
function parseReceipt(
row: Row,
): Readonly<PluginPackagePublisherTrustTransitionReceipt> {
try {
const receipt = normalizePluginPackagePublisherTrustTransitionReceipt(
postgresRequiredJsonObject(
row.receiptJson,
unavailable,
) as unknown as PluginPackagePublisherTrustTransitionReceipt,
);
if (
receipt.receiptDigest !==
postgresRequiredString(row.receiptDigest, unavailable)
) {
throw unavailable();
}
return receipt;
} catch (error) {
if (error instanceof PluginPackagePublisherTrustTransitionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async function publisherSignerLock(
queryable: Queryable,
publisher: string,
keyId: string,
): Promise<void> {
await queryable.query(
`SELECT pg_advisory_xact_lock(hashtextextended($1, $2))`,
[JSON.stringify([publisher, keyId]), SIGNER_ADVISORY_LOCK_SEED],
);
}
export class PostgresPluginPackagePublisherTrustTransitionRepository {
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL publisher trust transition pool is invalid',
);
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
applyApprovedTransition(
input: ApplyPostgresPluginPackagePublisherTrustTransitionInput,
): Promise<
Readonly<ApplyPostgresPluginPackagePublisherTrustTransitionResult>
> {
if (
!input ||
typeof input !== 'object' ||
Array.isArray(input) ||
Object.keys(input).length !== 2 ||
!Object.hasOwn(input, 'dispatch') ||
!Object.hasOwn(input, 'executedAtMs')
) {
throw new TypeError('publisher trust transition execution is invalid');
}
const dispatch = normalizeApprovedActionDispatchRecord(input.dispatch);
const executedAtMs = timestamp(input.executedAtMs);
return this.#transaction(async (client) => {
const authority = await client.query<Row>(
`SELECT proposal.proposal_json AS "proposalJson",
proposal.proposal_digest AS "proposalDigest",
dispatch.dispatch_json AS "dispatchJson",
dispatch.dispatch_digest AS "dispatchDigest",
request.request_json AS "approvalJson",
request.request_digest AS "approvalDigest"
FROM "ql3"."approved_action_dispatches" AS dispatch
JOIN "ql3"."plugin_package_publisher_trust_transition_proposals"
AS proposal
ON proposal.action_ref = dispatch.action_ref
JOIN "ql3"."approval_requests" AS request
ON request.dispatch_id = dispatch.dispatch_id
WHERE dispatch.dispatch_id = $1
LIMIT 2`,
[dispatch.id],
);
if (authority.rows.length !== 1) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const proposal = parseProposal(authority.rows[0]!);
const durableDispatch = parseDispatch(authority.rows[0]!);
const approval = parseApproval(authority.rows[0]!);
if (
!same(durableDispatch, dispatch) ||
approval.state !== 'consumed' ||
approval.version !== 3 ||
approval.decisionMode !== 'separation_of_duty' ||
approval.decision !== 'approved' ||
approval.id !== durableDispatch.approvalRequestId ||
approval.dispatchId !== durableDispatch.id ||
approval.projectId !== durableDispatch.projectId ||
!same(approval.action, durableDispatch.action) ||
!same(approval.requestedBy, durableDispatch.requestedBy) ||
!same(approval.consumedBy, durableDispatch.consumedBy) ||
!same(approval.decidedBy, durableDispatch.approvedBy) ||
approval.decisionAuthenticationId !==
durableDispatch.approvalAuthenticationId ||
approval.decisionAssurance !==
durableDispatch.approvalAssurance ||
approval.decidedAtMs !== durableDispatch.approvedAtMs ||
approval.expiresAtMs !== durableDispatch.expiresAtMs ||
!same(approval.decisionFence, durableDispatch.approvalFence) ||
approval.consumedAtMs !== durableDispatch.createdAtMs
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
await publisherSignerLock(
client,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
);
const existingReceipt = await client.query<Row>(
`SELECT receipt_json AS "receiptJson",
receipt_digest AS "receiptDigest"
FROM "ql3"."plugin_package_publisher_trust_transition_receipts"
WHERE mutation_id = $1 OR proposal_digest = $2
ORDER BY receipt_digest
LIMIT 2`,
[dispatch.id, proposal.proposalDigest],
);
if (existingReceipt.rows.length > 0) {
if (existingReceipt.rows.length !== 1) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const receipt = parseReceipt(existingReceipt.rows[0]!);
const expected = resolvePluginPackagePublisherTrustTransitionProposal(
proposal,
dispatch,
executedAtMs,
proposal.actionInput.mode === 'safe_retire' ? 0 : null,
);
if (!same(receipt, expected)) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const current = await client.query<Row>(
`SELECT head_json AS "headJson", head_digest AS "headDigest"
FROM "ql3"."plugin_package_publisher_trust_heads"
WHERE authority_id = $1
LIMIT 2`,
[receipt.trustAuthorityId],
);
if (current.rows.length !== 1) throw unavailable();
const head = parseHead(current.rows[0]!);
if (
head.generation !== receipt.currentGeneration ||
head.effectiveTrustDigest !== receipt.currentTrustDigest
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
return Object.freeze({
status: 'existing' as const,
receipt,
head,
});
}
const trust = await client.query<Row>(
`SELECT head.head_json AS "headJson",
head.head_digest AS "headDigest",
effective.snapshot_json AS "effectiveSnapshotJson",
effective.snapshot_digest AS "effectiveSnapshotDigest",
candidate.snapshot_json AS "candidateSnapshotJson",
candidate.snapshot_digest AS "candidateSnapshotDigest"
FROM "ql3"."plugin_package_publisher_trust_heads" AS head
JOIN "ql3"."plugin_package_publisher_trust_snapshots" AS effective
ON effective.snapshot_digest = head.effective_trust_digest
JOIN "ql3"."plugin_package_publisher_trust_snapshots" AS candidate
ON candidate.snapshot_digest = $2
WHERE head.authority_id = $1
LIMIT 2
FOR UPDATE OF head`,
[
proposal.actionInput.trustAuthorityId,
proposal.actionInput.currentTrustDigest,
],
);
if (trust.rows.length !== 1) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const head = parseHead(trust.rows[0]!);
const effectiveSnapshot = parseSnapshot(
trust.rows[0]!,
'effectiveSnapshotJson',
'effectiveSnapshotDigest',
);
const candidateSnapshot = parseSnapshot(
trust.rows[0]!,
'candidateSnapshotJson',
'candidateSnapshotDigest',
);
if (
head.generation !== proposal.actionInput.trustGeneration ||
head.effectiveTrustDigest !==
proposal.actionInput.previousTrustDigest ||
effectiveSnapshot.snapshotDigest !==
proposal.actionInput.previousTrustDigest ||
candidateSnapshot.snapshotDigest !==
proposal.actionInput.currentTrustDigest
) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
let expectedCandidate: Readonly<PluginPackagePublisherTrustSnapshot>;
try {
expectedCandidate =
proposal.actionInput.mode === 'overlap_add'
? createPluginPackagePublisherTrustOverlapAdditionSnapshot(
effectiveSnapshot,
candidateSnapshot,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
executedAtMs,
)
: createPluginPackagePublisherTrustRetirementSnapshot(
effectiveSnapshot,
proposal.actionInput.publisher,
proposal.actionInput.keyId,
executedAtMs,
);
} catch {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
if (!same(expectedCandidate, candidateSnapshot)) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
let retirementMatchingInstallations: 0 | null = null;
if (proposal.actionInput.mode === 'safe_retire') {
const matching = await client.query<Row>(
`SELECT provenance.installation_id AS "installationId"
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
JOIN "ql3"."plugin_package_install_heads" AS head
ON head.project_id = provenance.project_id
AND head.package_name = provenance.package_name
AND head.installation_id = provenance.installation_id
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = provenance.installation_id
WHERE provenance.publisher = $1 AND provenance.key_id = $2
AND install.state IN ('staged', 'activating', 'active')
LIMIT 1`,
[proposal.actionInput.publisher, proposal.actionInput.keyId],
);
if (matching.rows.length !== 0) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
retirementMatchingInstallations = 0;
}
const receipt = resolvePluginPackagePublisherTrustTransitionProposal(
proposal,
dispatch,
executedAtMs,
retirementMatchingInstallations,
);
const nextHead = advancePluginPackagePublisherTrustHead(
head,
candidateSnapshot,
executedAtMs,
);
const headUpdate = await client.query(
`UPDATE "ql3"."plugin_package_publisher_trust_heads"
SET generation = $2, effective_trust_digest = $3,
updated_at_ms = $4, head_digest = $5, head_json = $6::jsonb
WHERE authority_id = $1 AND generation = $7
AND effective_trust_digest = $8 AND head_digest = $9`,
[
nextHead.authorityId,
nextHead.generation,
nextHead.effectiveTrustDigest,
nextHead.updatedAtMs,
nextHead.headDigest,
JSON.stringify(nextHead),
head.generation,
head.effectiveTrustDigest,
head.headDigest,
],
);
if (headUpdate.rowCount !== 1) {
throw new PluginPackagePublisherTrustTransitionConflictError();
}
const receiptInsert = await client.query(
`INSERT INTO
"ql3"."plugin_package_publisher_trust_transition_receipts" (
mutation_id, proposal_digest, authority_id,
previous_generation, current_generation, mode, publisher,
key_id, previous_trust_digest, current_trust_digest,
proposer_type, proposer_id, confirmer_type, confirmer_id,
retirement_matching_installations, executed_at_ms,
receipt_json, receipt_digest
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14, $15, $16, $17::jsonb, $18
)`,
[
receipt.mutationId,
receipt.proposalDigest,
receipt.trustAuthorityId,
receipt.previousGeneration,
receipt.currentGeneration,
receipt.mode,
receipt.publisher,
receipt.keyId,
receipt.previousTrustDigest,
receipt.currentTrustDigest,
receipt.proposer.type,
receipt.proposer.id,
receipt.confirmer.type,
receipt.confirmer.id,
receipt.retirementMatchingInstallations,
receipt.executedAtMs,
JSON.stringify(receipt),
receipt.receiptDigest,
],
);
if (receiptInsert.rowCount !== 1) throw unavailable();
return Object.freeze({
status: 'created' as const,
receipt,
head: nextHead,
});
});
}
}
@@ -0,0 +1,840 @@
// PostgreSQL authorization authority for Plugin Package Workflow admission.
import {
RUN_CANCELLATION_REASONS,
RUN_STATUSES,
type PostgresClient,
type PostgresPool,
type RunCancellationReason,
type RunStatus,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageWorkflowAdministrationMutationError,
PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_SCHEMA,
PLUGIN_PACKAGE_WORKFLOW_RUN_INSPECTION_SCHEMA,
PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_SCHEMA,
PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_SCHEMA,
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError,
PluginPackageWorkflowAdministrationMutationConflictError,
normalizeAuthorizedPluginPackageWorkflowRunEventList,
normalizeAuthorizedPluginPackageWorkflowRunInspection,
normalizeAuthorizedPluginPackageWorkflowRunList,
normalizeAuthorizedPluginPackageWorkflowStepRunList,
normalizeAuthorizedPluginPackageWorkflowAdmission,
normalizePluginPackageWorkflowRunEventListResult,
normalizePluginPackageWorkflowRunInspectionResult,
normalizePluginPackageWorkflowRunListResult,
normalizePluginPackageWorkflowStepRunListResult,
type AuthorizedPluginPackageWorkflowAdmission,
type AuthorizedPluginPackageWorkflowRunEventList,
type AuthorizedPluginPackageWorkflowRunInspection,
type AuthorizedPluginPackageWorkflowRunList,
type AuthorizedPluginPackageWorkflowStepRunList,
type PluginPackageWorkflowAdministrationRepository,
type PluginPackageWorkflowRunEventListRepository,
type PluginPackageWorkflowRunEventListResult,
type PluginPackageWorkflowRunInspectionRepository,
type PluginPackageWorkflowRunInspectionResult,
type PluginPackageWorkflowRunListRepository,
type PluginPackageWorkflowRunListResult,
type PluginPackageWorkflowStepRunListItem,
type PluginPackageWorkflowStepRunListRepository,
type PluginPackageWorkflowStepRunListResult,
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
import type { PluginPackageWorkflowExecutionPlan } from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import {
STEP_RUN_STATUSES,
type StepRunStatus,
} from '@qinglong/runtime-core/step-run';
import { PostgresPluginPackageWorkflowAdmissionRepository } from './pluginPackageWorkflowAdmissionRepository';
import {
configureAdministrationTransaction,
insertAdministrationAudit,
requiredInteger,
requiredString,
rollbackAdministrationTransaction,
} from '../../repository/administrationSupport';
type Row = Record<string, unknown>;
const API_CREDENTIAL_AUTHENTICATION =
/^api_credential:([A-Za-z0-9][A-Za-z0-9._:-]{0,63}):([1-9]\d*)$/;
interface WorkflowAuthorizationContext {
readonly projectId: string;
readonly actor: AuthorizedPluginPackageWorkflowAdmission['actor'];
readonly fence: AuthorizedPluginPackageWorkflowAdmission['fence'];
readonly audit: AuthorizedPluginPackageWorkflowAdmission['audit'];
}
function integer(value: unknown): number | null {
if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
const parsed = Number(value);
if (Number.isSafeInteger(parsed)) return parsed;
}
return null;
}
function nullableInteger(row: Row, name: string): number | null {
if (row[name] === null) return null;
return requiredInteger(row, name);
}
function nullableString(row: Row, name: string): string | null {
if (row[name] === null) return null;
return requiredString(row, name);
}
function mutationConflict(): never {
throw new PluginPackageWorkflowAdministrationMutationConflictError();
}
function fenceConflict(): never {
throw new PluginPackageWorkflowAdministrationAuthorizationFenceConflictError();
}
async function confirmCredential(
client: PostgresClient,
authorization: Readonly<WorkflowAuthorizationContext>,
): Promise<void> {
const match = API_CREDENTIAL_AUTHENTICATION.exec(
authorization.audit.authenticationId ?? '',
);
const credentialVersion = integer(match?.[2]);
if (!match || credentialVersion === null || credentialVersion < 1) {
return fenceConflict();
}
await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [
`ql3-api-credential:${match[1]}`,
]);
await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [
`ql3-identity:${authorization.actor.type}:${authorization.actor.id}`,
]);
const result = await client.query<Row>(
`SELECT credential.version,
credential.state,
credential.subject_type AS "subjectType",
credential.subject_id AS "subjectId",
credential.not_before_at_ms AS "notBeforeAtMs",
credential.expires_at_ms AS "expiresAtMs",
subject.status AS "subjectStatus",
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
AS "nowMs"
FROM "ql3"."api_credentials" AS credential
JOIN "ql3"."identity_subjects" AS subject
ON subject.subject_type = credential.subject_type
AND subject.subject_id = credential.subject_id
WHERE credential.credential_id = $1
ORDER BY credential.version DESC
LIMIT 1`,
[match[1]],
);
const row = result.rows.length === 1 ? result.rows[0]! : null;
const nowMs = integer(row?.nowMs);
if (
!row ||
result.rows.length !== 1 ||
integer(row.version) !== credentialVersion ||
row.state !== 'active' ||
row.subjectStatus !== 'active' ||
row.subjectType !== authorization.actor.type ||
row.subjectId !== authorization.actor.id ||
nowMs === null ||
(integer(row.notBeforeAtMs) ?? Number.MAX_SAFE_INTEGER) > nowMs ||
(integer(row.expiresAtMs) ?? -1) <= nowMs
) {
return fenceConflict();
}
}
async function confirmProjectPolicyFence(
client: PostgresClient,
authorization: Readonly<WorkflowAuthorizationContext>,
): Promise<void> {
const project = await client.query<Row>(
`SELECT status, version
FROM "ql3"."projects"
WHERE id = $1
LIMIT 1
FOR SHARE`,
[authorization.projectId],
);
const projectRow = project.rows.length === 1 ? project.rows[0]! : null;
if (
!projectRow ||
projectRow.status !== 'active' ||
integer(projectRow.version) !== authorization.fence.projectVersion
) {
return fenceConflict();
}
const binding = await client.query<Row>(
`SELECT version, state
FROM "ql3"."project_role_bindings"
WHERE project_id = $1
AND subject_type = $2
AND subject_id = $3
ORDER BY version DESC
LIMIT 1`,
[authorization.projectId, authorization.actor.type, authorization.actor.id],
);
const bindingRow = binding.rows.length === 1 ? binding.rows[0]! : null;
if (
!bindingRow ||
bindingRow.state !== 'active' ||
integer(bindingRow.version) !== authorization.fence.bindingVersion
) {
return fenceConflict();
}
}
async function insertAtomicAudit(
client: PostgresClient,
admission: Readonly<AuthorizedPluginPackageWorkflowAdmission>,
replay: boolean,
): Promise<void> {
if (replay) return;
const audit = admission.audit;
await client.query(
`INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id,
subject_type, subject_id, authentication_id, outcome, reasons,
project_version, binding_version, occurred_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
)`,
[
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
],
);
}
/**
* Adds API credential, Project Policy fence and mutation-audit checks to the
* PostgreSQL Workflow admission transaction used by cluster-control.
*/
export class PostgresAuthorizedPluginPackageWorkflowAdmissionRepository
implements PluginPackageWorkflowAdministrationRepository
{
private readonly admissions: PostgresPluginPackageWorkflowAdmissionRepository;
constructor(pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new InvalidPluginPackageWorkflowAdministrationMutationError(
'PostgreSQL pool is invalid',
);
}
this.admissions = new PostgresPluginPackageWorkflowAdmissionRepository(
pool,
);
}
findPlanByPlanId(
planId: string,
): Promise<Readonly<PluginPackageWorkflowExecutionPlan> | null> {
return this.admissions.findPlanByPlanId(planId);
}
async admitAuthorized(input: AuthorizedPluginPackageWorkflowAdmission) {
const admission = normalizeAuthorizedPluginPackageWorkflowAdmission(input);
const authorization = Object.freeze({
projectId: admission.plan.target.projectId,
actor: admission.actor,
fence: admission.fence,
audit: admission.audit,
});
return this.admissions.admit(admission.plan, async ({ client, replay }) => {
await confirmCredential(client, authorization);
await confirmProjectPolicyFence(client, authorization);
await insertAtomicAudit(client, admission, replay);
});
}
}
/**
* Reads the low-sensitive Package-bound Workflow Run projection from one
* serializable PostgreSQL snapshot after revalidating the credential and the
* latest Project Policy fence. It deliberately does not expose plan, task,
* attempt, executor, error, input/output or Secret material.
*/
export class PostgresAuthorizedPluginPackageWorkflowRunInspectionRepository
implements PluginPackageWorkflowRunInspectionRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new InvalidPluginPackageWorkflowAdministrationMutationError(
'PostgreSQL pool is invalid',
);
}
}
async inspectRunAuthorized(
input: AuthorizedPluginPackageWorkflowRunInspection,
): Promise<Readonly<PluginPackageWorkflowRunInspectionResult>> {
const inspection =
normalizeAuthorizedPluginPackageWorkflowRunInspection(input);
let client: PostgresClient | undefined;
let began = false;
try {
client = await this.pool.connect();
await configureAdministrationTransaction(client);
began = true;
await confirmCredential(client, inspection);
await confirmProjectPolicyFence(client, inspection);
const targetRows = await client.query<Row>(
`SELECT admission.workflow_id AS "workflowId",
admission.step_count AS "stepCount",
run.status AS "runStatus",
run.version AS "runVersion",
run.event_sequence AS "eventSequence",
run.created_at_ms AS "createdAtMs",
run.queued_at_ms AS "queuedAtMs",
run.started_at_ms AS "startedAtMs",
run.finished_at_ms AS "finishedAtMs",
run.cancel_requested_at_ms AS "cancelRequestedAtMs",
run.cancel_reason AS "cancelReason"
FROM "ql3"."plugin_package_workflow_admissions" AS admission
JOIN "ql3"."runs" AS run
ON run.id = admission.run_id
AND run.project_id = admission.project_id
WHERE admission.run_id = $1
AND admission.project_id = $2
AND admission.package_name = $3
AND admission.workflow_id = $4
LIMIT 2`,
[
inspection.runId,
inspection.projectId,
inspection.packageName,
inspection.workflowId,
],
);
if (targetRows.rows.length === 0) {
await insertAdministrationAudit(client, inspection.audit);
const missing = normalizePluginPackageWorkflowRunInspectionResult({
schema: PLUGIN_PACKAGE_WORKFLOW_RUN_INSPECTION_SCHEMA,
found: false,
projectId: inspection.projectId,
packageName: inspection.packageName,
workflowId: inspection.workflowId,
runId: inspection.runId,
run: null,
stepCount: null,
stepStatusCounts: null,
});
await client.query('COMMIT');
began = false;
return missing;
}
if (targetRows.rows.length !== 1) mutationConflict();
const row = targetRows.rows[0]!;
if (requiredString(row, 'workflowId') !== inspection.workflowId) {
mutationConflict();
}
const stepCount = requiredInteger(row, 'stepCount');
if (stepCount < 1 || stepCount > 128) mutationConflict();
const statusRows = await client.query<Row>(
`SELECT status AS "stepStatus", COUNT(*) AS "statusCount"
FROM "ql3"."step_runs"
WHERE run_id = $1
GROUP BY status
ORDER BY status`,
[inspection.runId],
);
const stepStatusCounts = Object.fromEntries(
STEP_RUN_STATUSES.map((status) => [status, 0]),
) as Record<StepRunStatus, number>;
const observedStatuses = new Set<StepRunStatus>();
for (const statusRow of statusRows.rows) {
const status = requiredString(statusRow, 'stepStatus') as StepRunStatus;
if (
!STEP_RUN_STATUSES.includes(status) ||
observedStatuses.has(status)
) {
mutationConflict();
}
observedStatuses.add(status);
stepStatusCounts[status] = requiredInteger(statusRow, 'statusCount');
}
if (
Object.values(stepStatusCounts).reduce(
(total, count) => total + count,
0,
) !== stepCount
) {
mutationConflict();
}
const runStatus = requiredString(row, 'runStatus') as RunStatus;
const cancelReason = nullableString(
row,
'cancelReason',
) as RunCancellationReason | null;
if (
!RUN_STATUSES.includes(runStatus) ||
(cancelReason !== null &&
!RUN_CANCELLATION_REASONS.includes(cancelReason))
) {
mutationConflict();
}
const result = normalizePluginPackageWorkflowRunInspectionResult({
schema: PLUGIN_PACKAGE_WORKFLOW_RUN_INSPECTION_SCHEMA,
found: true,
projectId: inspection.projectId,
packageName: inspection.packageName,
workflowId: inspection.workflowId,
runId: inspection.runId,
run: {
status: runStatus,
version: requiredInteger(row, 'runVersion'),
eventSequence: requiredInteger(row, 'eventSequence'),
createdAtMs: requiredInteger(row, 'createdAtMs'),
queuedAtMs: nullableInteger(row, 'queuedAtMs'),
startedAtMs: nullableInteger(row, 'startedAtMs'),
finishedAtMs: nullableInteger(row, 'finishedAtMs'),
cancelRequestedAtMs: nullableInteger(row, 'cancelRequestedAtMs'),
cancelReason,
},
stepCount,
stepStatusCounts,
});
await insertAdministrationAudit(client, inspection.audit);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (client && began) await rollbackAdministrationTransaction(client);
if (
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof
PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError
) {
throw error;
}
throw new PluginPackageWorkflowAdministrationMutationConflictError();
} finally {
client?.release();
}
}
}
/**
* Lists one newest-first, low-sensitive Workflow Run history page from a
* serializable authorization snapshot. The dedicated target/time index keeps
* the query bounded even when a Package owns many other Workflows.
*/
export class PostgresAuthorizedPluginPackageWorkflowRunListRepository
implements PluginPackageWorkflowRunListRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new InvalidPluginPackageWorkflowAdministrationMutationError(
'PostgreSQL pool is invalid',
);
}
}
async listRunsAuthorized(
input: AuthorizedPluginPackageWorkflowRunList,
): Promise<Readonly<PluginPackageWorkflowRunListResult>> {
const query = normalizeAuthorizedPluginPackageWorkflowRunList(input);
let client: PostgresClient | undefined;
let began = false;
try {
client = await this.pool.connect();
await configureAdministrationTransaction(client);
began = true;
await confirmCredential(client, query);
await confirmProjectPolicyFence(client, query);
const page = await client.query<Row>(
`SELECT admission.run_id AS "runId",
admission.step_count AS "stepCount",
admission.admitted_at_ms AS "admittedAtMs",
run.status AS "runStatus",
run.version AS "runVersion",
run.event_sequence AS "eventSequence",
run.queued_at_ms AS "queuedAtMs",
run.started_at_ms AS "startedAtMs",
run.finished_at_ms AS "finishedAtMs",
run.cancel_requested_at_ms AS "cancelRequestedAtMs",
run.cancel_reason AS "cancelReason"
FROM "ql3"."plugin_package_workflow_admissions" AS admission
JOIN "ql3"."runs" AS run
ON run.id = admission.run_id
AND run.project_id = admission.project_id
WHERE admission.project_id = $1
AND admission.package_name = $2
AND admission.workflow_id = $3
AND ($4::bigint IS NULL OR admission.admitted_at_ms < $4 OR
(admission.admitted_at_ms = $4 AND admission.run_id < $5))
ORDER BY admission.admitted_at_ms DESC, admission.run_id DESC
LIMIT $6`,
[
query.projectId,
query.packageName,
query.workflowId,
query.after?.admittedAtMs ?? null,
query.after?.runId ?? null,
query.limit + 1,
],
);
const truncated = page.rows.length > query.limit;
const runs = page.rows.slice(0, query.limit).map((row) => ({
runId: requiredString(row, 'runId'),
status: requiredString(row, 'runStatus') as RunStatus,
version: requiredInteger(row, 'runVersion'),
eventSequence: requiredInteger(row, 'eventSequence'),
stepCount: requiredInteger(row, 'stepCount'),
admittedAtMs: requiredInteger(row, 'admittedAtMs'),
queuedAtMs: nullableInteger(row, 'queuedAtMs'),
startedAtMs: nullableInteger(row, 'startedAtMs'),
finishedAtMs: nullableInteger(row, 'finishedAtMs'),
cancelRequestedAtMs: nullableInteger(row, 'cancelRequestedAtMs'),
cancelReason: nullableString(
row,
'cancelReason',
) as RunCancellationReason | null,
}));
const last = runs.at(-1);
const result = normalizePluginPackageWorkflowRunListResult({
schema: PLUGIN_PACKAGE_WORKFLOW_RUN_LIST_SCHEMA,
projectId: query.projectId,
packageName: query.packageName,
workflowId: query.workflowId,
after: query.after,
runs,
truncated,
next:
truncated && last
? { admittedAtMs: last.admittedAtMs, runId: last.runId }
: null,
});
await insertAdministrationAudit(client, query.audit);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (client && began) await rollbackAdministrationTransaction(client);
if (
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof
PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError
) {
throw error;
}
throw new PluginPackageWorkflowAdministrationMutationConflictError();
} finally {
client?.release();
}
}
}
/**
* Lists one bounded low-sensitive StepRun page behind the exact Package-bound
* Workflow target and current authorization fence. The runtime role only
* appends the allowed audit and never receives audit read authority.
*/
export class PostgresAuthorizedPluginPackageWorkflowStepRunListRepository
implements PluginPackageWorkflowStepRunListRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new InvalidPluginPackageWorkflowAdministrationMutationError(
'PostgreSQL pool is invalid',
);
}
}
async listStepRunsAuthorized(
input: AuthorizedPluginPackageWorkflowStepRunList,
): Promise<Readonly<PluginPackageWorkflowStepRunListResult>> {
const query = normalizeAuthorizedPluginPackageWorkflowStepRunList(input);
let client: PostgresClient | undefined;
let began = false;
try {
client = await this.pool.connect();
await configureAdministrationTransaction(client);
began = true;
await confirmCredential(client, query);
await confirmProjectPolicyFence(client, query);
const targets = await client.query<Row>(
`SELECT admission.step_count AS "stepCount",
(SELECT COUNT(*) FROM "ql3"."step_runs" AS observed
WHERE observed.run_id = admission.run_id) AS "observedStepCount"
FROM "ql3"."plugin_package_workflow_admissions" AS admission
JOIN "ql3"."runs" AS run
ON run.id = admission.run_id
AND run.project_id = admission.project_id
WHERE admission.run_id = $1
AND admission.project_id = $2
AND admission.package_name = $3
AND admission.workflow_id = $4
LIMIT 2`,
[query.runId, query.projectId, query.packageName, query.workflowId],
);
if (targets.rows.length === 0) {
await insertAdministrationAudit(client, query.audit);
const missing = normalizePluginPackageWorkflowStepRunListResult({
schema: PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_SCHEMA,
found: false,
projectId: query.projectId,
packageName: query.packageName,
workflowId: query.workflowId,
runId: query.runId,
stepRuns: [],
truncated: false,
next: null,
});
await client.query('COMMIT');
began = false;
return missing;
}
if (
targets.rows.length !== 1 ||
requiredInteger(targets.rows[0]!, 'stepCount') !==
requiredInteger(targets.rows[0]!, 'observedStepCount')
) {
mutationConflict();
}
const page = await client.query<Row>(
`SELECT id AS "id",
parent_step_run_id AS "parentStepRunId",
step_key AS "stepKey",
kind AS "kind",
required AS "required",
status AS "status",
version AS "version",
attempt_count AS "attemptCount",
ready_at_ms AS "readyAtMs",
started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs",
result_code AS "resultCode",
created_at_ms AS "createdAtMs",
updated_at_ms AS "updatedAtMs"
FROM "ql3"."step_runs"
WHERE run_id = $1
AND ($2::varchar IS NULL OR step_key > $3 OR
(step_key = $3 AND id > $2))
ORDER BY step_key, id
LIMIT $4`,
[
query.runId,
query.after?.id ?? null,
query.after?.stepKey ?? '',
query.limit + 1,
],
);
const truncated = page.rows.length > query.limit;
const stepRuns = page.rows.slice(0, query.limit).map((row) => {
if (typeof row.required !== 'boolean') mutationConflict();
return {
id: requiredString(row, 'id'),
parentStepRunId: nullableString(row, 'parentStepRunId'),
stepKey: requiredString(row, 'stepKey'),
kind: requiredString(
row,
'kind',
) as PluginPackageWorkflowStepRunListItem['kind'],
required: row.required,
status: requiredString(row, 'status') as StepRunStatus,
version: requiredInteger(row, 'version'),
attemptCount: requiredInteger(row, 'attemptCount'),
readyAtMs: nullableInteger(row, 'readyAtMs'),
startedAtMs: nullableInteger(row, 'startedAtMs'),
finishedAtMs: nullableInteger(row, 'finishedAtMs'),
resultCode: nullableString(row, 'resultCode'),
createdAtMs: requiredInteger(row, 'createdAtMs'),
updatedAtMs: requiredInteger(row, 'updatedAtMs'),
};
});
const last = stepRuns.at(-1);
const result = normalizePluginPackageWorkflowStepRunListResult({
schema: PLUGIN_PACKAGE_WORKFLOW_STEP_RUN_LIST_SCHEMA,
found: true,
projectId: query.projectId,
packageName: query.packageName,
workflowId: query.workflowId,
runId: query.runId,
stepRuns,
truncated,
next: truncated && last ? { stepKey: last.stepKey, id: last.id } : null,
});
await insertAdministrationAudit(client, query.audit);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (client && began) await rollbackAdministrationTransaction(client);
if (
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof
PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError
) {
throw error;
}
throw new PluginPackageWorkflowAdministrationMutationConflictError();
} finally {
client?.release();
}
}
}
/**
* Lists one bounded content-free RunEvent page behind the exact Package-bound
* Workflow target and current authorization fence.
*/
export class PostgresAuthorizedPluginPackageWorkflowRunEventListRepository
implements PluginPackageWorkflowRunEventListRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new InvalidPluginPackageWorkflowAdministrationMutationError(
'PostgreSQL pool is invalid',
);
}
}
async listRunEventsAuthorized(
input: AuthorizedPluginPackageWorkflowRunEventList,
): Promise<Readonly<PluginPackageWorkflowRunEventListResult>> {
const query = normalizeAuthorizedPluginPackageWorkflowRunEventList(input);
let client: PostgresClient | undefined;
let began = false;
try {
client = await this.pool.connect();
await configureAdministrationTransaction(client);
began = true;
await confirmCredential(client, query);
await confirmProjectPolicyFence(client, query);
const targets = await client.query<Row>(
`SELECT run.event_sequence AS "headSequence"
FROM "ql3"."plugin_package_workflow_admissions" AS admission
JOIN "ql3"."runs" AS run
ON run.id = admission.run_id
AND run.project_id = admission.project_id
WHERE admission.run_id = $1
AND admission.project_id = $2
AND admission.package_name = $3
AND admission.workflow_id = $4
LIMIT 2`,
[query.runId, query.projectId, query.packageName, query.workflowId],
);
if (targets.rows.length === 0) {
await insertAdministrationAudit(client, query.audit);
const missing = normalizePluginPackageWorkflowRunEventListResult({
schema: PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_SCHEMA,
found: false,
projectId: query.projectId,
packageName: query.packageName,
workflowId: query.workflowId,
runId: query.runId,
afterSequence: query.afterSequence,
headSequence: null,
events: [],
truncated: false,
nextAfterSequence: null,
});
await client.query('COMMIT');
began = false;
return missing;
}
if (targets.rows.length !== 1) mutationConflict();
const headSequence = requiredInteger(targets.rows[0]!, 'headSequence');
const page = await client.query<Row>(
`SELECT id AS "id",
sequence AS "sequence",
type AS "type",
step_run_id AS "stepRunId",
created_at_ms AS "createdAtMs"
FROM "ql3"."run_events"
WHERE run_id = $1 AND sequence > $2
ORDER BY sequence, id
LIMIT $3`,
[query.runId, query.afterSequence, query.limit + 1],
);
const truncated = page.rows.length > query.limit;
const events = page.rows.slice(0, query.limit).map((row) => ({
id: requiredString(row, 'id'),
sequence: requiredInteger(row, 'sequence'),
type: requiredString(row, 'type'),
stepRunId: nullableString(row, 'stepRunId'),
createdAtMs: requiredInteger(row, 'createdAtMs'),
}));
const lastSequence = events.at(-1)?.sequence ?? null;
const result = normalizePluginPackageWorkflowRunEventListResult({
schema: PLUGIN_PACKAGE_WORKFLOW_RUN_EVENT_LIST_SCHEMA,
found: true,
projectId: query.projectId,
packageName: query.packageName,
workflowId: query.workflowId,
runId: query.runId,
afterSequence: query.afterSequence,
headSequence,
events,
truncated,
nextAfterSequence: truncated ? lastSequence : null,
});
await insertAdministrationAudit(client, query.audit);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (client && began) await rollbackAdministrationTransaction(client);
if (
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof
PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError
) {
throw error;
}
throw new PluginPackageWorkflowAdministrationMutationConflictError();
} finally {
client?.release();
}
}
}
@@ -0,0 +1,843 @@
// PostgreSQL authority for atomic Plugin Package Workflow admission.
import type {
PostgresClient,
PostgresPool,
PostgresQueryable,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageWorkflowAdministrationMutationError,
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError,
PluginPackageWorkflowAdministrationMutationConflictError,
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
import {
normalizePluginPackageAutomationPublication,
type PluginPackageAutomationPublication,
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import {
createPluginPackageWorkflowAdmissionBundle,
InvalidPluginPackageWorkflowExecutionPlanError,
normalizePluginPackageWorkflowAdmissionReceipt,
normalizePluginPackageWorkflowExecutionPlan,
pluginPackageWorkflowDefinitionDigest,
PluginPackageWorkflowAdmissionConflictError,
PluginPackageWorkflowAdmissionNotAllowedError,
PluginPackageWorkflowAdmissionUnavailableError,
type PluginPackageWorkflowAdmissionBundle,
type PluginPackageWorkflowAdmissionReceipt,
type PluginPackageWorkflowAdmissionRepository,
type PluginPackageWorkflowExecutionPlan,
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import {
normalizeStepRunRecord,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import {
configurePostgresDefinitionTransaction,
postgresRequiredJsonObject,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const WORKFLOW_RUN_STATUSES = new Set([
'running',
'succeeded',
'failed',
'cancelled',
'timed_out',
]);
function unavailable(
cause?: unknown,
): PluginPackageWorkflowAdmissionUnavailableError {
return new PluginPackageWorkflowAdmissionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY.test(value)) {
throw new InvalidPluginPackageWorkflowExecutionPlanError(
`${label} is invalid`,
);
}
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageWorkflowAdministrationMutationError ||
error instanceof
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError ||
error instanceof PluginPackageWorkflowAdministrationMutationConflictError ||
error instanceof InvalidPluginPackageWorkflowExecutionPlanError ||
error instanceof PluginPackageWorkflowAdmissionConflictError ||
error instanceof PluginPackageWorkflowAdmissionNotAllowedError ||
error instanceof PluginPackageWorkflowAdmissionUnavailableError
) {
return error;
}
if (['23503', '23505', '23514'].includes(postgresSqlState(error) ?? '')) {
return new PluginPackageWorkflowAdmissionConflictError(
'durable Run, plan, StepRun, event, or receipt identity changed',
);
}
return unavailable(error);
}
export interface PostgresPluginPackageWorkflowAdmissionTransactionContext {
readonly client: PostgresClient;
readonly replay: boolean;
readonly plan: Readonly<PluginPackageWorkflowExecutionPlan>;
readonly receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
}
export type PostgresPluginPackageWorkflowAdmissionTransactionGuard = (
context: Readonly<PostgresPluginPackageWorkflowAdmissionTransactionContext>,
) => void | Promise<void>;
function json(value: unknown): string {
if (
value === null ||
typeof value === 'boolean' ||
typeof value === 'number' ||
typeof value === 'string'
) {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map((entry) => json(entry ?? null)).join(',')}]`;
}
if (value && typeof value === 'object') {
const record = value as Readonly<Record<string, unknown>>;
return `{${Object.keys(record)
.filter((key) => record[key] !== undefined)
.sort()
.map((key) => `${JSON.stringify(key)}:${json(record[key])}`)
.join(',')}}`;
}
throw unavailable();
}
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') throw unavailable();
return value;
}
function optionalText(row: Row, key: string): string | undefined {
const value = row[key];
if (value === null || value === undefined) return undefined;
if (typeof value !== 'string') throw unavailable();
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
return value;
}
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
const parsed = Number(value);
if (Number.isSafeInteger(parsed)) return parsed;
}
throw unavailable();
}
function optionalInteger(row: Row, key: string): number | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return integer(row, key);
}
function exactArray(
left: readonly string[],
right: readonly string[],
): boolean {
return (
left.length === right.length &&
left.every((value, index) => value === right[index])
);
}
function parseStored(row: Row): Readonly<{
plan: Readonly<PluginPackageWorkflowExecutionPlan>;
receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>;
}> {
try {
const plan = normalizePluginPackageWorkflowExecutionPlan(
postgresRequiredJsonObject(
row.planJson,
unavailable,
) as unknown as PluginPackageWorkflowExecutionPlan,
);
const receipt = normalizePluginPackageWorkflowAdmissionReceipt(
postgresRequiredJsonObject(
row.receiptJson,
unavailable,
) as unknown as PluginPackageWorkflowAdmissionReceipt,
);
const bundle = createPluginPackageWorkflowAdmissionBundle(plan);
if (
plan.planDigest !== text(row, 'planDigest') ||
plan.planId !== text(row, 'planId') ||
plan.runId !== text(row, 'runId') ||
plan.target.projectId !== text(row, 'projectId') ||
plan.target.packageName !== text(row, 'packageName') ||
plan.target.installationId !== text(row, 'installationId') ||
plan.target.lockDigest !== text(row, 'lockDigest') ||
plan.target.generation !== integer(row, 'generation') ||
plan.target.generationDigest !== text(row, 'generationDigest') ||
plan.target.materializedRevisionDigest !==
text(row, 'materializedRevisionDigest') ||
plan.target.publicationDigest !== text(row, 'publicationDigest') ||
plan.target.workflowId !== text(row, 'workflowId') ||
plan.target.workflowDefinitionDigest !==
text(row, 'workflowDefinitionDigest') ||
plan.steps.length !== integer(row, 'stepCount') ||
receipt.admittedAtMs !== integer(row, 'admittedAtMs') ||
receipt.finalRunVersion !== integer(row, 'finalRunVersion') ||
receipt.finalRunEventSequence !== integer(row, 'finalRunEventSequence') ||
receipt.receiptDigest !== text(row, 'receiptDigest') ||
json(bundle.receipt) !== json(receipt)
) {
throw unavailable();
}
return Object.freeze({ plan, receipt, bundle });
} catch (error) {
if (error instanceof PluginPackageWorkflowAdmissionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async function assertStoredEvidence(
queryable: PostgresQueryable,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): Promise<void> {
const run = bundle.run;
const storedRun = await queryable.query<Row>(
`SELECT project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision",
task_snapshot_ref AS "taskSnapshotRef",
trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner",
request_id AS "requestId", status, version,
event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey",
created_at_ms AS "createdAtMs",
started_at_ms AS "startedAtMs"
FROM "ql3"."runs" WHERE id = $1`,
[run.id],
);
const runRow = storedRun.rows.length === 1 ? storedRun.rows[0]! : null;
const storedRunVersion = runRow ? integer(runRow, 'version') : -1;
const storedEventSequence = runRow ? integer(runRow, 'eventSequence') : -1;
if (
!runRow ||
text(runRow, 'projectId') !== run.projectId ||
text(runRow, 'taskId') !== run.taskId ||
text(runRow, 'taskRevision') !== run.taskRevision ||
optionalText(runRow, 'taskSnapshotRef') !== run.taskSnapshotRef ||
text(runRow, 'triggerType') !== run.triggerType ||
text(runRow, 'executionOrigin') !== run.executionOrigin ||
text(runRow, 'executionOwner') !== run.executionOwner ||
optionalText(runRow, 'requestId') !== run.requestId ||
!WORKFLOW_RUN_STATUSES.has(text(runRow, 'status')) ||
storedRunVersion < run.version ||
storedEventSequence < run.eventSequence ||
storedRunVersion !== storedEventSequence ||
integer(runRow, 'priority') !== run.priority ||
optionalText(runRow, 'idempotencyKey') !== run.idempotencyKey ||
integer(runRow, 'createdAtMs') !== run.createdAtMs ||
optionalInteger(runRow, 'startedAtMs') !== run.startedAtMs
) {
throw unavailable();
}
const storedEvents = await queryable.query<Row>(
`SELECT id, sequence, type, dedupe_key AS "dedupeKey",
actor_type AS "actorType", actor_id AS "actorId",
step_run_id AS "stepRunId", payload,
created_at_ms AS "createdAtMs"
FROM "ql3"."run_events"
WHERE run_id = $1 AND sequence <= $2
ORDER BY sequence`,
[run.id, bundle.receipt.finalRunEventSequence],
);
const expectedEvents = [
bundle.admissionEvent,
...bundle.stepMutations.map(({ event }) => event),
];
if (
storedEvents.rows.length !== expectedEvents.length ||
storedEvents.rows.some((row, index) => {
const event = expectedEvents[index]!;
return (
text(row, 'id') !== event.id ||
integer(row, 'sequence') !== event.sequence ||
text(row, 'type') !== event.type ||
optionalText(row, 'dedupeKey') !== event.dedupeKey ||
text(row, 'actorType') !== event.actorType ||
optionalText(row, 'actorId') !== event.actorId ||
optionalText(row, 'stepRunId') !== event.stepRunId ||
json(row.payload) !== json(event.payload) ||
integer(row, 'createdAtMs') !== event.createdAtMs
);
})
) {
throw unavailable();
}
for (const mutation of bundle.stepMutations) {
const stored = await queryable.query<Row>(
`SELECT
admission_step.step_run_id AS "stepRunId",
admission_step.task_id AS "taskId",
admission_step.task_definition_ref AS "taskDefinitionRef",
admission_step.task_definition_digest AS "taskDefinitionDigest",
admission_step.needs_json AS "needsJson",
admission_step.initial_status AS "initialStatus",
admission_step.mutation_id AS "mutationId",
admission_step.event_id AS "eventId",
runtime.step_key AS "currentStepKey",
runtime.kind AS "currentKind",
runtime.definition_ref AS "currentDefinitionRef",
runtime.definition_digest AS "currentDefinitionDigest",
runtime.required AS "currentRequired",
runtime.status AS "currentStatus",
runtime.version AS "currentVersion",
runtime.last_mutation_id AS "currentLastMutationId",
runtime.step_run_digest AS "currentStepRunDigest",
runtime.step_run_json AS "currentStepRunJson",
mutation.mutation_digest AS "mutationDigest",
mutation.event_sequence AS "eventSequence",
mutation.run_version AS "runVersion",
mutation.step_run_digest AS "initialStepRunDigest",
mutation.step_run_json AS "initialStepRunJson"
FROM "ql3"."plugin_package_workflow_admission_steps"
AS admission_step
JOIN "ql3"."step_runs" AS runtime
ON runtime.run_id = admission_step.run_id
AND runtime.id = admission_step.step_run_id
JOIN "ql3"."step_run_mutations" AS mutation
ON mutation.mutation_id = admission_step.mutation_id
WHERE admission_step.plan_digest = $1
AND admission_step.step_key = $2`,
[bundle.plan.planDigest, mutation.stepRun.stepKey],
);
const row = stored.rows.length === 1 ? stored.rows[0]! : null;
const planStep = bundle.plan.steps.find(
({ stepKey }) => stepKey === mutation.stepRun.stepKey,
);
let currentStepRun: Readonly<StepRunRecord> | null = null;
if (row) {
try {
currentStepRun = normalizeStepRunRecord(
postgresRequiredJsonObject(
row.currentStepRunJson,
unavailable,
) as unknown as StepRunRecord,
);
} catch {
throw unavailable();
}
}
if (
!row ||
!planStep ||
!currentStepRun ||
text(row, 'stepRunId') !== mutation.stepRun.id ||
text(row, 'taskId') !== planStep.taskId ||
text(row, 'taskDefinitionRef') !== planStep.taskDefinitionRef ||
text(row, 'taskDefinitionDigest') !== planStep.taskDefinitionDigest ||
json(row.needsJson) !== json(planStep.needs) ||
text(row, 'initialStatus') !== planStep.initialStatus ||
text(row, 'mutationId') !== mutation.mutationId ||
text(row, 'eventId') !== mutation.event.id ||
text(row, 'mutationDigest') !== mutation.mutationDigest ||
integer(row, 'eventSequence') !== mutation.event.sequence ||
integer(row, 'runVersion') !== mutation.expectedRunVersion + 1 ||
text(row, 'initialStepRunDigest') !== mutation.stepRun.stepRunDigest ||
json(row.initialStepRunJson) !== json(mutation.stepRun) ||
currentStepRun.id !== mutation.stepRun.id ||
currentStepRun.runId !== mutation.runId ||
currentStepRun.stepKey !== planStep.stepKey ||
currentStepRun.kind !== 'task' ||
currentStepRun.definitionRef !== planStep.taskDefinitionRef ||
currentStepRun.definitionDigest !== planStep.taskDefinitionDigest ||
currentStepRun.required !== planStep.required ||
currentStepRun.version < mutation.stepRun.version ||
text(row, 'currentStepKey') !== currentStepRun.stepKey ||
text(row, 'currentKind') !== currentStepRun.kind ||
text(row, 'currentDefinitionRef') !== currentStepRun.definitionRef ||
text(row, 'currentDefinitionDigest') !==
currentStepRun.definitionDigest ||
row.currentRequired !== currentStepRun.required ||
text(row, 'currentStatus') !== currentStepRun.status ||
integer(row, 'currentVersion') !== currentStepRun.version ||
text(row, 'currentLastMutationId') !== currentStepRun.lastMutationId ||
text(row, 'currentStepRunDigest') !== currentStepRun.stepRunDigest
) {
throw unavailable();
}
}
}
async function findStored(
queryable: PostgresQueryable,
column: 'plan_id' | 'run_id',
value: string,
): Promise<ReturnType<typeof parseStored> | null> {
const result = await queryable.query<Row>(
`SELECT
plan_digest AS "planDigest", plan_id AS "planId",
run_id AS "runId", project_id AS "projectId",
package_name AS "packageName",
installation_id AS "installationId",
lock_digest AS "lockDigest", generation,
generation_digest AS "generationDigest",
materialized_revision_digest AS "materializedRevisionDigest",
publication_digest AS "publicationDigest",
workflow_id AS "workflowId",
workflow_definition_digest AS "workflowDefinitionDigest",
step_count AS "stepCount", admitted_at_ms AS "admittedAtMs",
final_run_version AS "finalRunVersion",
final_run_event_sequence AS "finalRunEventSequence",
receipt_digest AS "receiptDigest",
plan_json AS "planJson", receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_workflow_admissions"
WHERE ${column} = $1
LIMIT 2`,
[value],
);
if (result.rows.length > 1) throw unavailable();
if (!result.rows[0]) return null;
const stored = parseStored(result.rows[0]);
await assertStoredEvidence(queryable, stored.bundle);
return stored;
}
function assertSnapshot(
plan: Readonly<PluginPackageWorkflowExecutionPlan>,
row: Row,
): void {
let publication: Readonly<PluginPackageAutomationPublication>;
let resources: unknown;
try {
publication = normalizePluginPackageAutomationPublication(
postgresRequiredJsonObject(
row.publicationJson,
unavailable,
) as unknown as PluginPackageAutomationPublication,
);
resources = postgresRequiredJsonObject(
row.revisionJson,
unavailable,
).resources;
} catch (error) {
throw unavailable(error);
}
if (!Array.isArray(resources)) throw unavailable();
const target = plan.target;
const workflow = publication.definitions.workflows.find(
({ id }) => id === target.workflowId,
);
if (
publication.publicationDigest !== target.publicationDigest ||
publication.target.projectId !== target.projectId ||
publication.target.packageName !== target.packageName ||
publication.target.installationId !== target.installationId ||
publication.target.lockDigest !== target.lockDigest ||
publication.target.generation !== target.generation ||
publication.target.generationDigest !== target.generationDigest ||
publication.target.materializedRevisionDigest !==
target.materializedRevisionDigest ||
publication.state !== 'active' ||
!workflow ||
!workflow.enabled ||
pluginPackageWorkflowDefinitionDigest(workflow) !==
target.workflowDefinitionDigest ||
workflow.steps.length !== plan.steps.length
) {
throw new PluginPackageWorkflowAdmissionConflictError(
'the exact Workflow publication drifted',
);
}
for (const step of plan.steps) {
const workflowStep = workflow.steps.find(({ id }) => id === step.stepKey);
const matches = resources.filter((resource) => {
if (!resource || typeof resource !== 'object') return false;
const candidate = resource as {
kind?: unknown;
sourceDigest?: unknown;
value?: { id?: unknown; enabled?: unknown };
};
return (
candidate.kind === 'task' &&
candidate.sourceDigest === step.taskDefinitionDigest &&
candidate.value?.id === step.taskId &&
candidate.value.enabled === true
);
});
if (
!workflowStep ||
workflowStep.task !== step.taskId ||
!exactArray(workflowStep.needs, step.needs) ||
step.initialStatus !==
(workflowStep.needs.length === 0 ? 'ready' : 'pending') ||
step.taskDefinitionRef !==
`plugin-package:${target.materializedRevisionDigest}:task:${step.taskId}` ||
matches.length !== 1
) {
throw new PluginPackageWorkflowAdmissionConflictError(
'the exact Workflow step or Task evidence drifted',
);
}
}
}
async function insertRun(
client: PostgresClient,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): Promise<void> {
const run = bundle.run;
await client.query(
`INSERT INTO "ql3"."runs" (
id, project_id, task_id, task_revision, task_snapshot_ref,
trigger_type, execution_origin, execution_owner, request_id,
status, version, event_sequence, priority, idempotency_key,
created_at_ms, started_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15, $16
)`,
[
run.id,
run.projectId,
run.taskId,
run.taskRevision,
run.taskSnapshotRef ?? null,
run.triggerType,
run.executionOrigin,
run.executionOwner,
run.requestId ?? null,
run.status,
run.version,
run.eventSequence,
run.priority,
run.idempotencyKey ?? null,
run.createdAtMs,
run.startedAtMs ?? null,
],
);
}
async function insertEvent(
client: PostgresClient,
event: Readonly<PluginPackageWorkflowAdmissionBundle['admissionEvent']>,
stepRunId: string | null,
): Promise<void> {
await client.query(
`INSERT INTO "ql3"."run_events" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8, $9::jsonb, $10)`,
[
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
stepRunId,
json(event.payload),
event.createdAtMs,
],
);
}
async function insertStepEvidence(
client: PostgresClient,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): Promise<void> {
for (const mutation of bundle.stepMutations) {
const step = mutation.stepRun;
await client.query(
`INSERT INTO "ql3"."step_runs" (
id, run_id, parent_step_run_id, step_key, kind, definition_ref,
definition_digest, required, status, version, attempt_count,
input_ref, output_ref, approval_request_id, ready_at_ms,
started_at_ms, finished_at_ms, result_code, error_summary,
created_at_ms, updated_at_ms, last_mutation_id, step_run_digest,
step_run_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15, $16, $17, $18, $19, $20, $21, $22, $23, $24::jsonb
)`,
[
step.id,
step.runId,
step.parentStepRunId,
step.stepKey,
step.kind,
step.definitionRef,
step.definitionDigest,
step.required,
step.status,
step.version,
step.attemptCount,
step.inputRef,
step.outputRef,
step.approvalRequestId,
step.readyAtMs,
step.startedAtMs,
step.finishedAtMs,
step.resultCode,
step.errorSummary,
step.createdAtMs,
step.updatedAtMs,
step.lastMutationId,
step.stepRunDigest,
json(step),
],
);
await insertEvent(client, mutation.event, step.id);
await client.query(
`INSERT INTO "ql3"."step_run_mutations" (
mutation_id, mutation_digest, run_id, step_run_id,
step_run_digest, event_id, event_sequence, run_version,
step_run_json, committed_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)`,
[
mutation.mutationId,
mutation.mutationDigest,
mutation.runId,
step.id,
step.stepRunDigest,
mutation.event.id,
mutation.event.sequence,
mutation.expectedRunVersion + 1,
json(step),
bundle.receipt.admittedAtMs,
],
);
}
}
async function insertAdmission(
client: PostgresClient,
bundle: Readonly<PluginPackageWorkflowAdmissionBundle>,
): Promise<void> {
const { plan, receipt } = bundle;
const target = plan.target;
await client.query(
`INSERT INTO "ql3"."plugin_package_workflow_admissions" (
plan_digest, plan_id, run_id, project_id, package_name,
installation_id, lock_digest, generation, generation_digest,
materialized_revision_digest, publication_digest, workflow_id,
workflow_definition_digest, step_count, admitted_at_ms,
final_run_version, final_run_event_sequence, receipt_digest,
plan_json, receipt_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15, $16, $17, $18, $19::jsonb, $20::jsonb
)`,
[
plan.planDigest,
plan.planId,
plan.runId,
target.projectId,
target.packageName,
target.installationId,
target.lockDigest,
target.generation,
target.generationDigest,
target.materializedRevisionDigest,
target.publicationDigest,
target.workflowId,
target.workflowDefinitionDigest,
plan.steps.length,
receipt.admittedAtMs,
receipt.finalRunVersion,
receipt.finalRunEventSequence,
receipt.receiptDigest,
json(plan),
json(receipt),
],
);
for (const step of plan.steps) {
const mutation = bundle.stepMutations.find(
({ stepRun }) => stepRun.stepKey === step.stepKey,
);
if (!mutation) throw unavailable();
await client.query(
`INSERT INTO "ql3"."plugin_package_workflow_admission_steps" (
plan_digest, run_id, step_key, step_run_id, task_id,
task_definition_ref, task_definition_digest, needs_json,
initial_status, mutation_id, event_id
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11
)`,
[
plan.planDigest,
plan.runId,
step.stepKey,
step.stepRunId,
step.taskId,
step.taskDefinitionRef,
step.taskDefinitionDigest,
json(step.needs),
step.initialStatus,
mutation.mutationId,
mutation.event.id,
],
);
}
}
export class PostgresPluginPackageWorkflowAdmissionRepository
implements PluginPackageWorkflowAdmissionRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw unavailable();
}
}
async findByPlanId(
planIdValue: string,
): Promise<Readonly<PluginPackageWorkflowAdmissionReceipt> | null> {
const planId = identity(planIdValue, 'planId');
try {
return (await findStored(this.pool, 'plan_id', planId))?.receipt ?? null;
} catch (error) {
throw mapStorageError(error);
}
}
async findByRunId(
runIdValue: string,
): Promise<Readonly<PluginPackageWorkflowAdmissionReceipt> | null> {
const runId = identity(runIdValue, 'runId');
try {
return (await findStored(this.pool, 'run_id', runId))?.receipt ?? null;
} catch (error) {
throw mapStorageError(error);
}
}
async findPlanByPlanId(
planIdValue: string,
): Promise<Readonly<PluginPackageWorkflowExecutionPlan> | null> {
const planId = identity(planIdValue, 'planId');
try {
return (await findStored(this.pool, 'plan_id', planId))?.plan ?? null;
} catch (error) {
throw mapStorageError(error);
}
}
async admit(
planValue: Readonly<PluginPackageWorkflowExecutionPlan>,
transactionGuard?: PostgresPluginPackageWorkflowAdmissionTransactionGuard,
): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<PluginPackageWorkflowAdmissionReceipt>;
}>
> {
if (
transactionGuard !== undefined &&
typeof transactionGuard !== 'function'
) {
throw new InvalidPluginPackageWorkflowExecutionPlanError(
'transaction guard is invalid',
);
}
const plan = normalizePluginPackageWorkflowExecutionPlan(planValue);
const bundle = createPluginPackageWorkflowAdmissionBundle(plan);
let client: PostgresClient | undefined;
let began = false;
try {
client = await this.pool.connect();
await configurePostgresDefinitionTransaction(client);
began = true;
const existing = await findStored(client, 'plan_id', plan.planId);
if (existing) {
if (
existing.plan.planDigest !== plan.planDigest ||
json(existing.plan) !== json(plan)
) {
throw new PluginPackageWorkflowAdmissionConflictError(
'planId is already bound to another plan',
);
}
await transactionGuard?.(
Object.freeze({
client,
replay: true,
plan: existing.plan,
receipt: existing.receipt,
}),
);
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'existing' as const,
receipt: existing.receipt,
});
}
await transactionGuard?.(
Object.freeze({
client,
replay: false,
plan,
receipt: bundle.receipt,
}),
);
const snapshot = await client.query<Row>(
`SELECT publication_json AS "publicationJson",
revision_json AS "revisionJson"
FROM "ql3"."plugin_package_workflow_admission_snapshot"(
$1, $2, $3
)`,
[
plan.target.projectId,
plan.target.packageName,
plan.target.publicationDigest,
],
);
if (snapshot.rows.length === 0) {
throw new PluginPackageWorkflowAdmissionNotAllowedError();
}
if (snapshot.rows.length !== 1) throw unavailable();
assertSnapshot(plan, snapshot.rows[0]!);
await insertRun(client, bundle);
await insertEvent(client, bundle.admissionEvent, null);
await insertStepEvidence(client, bundle);
await insertAdmission(client, bundle);
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'created' as const,
receipt: bundle.receipt,
});
} catch (error) {
if (client && began) await rollbackPostgresDefinitionTransaction(client);
throw mapStorageError(error);
} finally {
client?.release();
}
}
}
@@ -0,0 +1,736 @@
// PostgreSQL authority for advancing the Plugin Package Workflow frontier.
import type {
PostgresClient,
PostgresPool,
RunRecord,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageWorkflowFrontierError,
MAX_PLUGIN_PACKAGE_WORKFLOW_FRONTIER_PAGE_SIZE,
PluginPackageWorkflowFrontierConflictError,
PluginPackageWorkflowFrontierUnavailableError,
resolvePluginPackageWorkflowFrontier,
type PluginPackageWorkflowFrontierAdvanceResult,
type PluginPackageWorkflowFrontierCandidate,
type PluginPackageWorkflowFrontierCursor,
type PluginPackageWorkflowFrontierPage,
type PluginPackageWorkflowFrontierRepository,
type PluginPackageWorkflowTerminalStatus,
} from '@qinglong/runtime-core/plugin-package-workflow-frontier';
import {
normalizePluginPackageWorkflowExecutionPlan,
type PluginPackageWorkflowExecutionPlan,
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import {
normalizeStepRunRecord,
type StepRunMutation,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DIGEST = /^[0-9a-f]{64}$/;
const TERMINAL_RUN_STATUSES = new Set<PluginPackageWorkflowTerminalStatus>([
'succeeded',
'failed',
'cancelled',
'timed_out',
]);
const RUN_SELECT = `
id, project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision", task_name AS "taskName",
task_snapshot_ref AS "taskSnapshotRef", legacy_cron_id AS "legacyCronId",
parent_run_id AS "parentRunId", retry_of_run_id AS "retryOfRunId",
trigger_id AS "triggerId", trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner", triggered_by AS "triggeredBy",
request_id AS "requestId", scheduled_for_ms AS "scheduledForMs",
status, version, event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey", input_ref AS "inputRef",
output_ref AS "outputRef", created_at_ms AS "createdAtMs",
queued_at_ms AS "queuedAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs",
cancel_requested_at_ms AS "cancelRequestedAtMs",
cancel_reason AS "cancelReason", error_code AS "errorCode",
error_summary AS "errorSummary"
`.trim();
const STEP_RUN_SELECT = `
id, run_id AS "runId", parent_step_run_id AS "parentStepRunId",
step_key AS "stepKey", kind, definition_ref AS "definitionRef",
definition_digest AS "definitionDigest", required, status, version,
attempt_count AS "attemptCount", input_ref AS "inputRef",
output_ref AS "outputRef", approval_request_id AS "approvalRequestId",
ready_at_ms AS "readyAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs", result_code AS "resultCode",
error_summary AS "errorSummary", created_at_ms AS "createdAtMs",
updated_at_ms AS "updatedAtMs", last_mutation_id AS "lastMutationId",
step_run_digest AS "stepRunDigest", step_run_json AS "stepRunJson"
`.trim();
function unavailable(
cause?: unknown,
): PluginPackageWorkflowFrontierUnavailableError {
return new PluginPackageWorkflowFrontierUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function text(row: Row, key: string): string {
return postgresRequiredString(row[key], unavailable);
}
function integer(row: Row, key: string): number {
const value = postgresRequiredInteger(row[key], unavailable);
if (value < 0) throw unavailable();
return value;
}
function optionalText(row: Row, key: string): string | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return text(row, key);
}
function optionalInteger(row: Row, key: string): number | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return integer(row, key);
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY.test(value)) {
throw new InvalidPluginPackageWorkflowFrontierError(
`${label} is invalid`,
);
}
return value;
}
function digest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) throw unavailable();
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof InvalidPluginPackageWorkflowFrontierError ||
error instanceof PluginPackageWorkflowFrontierConflictError ||
error instanceof PluginPackageWorkflowFrontierUnavailableError
) {
return error;
}
return ['23503', '23505', '23514'].includes(
postgresSqlState(error) ?? '',
)
? new PluginPackageWorkflowFrontierConflictError()
: unavailable(error);
}
function pageLimit(value: unknown): number {
if (
!Number.isInteger(value) ||
(value as number) < 1 ||
(value as number) > MAX_PLUGIN_PACKAGE_WORKFLOW_FRONTIER_PAGE_SIZE
) {
throw new InvalidPluginPackageWorkflowFrontierError(
`page limit must be between 1 and ${MAX_PLUGIN_PACKAGE_WORKFLOW_FRONTIER_PAGE_SIZE}`,
);
}
return value as number;
}
function cursor(
value: Readonly<PluginPackageWorkflowFrontierCursor> | undefined,
): Readonly<PluginPackageWorkflowFrontierCursor> | undefined {
if (value === undefined) return undefined;
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Reflect.ownKeys(value).length !== 2 ||
!Reflect.has(value, 'admittedAtMs') ||
!Reflect.has(value, 'planDigest') ||
!Number.isSafeInteger(value.admittedAtMs) ||
value.admittedAtMs < 0 ||
!DIGEST.test(value.planDigest)
) {
throw new InvalidPluginPackageWorkflowFrontierError(
'frontier cursor is invalid',
);
}
return Object.freeze({
admittedAtMs: value.admittedAtMs,
planDigest: value.planDigest,
});
}
function runFromRow(row: Row): Readonly<RunRecord> {
const run: RunRecord = {
id: text(row, 'id'),
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
taskRevision: text(row, 'taskRevision'),
triggerType: text(row, 'triggerType'),
executionOrigin: text(
row,
'executionOrigin',
) as RunRecord['executionOrigin'],
executionOwner: text(row, 'executionOwner') as RunRecord['executionOwner'],
status: text(row, 'status') as RunRecord['status'],
version: integer(row, 'version'),
eventSequence: integer(row, 'eventSequence'),
priority: integer(row, 'priority'),
createdAtMs: integer(row, 'createdAtMs'),
};
const optionalTexts = [
['taskName', 'taskName'],
['taskSnapshotRef', 'taskSnapshotRef'],
['parentRunId', 'parentRunId'],
['retryOfRunId', 'retryOfRunId'],
['triggerId', 'triggerId'],
['triggeredBy', 'triggeredBy'],
['requestId', 'requestId'],
['idempotencyKey', 'idempotencyKey'],
['inputRef', 'inputRef'],
['outputRef', 'outputRef'],
['errorCode', 'errorCode'],
['errorSummary', 'errorSummary'],
] as const;
for (const [property, key] of optionalTexts) {
const value = optionalText(row, key);
if (value !== undefined) {
(run as unknown as Record<string, unknown>)[property] = value;
}
}
const optionalIntegers = [
['legacyCronId', 'legacyCronId'],
['scheduledForMs', 'scheduledForMs'],
['queuedAtMs', 'queuedAtMs'],
['startedAtMs', 'startedAtMs'],
['finishedAtMs', 'finishedAtMs'],
['cancelRequestedAtMs', 'cancelRequestedAtMs'],
] as const;
for (const [property, key] of optionalIntegers) {
const value = optionalInteger(row, key);
if (value !== undefined) {
(run as unknown as Record<string, unknown>)[property] = value;
}
}
const cancelReason = optionalText(row, 'cancelReason');
if (cancelReason !== undefined) {
run.cancelReason =
cancelReason as NonNullable<RunRecord['cancelReason']>;
}
return Object.freeze(run);
}
function stepRunFromRow(row: Row): Readonly<StepRunRecord> {
let stepRun: Readonly<StepRunRecord>;
try {
stepRun = normalizeStepRunRecord(
postgresRequiredJsonObject(
row.stepRunJson,
unavailable,
) as unknown as StepRunRecord,
);
} catch {
throw unavailable();
}
if (
text(row, 'id') !== stepRun.id ||
text(row, 'runId') !== stepRun.runId ||
optionalText(row, 'parentStepRunId') !==
(stepRun.parentStepRunId ?? undefined) ||
text(row, 'stepKey') !== stepRun.stepKey ||
text(row, 'kind') !== stepRun.kind ||
text(row, 'definitionRef') !== stepRun.definitionRef ||
text(row, 'definitionDigest') !== stepRun.definitionDigest ||
postgresRequiredBoolean(row.required, unavailable) !== stepRun.required ||
text(row, 'status') !== stepRun.status ||
integer(row, 'version') !== stepRun.version ||
integer(row, 'attemptCount') !== stepRun.attemptCount ||
optionalText(row, 'inputRef') !== (stepRun.inputRef ?? undefined) ||
optionalText(row, 'outputRef') !== (stepRun.outputRef ?? undefined) ||
optionalText(row, 'approvalRequestId') !==
(stepRun.approvalRequestId ?? undefined) ||
optionalInteger(row, 'readyAtMs') !==
(stepRun.readyAtMs ?? undefined) ||
optionalInteger(row, 'startedAtMs') !==
(stepRun.startedAtMs ?? undefined) ||
optionalInteger(row, 'finishedAtMs') !==
(stepRun.finishedAtMs ?? undefined) ||
optionalText(row, 'resultCode') !==
(stepRun.resultCode ?? undefined) ||
optionalText(row, 'errorSummary') !==
(stepRun.errorSummary ?? undefined) ||
integer(row, 'createdAtMs') !== stepRun.createdAtMs ||
integer(row, 'updatedAtMs') !== stepRun.updatedAtMs ||
text(row, 'lastMutationId') !== stepRun.lastMutationId ||
text(row, 'stepRunDigest') !== stepRun.stepRunDigest
) {
throw unavailable();
}
return stepRun;
}
function assertRunIdentity(
run: Readonly<RunRecord>,
plan: Readonly<PluginPackageWorkflowExecutionPlan>,
): void {
if (
run.id !== plan.runId ||
run.projectId !== plan.target.projectId ||
run.taskId !== plan.target.workflowId ||
run.taskRevision !== plan.target.publicationDigest ||
run.triggerType !== 'plugin_package_workflow' ||
run.executionOrigin !== 'system' ||
run.executionOwner !== 'runtime' ||
run.requestId !== plan.planId ||
run.idempotencyKey !== `plugin-package-workflow:${plan.planId}` ||
run.version !== run.eventSequence
) {
throw unavailable();
}
}
async function updateStepRun(
client: PostgresClient,
mutation: Readonly<StepRunMutation>,
): Promise<void> {
const stepRun = mutation.stepRun;
const updated = await client.query(
`UPDATE "ql3"."step_runs"
SET status = $1, version = $2, attempt_count = $3, output_ref = $4,
approval_request_id = $5, ready_at_ms = $6, started_at_ms = $7,
finished_at_ms = $8, result_code = $9, error_summary = $10,
updated_at_ms = $11, last_mutation_id = $12,
step_run_digest = $13, step_run_json = $14::jsonb
WHERE id = $15 AND run_id = $16 AND version = $17
AND step_run_digest = $18 AND status = $19`,
[
stepRun.status,
stepRun.version,
stepRun.attemptCount,
stepRun.outputRef,
stepRun.approvalRequestId,
stepRun.readyAtMs,
stepRun.startedAtMs,
stepRun.finishedAtMs,
stepRun.resultCode,
stepRun.errorSummary,
stepRun.updatedAtMs,
stepRun.lastMutationId,
stepRun.stepRunDigest,
JSON.stringify(stepRun),
stepRun.id,
stepRun.runId,
mutation.expectedStepRunVersion,
mutation.expectedStepRunDigest,
mutation.previousStatus,
],
);
if (updated.rowCount !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
}
async function insertStepMutation(
client: PostgresClient,
mutation: Readonly<StepRunMutation>,
committedAtMs: number,
): Promise<void> {
const event = mutation.event;
await client.query(
`INSERT INTO "ql3"."run_events" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8, $9::jsonb, $10)`,
[
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
mutation.stepRun.id,
JSON.stringify(event.payload),
event.createdAtMs,
],
);
await client.query(
`INSERT INTO "ql3"."step_run_mutations" (
mutation_id, mutation_digest, run_id, step_run_id, step_run_digest,
event_id, event_sequence, run_version, step_run_json, committed_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)`,
[
mutation.mutationId,
mutation.mutationDigest,
mutation.runId,
mutation.stepRun.id,
mutation.stepRun.stepRunDigest,
event.id,
event.sequence,
mutation.expectedRunVersion + 1,
JSON.stringify(mutation.stepRun),
committedAtMs,
],
);
}
export class PostgresPluginPackageWorkflowFrontierRepository
implements PluginPackageWorkflowFrontierRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw unavailable();
}
}
async listCandidates(queryValue: Readonly<{
limit: number;
after?: Readonly<PluginPackageWorkflowFrontierCursor>;
}>): Promise<Readonly<PluginPackageWorkflowFrontierPage>> {
if (
!queryValue ||
typeof queryValue !== 'object' ||
Array.isArray(queryValue) ||
!Reflect.has(queryValue, 'limit') ||
Reflect.ownKeys(queryValue).some(
(key) => key !== 'limit' && key !== 'after',
)
) {
throw new InvalidPluginPackageWorkflowFrontierError(
'page query is invalid',
);
}
const limit = pageLimit(queryValue.limit);
const after = cursor(queryValue.after);
try {
const result = await this.pool.query<Row>(
`SELECT admission.run_id AS "runId",
admission.plan_digest AS "planDigest",
admission.admitted_at_ms AS "admittedAtMs"
FROM "ql3"."plugin_package_workflow_admissions" AS admission
JOIN "ql3"."runs" AS run ON run.id = admission.run_id
WHERE run.status = 'running'
AND run.cancel_requested_at_ms IS NULL
AND (
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_workflow_admission_steps" AS step
JOIN "ql3"."step_runs" AS current
ON current.run_id = step.run_id
AND current.id = step.step_run_id
WHERE step.plan_digest = admission.plan_digest
AND current.status = 'pending'
AND (
NOT EXISTS (
SELECT 1
FROM jsonb_array_elements_text(step.needs_json)
AS need(value)
LEFT JOIN
"ql3"."plugin_package_workflow_admission_steps"
AS dependency_step
ON dependency_step.plan_digest = step.plan_digest
AND dependency_step.step_key = need.value
LEFT JOIN "ql3"."step_runs" AS dependency
ON dependency.run_id = dependency_step.run_id
AND dependency.id = dependency_step.step_run_id
WHERE dependency.id IS NULL
OR dependency.status <> 'succeeded'
)
OR EXISTS (
SELECT 1
FROM jsonb_array_elements_text(step.needs_json)
AS need(value)
JOIN "ql3"."plugin_package_workflow_admission_steps"
AS dependency_step
ON dependency_step.plan_digest = step.plan_digest
AND dependency_step.step_key = need.value
JOIN "ql3"."step_runs" AS dependency
ON dependency.run_id = dependency_step.run_id
AND dependency.id = dependency_step.step_run_id
WHERE dependency.status IN (
'failed', 'skipped', 'cancelled', 'timed_out'
)
)
)
)
OR NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_workflow_admission_steps" AS step
JOIN "ql3"."step_runs" AS current
ON current.run_id = step.run_id
AND current.id = step.step_run_id
WHERE step.plan_digest = admission.plan_digest
AND current.status NOT IN (
'succeeded', 'failed', 'skipped', 'cancelled', 'timed_out'
)
)
)
AND (
$1::bigint IS NULL OR admission.admitted_at_ms > $1 OR
(admission.admitted_at_ms = $1
AND admission.plan_digest > $2)
)
ORDER BY admission.admitted_at_ms, admission.plan_digest
LIMIT $3`,
[
after?.admittedAtMs ?? null,
after?.planDigest ?? '',
limit + 1,
],
);
const mapped = result.rows.map(
(row): Readonly<PluginPackageWorkflowFrontierCandidate> =>
Object.freeze({
runId: identity(text(row, 'runId'), 'candidate runId'),
planDigest: digest(row.planDigest),
admittedAtMs: integer(row, 'admittedAtMs'),
}),
);
const truncated = mapped.length > limit;
const candidates = Object.freeze(mapped.slice(0, limit));
const last = candidates.at(-1);
return Object.freeze({
candidates,
truncated,
...(truncated && last
? {
next: Object.freeze({
admittedAtMs: last.admittedAtMs,
planDigest: last.planDigest,
}),
}
: {}),
});
} catch (error) {
throw mapStorageError(error);
}
}
async advance(
runIdValue: string,
): Promise<Readonly<PluginPackageWorkflowFrontierAdvanceResult>> {
const runId = identity(runIdValue, 'runId');
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw unavailable(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const admission = await client.query<Row>(
`SELECT plan_digest AS "planDigest", plan_json AS "planJson"
FROM "ql3"."plugin_package_workflow_admissions"
WHERE run_id = $1 LIMIT 2`,
[runId],
);
if (admission.rows.length !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
let plan: Readonly<PluginPackageWorkflowExecutionPlan>;
try {
plan = normalizePluginPackageWorkflowExecutionPlan(
postgresRequiredJsonObject(
admission.rows[0]!.planJson,
unavailable,
) as unknown as PluginPackageWorkflowExecutionPlan,
);
} catch {
throw unavailable();
}
if (plan.planDigest !== digest(admission.rows[0]!.planDigest)) {
throw unavailable();
}
const runRows = await client.query<Row>(
`SELECT ${RUN_SELECT}
FROM "ql3"."runs" WHERE id = $1 LIMIT 2 FOR UPDATE`,
[runId],
);
if (runRows.rows.length !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
const run = runFromRow(runRows.rows[0]!);
assertRunIdentity(run, plan);
const stepRows = await client.query<Row>(
`SELECT ${STEP_RUN_SELECT}
FROM "ql3"."step_runs" WHERE run_id = $1
ORDER BY step_key, id FOR UPDATE`,
[runId],
);
const stepRuns = stepRows.rows.map(stepRunFromRow);
const clock = await client.query<Row>(
`SELECT floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint AS "observedAtMs"`,
);
if (clock.rows.length !== 1) throw unavailable();
const observedAtMs = integer(clock.rows[0]!, 'observedAtMs');
const currentStatus = run.status;
const resolution = resolvePluginPackageWorkflowFrontier({
plan,
run: {
...run,
...(TERMINAL_RUN_STATUSES.has(
currentStatus as PluginPackageWorkflowTerminalStatus,
)
? { status: 'running' as const }
: {}),
},
stepRuns,
observedAtMs,
});
if (
TERMINAL_RUN_STATUSES.has(
currentStatus as PluginPackageWorkflowTerminalStatus,
)
) {
if (
resolution.stepMutations.length !== 0 ||
resolution.terminalStatus !== currentStatus
) {
throw unavailable();
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'settled' as const,
runId,
planDigest: plan.planDigest,
stepMutationCount: 0,
readyStepRunIds: Object.freeze([]),
terminalStatus:
currentStatus as PluginPackageWorkflowTerminalStatus,
runVersion: run.version,
runEventSequence: run.eventSequence,
observedAtMs,
});
}
if (currentStatus !== 'running') throw unavailable();
const increment =
resolution.stepMutations.length +
(resolution.terminalTransition === null ? 0 : 1);
if (increment === 0) {
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'unchanged' as const,
runId,
planDigest: plan.planDigest,
stepMutationCount: 0,
readyStepRunIds: resolution.readyStepRunIds,
terminalStatus: null,
runVersion: run.version,
runEventSequence: run.eventSequence,
observedAtMs,
});
}
for (const mutation of resolution.stepMutations) {
await updateStepRun(client, mutation);
}
const terminal = resolution.terminalTransition;
const updatedRun = await client.query(
`UPDATE "ql3"."runs"
SET status = $1, version = version + $2,
event_sequence = event_sequence + $2,
finished_at_ms = $3, error_code = $4, error_summary = NULL
WHERE id = $5 AND status = 'running'
AND version = $6 AND event_sequence = $7`,
[
terminal?.status ?? 'running',
increment,
terminal?.finishedAtMs ?? null,
terminal?.errorCode ?? null,
runId,
run.version,
run.eventSequence,
],
);
if (updatedRun.rowCount !== 1) {
throw new PluginPackageWorkflowFrontierConflictError();
}
for (const mutation of resolution.stepMutations) {
await insertStepMutation(client, mutation, observedAtMs);
}
if (terminal) {
const event = terminal.event;
await client.query(
`INSERT INTO "ql3"."run_events" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, NULL, NULL, $8::jsonb, $9
)`,
[
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
JSON.stringify(event.payload),
event.createdAtMs,
],
);
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: terminal ? ('terminal' as const) : ('advanced' as const),
runId,
planDigest: plan.planDigest,
stepMutationCount: resolution.stepMutations.length,
readyStepRunIds: terminal
? Object.freeze([])
: resolution.readyStepRunIds,
terminalStatus: terminal?.status ?? null,
runVersion: run.version + increment,
runEventSequence: run.eventSequence + increment,
observedAtMs,
});
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mapStorageError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}
@@ -0,0 +1,777 @@
// PostgreSQL authority for admitting Plugin Package Workflow task attempts.
import type {
PostgresClient,
PostgresPool,
RunRecord,
} from '@qinglong/runtime-core';
import {
normalizeClusterTaskExecutionRevision,
type ClusterTaskExecutionRevision,
} from '@qinglong/runtime-core/cluster-execution-revision';
import {
createPluginPackageWorkflowTaskAttemptAdmission,
InvalidPluginPackageWorkflowTaskAttemptAdmissionError,
MAX_PLUGIN_PACKAGE_WORKFLOW_TASK_ATTEMPT_PAGE_SIZE,
normalizePluginPackageWorkflowTaskAttemptAdmissionReceipt,
PluginPackageWorkflowTaskAttemptAdmissionConflictError,
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError,
type PluginPackageWorkflowTaskAttemptAdmissionCandidate,
type PluginPackageWorkflowTaskAttemptAdmissionCursor,
type PluginPackageWorkflowTaskAttemptAdmissionPage,
type PluginPackageWorkflowTaskAttemptAdmissionReceipt,
type PluginPackageWorkflowTaskAttemptAdmissionRepository,
type PluginPackageWorkflowTaskAttemptAdmissionResult,
} from '@qinglong/runtime-core/plugin-package-workflow-task-attempt-admission';
import {
normalizePluginPackageTaskReconciliationReceipt,
type PluginPackageTaskReconciliationReceipt,
} from '@qinglong/runtime-core/plugin-package-task-reconciliation';
import {
normalizePluginPackageWorkflowExecutionPlan,
type PluginPackageWorkflowExecutionPlan,
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
import {
normalizeStepRunRecord,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredBoolean,
postgresRequiredInteger,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
type Row = Record<string, unknown>;
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DIGEST = /^[0-9a-f]{64}$/;
const RUN_SELECT = `
id, project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision", task_name AS "taskName",
task_snapshot_ref AS "taskSnapshotRef", legacy_cron_id AS "legacyCronId",
parent_run_id AS "parentRunId", retry_of_run_id AS "retryOfRunId",
trigger_id AS "triggerId", trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner", triggered_by AS "triggeredBy",
request_id AS "requestId", scheduled_for_ms AS "scheduledForMs",
status, version, event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey", input_ref AS "inputRef",
output_ref AS "outputRef", created_at_ms AS "createdAtMs",
queued_at_ms AS "queuedAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs",
cancel_requested_at_ms AS "cancelRequestedAtMs",
cancel_reason AS "cancelReason", error_code AS "errorCode",
error_summary AS "errorSummary"
`.trim();
const STEP_RUN_SELECT = `
id, run_id AS "runId", parent_step_run_id AS "parentStepRunId",
step_key AS "stepKey", kind, definition_ref AS "definitionRef",
definition_digest AS "definitionDigest", required, status, version,
attempt_count AS "attemptCount", input_ref AS "inputRef",
output_ref AS "outputRef", approval_request_id AS "approvalRequestId",
ready_at_ms AS "readyAtMs", started_at_ms AS "startedAtMs",
finished_at_ms AS "finishedAtMs", result_code AS "resultCode",
error_summary AS "errorSummary", created_at_ms AS "createdAtMs",
updated_at_ms AS "updatedAtMs", last_mutation_id AS "lastMutationId",
step_run_digest AS "stepRunDigest", step_run_json AS "stepRunJson"
`.trim();
function unavailable(
cause?: unknown,
): PluginPackageWorkflowTaskAttemptAdmissionUnavailableError {
return new PluginPackageWorkflowTaskAttemptAdmissionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function text(row: Row, key: string): string {
return postgresRequiredString(row[key], unavailable);
}
function integer(row: Row, key: string): number {
const value = postgresRequiredInteger(row[key], unavailable);
if (value < 0) throw unavailable();
return value;
}
function optionalText(row: Row, key: string): string | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return text(row, key);
}
function optionalInteger(row: Row, key: string): number | undefined {
if (row[key] === null || row[key] === undefined) return undefined;
return integer(row, key);
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY.test(value)) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
`${label} is invalid`,
);
}
return value;
}
function digest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) throw unavailable();
return value;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof
InvalidPluginPackageWorkflowTaskAttemptAdmissionError ||
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionConflictError ||
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError
) {
return error;
}
return ['23503', '23505', '23514'].includes(
postgresSqlState(error) ?? '',
)
? new PluginPackageWorkflowTaskAttemptAdmissionConflictError()
: unavailable(error);
}
function pageLimit(value: unknown): number {
if (
!Number.isInteger(value) ||
(value as number) < 1 ||
(value as number) >
MAX_PLUGIN_PACKAGE_WORKFLOW_TASK_ATTEMPT_PAGE_SIZE
) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
`page limit must be between 1 and ${MAX_PLUGIN_PACKAGE_WORKFLOW_TASK_ATTEMPT_PAGE_SIZE}`,
);
}
return value as number;
}
function cursor(
value:
| Readonly<PluginPackageWorkflowTaskAttemptAdmissionCursor>
| undefined,
):
| Readonly<PluginPackageWorkflowTaskAttemptAdmissionCursor>
| undefined {
if (value === undefined) return undefined;
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Reflect.ownKeys(value).length !== 2 ||
!Reflect.has(value, 'readyAtMs') ||
!Reflect.has(value, 'stepRunId') ||
!Number.isSafeInteger(value.readyAtMs) ||
value.readyAtMs < 0 ||
typeof value.stepRunId !== 'string' ||
!IDENTITY.test(value.stepRunId)
) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
'candidate cursor is invalid',
);
}
return Object.freeze({
readyAtMs: value.readyAtMs,
stepRunId: value.stepRunId,
});
}
function runFromRow(row: Row): Readonly<RunRecord> {
const run: RunRecord = {
id: text(row, 'id'),
projectId: text(row, 'projectId'),
taskId: text(row, 'taskId'),
taskRevision: text(row, 'taskRevision'),
triggerType: text(row, 'triggerType'),
executionOrigin: text(
row,
'executionOrigin',
) as RunRecord['executionOrigin'],
executionOwner: text(row, 'executionOwner') as RunRecord['executionOwner'],
status: text(row, 'status') as RunRecord['status'],
version: integer(row, 'version'),
eventSequence: integer(row, 'eventSequence'),
priority: integer(row, 'priority'),
createdAtMs: integer(row, 'createdAtMs'),
};
const optionalTexts = [
['taskName', 'taskName'],
['taskSnapshotRef', 'taskSnapshotRef'],
['parentRunId', 'parentRunId'],
['retryOfRunId', 'retryOfRunId'],
['triggerId', 'triggerId'],
['triggeredBy', 'triggeredBy'],
['requestId', 'requestId'],
['idempotencyKey', 'idempotencyKey'],
['inputRef', 'inputRef'],
['outputRef', 'outputRef'],
['errorCode', 'errorCode'],
['errorSummary', 'errorSummary'],
] as const;
for (const [property, key] of optionalTexts) {
const value = optionalText(row, key);
if (value !== undefined) {
(run as unknown as Record<string, unknown>)[property] = value;
}
}
const optionalIntegers = [
['legacyCronId', 'legacyCronId'],
['scheduledForMs', 'scheduledForMs'],
['queuedAtMs', 'queuedAtMs'],
['startedAtMs', 'startedAtMs'],
['finishedAtMs', 'finishedAtMs'],
['cancelRequestedAtMs', 'cancelRequestedAtMs'],
] as const;
for (const [property, key] of optionalIntegers) {
const value = optionalInteger(row, key);
if (value !== undefined) {
(run as unknown as Record<string, unknown>)[property] = value;
}
}
const cancelReason = optionalText(row, 'cancelReason');
if (cancelReason !== undefined) {
run.cancelReason =
cancelReason as NonNullable<RunRecord['cancelReason']>;
}
return Object.freeze(run);
}
function stepRunFromRow(row: Row): Readonly<StepRunRecord> {
let stepRun: Readonly<StepRunRecord>;
try {
stepRun = normalizeStepRunRecord(
postgresRequiredJsonObject(
row.stepRunJson,
unavailable,
) as unknown as StepRunRecord,
);
} catch {
throw unavailable();
}
if (
text(row, 'id') !== stepRun.id ||
text(row, 'runId') !== stepRun.runId ||
optionalText(row, 'parentStepRunId') !==
(stepRun.parentStepRunId ?? undefined) ||
text(row, 'stepKey') !== stepRun.stepKey ||
text(row, 'kind') !== stepRun.kind ||
text(row, 'definitionRef') !== stepRun.definitionRef ||
text(row, 'definitionDigest') !== stepRun.definitionDigest ||
postgresRequiredBoolean(row.required, unavailable) !== stepRun.required ||
text(row, 'status') !== stepRun.status ||
integer(row, 'version') !== stepRun.version ||
integer(row, 'attemptCount') !== stepRun.attemptCount ||
optionalText(row, 'inputRef') !== (stepRun.inputRef ?? undefined) ||
optionalText(row, 'outputRef') !== (stepRun.outputRef ?? undefined) ||
optionalText(row, 'approvalRequestId') !==
(stepRun.approvalRequestId ?? undefined) ||
optionalInteger(row, 'readyAtMs') !==
(stepRun.readyAtMs ?? undefined) ||
optionalInteger(row, 'startedAtMs') !==
(stepRun.startedAtMs ?? undefined) ||
optionalInteger(row, 'finishedAtMs') !==
(stepRun.finishedAtMs ?? undefined) ||
optionalText(row, 'resultCode') !==
(stepRun.resultCode ?? undefined) ||
optionalText(row, 'errorSummary') !==
(stepRun.errorSummary ?? undefined) ||
integer(row, 'createdAtMs') !== stepRun.createdAtMs ||
integer(row, 'updatedAtMs') !== stepRun.updatedAtMs ||
text(row, 'lastMutationId') !== stepRun.lastMutationId ||
text(row, 'stepRunDigest') !== stepRun.stepRunDigest
) {
throw unavailable();
}
return stepRun;
}
function planFromRow(row: Row): Readonly<PluginPackageWorkflowExecutionPlan> {
try {
return normalizePluginPackageWorkflowExecutionPlan(
postgresRequiredJsonObject(
row.planJson,
unavailable,
) as unknown as PluginPackageWorkflowExecutionPlan,
);
} catch {
throw unavailable();
}
}
function reconciliationFromRow(
row: Row,
): Readonly<PluginPackageTaskReconciliationReceipt> {
try {
return normalizePluginPackageTaskReconciliationReceipt(
postgresRequiredJsonObject(
row.reconciliationJson,
unavailable,
) as unknown as PluginPackageTaskReconciliationReceipt,
);
} catch {
throw unavailable();
}
}
function executionFromRow(
row: Row,
): Readonly<ClusterTaskExecutionRevision> {
try {
const plan = postgresRequiredJsonObject(row.executionPlanJson, unavailable);
const keys = Object.keys(plan);
if (
!keys.includes('command') ||
!keys.includes('environment') ||
keys.some(
(key) =>
![
'command',
'environment',
'placement',
'timeoutMs',
'workingDirectory',
].includes(key),
)
) {
throw unavailable();
}
return normalizeClusterTaskExecutionRevision({
projectId: text(row, 'executionProjectId'),
taskId: text(row, 'executionTaskId'),
sourceRevision: integer(row, 'executionSourceRevision'),
taskRevision: text(row, 'executionTaskRevision'),
sourceContentDigest: text(row, 'executionSourceContentDigest'),
executorType: text(
row,
'executionExecutorType',
) as ClusterTaskExecutionRevision['executorType'],
planSchema: text(
row,
'executionPlanSchema',
) as ClusterTaskExecutionRevision['planSchema'],
command: plan.command as ClusterTaskExecutionRevision['command'],
environment:
plan.environment as ClusterTaskExecutionRevision['environment'],
...(plan.workingDirectory === undefined
? {}
: { workingDirectory: plan.workingDirectory as string }),
...(plan.timeoutMs === undefined
? {}
: { timeoutMs: plan.timeoutMs as number }),
...(plan.placement === undefined
? {}
: {
placement:
plan.placement as unknown as NonNullable<
ClusterTaskExecutionRevision['placement']
>,
}),
contentDigest: text(row, 'executionContentDigest'),
createdAtMs: integer(row, 'executionCreatedAtMs'),
});
} catch (error) {
if (
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError
) {
throw error;
}
throw unavailable();
}
}
function receiptFromRow(
row: Row,
): Readonly<PluginPackageWorkflowTaskAttemptAdmissionReceipt> {
try {
const receipt =
normalizePluginPackageWorkflowTaskAttemptAdmissionReceipt(
postgresRequiredJsonObject(
row.receiptJson,
unavailable,
) as unknown as PluginPackageWorkflowTaskAttemptAdmissionReceipt,
);
if (
row.receiptDigest !== undefined &&
text(row, 'receiptDigest') !== receipt.receiptDigest
) {
throw unavailable();
}
return receipt;
} catch (error) {
if (
error instanceof
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError
) {
throw error;
}
throw unavailable();
}
}
export class PostgresPluginPackageWorkflowTaskAttemptAdmissionRepository
implements PluginPackageWorkflowTaskAttemptAdmissionRepository
{
constructor(private readonly pool: PostgresPool) {
if (!pool || typeof pool.connect !== 'function') {
throw new TypeError(
'PostgreSQL Workflow Task Attempt admission pool is invalid',
);
}
}
async listCandidates(queryValue: Readonly<{
limit: number;
after?: Readonly<PluginPackageWorkflowTaskAttemptAdmissionCursor>;
}>): Promise<
Readonly<PluginPackageWorkflowTaskAttemptAdmissionPage>
> {
if (
!queryValue ||
typeof queryValue !== 'object' ||
Array.isArray(queryValue) ||
!Reflect.has(queryValue, 'limit') ||
Reflect.ownKeys(queryValue).some(
(key) => key !== 'limit' && key !== 'after',
)
) {
throw new InvalidPluginPackageWorkflowTaskAttemptAdmissionError(
'page query is invalid',
);
}
const limit = pageLimit(queryValue.limit);
const after = cursor(queryValue.after);
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw unavailable(error);
}
try {
const result = await client.query<Row>(
`SELECT current.run_id AS "runId",
current.id AS "stepRunId",
current.ready_at_ms AS "readyAtMs",
admission.plan_digest AS "planDigest"
FROM "ql3"."step_runs" AS current
JOIN "ql3"."plugin_package_workflow_admission_steps" AS source
ON source.run_id = current.run_id
AND source.step_run_id = current.id
JOIN "ql3"."plugin_package_workflow_admissions" AS admission
ON admission.plan_digest = source.plan_digest
AND admission.run_id = source.run_id
JOIN "ql3"."runs" AS run ON run.id = current.run_id
WHERE run.status = 'running'
AND run.cancel_requested_at_ms IS NULL
AND current.kind = 'task'
AND current.status = 'ready'
AND current.ready_at_ms IS NOT NULL
AND current.attempt_count < 64
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_workflow_task_attempt_admissions"
AS task_attempt
WHERE task_attempt.run_id = current.run_id
AND task_attempt.step_run_id = current.id
AND task_attempt.step_run_version = current.version
)
AND (
$1::varchar IS NULL OR current.ready_at_ms > $2 OR
(current.ready_at_ms = $2 AND current.id > $1)
)
ORDER BY current.ready_at_ms, current.id
LIMIT $3`,
[
after?.stepRunId ?? null,
after?.readyAtMs ?? 0,
limit + 1,
],
);
const mapped = result.rows.map(
(row): Readonly<PluginPackageWorkflowTaskAttemptAdmissionCandidate> =>
Object.freeze({
runId: identity(text(row, 'runId'), 'candidate runId'),
stepRunId: identity(
text(row, 'stepRunId'),
'candidate stepRunId',
),
readyAtMs: integer(row, 'readyAtMs'),
planDigest: digest(row.planDigest),
}),
);
const truncated = mapped.length > limit;
const candidates = Object.freeze(mapped.slice(0, limit));
const last = candidates.at(-1);
return Object.freeze({
candidates,
truncated,
...(truncated && last
? {
next: Object.freeze({
readyAtMs: last.readyAtMs,
stepRunId: last.stepRunId,
}),
}
: {}),
});
} catch (error) {
throw mapStorageError(error);
} finally {
client.release();
}
}
async admit(
runIdValue: string,
stepRunIdValue: string,
): Promise<
Readonly<PluginPackageWorkflowTaskAttemptAdmissionResult>
> {
const runId = identity(runIdValue, 'runId');
const stepRunId = identity(stepRunIdValue, 'stepRunId');
for (
let transactionAttempt = 0;
transactionAttempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
transactionAttempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw unavailable(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const runRows = await client.query<Row>(
`SELECT ${RUN_SELECT}
FROM "ql3"."runs" WHERE id = $1 LIMIT 2 FOR UPDATE`,
[runId],
);
if (runRows.rows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const run = runFromRow(runRows.rows[0]!);
const stepRows = await client.query<Row>(
`SELECT ${STEP_RUN_SELECT}
FROM "ql3"."step_runs"
WHERE run_id = $1 AND id = $2 LIMIT 2 FOR UPDATE`,
[runId, stepRunId],
);
if (stepRows.rows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const stepRun = stepRunFromRow(stepRows.rows[0]!);
const existing = await client.query<Row>(
`SELECT receipt_digest AS "receiptDigest",
receipt_json AS "receiptJson"
FROM "ql3"."plugin_package_workflow_task_attempt_admissions"
WHERE run_id = $1 AND step_run_id = $2
AND step_run_version = $3
LIMIT 2`,
[runId, stepRunId, stepRun.version],
);
if (existing.rows.length > 1) throw unavailable();
if (existing.rows.length === 1) {
const receipt = receiptFromRow(existing.rows[0]!);
if (
receipt.runId !== runId ||
receipt.stepRunId !== stepRunId ||
receipt.stepRunVersion !== stepRun.version ||
receipt.stepRunDigest !== stepRun.stepRunDigest
) {
throw unavailable();
}
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'existing' as const,
receipt,
});
}
if (stepRun.status !== 'ready') {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const snapshot = await client.query<Row>(
`SELECT plan_json AS "planJson",
reconciliation_json AS "reconciliationJson",
execution_project_id AS "executionProjectId",
execution_task_id AS "executionTaskId",
execution_source_revision AS "executionSourceRevision",
execution_task_revision AS "executionTaskRevision",
execution_source_content_digest
AS "executionSourceContentDigest",
execution_executor_type AS "executionExecutorType",
execution_plan_schema AS "executionPlanSchema",
execution_plan_json AS "executionPlanJson",
execution_content_digest AS "executionContentDigest",
execution_created_at_ms AS "executionCreatedAtMs"
FROM "ql3"."plugin_package_workflow_task_attempt_snapshot"($1, $2)`,
[runId, stepRunId],
);
if (snapshot.rows.length !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const snapshotRow = snapshot.rows[0]!;
const plan = planFromRow(snapshotRow);
const taskReconciliation = reconciliationFromRow(snapshotRow);
const execution = executionFromRow(snapshotRow);
const clock = await client.query<Row>(
`SELECT floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint AS "admittedAtMs"`,
);
if (clock.rows.length !== 1) throw unavailable();
const attemptNumberRows = await client.query<Row>(
`SELECT COALESCE(MAX(attempt), 0) + 1 AS "attemptNumber"
FROM "ql3"."run_attempts" WHERE run_id = $1`,
[runId],
);
if (attemptNumberRows.rows.length !== 1) throw unavailable();
const bundle = createPluginPackageWorkflowTaskAttemptAdmission({
plan,
run,
stepRun,
taskReconciliation,
execution,
attemptNumber: integer(
attemptNumberRows.rows[0]!,
'attemptNumber',
),
admittedAtMs: integer(clock.rows[0]!, 'admittedAtMs'),
});
const updated = await client.query(
`UPDATE "ql3"."runs"
SET version = $1, event_sequence = $2
WHERE id = $3 AND status = 'running'
AND cancel_requested_at_ms IS NULL
AND version = $4 AND event_sequence = $5`,
[
bundle.run.version,
bundle.run.eventSequence,
run.id,
run.version,
run.eventSequence,
],
);
if (updated.rowCount !== 1) {
throw new PluginPackageWorkflowTaskAttemptAdmissionConflictError();
}
const attempt = bundle.attempt;
await client.query(
`INSERT INTO "ql3"."run_attempts" (
id, run_id, step_run_id, attempt, status, executor_type,
callback_sequence, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
attempt.id,
attempt.runId,
attempt.stepRunId ?? null,
attempt.attempt,
attempt.status,
attempt.executorType,
attempt.callbackSequence,
attempt.createdAtMs,
],
);
const event = bundle.event;
await client.query(
`INSERT INTO "ql3"."run_events" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11
)`,
[
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
event.attemptId ?? null,
event.stepRunId ?? null,
JSON.stringify(event.payload),
event.createdAtMs,
],
);
const receipt = bundle.receipt;
await client.query(
`INSERT INTO
"ql3"."plugin_package_workflow_task_attempt_admissions" (
receipt_digest, attempt_id, plan_digest, run_id,
step_run_id, step_run_version, step_run_digest,
generation_digest, resource_task_id,
task_reconciliation_receipt_digest, project_id, task_id,
source_revision, task_revision, task_definition_digest,
executor_type, execution_digest, attempt_number, event_id,
run_version, run_event_sequence, admitted_at_ms, receipt_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14, $15, $16, $17, $18, $19, $20, $21, $22, $23::jsonb
)`,
[
receipt.receiptDigest,
receipt.attemptId,
receipt.planDigest,
receipt.runId,
receipt.stepRunId,
receipt.stepRunVersion,
receipt.stepRunDigest,
plan.target.generationDigest,
receipt.resourceTaskId,
receipt.taskReconciliationReceiptDigest,
execution.projectId,
receipt.taskId,
execution.sourceRevision,
receipt.taskRevision,
receipt.taskDefinitionDigest,
receipt.executorType,
receipt.executionDigest,
receipt.attemptNumber,
receipt.eventId,
receipt.runVersion,
receipt.runEventSequence,
receipt.admittedAtMs,
JSON.stringify(receipt),
],
);
await client.query('COMMIT');
began = false;
return Object.freeze({
status: 'created' as const,
receipt,
});
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
transactionAttempt + 1 <
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mapStorageError(error);
} finally {
client.release();
}
}
throw unavailable();
}
}