mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1 @@
|
||||
dist/
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'postgresql',
|
||||
schema: './src/schema/schema.ts',
|
||||
out: './drizzle',
|
||||
strict: true,
|
||||
verbose: true,
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"name": "@qinglong/cluster-postgres",
|
||||
"version": "3.0.0-alpha.0",
|
||||
"private": true,
|
||||
"description": "QingLong 3.0 cluster-only PostgreSQL driver and schema package",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=24.18.0 <25"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"types": "./dist/entrypoints/runtime.d.ts",
|
||||
"require": "./dist/entrypoints/runtime.js",
|
||||
"default": "./dist/entrypoints/runtime.js"
|
||||
},
|
||||
"./admin": {
|
||||
"types": "./dist/entrypoints/admin.d.ts",
|
||||
"require": "./dist/entrypoints/admin.js",
|
||||
"default": "./dist/entrypoints/admin.js"
|
||||
},
|
||||
"./package-manager": {
|
||||
"types": "./dist/entrypoints/packageManager.d.ts",
|
||||
"require": "./dist/entrypoints/packageManager.js",
|
||||
"default": "./dist/entrypoints/packageManager.js"
|
||||
},
|
||||
"./package-executor": {
|
||||
"types": "./dist/entrypoints/packageExecutor.d.ts",
|
||||
"require": "./dist/entrypoints/packageExecutor.js",
|
||||
"default": "./dist/entrypoints/packageExecutor.js"
|
||||
},
|
||||
"./ai-maintenance": {
|
||||
"types": "./dist/entrypoints/aiMaintenance.d.ts",
|
||||
"require": "./dist/entrypoints/aiMaintenance.js",
|
||||
"default": "./dist/entrypoints/aiMaintenance.js"
|
||||
},
|
||||
"./ai-credential-manager": {
|
||||
"types": "./dist/entrypoints/aiCredentialManager.d.ts",
|
||||
"require": "./dist/entrypoints/aiCredentialManager.js",
|
||||
"default": "./dist/entrypoints/aiCredentialManager.js"
|
||||
},
|
||||
"./ai-credential-tester": {
|
||||
"types": "./dist/entrypoints/aiCredentialTester.d.ts",
|
||||
"require": "./dist/entrypoints/aiCredentialTester.js",
|
||||
"default": "./dist/entrypoints/aiCredentialTester.js"
|
||||
},
|
||||
"./worker-credential-manager": {
|
||||
"types": "./dist/worker-credential/workerCredentialManager.d.ts",
|
||||
"require": "./dist/worker-credential/workerCredentialManager.js",
|
||||
"default": "./dist/worker-credential/workerCredentialManager.js"
|
||||
},
|
||||
"./worker-credential-executor": {
|
||||
"types": "./dist/worker-credential/workerCredentialExecutor.d.ts",
|
||||
"require": "./dist/worker-credential/workerCredentialExecutor.js",
|
||||
"default": "./dist/worker-credential/workerCredentialExecutor.js"
|
||||
},
|
||||
"./automation-manager": {
|
||||
"types": "./dist/entrypoints/automationManager.d.ts",
|
||||
"require": "./dist/entrypoints/automationManager.js",
|
||||
"default": "./dist/entrypoints/automationManager.js"
|
||||
},
|
||||
"./task-start": {
|
||||
"types": "./dist/task-start/taskStartRepository.d.ts",
|
||||
"require": "./dist/task-start/taskStartRepository.js",
|
||||
"default": "./dist/task-start/taskStartRepository.js"
|
||||
},
|
||||
"./approval-manager": {
|
||||
"types": "./dist/approval-management/index.d.ts",
|
||||
"require": "./dist/approval-management/index.js",
|
||||
"default": "./dist/approval-management/index.js"
|
||||
},
|
||||
"./worker-ingress": {
|
||||
"types": "./dist/entrypoints/workerIngress.d.ts",
|
||||
"require": "./dist/entrypoints/workerIngress.js",
|
||||
"default": "./dist/entrypoints/workerIngress.js"
|
||||
},
|
||||
"./migration": {
|
||||
"types": "./dist/migration/migration.d.ts",
|
||||
"require": "./dist/migration/migration.js",
|
||||
"default": "./dist/migration/migration.js"
|
||||
},
|
||||
"./migration-process": {
|
||||
"types": "./dist/migration/migrationProcess.d.ts",
|
||||
"require": "./dist/migration/migrationProcess.js",
|
||||
"default": "./dist/migration/migrationProcess.js"
|
||||
},
|
||||
"./plugin-package-install": {
|
||||
"types": "./dist/plugin-package/installation/pluginPackageInstallRepository.d.ts",
|
||||
"require": "./dist/plugin-package/installation/pluginPackageInstallRepository.js",
|
||||
"default": "./dist/plugin-package/installation/pluginPackageInstallRepository.js"
|
||||
},
|
||||
"./plugin-package-quarantine": {
|
||||
"types": "./dist/plugin-package/lifecycle/pluginPackageQuarantineRepository.d.ts",
|
||||
"require": "./dist/plugin-package/lifecycle/pluginPackageQuarantineRepository.js",
|
||||
"default": "./dist/plugin-package/lifecycle/pluginPackageQuarantineRepository.js"
|
||||
},
|
||||
"./plugin-package-lifecycle": {
|
||||
"types": "./dist/plugin-package/lifecycle/pluginPackageLifecycleRepository.d.ts",
|
||||
"require": "./dist/plugin-package/lifecycle/pluginPackageLifecycleRepository.js",
|
||||
"default": "./dist/plugin-package/lifecycle/pluginPackageLifecycleRepository.js"
|
||||
},
|
||||
"./plugin-package-lifecycle-plan": {
|
||||
"types": "./dist/plugin-package/lifecycle/pluginPackageLifecyclePlanRepository.d.ts",
|
||||
"require": "./dist/plugin-package/lifecycle/pluginPackageLifecyclePlanRepository.js",
|
||||
"default": "./dist/plugin-package/lifecycle/pluginPackageLifecyclePlanRepository.js"
|
||||
},
|
||||
"./plugin-package-publisher-provenance": {
|
||||
"types": "./dist/plugin-package/publisher/pluginPackagePublisherProvenanceRepository.d.ts",
|
||||
"require": "./dist/plugin-package/publisher/pluginPackagePublisherProvenanceRepository.js",
|
||||
"default": "./dist/plugin-package/publisher/pluginPackagePublisherProvenanceRepository.js"
|
||||
},
|
||||
"./plugin-package-publisher-trust-authority": {
|
||||
"types": "./dist/plugin-package/publisher/pluginPackagePublisherTrustAuthorityRepository.d.ts",
|
||||
"require": "./dist/plugin-package/publisher/pluginPackagePublisherTrustAuthorityRepository.js",
|
||||
"default": "./dist/plugin-package/publisher/pluginPackagePublisherTrustAuthorityRepository.js"
|
||||
},
|
||||
"./plugin-package-publisher-revocation-proposal": {
|
||||
"types": "./dist/plugin-package/publisher/pluginPackagePublisherRevocationProposalRepository.d.ts",
|
||||
"require": "./dist/plugin-package/publisher/pluginPackagePublisherRevocationProposalRepository.js",
|
||||
"default": "./dist/plugin-package/publisher/pluginPackagePublisherRevocationProposalRepository.js"
|
||||
},
|
||||
"./plugin-package-materialized-revision": {
|
||||
"types": "./dist/plugin-package/installation/pluginPackageMaterializedRevisionRepository.d.ts",
|
||||
"require": "./dist/plugin-package/installation/pluginPackageMaterializedRevisionRepository.js",
|
||||
"default": "./dist/plugin-package/installation/pluginPackageMaterializedRevisionRepository.js"
|
||||
},
|
||||
"./plugin-package-automation-publication": {
|
||||
"types": "./dist/plugin-package/publication/pluginPackageAutomationPublicationRepository.d.ts",
|
||||
"require": "./dist/plugin-package/publication/pluginPackageAutomationPublicationRepository.js",
|
||||
"default": "./dist/plugin-package/publication/pluginPackageAutomationPublicationRepository.js"
|
||||
},
|
||||
"./plugin-package-workflow-admission": {
|
||||
"types": "./dist/plugin-package/workflow/pluginPackageWorkflowAdmissionRepository.d.ts",
|
||||
"require": "./dist/plugin-package/workflow/pluginPackageWorkflowAdmissionRepository.js",
|
||||
"default": "./dist/plugin-package/workflow/pluginPackageWorkflowAdmissionRepository.js"
|
||||
},
|
||||
"./plugin-package-workflow-administration": {
|
||||
"types": "./dist/plugin-package/workflow/pluginPackageWorkflowAdministration.d.ts",
|
||||
"require": "./dist/plugin-package/workflow/pluginPackageWorkflowAdministration.js",
|
||||
"default": "./dist/plugin-package/workflow/pluginPackageWorkflowAdministration.js"
|
||||
},
|
||||
"./plugin-package-workflow-frontier": {
|
||||
"types": "./dist/plugin-package/workflow/pluginPackageWorkflowFrontierRepository.d.ts",
|
||||
"require": "./dist/plugin-package/workflow/pluginPackageWorkflowFrontierRepository.js",
|
||||
"default": "./dist/plugin-package/workflow/pluginPackageWorkflowFrontierRepository.js"
|
||||
},
|
||||
"./plugin-package-workflow-task-attempt-admission": {
|
||||
"types": "./dist/plugin-package/workflow/pluginPackageWorkflowTaskAttemptAdmissionRepository.d.ts",
|
||||
"require": "./dist/plugin-package/workflow/pluginPackageWorkflowTaskAttemptAdmissionRepository.js",
|
||||
"default": "./dist/plugin-package/workflow/pluginPackageWorkflowTaskAttemptAdmissionRepository.js"
|
||||
},
|
||||
"./project-tool-definition-snapshot": {
|
||||
"types": "./dist/tool-execution/projectToolDefinitionSnapshotRepository.d.ts",
|
||||
"require": "./dist/tool-execution/projectToolDefinitionSnapshotRepository.js",
|
||||
"default": "./dist/tool-execution/projectToolDefinitionSnapshotRepository.js"
|
||||
},
|
||||
"./step-run": {
|
||||
"types": "./dist/run/stepRunRepository.d.ts",
|
||||
"require": "./dist/run/stepRunRepository.js",
|
||||
"default": "./dist/run/stepRunRepository.js"
|
||||
},
|
||||
"./tool-execution-evidence": {
|
||||
"types": "./dist/tool-execution/toolExecutionEvidenceRepository.d.ts",
|
||||
"require": "./dist/tool-execution/toolExecutionEvidenceRepository.js",
|
||||
"default": "./dist/tool-execution/toolExecutionEvidenceRepository.js"
|
||||
},
|
||||
"./tool-execution-start-barrier": {
|
||||
"types": "./dist/tool-execution/toolExecutionStartBarrierRepository.d.ts",
|
||||
"require": "./dist/tool-execution/toolExecutionStartBarrierRepository.js",
|
||||
"default": "./dist/tool-execution/toolExecutionStartBarrierRepository.js"
|
||||
},
|
||||
"./tool-execution-completion": {
|
||||
"types": "./dist/tool-execution/toolExecutionCompletionRepository.d.ts",
|
||||
"require": "./dist/tool-execution/toolExecutionCompletionRepository.js",
|
||||
"default": "./dist/tool-execution/toolExecutionCompletionRepository.js"
|
||||
},
|
||||
"./tool-execution-failure-completion": {
|
||||
"types": "./dist/tool-execution/toolExecutionFailureCompletionRepository.d.ts",
|
||||
"require": "./dist/tool-execution/toolExecutionFailureCompletionRepository.js",
|
||||
"default": "./dist/tool-execution/toolExecutionFailureCompletionRepository.js"
|
||||
},
|
||||
"./tool-result-key-catalog": {
|
||||
"types": "./dist/tool-execution/toolResultKeyCatalogRepository.d.ts",
|
||||
"require": "./dist/tool-execution/toolResultKeyCatalogRepository.js",
|
||||
"default": "./dist/tool-execution/toolResultKeyCatalogRepository.js"
|
||||
},
|
||||
"./tool-result-rekey": {
|
||||
"types": "./dist/tool-execution/toolResultRekeyRepository.d.ts",
|
||||
"require": "./dist/tool-execution/toolResultRekeyRepository.js",
|
||||
"default": "./dist/tool-execution/toolResultRekeyRepository.js"
|
||||
},
|
||||
"./tool-invocation-artifact": {
|
||||
"types": "./dist/tool-execution/toolInvocationArtifactRepository.d.ts",
|
||||
"require": "./dist/tool-execution/toolInvocationArtifactRepository.js",
|
||||
"default": "./dist/tool-execution/toolInvocationArtifactRepository.js"
|
||||
},
|
||||
"./approved-action": {
|
||||
"types": "./dist/approved-action/approvalRequestRepository.d.ts",
|
||||
"require": "./dist/approved-action/approvalRequestRepository.js",
|
||||
"default": "./dist/approved-action/approvalRequestRepository.js"
|
||||
},
|
||||
"./approval-discovery": {
|
||||
"types": "./dist/approved-action/approvalRequestSource.d.ts",
|
||||
"require": "./dist/approved-action/approvalRequestSource.js",
|
||||
"default": "./dist/approved-action/approvalRequestSource.js"
|
||||
},
|
||||
"./worker-credential-management-plan": {
|
||||
"types": "./dist/worker-credential/workerCredentialManagementPlanRepository.d.ts",
|
||||
"require": "./dist/worker-credential/workerCredentialManagementPlanRepository.js",
|
||||
"default": "./dist/worker-credential/workerCredentialManagementPlanRepository.js"
|
||||
},
|
||||
"./approved-action-execution": {
|
||||
"types": "./dist/approved-action/approvedActionExecutionRepository.d.ts",
|
||||
"require": "./dist/approved-action/approvedActionExecutionRepository.js",
|
||||
"default": "./dist/approved-action/approvedActionExecutionRepository.js"
|
||||
},
|
||||
"./plugin-package-proposal": {
|
||||
"types": "./dist/plugin-package/installation/pluginPackageProposalRepository.d.ts",
|
||||
"require": "./dist/plugin-package/installation/pluginPackageProposalRepository.js",
|
||||
"default": "./dist/plugin-package/installation/pluginPackageProposalRepository.js"
|
||||
},
|
||||
"./project-policy": {
|
||||
"types": "./dist/security/projectPolicyRepository.d.ts",
|
||||
"require": "./dist/security/projectPolicyRepository.js",
|
||||
"default": "./dist/security/projectPolicyRepository.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*.js",
|
||||
"dist/**/*.d.ts"
|
||||
],
|
||||
"bin": {
|
||||
"ql3-cluster-migrate": "dist/migration/migrationCli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"check": "node ../../scripts/ql3-build-package-closure.cjs && tsc -p tsconfig.json --noEmit",
|
||||
"test": "node ../../scripts/ql3-build-package-closure.cjs && node --test test/*.test.cjs",
|
||||
"schema:check": "node ../../scripts/ql3-build-package-closure.cjs && node --test test/schema.test.cjs",
|
||||
"test:integration": "node ../../scripts/ql3-build-package-closure.cjs && node --test test/postgres.integration.test.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@qinglong/runtime-core": "workspace:*",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"pg": "8.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.13.3",
|
||||
"@types/pg": "8.20.0",
|
||||
"drizzle-kit": "0.31.10",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export {
|
||||
PgPoolBinding,
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresTlsDnsServername,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
} from '../connection/pool';
|
||||
export {
|
||||
PostgresConnectionEnvironmentError,
|
||||
loadPostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironmentKeys,
|
||||
} from '../connection/connectionEnvironment';
|
||||
export {
|
||||
loadPostgresCertificateAuthorityFile,
|
||||
type PostgresCertificateAuthorityFileInspection,
|
||||
} from '../connection/certificateAuthority';
|
||||
export {
|
||||
PostgresSchemaReadinessError,
|
||||
assertPostgresApprovalManagerSchemaReady,
|
||||
type PostgresSchemaReadinessReport,
|
||||
} from '../schema/schemaReadiness';
|
||||
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
|
||||
export { PostgresApprovalRequestRepository } from '../approved-action/approvalRequestRepository';
|
||||
export { PostgresApprovalRequestSource } from '../approved-action/approvalRequestSource';
|
||||
export { PostgresProjectPolicyRepository } from '../security/projectPolicyRepository';
|
||||
export { PostgresSecurityAuditRepository } from '../security/securityAuditRepository';
|
||||
export {
|
||||
PostgresPluginPackageIdentityKeysetLedgerRepository as PostgresApprovalManagementIdentityKeysetLedgerRepository,
|
||||
type ClusterManagementIdentityAuthority,
|
||||
} from '../management/pluginPackageIdentityKeysetLedgerRepository';
|
||||
@@ -0,0 +1,654 @@
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresPool,
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
ApprovalMutationConflictError,
|
||||
ApprovalPolicyFenceConflictError,
|
||||
ApprovalRequestNotFoundError,
|
||||
ApprovalRequestStateConflictError,
|
||||
ApprovalUnavailableError,
|
||||
approvalRequestDigest,
|
||||
approvedActionDispatchDigest,
|
||||
consumeApprovalRequest,
|
||||
decideApprovalRequest,
|
||||
normalizeApprovalRequestRecord,
|
||||
normalizeApprovedActionDispatchRecord,
|
||||
type ApprovalRequestRecord,
|
||||
type ApprovalRequestRepository,
|
||||
type ApprovedActionDispatchRecord,
|
||||
type ConsumeDurableApprovalRequestCommand,
|
||||
type ConsumeDurableApprovalRequestResult,
|
||||
type CreateApprovalRequestCommand,
|
||||
type CreateApprovalRequestResult,
|
||||
type DecideApprovalRequestResult,
|
||||
type DecideDurableApprovalRequestCommand,
|
||||
} from '@qinglong/runtime-core/approved-action';
|
||||
import type {
|
||||
SecurityPolicyFence,
|
||||
SecuritySubject,
|
||||
} from '@qinglong/runtime-core/security';
|
||||
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';
|
||||
import {
|
||||
findPostgresApprovedActionExecution,
|
||||
insertPostgresApprovedActionExecutionBaseline,
|
||||
} from './approvedActionExecutionRepository';
|
||||
import { createApprovedActionExecution } from '@qinglong/runtime-core/approved-action-execution';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
|
||||
|
||||
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
function unavailable(): ApprovalUnavailableError {
|
||||
return new ApprovalUnavailableError();
|
||||
}
|
||||
|
||||
function nullableString(value: unknown): string | null {
|
||||
if (value === null) return null;
|
||||
return postgresRequiredString(value, unavailable);
|
||||
}
|
||||
|
||||
function nullableInteger(value: unknown): number | null {
|
||||
if (value === null) return null;
|
||||
return postgresRequiredInteger(value, unavailable);
|
||||
}
|
||||
|
||||
function same(left: unknown, right: unknown): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function parseRequest(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 ApprovalUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
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 ApprovalUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
|
||||
try {
|
||||
const subjectType = nullableString(row.subjectType);
|
||||
const subjectId = nullableString(row.subjectId);
|
||||
const fenceProjectVersion = nullableInteger(row.fenceProjectVersion);
|
||||
const reasons = row.reasonsJson;
|
||||
if (!Array.isArray(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 as SecuritySubject['type'],
|
||||
id: subjectId,
|
||||
},
|
||||
authenticationId: nullableString(row.authenticationId),
|
||||
outcome: postgresRequiredString(
|
||||
row.outcome,
|
||||
unavailable,
|
||||
) as SecurityAuditRecord['outcome'],
|
||||
reasons: reasons as readonly string[],
|
||||
fence:
|
||||
fenceProjectVersion === null
|
||||
? null
|
||||
: {
|
||||
projectVersion: fenceProjectVersion,
|
||||
bindingVersion: nullableInteger(row.fenceBindingVersion),
|
||||
},
|
||||
occurredAtMs: postgresRequiredInteger(row.occurredAtMs, unavailable),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovalUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
function mappedError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof ApprovalMutationConflictError ||
|
||||
error instanceof ApprovalPolicyFenceConflictError ||
|
||||
error instanceof ApprovalRequestNotFoundError ||
|
||||
error instanceof ApprovalRequestStateConflictError ||
|
||||
error instanceof ApprovalUnavailableError ||
|
||||
(error instanceof Error &&
|
||||
error.name.startsWith('Approval') &&
|
||||
'code' in error)
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
const state = postgresSqlState(error);
|
||||
if (state === '23503' || state === '23505' || state === '23514') {
|
||||
return new ApprovalMutationConflictError();
|
||||
}
|
||||
return new ApprovalUnavailableError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function auditMatches(
|
||||
audit: Readonly<SecurityAuditRecord>,
|
||||
expected: Readonly<{
|
||||
operationId: string;
|
||||
projectId: string;
|
||||
subject: Readonly<SecuritySubject>;
|
||||
authenticationId?: string;
|
||||
outcome: SecurityAuditRecord['outcome'];
|
||||
fence: Readonly<SecurityPolicyFence>;
|
||||
}>,
|
||||
): boolean {
|
||||
return (
|
||||
audit.operationId === expected.operationId &&
|
||||
audit.projectId === expected.projectId &&
|
||||
audit.subject?.type === expected.subject.type &&
|
||||
audit.subject.id === expected.subject.id &&
|
||||
(expected.authenticationId === undefined ||
|
||||
audit.authenticationId === expected.authenticationId) &&
|
||||
audit.outcome === expected.outcome &&
|
||||
audit.fence?.projectVersion === expected.fence.projectVersion &&
|
||||
audit.fence.bindingVersion === expected.fence.bindingVersion
|
||||
);
|
||||
}
|
||||
|
||||
async function requestById(
|
||||
queryable: Queryable,
|
||||
id: string,
|
||||
): Promise<Readonly<ApprovalRequestRecord> | null> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT request_json AS "requestJson",
|
||||
request_digest AS "requestDigest"
|
||||
FROM "ql3"."approval_requests"
|
||||
WHERE request_id = $1
|
||||
LIMIT 2`,
|
||||
[id],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return parseRequest(result.rows[0]!);
|
||||
}
|
||||
|
||||
async function dispatchById(
|
||||
queryable: Queryable,
|
||||
id: string,
|
||||
): Promise<Readonly<ApprovedActionDispatchRecord> | null> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT dispatch_json AS "dispatchJson",
|
||||
dispatch_digest AS "dispatchDigest"
|
||||
FROM "ql3"."approved_action_dispatches"
|
||||
WHERE dispatch_id = $1
|
||||
LIMIT 2`,
|
||||
[id],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return parseDispatch(result.rows[0]!);
|
||||
}
|
||||
|
||||
async function auditById(
|
||||
queryable: Queryable,
|
||||
id: 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 AS "outcome",
|
||||
reasons AS "reasonsJson",
|
||||
project_version AS "fenceProjectVersion",
|
||||
binding_version AS "fenceBindingVersion",
|
||||
occurred_at_ms AS "occurredAtMs"
|
||||
FROM "ql3"."security_audit_events"
|
||||
WHERE event_id = $1
|
||||
LIMIT 2`,
|
||||
[id],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return parseAudit(result.rows[0]!);
|
||||
}
|
||||
|
||||
async function assertFence(
|
||||
client: PostgresClient,
|
||||
projectId: string,
|
||||
subject: Readonly<SecuritySubject>,
|
||||
fence: Readonly<SecurityPolicyFence>,
|
||||
): Promise<void> {
|
||||
const result = await client.query<Row>(
|
||||
`SELECT "ql3"."lock_approval_policy_fence"(
|
||||
$1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
|
||||
) AS "matches"`,
|
||||
[
|
||||
projectId,
|
||||
subject.type,
|
||||
subject.id,
|
||||
fence.projectVersion,
|
||||
fence.bindingVersion,
|
||||
],
|
||||
);
|
||||
if (
|
||||
result.rows.length !== 1 ||
|
||||
!postgresRequiredBoolean(result.rows[0]!.matches, unavailable)
|
||||
) {
|
||||
throw new ApprovalPolicyFenceConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
async function insertAudit(
|
||||
client: PostgresClient,
|
||||
value: SecurityAuditRecord,
|
||||
): Promise<void> {
|
||||
const audit = normalizeSecurityAuditRecord(value);
|
||||
const result = 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 (result.rowCount !== 1) throw unavailable();
|
||||
}
|
||||
|
||||
async function insertRequest(
|
||||
client: PostgresClient,
|
||||
request: Readonly<ApprovalRequestRecord>,
|
||||
): Promise<void> {
|
||||
const result = await client.query(
|
||||
`INSERT INTO "ql3"."approval_requests" (
|
||||
request_id, project_id, version, state, action_type, action_ref,
|
||||
action_digest, preview_digest, requested_by_type, requested_by_id,
|
||||
decision_id, consumption_id, dispatch_id, expires_at_ms, request_json,
|
||||
request_digest, updated_at_ms
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
|
||||
$15::jsonb, $16, $17
|
||||
)`,
|
||||
[
|
||||
request.id,
|
||||
request.projectId,
|
||||
request.version,
|
||||
request.state,
|
||||
request.action.actionType,
|
||||
request.action.actionRef,
|
||||
request.action.actionDigest,
|
||||
request.action.previewDigest,
|
||||
request.requestedBy.type,
|
||||
request.requestedBy.id,
|
||||
request.decisionId,
|
||||
request.consumptionId,
|
||||
request.dispatchId,
|
||||
request.expiresAtMs,
|
||||
JSON.stringify(request),
|
||||
approvalRequestDigest(request),
|
||||
request.consumedAtMs ?? request.decidedAtMs ?? request.requestedAtMs,
|
||||
],
|
||||
);
|
||||
if (result.rowCount !== 1) throw unavailable();
|
||||
}
|
||||
|
||||
async function updateRequest(
|
||||
client: PostgresClient,
|
||||
request: Readonly<ApprovalRequestRecord>,
|
||||
expectedVersion: number,
|
||||
): Promise<void> {
|
||||
const result = await client.query(
|
||||
`UPDATE "ql3"."approval_requests"
|
||||
SET version = $1, state = $2, decision_id = $3, consumption_id = $4,
|
||||
dispatch_id = $5, request_json = $6::jsonb, request_digest = $7,
|
||||
updated_at_ms = $8
|
||||
WHERE request_id = $9 AND version = $10`,
|
||||
[
|
||||
request.version,
|
||||
request.state,
|
||||
request.decisionId,
|
||||
request.consumptionId,
|
||||
request.dispatchId,
|
||||
JSON.stringify(request),
|
||||
approvalRequestDigest(request),
|
||||
request.consumedAtMs ?? request.decidedAtMs ?? request.requestedAtMs,
|
||||
request.id,
|
||||
expectedVersion,
|
||||
],
|
||||
);
|
||||
if (result.rowCount !== 1) throw new ApprovalRequestStateConflictError();
|
||||
}
|
||||
|
||||
async function insertDispatch(
|
||||
client: PostgresClient,
|
||||
dispatch: Readonly<ApprovedActionDispatchRecord>,
|
||||
): Promise<void> {
|
||||
const result = await client.query(
|
||||
`INSERT INTO "ql3"."approved_action_dispatches" (
|
||||
dispatch_id, approval_request_id, project_id, action_type, action_ref,
|
||||
action_digest, preview_digest, dispatch_json, dispatch_digest,
|
||||
created_at_ms
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10
|
||||
)`,
|
||||
[
|
||||
dispatch.id,
|
||||
dispatch.approvalRequestId,
|
||||
dispatch.projectId,
|
||||
dispatch.action.actionType,
|
||||
dispatch.action.actionRef,
|
||||
dispatch.action.actionDigest,
|
||||
dispatch.action.previewDigest,
|
||||
JSON.stringify(dispatch),
|
||||
approvedActionDispatchDigest(dispatch),
|
||||
dispatch.createdAtMs,
|
||||
],
|
||||
);
|
||||
if (result.rowCount !== 1) throw unavailable();
|
||||
}
|
||||
|
||||
/** Administration-only PostgreSQL Approval/Approved Action authority. */
|
||||
export class PostgresApprovalRequestRepository
|
||||
implements ApprovalRequestRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError('PostgreSQL Approval 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 findById(
|
||||
id: string,
|
||||
): Promise<Readonly<ApprovalRequestRecord> | null> {
|
||||
if (typeof id !== 'string' || !IDENTIFIER_PATTERN.test(id)) {
|
||||
throw new TypeError('Approval request lookup identity is invalid');
|
||||
}
|
||||
try {
|
||||
return await requestById(this.pool, id);
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async findDispatchById(
|
||||
id: string,
|
||||
): Promise<Readonly<ApprovedActionDispatchRecord> | null> {
|
||||
if (typeof id !== 'string' || !IDENTIFIER_PATTERN.test(id)) {
|
||||
throw new TypeError('Approved action dispatch identity is invalid');
|
||||
}
|
||||
try {
|
||||
return await dispatchById(this.pool, id);
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
create(
|
||||
command: CreateApprovalRequestCommand,
|
||||
): Promise<CreateApprovalRequestResult> {
|
||||
const request = normalizeApprovalRequestRecord(command.request);
|
||||
const audit = normalizeSecurityAuditRecord(command.audit);
|
||||
if (
|
||||
request.state !== 'pending' ||
|
||||
request.version !== 1 ||
|
||||
!auditMatches(audit, {
|
||||
operationId: 'approval.request',
|
||||
projectId: request.projectId,
|
||||
subject: request.requestedBy,
|
||||
outcome: 'approval_required',
|
||||
fence: request.requestFence,
|
||||
})
|
||||
) {
|
||||
throw new ApprovalMutationConflictError();
|
||||
}
|
||||
return this.#transaction(async (client) => {
|
||||
const existing = await requestById(client, request.id);
|
||||
if (existing) {
|
||||
const storedAudit = await auditById(client, audit.eventId);
|
||||
if (
|
||||
!same(existing, request) ||
|
||||
!storedAudit ||
|
||||
!same(storedAudit, audit)
|
||||
) {
|
||||
throw new ApprovalMutationConflictError();
|
||||
}
|
||||
return Object.freeze({ status: 'existing' as const, request });
|
||||
}
|
||||
await assertFence(
|
||||
client,
|
||||
request.projectId,
|
||||
request.requestedBy,
|
||||
request.requestFence,
|
||||
);
|
||||
await insertRequest(client, request);
|
||||
await insertAudit(client, audit);
|
||||
return Object.freeze({ status: 'created' as const, request });
|
||||
});
|
||||
}
|
||||
|
||||
decide(
|
||||
command: DecideDurableApprovalRequestCommand,
|
||||
): Promise<DecideApprovalRequestResult> {
|
||||
const audit = normalizeSecurityAuditRecord(command.audit);
|
||||
return this.#transaction(async (client) => {
|
||||
const current = await requestById(client, command.requestId);
|
||||
if (!current) throw new ApprovalRequestNotFoundError();
|
||||
const request = decideApprovalRequest(current, {
|
||||
expectedVersion: command.expectedVersion,
|
||||
decisionId: command.decisionId,
|
||||
decision: command.decision,
|
||||
reasonCode: command.reasonCode,
|
||||
principal: command.principal,
|
||||
decidedAtMs: command.decidedAtMs,
|
||||
authorizationFence: command.authorizationFence,
|
||||
});
|
||||
if (
|
||||
!auditMatches(audit, {
|
||||
operationId: 'approval.decide',
|
||||
projectId: request.projectId,
|
||||
subject: command.principal.subject,
|
||||
authenticationId: command.principal.authenticationId,
|
||||
outcome: 'allowed',
|
||||
fence: command.authorizationFence,
|
||||
})
|
||||
) {
|
||||
throw new ApprovalMutationConflictError();
|
||||
}
|
||||
if (current.version === 2 && current.decisionId === command.decisionId) {
|
||||
const storedAudit = await auditById(client, audit.eventId);
|
||||
if (!storedAudit || !same(storedAudit, audit)) {
|
||||
throw new ApprovalMutationConflictError();
|
||||
}
|
||||
return Object.freeze({ status: 'existing' as const, request });
|
||||
}
|
||||
await assertFence(
|
||||
client,
|
||||
request.projectId,
|
||||
command.principal.subject,
|
||||
command.authorizationFence,
|
||||
);
|
||||
await updateRequest(client, request, command.expectedVersion);
|
||||
await insertAudit(client, audit);
|
||||
return Object.freeze({ status: 'decided' as const, request });
|
||||
});
|
||||
}
|
||||
|
||||
consume(
|
||||
command: ConsumeDurableApprovalRequestCommand,
|
||||
): Promise<ConsumeDurableApprovalRequestResult> {
|
||||
const audit = normalizeSecurityAuditRecord(command.audit);
|
||||
return this.#transaction(async (client) => {
|
||||
const current = await requestById(client, command.requestId);
|
||||
if (!current) throw new ApprovalRequestNotFoundError();
|
||||
const result = consumeApprovalRequest(current, {
|
||||
expectedVersion: command.expectedVersion,
|
||||
consumptionId: command.consumptionId,
|
||||
dispatchId: command.dispatchId,
|
||||
action: command.action,
|
||||
requestedBy: command.requestedBy,
|
||||
consumedBy: command.consumedBy,
|
||||
consumedAtMs: command.consumedAtMs,
|
||||
authorizationFence: command.authorizationFence,
|
||||
});
|
||||
if (
|
||||
!auditMatches(audit, {
|
||||
operationId: 'approval.consume',
|
||||
projectId: result.request.projectId,
|
||||
subject: command.consumedBy,
|
||||
outcome: 'allowed',
|
||||
fence: command.authorizationFence,
|
||||
})
|
||||
) {
|
||||
throw new ApprovalMutationConflictError();
|
||||
}
|
||||
if (
|
||||
current.version === 3 &&
|
||||
current.consumptionId === command.consumptionId
|
||||
) {
|
||||
const storedAudit = await auditById(client, audit.eventId);
|
||||
const dispatch = await dispatchById(client, result.dispatch.id);
|
||||
const execution = await findPostgresApprovedActionExecution(
|
||||
client,
|
||||
result.dispatch.id,
|
||||
);
|
||||
if (
|
||||
!storedAudit ||
|
||||
!same(storedAudit, audit) ||
|
||||
!dispatch ||
|
||||
!same(dispatch, result.dispatch) ||
|
||||
!execution ||
|
||||
!same(execution, createApprovedActionExecution(result.dispatch))
|
||||
) {
|
||||
throw new ApprovalMutationConflictError();
|
||||
}
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
request: result.request,
|
||||
dispatch: result.dispatch,
|
||||
});
|
||||
}
|
||||
await assertFence(
|
||||
client,
|
||||
result.request.projectId,
|
||||
command.requestedBy,
|
||||
command.authorizationFence,
|
||||
);
|
||||
await insertDispatch(client, result.dispatch);
|
||||
await insertPostgresApprovedActionExecutionBaseline(
|
||||
client,
|
||||
result.dispatch,
|
||||
);
|
||||
await updateRequest(client, result.request, command.expectedVersion);
|
||||
await insertAudit(client, audit);
|
||||
return Object.freeze({
|
||||
status: 'consumed' as const,
|
||||
request: result.request,
|
||||
dispatch: result.dispatch,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import type { PostgresPool } from '@qinglong/runtime-core';
|
||||
import {
|
||||
ApprovalUnavailableError,
|
||||
approvalRequestDigest,
|
||||
normalizeApprovalRequestRecord,
|
||||
type ApprovalRequestRecord,
|
||||
} from '@qinglong/runtime-core/approved-action';
|
||||
import {
|
||||
approvalRequestUpdatedAtMs,
|
||||
assertApprovalDiscoveryProjectId,
|
||||
assertApprovalDiscoveryRequestId,
|
||||
assertApprovalRequestPageSize,
|
||||
normalizeApprovalRequestCursor,
|
||||
type ApprovalRequestDetail,
|
||||
type ApprovalRequestDetailSource,
|
||||
type ApprovalRequestPage,
|
||||
type ApprovalRequestSource,
|
||||
} from '@qinglong/runtime-core/approval-discovery';
|
||||
import {
|
||||
normalizeToolInvocationPreviewArtifact,
|
||||
type ToolInvocationPreviewArtifact,
|
||||
} from '@qinglong/runtime-core/tool-invocation-artifact';
|
||||
|
||||
import {
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
function unavailable(): ApprovalUnavailableError {
|
||||
return new ApprovalUnavailableError();
|
||||
}
|
||||
|
||||
function request(row: Row): Readonly<ApprovalRequestRecord> {
|
||||
try {
|
||||
const value = normalizeApprovalRequestRecord(
|
||||
postgresRequiredJsonObject(
|
||||
row.requestJson,
|
||||
unavailable,
|
||||
) as unknown as ApprovalRequestRecord,
|
||||
);
|
||||
if (
|
||||
approvalRequestDigest(value) !==
|
||||
postgresRequiredString(row.requestDigest, unavailable) ||
|
||||
approvalRequestUpdatedAtMs(value) !==
|
||||
postgresRequiredInteger(row.updatedAtMs, unavailable)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovalUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
function previewArtifact(
|
||||
row: Row,
|
||||
): Readonly<ToolInvocationPreviewArtifact> | null {
|
||||
if (row.previewArtifactId === null) return null;
|
||||
try {
|
||||
const value = normalizeToolInvocationPreviewArtifact(
|
||||
postgresRequiredJsonObject(
|
||||
row.previewArtifactJson,
|
||||
unavailable,
|
||||
) as unknown as ToolInvocationPreviewArtifact,
|
||||
);
|
||||
if (
|
||||
value.artifactId !== postgresRequiredString(row.previewArtifactId, unavailable) ||
|
||||
value.projectId !== postgresRequiredString(row.previewProjectId, unavailable) ||
|
||||
value.actionRef !== postgresRequiredString(row.previewActionRef, unavailable) ||
|
||||
value.actionDigest !== postgresRequiredString(row.previewActionDigest, unavailable) ||
|
||||
value.previewDigest !== postgresRequiredString(row.storedPreviewDigest, unavailable) ||
|
||||
value.redactionContractDigest !==
|
||||
postgresRequiredString(row.redactionContractDigest, unavailable) ||
|
||||
value.artifactDigest !== postgresRequiredString(row.previewArtifactDigest, unavailable) ||
|
||||
value.byteLength !== postgresRequiredInteger(row.previewByteLength, unavailable) ||
|
||||
value.sealedAtMs !== postgresRequiredInteger(row.previewSealedAtMs, unavailable)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovalUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
/** Read-only Approval history using the existing Project keyset index. */
|
||||
export class PostgresApprovalRequestSource
|
||||
implements ApprovalRequestSource, ApprovalRequestDetailSource
|
||||
{
|
||||
constructor(private readonly pool: Pick<PostgresPool, 'query'>) {
|
||||
if (!pool || typeof pool.query !== 'function') {
|
||||
throw new TypeError('PostgreSQL Approval discovery pool is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
async listApprovalRequests(options: {
|
||||
readonly projectId: string;
|
||||
readonly limit: number;
|
||||
readonly after?: { readonly updatedAtMs: number; readonly requestId: string };
|
||||
}): Promise<Readonly<ApprovalRequestPage>> {
|
||||
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
||||
throw new TypeError('Approval discovery options are invalid');
|
||||
}
|
||||
const keys = Object.keys(options);
|
||||
if (
|
||||
!keys.includes('projectId') ||
|
||||
!keys.includes('limit') ||
|
||||
keys.some((key) => !['projectId', 'limit', 'after'].includes(key))
|
||||
) {
|
||||
throw new TypeError('Approval discovery options shape is invalid');
|
||||
}
|
||||
assertApprovalDiscoveryProjectId(options.projectId);
|
||||
assertApprovalRequestPageSize(options.limit);
|
||||
const after = options.after
|
||||
? normalizeApprovalRequestCursor(options.after)
|
||||
: undefined;
|
||||
try {
|
||||
const result = await this.pool.query<Row>(
|
||||
`SELECT request_json AS "requestJson",
|
||||
request_digest AS "requestDigest",
|
||||
updated_at_ms AS "updatedAtMs"
|
||||
FROM "ql3"."approval_requests"
|
||||
WHERE project_id = $1
|
||||
AND (
|
||||
$2::bigint IS NULL OR updated_at_ms < $2 OR
|
||||
(updated_at_ms = $2 AND request_id < $3)
|
||||
)
|
||||
ORDER BY updated_at_ms DESC, request_id DESC
|
||||
LIMIT $4`,
|
||||
[
|
||||
options.projectId,
|
||||
after?.updatedAtMs ?? null,
|
||||
after?.requestId ?? '',
|
||||
options.limit + 1,
|
||||
],
|
||||
);
|
||||
const truncated = result.rows.length > options.limit;
|
||||
const requests = Object.freeze(
|
||||
result.rows.slice(0, options.limit).map(request),
|
||||
);
|
||||
const last = requests.at(-1);
|
||||
return Object.freeze({
|
||||
requests,
|
||||
truncated,
|
||||
...(truncated && last
|
||||
? {
|
||||
next: Object.freeze({
|
||||
updatedAtMs: approvalRequestUpdatedAtMs(last),
|
||||
requestId: last.id,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async getApprovalRequestDetail(options: {
|
||||
readonly projectId: string;
|
||||
readonly requestId: string;
|
||||
}): Promise<Readonly<ApprovalRequestDetail> | null> {
|
||||
if (
|
||||
!options ||
|
||||
typeof options !== 'object' ||
|
||||
Array.isArray(options) ||
|
||||
Object.keys(options).length !== 2 ||
|
||||
!Object.hasOwn(options, 'projectId') ||
|
||||
!Object.hasOwn(options, 'requestId')
|
||||
) {
|
||||
throw new TypeError('Approval detail options are invalid');
|
||||
}
|
||||
assertApprovalDiscoveryProjectId(options.projectId);
|
||||
assertApprovalDiscoveryRequestId(options.requestId);
|
||||
try {
|
||||
const result = await this.pool.query<Row>(
|
||||
`SELECT a.request_json AS "requestJson",
|
||||
a.request_digest AS "requestDigest",
|
||||
a.updated_at_ms AS "updatedAtMs",
|
||||
p.artifact_id AS "previewArtifactId",
|
||||
p.project_id AS "previewProjectId",
|
||||
p.action_ref AS "previewActionRef",
|
||||
p.action_digest AS "previewActionDigest",
|
||||
p.preview_digest AS "storedPreviewDigest",
|
||||
p.redaction_contract_digest AS "redactionContractDigest",
|
||||
p.artifact_digest AS "previewArtifactDigest",
|
||||
p.byte_length AS "previewByteLength",
|
||||
p.sealed_at_ms AS "previewSealedAtMs",
|
||||
p.artifact_json AS "previewArtifactJson"
|
||||
FROM "ql3"."approval_requests" a
|
||||
LEFT JOIN "ql3"."tool_invocation_preview_artifacts" p
|
||||
ON a.action_type = 'tool.invoke'
|
||||
AND p.project_id = a.project_id
|
||||
AND p.action_ref = a.action_ref
|
||||
AND p.action_digest = a.action_digest
|
||||
AND p.preview_digest = a.preview_digest
|
||||
WHERE a.project_id = $1 AND a.request_id = $2
|
||||
LIMIT 2`,
|
||||
[options.projectId, options.requestId],
|
||||
);
|
||||
if (result.rows.length > 1) throw unavailable();
|
||||
if (!result.rows[0]) return null;
|
||||
const approval = request(result.rows[0]);
|
||||
const preview = previewArtifact(result.rows[0]);
|
||||
if (
|
||||
preview &&
|
||||
(approval.action.actionType !== 'tool.invoke' ||
|
||||
approval.projectId !== preview.projectId ||
|
||||
approval.action.actionRef !== preview.actionRef ||
|
||||
approval.action.actionDigest !== preview.actionDigest ||
|
||||
approval.action.previewDigest !== preview.previewDigest)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return Object.freeze({
|
||||
request: approval,
|
||||
preview: preview?.preview ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovalUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
}
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresPool,
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
normalizeApprovedActionDispatchRecord,
|
||||
type ApprovedActionDispatchRecord,
|
||||
} from '@qinglong/runtime-core/approved-action';
|
||||
import {
|
||||
ApprovedActionExecutionFenceConflictError,
|
||||
ApprovedActionExecutionStateConflictError,
|
||||
ApprovedActionExecutionUnavailableError,
|
||||
approvedActionExecutionEffectiveStatus,
|
||||
claimApprovedActionExecution,
|
||||
completeApprovedActionExecution,
|
||||
createApprovedActionExecution,
|
||||
normalizeApprovedActionExecutionCursor,
|
||||
normalizeApprovedActionExecutionRecord,
|
||||
normalizeApprovedActionExecutionSnapshot,
|
||||
releaseApprovedActionExecutionBeforeStart,
|
||||
renewApprovedActionExecution,
|
||||
startApprovedActionExecution,
|
||||
type ApprovedActionExecutionRecord,
|
||||
type ApprovedActionExecutionRepository,
|
||||
type ApprovedActionExecutionSnapshot,
|
||||
type ClaimApprovedActionExecutionCommand,
|
||||
type ClaimApprovedActionExecutionResult,
|
||||
type CompleteApprovedActionExecutionCommand,
|
||||
type ListDueApprovedActionExecutionsQuery,
|
||||
type ListDueApprovedActionExecutionsResult,
|
||||
type ReleaseApprovedActionExecutionBeforeStartCommand,
|
||||
type RenewApprovedActionExecutionCommand,
|
||||
type StartApprovedActionExecutionCommand,
|
||||
} from '@qinglong/runtime-core/approved-action-execution';
|
||||
|
||||
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'>;
|
||||
|
||||
function unavailable(): ApprovedActionExecutionUnavailableError {
|
||||
return new ApprovedActionExecutionUnavailableError();
|
||||
}
|
||||
|
||||
function parseExecution(row: Row): Readonly<ApprovedActionExecutionRecord> {
|
||||
try {
|
||||
const execution = normalizeApprovedActionExecutionRecord(
|
||||
postgresRequiredJsonObject(
|
||||
row.executionJson,
|
||||
unavailable,
|
||||
) as unknown as ApprovedActionExecutionRecord,
|
||||
);
|
||||
if (
|
||||
execution.executionDigest !==
|
||||
postgresRequiredString(row.executionDigest, unavailable)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return execution;
|
||||
} catch (error) {
|
||||
if (error instanceof ApprovedActionExecutionUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
function parseDispatch(row: Row): Readonly<ApprovedActionDispatchRecord> {
|
||||
try {
|
||||
return normalizeApprovedActionDispatchRecord(
|
||||
postgresRequiredJsonObject(
|
||||
row.dispatchJson,
|
||||
unavailable,
|
||||
) as unknown as ApprovedActionDispatchRecord,
|
||||
);
|
||||
} catch {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
function mappedError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof ApprovedActionExecutionFenceConflictError ||
|
||||
error instanceof ApprovedActionExecutionStateConflictError ||
|
||||
error instanceof ApprovedActionExecutionUnavailableError ||
|
||||
(error instanceof Error &&
|
||||
error.name.startsWith('ApprovedActionExecution'))
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
const state = postgresSqlState(error);
|
||||
if (state === '23503' || state === '23505' || state === '23514') {
|
||||
return new ApprovedActionExecutionStateConflictError();
|
||||
}
|
||||
return new ApprovedActionExecutionUnavailableError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function executionValues(
|
||||
execution: Readonly<ApprovedActionExecutionRecord>,
|
||||
): readonly (string | number | null)[] {
|
||||
return [
|
||||
execution.dispatchId,
|
||||
execution.dispatchDigest,
|
||||
execution.projectId,
|
||||
execution.status,
|
||||
execution.version,
|
||||
execution.attemptCount,
|
||||
execution.maxAttempts,
|
||||
execution.eligibleAtMs,
|
||||
execution.nextAttemptAtMs,
|
||||
execution.leaseOwner,
|
||||
execution.leaseToken,
|
||||
execution.leaseExpiresAtMs,
|
||||
execution.startedAtMs,
|
||||
execution.resultMutationId,
|
||||
execution.resultCode,
|
||||
execution.resultDigest,
|
||||
execution.completedAtMs,
|
||||
execution.createdAtMs,
|
||||
execution.updatedAtMs,
|
||||
JSON.stringify(execution),
|
||||
execution.executionDigest,
|
||||
];
|
||||
}
|
||||
|
||||
export async function insertPostgresApprovedActionExecutionBaseline(
|
||||
client: PostgresClient,
|
||||
dispatchValue: ApprovedActionDispatchRecord,
|
||||
): Promise<Readonly<ApprovedActionExecutionRecord>> {
|
||||
const execution = createApprovedActionExecution(dispatchValue);
|
||||
const result = await client.query(
|
||||
`INSERT INTO "ql3"."approved_action_executions" (
|
||||
dispatch_id, dispatch_digest, project_id, status, version,
|
||||
attempt_count, max_attempts, eligible_at_ms, next_attempt_at_ms,
|
||||
lease_owner, lease_token, lease_expires_at_ms, started_at_ms,
|
||||
result_mutation_id, result_code, result_digest, completed_at_ms,
|
||||
created_at_ms, updated_at_ms, execution_json, execution_digest
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
|
||||
$15, $16, $17, $18, $19, $20::jsonb, $21
|
||||
)`,
|
||||
executionValues(execution),
|
||||
);
|
||||
if (result.rowCount !== 1) throw unavailable();
|
||||
return execution;
|
||||
}
|
||||
|
||||
export async function findPostgresApprovedActionExecution(
|
||||
queryable: Queryable,
|
||||
dispatchId: string,
|
||||
): Promise<Readonly<ApprovedActionExecutionRecord> | null> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT execution_json AS "executionJson",
|
||||
execution_digest AS "executionDigest"
|
||||
FROM "ql3"."approved_action_executions"
|
||||
WHERE dispatch_id = $1
|
||||
LIMIT 2`,
|
||||
[dispatchId],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return parseExecution(result.rows[0]!);
|
||||
}
|
||||
|
||||
async function loadSnapshot(
|
||||
queryable: Queryable,
|
||||
dispatchId: string,
|
||||
lock = false,
|
||||
): Promise<Readonly<ApprovedActionExecutionSnapshot> | null> {
|
||||
const result = await queryable.query<Row>(
|
||||
`SELECT execution.execution_json AS "executionJson",
|
||||
execution.execution_digest AS "executionDigest",
|
||||
dispatch.dispatch_json AS "dispatchJson"
|
||||
FROM "ql3"."approved_action_executions" AS execution
|
||||
JOIN "ql3"."approved_action_dispatches" AS dispatch
|
||||
ON dispatch.dispatch_id = execution.dispatch_id
|
||||
WHERE execution.dispatch_id = $1
|
||||
LIMIT 2
|
||||
${lock ? 'FOR UPDATE OF execution' : ''}`,
|
||||
[dispatchId],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return normalizeApprovedActionExecutionSnapshot({
|
||||
dispatch: parseDispatch(result.rows[0]!),
|
||||
execution: parseExecution(result.rows[0]!),
|
||||
});
|
||||
}
|
||||
|
||||
async function replaceExecution(
|
||||
client: PostgresClient,
|
||||
previous: Readonly<ApprovedActionExecutionRecord>,
|
||||
next: Readonly<ApprovedActionExecutionRecord>,
|
||||
): Promise<void> {
|
||||
const values = executionValues(next);
|
||||
const result = await client.query(
|
||||
`UPDATE "ql3"."approved_action_executions"
|
||||
SET dispatch_digest = $1, project_id = $2, status = $3,
|
||||
version = $4, attempt_count = $5, max_attempts = $6,
|
||||
eligible_at_ms = $7, next_attempt_at_ms = $8, lease_owner = $9,
|
||||
lease_token = $10, lease_expires_at_ms = $11, started_at_ms = $12,
|
||||
result_mutation_id = $13, result_code = $14, result_digest = $15,
|
||||
completed_at_ms = $16, created_at_ms = $17, updated_at_ms = $18,
|
||||
execution_json = $19::jsonb, execution_digest = $20
|
||||
WHERE dispatch_id = $21 AND version = $22
|
||||
AND execution_digest = $23`,
|
||||
[
|
||||
...values.slice(1),
|
||||
previous.dispatchId,
|
||||
previous.version,
|
||||
previous.executionDigest,
|
||||
],
|
||||
);
|
||||
if (result.rowCount !== 1) {
|
||||
throw new ApprovedActionExecutionFenceConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
function assertPageSize(value: number): void {
|
||||
if (!Number.isSafeInteger(value) || value < 1 || value > 64) {
|
||||
throw new ApprovedActionExecutionStateConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
function actionTypes(values: readonly string[]): readonly string[] {
|
||||
if (
|
||||
!Array.isArray(values) ||
|
||||
values.length > 64 ||
|
||||
values.some(
|
||||
(value) =>
|
||||
typeof value !== 'string' ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value),
|
||||
) ||
|
||||
new Set(values).size !== values.length
|
||||
) {
|
||||
throw new ApprovedActionExecutionStateConflictError();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export class PostgresApprovedActionExecutionRepository
|
||||
implements ApprovedActionExecutionRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError('PostgreSQL Approved Action execution 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 findExecutionByDispatchId(
|
||||
dispatchId: string,
|
||||
): Promise<Readonly<ApprovedActionExecutionSnapshot> | null> {
|
||||
try {
|
||||
return await loadSnapshot(this.pool, dispatchId);
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async listDueExecutions(
|
||||
query: ListDueApprovedActionExecutionsQuery,
|
||||
): Promise<ListDueApprovedActionExecutionsResult> {
|
||||
assertPageSize(query.limit);
|
||||
const handledActionTypes = actionTypes(query.actionTypes);
|
||||
if (!Number.isSafeInteger(query.nowMs) || query.nowMs < 0) {
|
||||
throw new ApprovedActionExecutionStateConflictError();
|
||||
}
|
||||
if (handledActionTypes.length === 0) {
|
||||
return Object.freeze({ executions: Object.freeze([]), truncated: false });
|
||||
}
|
||||
const cursor = query.cursor
|
||||
? normalizeApprovedActionExecutionCursor(query.cursor)
|
||||
: undefined;
|
||||
try {
|
||||
const result = await this.pool.query<Row>(
|
||||
`SELECT execution.execution_json AS "executionJson",
|
||||
execution.execution_digest AS "executionDigest",
|
||||
dispatch.dispatch_json AS "dispatchJson"
|
||||
FROM "ql3"."approved_action_executions" AS execution
|
||||
JOIN "ql3"."approved_action_dispatches" AS dispatch
|
||||
ON dispatch.dispatch_id = execution.dispatch_id
|
||||
WHERE execution.status IN ('pending','leased','retry_wait')
|
||||
AND execution.eligible_at_ms <= $1
|
||||
AND dispatch.action_type = ANY($2::varchar[])
|
||||
AND (
|
||||
$3::varchar IS NULL OR execution.eligible_at_ms > $4 OR
|
||||
(
|
||||
execution.eligible_at_ms = $4
|
||||
AND execution.dispatch_id > $3
|
||||
)
|
||||
)
|
||||
ORDER BY execution.eligible_at_ms, execution.dispatch_id
|
||||
LIMIT $5`,
|
||||
[
|
||||
query.nowMs,
|
||||
handledActionTypes,
|
||||
cursor?.dispatchId ?? null,
|
||||
cursor?.eligibleAtMs ?? 0,
|
||||
query.limit + 1,
|
||||
],
|
||||
);
|
||||
const truncated = result.rows.length > query.limit;
|
||||
const rows = truncated
|
||||
? result.rows.slice(0, query.limit)
|
||||
: result.rows;
|
||||
const executions = rows.map((row) =>
|
||||
normalizeApprovedActionExecutionSnapshot({
|
||||
dispatch: parseDispatch(row),
|
||||
execution: parseExecution(row),
|
||||
}),
|
||||
);
|
||||
const last = executions.at(-1)?.execution;
|
||||
return Object.freeze({
|
||||
executions: Object.freeze(executions),
|
||||
truncated,
|
||||
...(truncated && last?.eligibleAtMs !== null
|
||||
? {
|
||||
nextCursor: Object.freeze({
|
||||
eligibleAtMs: last!.eligibleAtMs!,
|
||||
dispatchId: last!.dispatchId,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
claimExecution(
|
||||
command: ClaimApprovedActionExecutionCommand,
|
||||
): Promise<ClaimApprovedActionExecutionResult> {
|
||||
return this.#transaction(async (client) => {
|
||||
const current = await loadSnapshot(client, command.dispatchId, true);
|
||||
if (!current) return Object.freeze({ status: 'not_found' as const });
|
||||
const effective = approvedActionExecutionEffectiveStatus(
|
||||
current.execution,
|
||||
command.nowMs,
|
||||
);
|
||||
if (
|
||||
current.execution.status === 'leased' &&
|
||||
current.execution.leaseExpiresAtMs !== null &&
|
||||
current.execution.leaseExpiresAtMs <= command.nowMs &&
|
||||
current.execution.attemptCount >= current.execution.maxAttempts
|
||||
) {
|
||||
return Object.freeze({
|
||||
status: 'recovery_required' as const,
|
||||
snapshot: current,
|
||||
});
|
||||
}
|
||||
const due =
|
||||
current.execution.eligibleAtMs !== null &&
|
||||
current.execution.eligibleAtMs <= command.nowMs;
|
||||
if (
|
||||
effective !== 'pending' &&
|
||||
effective !== 'retry_wait' &&
|
||||
!(effective === 'leased' && due)
|
||||
) {
|
||||
return Object.freeze({ status: effective, snapshot: current });
|
||||
}
|
||||
if (!due) {
|
||||
return Object.freeze({
|
||||
status: 'not_due' as const,
|
||||
snapshot: current,
|
||||
});
|
||||
}
|
||||
const next = claimApprovedActionExecution(current.execution, {
|
||||
owner: command.owner,
|
||||
leaseToken: command.leaseToken,
|
||||
nowMs: command.nowMs,
|
||||
leaseDurationMs: command.leaseDurationMs,
|
||||
});
|
||||
await replaceExecution(client, current.execution, next);
|
||||
return Object.freeze({
|
||||
status: 'claimed' as const,
|
||||
snapshot: Object.freeze({
|
||||
dispatch: current.dispatch,
|
||||
execution: next,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
startExecution(
|
||||
command: StartApprovedActionExecutionCommand,
|
||||
): Promise<Readonly<ApprovedActionExecutionSnapshot>> {
|
||||
return this.#mutate(command.dispatchId, (current) =>
|
||||
startApprovedActionExecution(current, command),
|
||||
);
|
||||
}
|
||||
|
||||
renewExecution(
|
||||
command: RenewApprovedActionExecutionCommand,
|
||||
): Promise<Readonly<ApprovedActionExecutionSnapshot>> {
|
||||
return this.#mutate(command.dispatchId, (current) =>
|
||||
renewApprovedActionExecution(current.execution, {
|
||||
owner: command.owner,
|
||||
leaseToken: command.leaseToken,
|
||||
expectedVersion: command.expectedVersion,
|
||||
nowMs: command.nowMs,
|
||||
leaseDurationMs: command.leaseDurationMs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
releaseExecutionBeforeStart(
|
||||
command: ReleaseApprovedActionExecutionBeforeStartCommand,
|
||||
): Promise<Readonly<ApprovedActionExecutionSnapshot>> {
|
||||
return this.#mutate(command.dispatchId, (current) =>
|
||||
releaseApprovedActionExecutionBeforeStart(current.execution, {
|
||||
owner: command.owner,
|
||||
leaseToken: command.leaseToken,
|
||||
expectedVersion: command.expectedVersion,
|
||||
resultMutationId: command.resultMutationId,
|
||||
resultCode: command.resultCode,
|
||||
atMs: command.atMs,
|
||||
...(command.retryAtMs === undefined
|
||||
? {}
|
||||
: { retryAtMs: command.retryAtMs }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
completeExecution(
|
||||
command: CompleteApprovedActionExecutionCommand,
|
||||
): Promise<Readonly<ApprovedActionExecutionSnapshot>> {
|
||||
return this.#mutate(command.dispatchId, (current) =>
|
||||
completeApprovedActionExecution(current.execution, {
|
||||
owner: command.owner,
|
||||
leaseToken: command.leaseToken,
|
||||
expectedVersion: command.expectedVersion,
|
||||
resultMutationId: command.resultMutationId,
|
||||
outcome: command.outcome,
|
||||
resultCode: command.resultCode,
|
||||
...(command.resultDigest === undefined
|
||||
? {}
|
||||
: { resultDigest: command.resultDigest }),
|
||||
completedAtMs: command.completedAtMs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#mutate(
|
||||
dispatchId: string,
|
||||
transition: (
|
||||
current: Readonly<ApprovedActionExecutionSnapshot>,
|
||||
) => Readonly<ApprovedActionExecutionRecord>,
|
||||
): Promise<Readonly<ApprovedActionExecutionSnapshot>> {
|
||||
return this.#transaction(async (client) => {
|
||||
const current = await loadSnapshot(client, dispatchId, true);
|
||||
if (!current) throw new ApprovedActionExecutionStateConflictError();
|
||||
const next = transition(current);
|
||||
await replaceExecution(client, current.execution, next);
|
||||
return Object.freeze({
|
||||
dispatch: current.dispatch,
|
||||
execution: next,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
// PostgreSQL authorized administration authority for Task and Trigger definitions.
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresPool,
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
InvalidTaskDefinitionAdministrationReadError,
|
||||
TaskDefinitionAdministrationAuthorizationFenceConflictError,
|
||||
TaskDefinitionAdministrationMutationConflictError,
|
||||
TaskDefinitionAdministrationReadConflictError,
|
||||
normalizeAuthorizedTaskDefinitionInspection,
|
||||
normalizeAuthorizedTaskDefinitionList,
|
||||
normalizeAuthorizedTaskDefinitionRevisionMutation,
|
||||
type AuthorizedTaskDefinitionInspection,
|
||||
type AuthorizedTaskDefinitionList,
|
||||
type AuthorizedTaskDefinitionRevisionMutation,
|
||||
type TaskDefinitionAdministrationRepository,
|
||||
type TaskDefinitionAdministrationSource,
|
||||
} from '@qinglong/runtime-core/task-definition-administration';
|
||||
import {
|
||||
InvalidTriggerAdministrationReadError,
|
||||
TriggerAdministrationAuthorizationFenceConflictError,
|
||||
TriggerAdministrationMutationConflictError,
|
||||
TriggerAdministrationReadConflictError,
|
||||
normalizeAuthorizedTriggerInspection,
|
||||
normalizeAuthorizedTriggerList,
|
||||
normalizeAuthorizedTriggerRevisionMutation,
|
||||
type AuthorizedTriggerInspection,
|
||||
type AuthorizedTriggerList,
|
||||
type AuthorizedTriggerRevisionMutation,
|
||||
type TriggerAdministrationRepository,
|
||||
type TriggerAdministrationSource,
|
||||
} from '@qinglong/runtime-core/trigger-administration';
|
||||
import {
|
||||
TaskDefinitionUnavailableError,
|
||||
type TaskDefinitionPage,
|
||||
type TaskDefinitionRecord,
|
||||
} from '@qinglong/runtime-core/task-definition';
|
||||
import {
|
||||
TriggerUnavailableError,
|
||||
type TriggerPage,
|
||||
type TriggerRecord,
|
||||
} from '@qinglong/runtime-core/trigger';
|
||||
import {
|
||||
ADMINISTRATION_AUDIT_SELECT,
|
||||
auditFromRow,
|
||||
configureAdministrationTransaction,
|
||||
insertAdministrationAudit,
|
||||
requiredInteger,
|
||||
requiredString,
|
||||
retryableAdministrationError,
|
||||
rollbackAdministrationTransaction,
|
||||
sameAdministrationReplayAudit,
|
||||
type AdministrationAuditRow,
|
||||
type AdministrationRow,
|
||||
} from '../repository/administrationSupport';
|
||||
import {
|
||||
PostgresTaskDefinitionRepository,
|
||||
PostgresTaskDefinitionSource,
|
||||
} from './taskDefinitionRepository';
|
||||
import {
|
||||
PostgresTriggerRepository,
|
||||
PostgresTriggerSource,
|
||||
} from '../scheduling/triggerRepository';
|
||||
|
||||
type FenceConflict = new () => Error;
|
||||
type MutationConflict = new () => Error;
|
||||
type ReadConflict = new () => Error;
|
||||
type StorageUnavailable = new () => Error;
|
||||
type InvalidRead = new (message: string) => Error;
|
||||
|
||||
const AUTHORIZED_READ_TRANSACTION_ATTEMPTS = 3;
|
||||
|
||||
function assertPool(pool: PostgresPool): void {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError('PostgreSQL automation administration pool is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmAuthorizationFence(
|
||||
client: PostgresClient,
|
||||
mutation: Readonly<{
|
||||
command: Readonly<{ projectId: string }>;
|
||||
actor: Readonly<{ type: string; id: string }>;
|
||||
fence: Readonly<{ projectVersion: number; bindingVersion: number | null }>;
|
||||
}>,
|
||||
FenceConflictError: FenceConflict,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const project = await client.query<AdministrationRow>(
|
||||
`SELECT status, version
|
||||
FROM "ql3"."projects"
|
||||
WHERE id = $1`,
|
||||
[mutation.command.projectId],
|
||||
);
|
||||
const binding = await client.query<AdministrationRow>(
|
||||
`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`,
|
||||
[
|
||||
mutation.command.projectId,
|
||||
mutation.actor.type,
|
||||
mutation.actor.id,
|
||||
],
|
||||
);
|
||||
if (
|
||||
project.rows.length !== 1 ||
|
||||
requiredString(project.rows[0]!, 'status') !== 'active' ||
|
||||
requiredInteger(project.rows[0]!, 'version') !==
|
||||
mutation.fence.projectVersion ||
|
||||
binding.rows.length !== 1 ||
|
||||
requiredString(binding.rows[0]!, 'state') !== 'active' ||
|
||||
requiredInteger(binding.rows[0]!, 'version') !==
|
||||
mutation.fence.bindingVersion
|
||||
) {
|
||||
throw new FenceConflictError();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof FenceConflictError) throw error;
|
||||
throw new FenceConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmOrAppendAllowedAudit(
|
||||
client: PostgresClient,
|
||||
replay: boolean,
|
||||
audit: AuthorizedTaskDefinitionRevisionMutation['audit'],
|
||||
MutationConflictError: MutationConflict,
|
||||
): Promise<void> {
|
||||
const result = await client.query<AdministrationAuditRow>(
|
||||
`SELECT ${ADMINISTRATION_AUDIT_SELECT}
|
||||
FROM "ql3"."security_audit_events" AS audit
|
||||
WHERE audit.event_id = $1
|
||||
LIMIT 2`,
|
||||
[audit.eventId],
|
||||
);
|
||||
if (replay) {
|
||||
if (result.rows.length !== 1) throw new MutationConflictError();
|
||||
try {
|
||||
if (!sameAdministrationReplayAudit(auditFromRow(result.rows[0]!), audit)) {
|
||||
throw new MutationConflictError();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof MutationConflictError) throw error;
|
||||
throw new MutationConflictError();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (result.rows.length !== 0) throw new MutationConflictError();
|
||||
await insertAdministrationAudit(client, audit);
|
||||
}
|
||||
|
||||
function clientReadPool(client: PostgresClient): PostgresPool {
|
||||
return Object.freeze({
|
||||
query: client.query.bind(client),
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function sqlState(error: unknown): string | undefined {
|
||||
if (!error || typeof error !== 'object') return undefined;
|
||||
const code = (error as { code?: unknown }).code;
|
||||
return typeof code === 'string' ? code : undefined;
|
||||
}
|
||||
|
||||
async function confirmReadAuthorizationFence(
|
||||
client: PostgresClient,
|
||||
read: Readonly<{
|
||||
projectId: string;
|
||||
actor: Readonly<{ type: string; id: string }>;
|
||||
fence: Readonly<{ projectVersion: number; bindingVersion: number | null }>;
|
||||
}>,
|
||||
FenceConflictError: FenceConflict,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const project = await client.query<AdministrationRow>(
|
||||
`SELECT status, version
|
||||
FROM "ql3"."projects"
|
||||
WHERE id = $1
|
||||
LIMIT 1`,
|
||||
[read.projectId],
|
||||
);
|
||||
const binding = await client.query<AdministrationRow>(
|
||||
`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`,
|
||||
[read.projectId, read.actor.type, read.actor.id],
|
||||
);
|
||||
if (
|
||||
project.rows.length !== 1 ||
|
||||
requiredString(project.rows[0]!, 'status') !== 'active' ||
|
||||
requiredInteger(project.rows[0]!, 'version') !==
|
||||
read.fence.projectVersion ||
|
||||
binding.rows.length !== 1 ||
|
||||
requiredString(binding.rows[0]!, 'state') !== 'active' ||
|
||||
requiredInteger(binding.rows[0]!, 'version') !==
|
||||
read.fence.bindingVersion
|
||||
) {
|
||||
throw new FenceConflictError();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof FenceConflictError) throw error;
|
||||
throw new FenceConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
async function assertFreshReadAudit(
|
||||
client: PostgresClient,
|
||||
eventId: string,
|
||||
ReadConflictError: ReadConflict,
|
||||
): Promise<void> {
|
||||
const result = await client.query<AdministrationRow>(
|
||||
`SELECT event_id
|
||||
FROM "ql3"."security_audit_events"
|
||||
WHERE event_id = $1
|
||||
LIMIT 2`,
|
||||
[eventId],
|
||||
);
|
||||
if (result.rows.length !== 0) throw new ReadConflictError();
|
||||
}
|
||||
|
||||
async function executeAuthorizedRead<TResult>(
|
||||
pool: PostgresPool,
|
||||
read: Readonly<{
|
||||
projectId: string;
|
||||
actor: Readonly<{ type: string; id: string }>;
|
||||
fence: Readonly<{ projectVersion: number; bindingVersion: number | null }>;
|
||||
audit: AuthorizedTaskDefinitionInspection['audit'];
|
||||
}>,
|
||||
FenceConflictError: FenceConflict,
|
||||
ReadConflictError: ReadConflict,
|
||||
InvalidReadError: InvalidRead,
|
||||
UnavailableError: StorageUnavailable,
|
||||
operation: (client: PostgresClient) => Promise<TResult>,
|
||||
): Promise<TResult> {
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < AUTHORIZED_READ_TRANSACTION_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await pool.connect();
|
||||
} catch {
|
||||
throw new UnavailableError();
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await configureAdministrationTransaction(client);
|
||||
began = true;
|
||||
await confirmReadAuthorizationFence(client, read, FenceConflictError);
|
||||
await assertFreshReadAudit(
|
||||
client,
|
||||
read.audit.eventId,
|
||||
ReadConflictError,
|
||||
);
|
||||
const result = await operation(client);
|
||||
await insertAdministrationAudit(client, read.audit);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (began) await rollbackAdministrationTransaction(client);
|
||||
if (
|
||||
error instanceof FenceConflictError ||
|
||||
error instanceof ReadConflictError ||
|
||||
error instanceof InvalidReadError ||
|
||||
error instanceof UnavailableError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
if (sqlState(error) === '23505') throw new ReadConflictError();
|
||||
if (
|
||||
retryableAdministrationError(error) &&
|
||||
attempt + 1 < AUTHORIZED_READ_TRANSACTION_ATTEMPTS
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
throw new UnavailableError();
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw new UnavailableError();
|
||||
}
|
||||
|
||||
/** Least-privilege automation-manager Task mutation adapter. */
|
||||
export class PostgresTaskDefinitionAdministrationRepository
|
||||
implements
|
||||
TaskDefinitionAdministrationRepository,
|
||||
TaskDefinitionAdministrationSource
|
||||
{
|
||||
private readonly taskDefinitions: PostgresTaskDefinitionRepository;
|
||||
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
assertPool(pool);
|
||||
this.taskDefinitions = new PostgresTaskDefinitionRepository(pool);
|
||||
}
|
||||
|
||||
appendAuthorizedTaskDefinitionRevision(
|
||||
input: AuthorizedTaskDefinitionRevisionMutation,
|
||||
) {
|
||||
const mutation = normalizeAuthorizedTaskDefinitionRevisionMutation(input);
|
||||
return this.taskDefinitions.appendTaskDefinitionRevision(
|
||||
mutation.command,
|
||||
async (client, { replay }) => {
|
||||
await confirmAuthorizationFence(
|
||||
client,
|
||||
mutation,
|
||||
TaskDefinitionAdministrationAuthorizationFenceConflictError,
|
||||
);
|
||||
await confirmOrAppendAllowedAudit(
|
||||
client,
|
||||
replay !== null,
|
||||
mutation.audit,
|
||||
TaskDefinitionAdministrationMutationConflictError,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
findAuthorizedCurrentTaskDefinition(
|
||||
input: AuthorizedTaskDefinitionInspection,
|
||||
): Promise<TaskDefinitionRecord | null> {
|
||||
const inspection = normalizeAuthorizedTaskDefinitionInspection(input);
|
||||
return executeAuthorizedRead(
|
||||
this.pool,
|
||||
inspection,
|
||||
TaskDefinitionAdministrationAuthorizationFenceConflictError,
|
||||
TaskDefinitionAdministrationReadConflictError,
|
||||
InvalidTaskDefinitionAdministrationReadError,
|
||||
TaskDefinitionUnavailableError,
|
||||
(client) =>
|
||||
new PostgresTaskDefinitionSource(
|
||||
clientReadPool(client),
|
||||
).findCurrentTaskDefinition(inspection.projectId, inspection.taskId),
|
||||
);
|
||||
}
|
||||
|
||||
listAuthorizedTaskDefinitions(
|
||||
input: AuthorizedTaskDefinitionList,
|
||||
): Promise<TaskDefinitionPage> {
|
||||
const query = normalizeAuthorizedTaskDefinitionList(input);
|
||||
return executeAuthorizedRead(
|
||||
this.pool,
|
||||
query,
|
||||
TaskDefinitionAdministrationAuthorizationFenceConflictError,
|
||||
TaskDefinitionAdministrationReadConflictError,
|
||||
InvalidTaskDefinitionAdministrationReadError,
|
||||
TaskDefinitionUnavailableError,
|
||||
(client) =>
|
||||
new PostgresTaskDefinitionSource(clientReadPool(client)).listTaskDefinitions(
|
||||
{
|
||||
projectId: query.projectId,
|
||||
limit: query.limit,
|
||||
...(query.after ? { after: query.after } : {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Least-privilege automation-manager Trigger mutation adapter. */
|
||||
export class PostgresTriggerAdministrationRepository
|
||||
implements TriggerAdministrationRepository, TriggerAdministrationSource
|
||||
{
|
||||
private readonly triggers: PostgresTriggerRepository;
|
||||
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
assertPool(pool);
|
||||
this.triggers = new PostgresTriggerRepository(pool);
|
||||
}
|
||||
|
||||
appendAuthorizedTriggerRevision(input: AuthorizedTriggerRevisionMutation) {
|
||||
const mutation = normalizeAuthorizedTriggerRevisionMutation(input);
|
||||
return this.triggers.appendTriggerRevision(
|
||||
mutation.command,
|
||||
async (client, { replay }) => {
|
||||
await confirmAuthorizationFence(
|
||||
client,
|
||||
mutation,
|
||||
TriggerAdministrationAuthorizationFenceConflictError,
|
||||
);
|
||||
await confirmOrAppendAllowedAudit(
|
||||
client,
|
||||
replay !== null,
|
||||
mutation.audit,
|
||||
TriggerAdministrationMutationConflictError,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
findAuthorizedCurrentTrigger(
|
||||
input: AuthorizedTriggerInspection,
|
||||
): Promise<TriggerRecord | null> {
|
||||
const inspection = normalizeAuthorizedTriggerInspection(input);
|
||||
return executeAuthorizedRead(
|
||||
this.pool,
|
||||
inspection,
|
||||
TriggerAdministrationAuthorizationFenceConflictError,
|
||||
TriggerAdministrationReadConflictError,
|
||||
InvalidTriggerAdministrationReadError,
|
||||
TriggerUnavailableError,
|
||||
(client) =>
|
||||
new PostgresTriggerSource(clientReadPool(client)).findCurrentTrigger(
|
||||
inspection.projectId,
|
||||
inspection.triggerId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
listAuthorizedTriggers(input: AuthorizedTriggerList): Promise<TriggerPage> {
|
||||
const query = normalizeAuthorizedTriggerList(input);
|
||||
return executeAuthorizedRead(
|
||||
this.pool,
|
||||
query,
|
||||
TriggerAdministrationAuthorizationFenceConflictError,
|
||||
TriggerAdministrationReadConflictError,
|
||||
InvalidTriggerAdministrationReadError,
|
||||
TriggerUnavailableError,
|
||||
(client) =>
|
||||
new PostgresTriggerSource(clientReadPool(client)).listTriggers({
|
||||
projectId: query.projectId,
|
||||
limit: query.limit,
|
||||
...(query.after ? { after: query.after } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
// PostgreSQL authority for Task Definition publication and execution revisions.
|
||||
import {
|
||||
InvalidTaskDefinitionError,
|
||||
TaskDefinitionConflictError,
|
||||
TaskDefinitionUnavailableError,
|
||||
assertTaskDefinitionIdentifier,
|
||||
assertTaskDefinitionPageSize,
|
||||
assertTaskDefinitionRevision,
|
||||
createTaskDefinitionRecord,
|
||||
normalizeAppendTaskDefinitionRevisionCommand,
|
||||
normalizeTaskDefinitionCursor,
|
||||
normalizeTaskDefinitionRecord,
|
||||
type AppendTaskDefinitionRevisionCommand,
|
||||
type TaskDefinitionPage,
|
||||
type TaskDefinitionRecord,
|
||||
type TaskDefinitionRepository,
|
||||
type TaskDefinitionSource,
|
||||
} from '@qinglong/runtime-core/task-definition';
|
||||
import {
|
||||
BUILT_IN_COMMAND_TASK_SPEC_SCHEMA,
|
||||
TaskSpecSemanticRegistry,
|
||||
createBuiltInTaskSpecSemanticRegistry,
|
||||
} from '@qinglong/runtime-core/task-spec-semantic';
|
||||
import {
|
||||
compileClusterCommandTaskDefinition,
|
||||
normalizeClusterTaskExecutionRevision,
|
||||
type ClusterTaskExecutionRevision,
|
||||
type ClusterTaskExecutionRevisionSource,
|
||||
} from '@qinglong/runtime-core/cluster-execution-revision';
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresPool,
|
||||
PostgresQueryResult,
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
||||
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
||||
configurePostgresDefinitionTransaction,
|
||||
postgresRequiredBoolean,
|
||||
postgresRequiredInteger,
|
||||
postgresRequiredJsonObject,
|
||||
postgresRequiredString,
|
||||
postgresSqlState,
|
||||
rollbackPostgresDefinitionTransaction,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
type TaskDefinitionRow = Record<string, unknown>;
|
||||
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
|
||||
|
||||
export interface PostgresTaskDefinitionRevisionTransactionContext {
|
||||
readonly command: Readonly<AppendTaskDefinitionRevisionCommand>;
|
||||
readonly replay: Readonly<TaskDefinitionRecord> | null;
|
||||
readonly record: Readonly<TaskDefinitionRecord>;
|
||||
}
|
||||
|
||||
export type PostgresTaskDefinitionRevisionTransactionHook = (
|
||||
client: PostgresClient,
|
||||
context: Readonly<PostgresTaskDefinitionRevisionTransactionContext>,
|
||||
) => Promise<void>;
|
||||
|
||||
const SELECT_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(): TaskDefinitionUnavailableError {
|
||||
return new TaskDefinitionUnavailableError();
|
||||
}
|
||||
|
||||
function taskDefinitionRecord(
|
||||
row: TaskDefinitionRow,
|
||||
): 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 TaskDefinitionUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
function sameRecord(
|
||||
left: TaskDefinitionRecord,
|
||||
right: TaskDefinitionRecord,
|
||||
): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function executionPlanJson(
|
||||
revision: ClusterTaskExecutionRevision,
|
||||
): Readonly<Record<string, unknown>> {
|
||||
return Object.freeze({
|
||||
command: revision.command,
|
||||
environment: revision.environment,
|
||||
...(revision.workingDirectory === undefined
|
||||
? {}
|
||||
: { workingDirectory: revision.workingDirectory }),
|
||||
...(revision.timeoutMs === undefined
|
||||
? {}
|
||||
: { timeoutMs: revision.timeoutMs }),
|
||||
...(revision.placement === undefined
|
||||
? {}
|
||||
: { placement: revision.placement }),
|
||||
});
|
||||
}
|
||||
|
||||
function executionRevision(row: TaskDefinitionRow): ClusterTaskExecutionRevision {
|
||||
try {
|
||||
const plan = postgresRequiredJsonObject(row.planJson, 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: postgresRequiredString(row.projectId, unavailable),
|
||||
taskId: postgresRequiredString(row.taskId, unavailable),
|
||||
sourceRevision: postgresRequiredInteger(row.sourceRevision, unavailable),
|
||||
taskRevision: postgresRequiredString(row.taskRevision, unavailable),
|
||||
sourceContentDigest: postgresRequiredString(
|
||||
row.sourceContentDigest,
|
||||
unavailable,
|
||||
),
|
||||
executorType: postgresRequiredString(
|
||||
row.executorType,
|
||||
unavailable,
|
||||
) as ClusterTaskExecutionRevision['executorType'],
|
||||
planSchema: postgresRequiredString(
|
||||
row.planSchema,
|
||||
unavailable,
|
||||
) 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: postgresRequiredString(row.contentDigest, unavailable),
|
||||
createdAtMs: postgresRequiredInteger(row.createdAtMs, unavailable),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof TaskDefinitionUnavailableError) throw error;
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
export async function findExecutionRevision(
|
||||
queryable: Queryable,
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
sourceRevision: number,
|
||||
): Promise<ClusterTaskExecutionRevision | null> {
|
||||
const result = await queryable.query<TaskDefinitionRow>(
|
||||
`SELECT project_id AS "projectId", task_id AS "taskId",
|
||||
source_revision AS "sourceRevision",
|
||||
task_revision AS "taskRevision",
|
||||
source_content_digest AS "sourceContentDigest",
|
||||
executor_type AS "executorType", plan_schema AS "planSchema",
|
||||
plan_json AS "planJson", content_digest AS "contentDigest",
|
||||
created_at_ms AS "createdAtMs"
|
||||
FROM "ql3"."task_execution_revisions"
|
||||
WHERE project_id = $1 AND task_id = $2 AND source_revision = $3
|
||||
AND executor_type = 'remote_worker'
|
||||
LIMIT 2`,
|
||||
[projectId, taskId, sourceRevision],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return executionRevision(result.rows[0]!);
|
||||
}
|
||||
|
||||
function sameExecutionRevision(
|
||||
left: ClusterTaskExecutionRevision | null,
|
||||
right: ClusterTaskExecutionRevision | null,
|
||||
): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function mappedError(error: unknown): Error {
|
||||
if (
|
||||
error instanceof TaskDefinitionConflictError ||
|
||||
error instanceof TaskDefinitionUnavailableError
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
const state = postgresSqlState(error);
|
||||
if (state === '23503' || state === '23505' || state === '23514') {
|
||||
return new TaskDefinitionConflictError();
|
||||
}
|
||||
return unavailable();
|
||||
}
|
||||
|
||||
async function findCurrent(
|
||||
queryable: Queryable,
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
): Promise<TaskDefinitionRecord | null> {
|
||||
const result = await queryable.query<TaskDefinitionRow>(
|
||||
`SELECT ${SELECT_FIELDS}
|
||||
FROM "ql3"."task_definitions" AS head
|
||||
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
|
||||
WHERE head.project_id = $1 AND head.task_id = $2
|
||||
LIMIT 2`,
|
||||
[projectId, taskId],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return taskDefinitionRecord(result.rows[0]!);
|
||||
}
|
||||
|
||||
async function findRevision(
|
||||
queryable: Queryable,
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
revision: number,
|
||||
): Promise<TaskDefinitionRecord | null> {
|
||||
const result = await queryable.query<TaskDefinitionRow>(
|
||||
`SELECT ${SELECT_FIELDS}
|
||||
FROM "ql3"."task_definitions" AS head
|
||||
JOIN "ql3"."task_definition_revisions" AS revision
|
||||
ON revision.project_id = head.project_id
|
||||
AND revision.task_id = head.task_id
|
||||
WHERE head.project_id = $1 AND head.task_id = $2
|
||||
AND revision.revision = $3
|
||||
LIMIT 2`,
|
||||
[projectId, taskId, revision],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return taskDefinitionRecord(result.rows[0]!);
|
||||
}
|
||||
|
||||
async function findByMutation(
|
||||
queryable: Queryable,
|
||||
mutationId: string,
|
||||
): Promise<TaskDefinitionRecord | null> {
|
||||
const result = await queryable.query<TaskDefinitionRow>(
|
||||
`SELECT ${SELECT_FIELDS}
|
||||
FROM "ql3"."task_definition_revisions" AS revision
|
||||
JOIN "ql3"."task_definitions" AS head
|
||||
ON head.project_id = revision.project_id
|
||||
AND head.task_id = revision.task_id
|
||||
WHERE revision.mutation_id = $1
|
||||
LIMIT 2`,
|
||||
[mutationId],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
return taskDefinitionRecord(result.rows[0]!);
|
||||
}
|
||||
|
||||
/** Read-only PostgreSQL TaskDefinition port for the runtime role. */
|
||||
export class PostgresTaskDefinitionSource implements TaskDefinitionSource {
|
||||
constructor(protected readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError('PostgreSQL TaskDefinition pool is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async findCurrentTaskDefinition(
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
): Promise<TaskDefinitionRecord | null> {
|
||||
assertTaskDefinitionIdentifier(projectId, 'projectId');
|
||||
assertTaskDefinitionIdentifier(taskId, 'taskId');
|
||||
try {
|
||||
return await findCurrent(this.pool, projectId, taskId);
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async findTaskDefinitionRevision(
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
revision: number,
|
||||
): Promise<TaskDefinitionRecord | null> {
|
||||
assertTaskDefinitionIdentifier(projectId, 'projectId');
|
||||
assertTaskDefinitionIdentifier(taskId, 'taskId');
|
||||
assertTaskDefinitionRevision(revision);
|
||||
try {
|
||||
return await findRevision(this.pool, projectId, taskId, revision);
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async listTaskDefinitions(options: {
|
||||
readonly projectId: string;
|
||||
readonly limit: number;
|
||||
readonly after?: { readonly taskId: string };
|
||||
}): Promise<TaskDefinitionPage> {
|
||||
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
||||
throw new InvalidTaskDefinitionError('list options are invalid');
|
||||
}
|
||||
const keys = Object.keys(options);
|
||||
if (
|
||||
!keys.includes('limit') ||
|
||||
!keys.includes('projectId') ||
|
||||
keys.some((key) => !['after', 'limit', 'projectId'].includes(key))
|
||||
) {
|
||||
throw new InvalidTaskDefinitionError(
|
||||
'list options have an invalid shape',
|
||||
);
|
||||
}
|
||||
assertTaskDefinitionIdentifier(options.projectId, 'projectId');
|
||||
assertTaskDefinitionPageSize(options.limit);
|
||||
const after = options.after
|
||||
? normalizeTaskDefinitionCursor(options.after)
|
||||
: undefined;
|
||||
try {
|
||||
const result = await this.pool.query<TaskDefinitionRow>(
|
||||
`SELECT ${SELECT_FIELDS}
|
||||
FROM "ql3"."task_definitions" AS head
|
||||
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
|
||||
WHERE head.project_id = $1 AND head.task_id > $2
|
||||
ORDER BY head.task_id
|
||||
LIMIT $3`,
|
||||
[options.projectId, after?.taskId ?? '', options.limit + 1],
|
||||
);
|
||||
const truncated = result.rows.length > options.limit;
|
||||
const definitions = Object.freeze(
|
||||
result.rows.slice(0, options.limit).map(taskDefinitionRecord),
|
||||
);
|
||||
const last = definitions.at(-1);
|
||||
return Object.freeze({
|
||||
definitions,
|
||||
truncated,
|
||||
...(truncated && last
|
||||
? { next: Object.freeze({ taskId: last.taskId }) }
|
||||
: {}),
|
||||
});
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Read-only, digest-verifying remote Worker execution revision source. */
|
||||
export class PostgresTaskExecutionRevisionSource
|
||||
implements ClusterTaskExecutionRevisionSource
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
throw new TypeError('PostgreSQL Task execution revision pool is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
async resolveClusterTaskExecutionRevision(identity: {
|
||||
readonly projectId: string;
|
||||
readonly taskId: string;
|
||||
readonly sourceRevision: number;
|
||||
}): Promise<ClusterTaskExecutionRevision | null> {
|
||||
if (!identity || typeof identity !== 'object' || Array.isArray(identity)) {
|
||||
throw new TypeError('Cluster Task execution revision identity is invalid');
|
||||
}
|
||||
assertTaskDefinitionIdentifier(identity.projectId, 'projectId');
|
||||
assertTaskDefinitionIdentifier(identity.taskId, 'taskId');
|
||||
assertTaskDefinitionRevision(identity.sourceRevision);
|
||||
try {
|
||||
return await findExecutionRevision(
|
||||
this.pool,
|
||||
identity.projectId,
|
||||
identity.taskId,
|
||||
identity.sourceRevision,
|
||||
);
|
||||
} catch (error) {
|
||||
throw mappedError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Administration-only TaskDefinition publisher; execution revisions are separate. */
|
||||
export class PostgresTaskDefinitionRepository
|
||||
extends PostgresTaskDefinitionSource
|
||||
implements TaskDefinitionRepository
|
||||
{
|
||||
constructor(
|
||||
pool: PostgresPool,
|
||||
private readonly semanticRegistry: TaskSpecSemanticRegistry =
|
||||
createBuiltInTaskSpecSemanticRegistry(),
|
||||
) {
|
||||
super(pool);
|
||||
}
|
||||
|
||||
async appendTaskDefinitionRevision(
|
||||
input: AppendTaskDefinitionRevisionCommand,
|
||||
transactionHook?: PostgresTaskDefinitionRevisionTransactionHook,
|
||||
): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'updated' | 'existing';
|
||||
definition: TaskDefinitionRecord;
|
||||
}>
|
||||
> {
|
||||
if (
|
||||
transactionHook !== undefined &&
|
||||
typeof transactionHook !== 'function'
|
||||
) {
|
||||
throw new InvalidTaskDefinitionError('transaction hook is invalid');
|
||||
}
|
||||
const normalized = normalizeAppendTaskDefinitionRevisionCommand(input);
|
||||
const command = Object.freeze({
|
||||
...normalized,
|
||||
spec: this.semanticRegistry.normalize({
|
||||
projectId: normalized.projectId,
|
||||
taskId: normalized.taskId,
|
||||
kind: normalized.kind,
|
||||
spec: normalized.spec,
|
||||
}),
|
||||
});
|
||||
const compiledExecution =
|
||||
command.enabled &&
|
||||
command.kind === 'command' &&
|
||||
command.spec.schema === BUILT_IN_COMMAND_TASK_SPEC_SCHEMA
|
||||
? compileClusterCommandTaskDefinition(
|
||||
createTaskDefinitionRecord(
|
||||
command,
|
||||
command.occurredAtMs,
|
||||
),
|
||||
this.semanticRegistry,
|
||||
)
|
||||
: null;
|
||||
|
||||
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;
|
||||
let transactionHookError: unknown;
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
const ownership = await client.query(
|
||||
`SELECT 1
|
||||
FROM "ql3"."plugin_package_task_ownerships"
|
||||
WHERE project_id = $1 AND task_id = $2`,
|
||||
[command.projectId, command.taskId],
|
||||
);
|
||||
if (ownership.rows.length !== 0) {
|
||||
throw new TaskDefinitionConflictError();
|
||||
}
|
||||
const replay = await findByMutation(client, command.mutationId);
|
||||
if (replay) {
|
||||
const expected = createTaskDefinitionRecord(
|
||||
command,
|
||||
replay.createdAtMs,
|
||||
);
|
||||
if (!sameRecord(replay, expected)) {
|
||||
throw new TaskDefinitionConflictError();
|
||||
}
|
||||
const persistedExecution = await findExecutionRevision(
|
||||
client,
|
||||
replay.projectId,
|
||||
replay.taskId,
|
||||
replay.revision,
|
||||
);
|
||||
if (!sameExecutionRevision(persistedExecution, compiledExecution)) {
|
||||
throw unavailable();
|
||||
}
|
||||
if (transactionHook) {
|
||||
try {
|
||||
const hookResult = await transactionHook(
|
||||
client,
|
||||
Object.freeze({ command, replay, record: replay }),
|
||||
);
|
||||
if (hookResult !== undefined) {
|
||||
throw new InvalidTaskDefinitionError(
|
||||
'transaction hook must not return a value',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
transactionHookError = error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({ status: 'existing', definition: replay });
|
||||
}
|
||||
|
||||
const project = await client.query<{ status: unknown }>(
|
||||
`SELECT status FROM "ql3"."projects"
|
||||
WHERE id = $1`,
|
||||
[command.projectId],
|
||||
);
|
||||
if (
|
||||
project.rows.length !== 1 ||
|
||||
project.rows[0]?.status !== 'active'
|
||||
) {
|
||||
throw new TaskDefinitionConflictError();
|
||||
}
|
||||
|
||||
const inserted = await client.query(
|
||||
`INSERT INTO "ql3"."task_definitions" (
|
||||
project_id, task_id, current_revision, created_at_ms, updated_at_ms
|
||||
) VALUES ($1, $2, 1, $3, $3)
|
||||
ON CONFLICT (project_id, task_id) DO NOTHING
|
||||
RETURNING task_id`,
|
||||
[command.projectId, command.taskId, command.occurredAtMs],
|
||||
);
|
||||
if (inserted.rows.length > 1) throw unavailable();
|
||||
const created = inserted.rows.length === 1;
|
||||
const head = await client.query<TaskDefinitionRow>(
|
||||
`SELECT current_revision AS "currentRevision",
|
||||
created_at_ms AS "createdAtMs",
|
||||
updated_at_ms AS "updatedAtMs"
|
||||
FROM "ql3"."task_definitions"
|
||||
WHERE project_id = $1 AND task_id = $2
|
||||
FOR UPDATE`,
|
||||
[command.projectId, command.taskId],
|
||||
);
|
||||
if (head.rows.length !== 1) throw unavailable();
|
||||
const currentRevision = postgresRequiredInteger(
|
||||
head.rows[0]!.currentRevision,
|
||||
unavailable,
|
||||
);
|
||||
const createdAtMs = postgresRequiredInteger(
|
||||
head.rows[0]!.createdAtMs,
|
||||
unavailable,
|
||||
);
|
||||
const previousUpdatedAtMs = postgresRequiredInteger(
|
||||
head.rows[0]!.updatedAtMs,
|
||||
unavailable,
|
||||
);
|
||||
if (
|
||||
(created ? command.expectedRevision !== null :
|
||||
currentRevision !== command.expectedRevision) ||
|
||||
command.occurredAtMs < previousUpdatedAtMs
|
||||
) {
|
||||
throw new TaskDefinitionConflictError();
|
||||
}
|
||||
const definition = createTaskDefinitionRecord(command, createdAtMs);
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."task_definition_revisions" (
|
||||
project_id, task_id, revision, mutation_id, name, description,
|
||||
kind, spec_json, labels_json, enabled, content_digest, created_at_ms
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb,
|
||||
$10, $11, $12)`,
|
||||
[
|
||||
definition.projectId,
|
||||
definition.taskId,
|
||||
definition.revision,
|
||||
definition.mutationId,
|
||||
definition.name,
|
||||
definition.description ?? null,
|
||||
definition.kind,
|
||||
JSON.stringify(definition.spec),
|
||||
JSON.stringify(definition.labels),
|
||||
definition.enabled,
|
||||
definition.contentDigest,
|
||||
definition.updatedAtMs,
|
||||
],
|
||||
);
|
||||
if (compiledExecution) {
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."task_execution_revisions" (
|
||||
project_id, task_id, source_revision, task_revision,
|
||||
source_content_digest, executor_type, plan_schema, plan_json,
|
||||
content_digest, created_at_ms
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10)`,
|
||||
[
|
||||
compiledExecution.projectId,
|
||||
compiledExecution.taskId,
|
||||
compiledExecution.sourceRevision,
|
||||
compiledExecution.taskRevision,
|
||||
compiledExecution.sourceContentDigest,
|
||||
compiledExecution.executorType,
|
||||
compiledExecution.planSchema,
|
||||
JSON.stringify(executionPlanJson(compiledExecution)),
|
||||
compiledExecution.contentDigest,
|
||||
compiledExecution.createdAtMs,
|
||||
],
|
||||
);
|
||||
}
|
||||
if (!created) {
|
||||
const update = await client.query(
|
||||
`UPDATE "ql3"."task_definitions"
|
||||
SET current_revision = $1, updated_at_ms = $2
|
||||
WHERE project_id = $3 AND task_id = $4
|
||||
AND current_revision = $5`,
|
||||
[
|
||||
definition.revision,
|
||||
definition.updatedAtMs,
|
||||
definition.projectId,
|
||||
definition.taskId,
|
||||
command.expectedRevision,
|
||||
],
|
||||
);
|
||||
if (update.rowCount !== 1) throw new TaskDefinitionConflictError();
|
||||
}
|
||||
if (transactionHook) {
|
||||
try {
|
||||
const hookResult = await transactionHook(
|
||||
client,
|
||||
Object.freeze({ command, replay: null, record: definition }),
|
||||
);
|
||||
if (hookResult !== undefined) {
|
||||
throw new InvalidTaskDefinitionError(
|
||||
'transaction hook must not return a value',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
transactionHookError = error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({
|
||||
status: created ? 'created' : 'updated',
|
||||
definition,
|
||||
});
|
||||
} 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;
|
||||
}
|
||||
if (error === transactionHookError && error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw mappedError(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { X509Certificate } from 'node:crypto';
|
||||
import { closeSync, fstatSync, openSync, readSync, constants } from 'node:fs';
|
||||
import { isAbsolute } from 'node:path';
|
||||
|
||||
export const POSTGRES_CA_MAX_FILE_BYTES = 256 * 1024;
|
||||
export const POSTGRES_CA_MAX_CERTIFICATES = 16;
|
||||
|
||||
export const POSTGRES_CA_FILE_ERROR_CODES = Object.freeze([
|
||||
'QL3_POSTGRES_CA_INVALID_PATH',
|
||||
'QL3_POSTGRES_CA_UNAVAILABLE',
|
||||
'QL3_POSTGRES_CA_NOT_REGULAR',
|
||||
'QL3_POSTGRES_CA_INSECURE_PERMISSIONS',
|
||||
'QL3_POSTGRES_CA_INVALID_SIZE',
|
||||
'QL3_POSTGRES_CA_CHANGED_DURING_READ',
|
||||
'QL3_POSTGRES_CA_INVALID_ENCODING',
|
||||
'QL3_POSTGRES_CA_INVALID_PEM',
|
||||
'QL3_POSTGRES_CA_TOO_MANY_CERTIFICATES',
|
||||
'QL3_POSTGRES_CA_NOT_CA',
|
||||
'QL3_POSTGRES_CA_DUPLICATE_CERTIFICATE',
|
||||
] as const);
|
||||
|
||||
export type PostgresCertificateAuthorityFileErrorCode =
|
||||
(typeof POSTGRES_CA_FILE_ERROR_CODES)[number];
|
||||
|
||||
export class PostgresCertificateAuthorityFileError extends TypeError {
|
||||
constructor(readonly code: PostgresCertificateAuthorityFileErrorCode) {
|
||||
super('PostgreSQL certificate authority file is invalid');
|
||||
this.name = 'PostgresCertificateAuthorityFileError';
|
||||
}
|
||||
}
|
||||
|
||||
export interface PostgresCertificateAuthorityFileInspection {
|
||||
readonly bundle: string;
|
||||
readonly fingerprints256: readonly string[];
|
||||
}
|
||||
|
||||
function fail(code: PostgresCertificateAuthorityFileErrorCode): never {
|
||||
throw new PostgresCertificateAuthorityFileError(code);
|
||||
}
|
||||
|
||||
function readBoundedRegularFile(filePath: string): Buffer {
|
||||
if (
|
||||
typeof filePath !== 'string' ||
|
||||
filePath.length < 1 ||
|
||||
filePath.length > 4096 ||
|
||||
/[\0\r\n]/.test(filePath) ||
|
||||
!isAbsolute(filePath)
|
||||
) {
|
||||
fail('QL3_POSTGRES_CA_INVALID_PATH');
|
||||
}
|
||||
|
||||
let descriptor: number;
|
||||
try {
|
||||
descriptor = openSync(filePath, constants.O_RDONLY);
|
||||
} catch {
|
||||
fail('QL3_POSTGRES_CA_UNAVAILABLE');
|
||||
}
|
||||
try {
|
||||
const stat = fstatSync(descriptor);
|
||||
if (!stat.isFile()) fail('QL3_POSTGRES_CA_NOT_REGULAR');
|
||||
if ((stat.mode & 0o022) !== 0) {
|
||||
fail('QL3_POSTGRES_CA_INSECURE_PERMISSIONS');
|
||||
}
|
||||
if (
|
||||
!Number.isSafeInteger(stat.size) ||
|
||||
stat.size < 1 ||
|
||||
stat.size > POSTGRES_CA_MAX_FILE_BYTES
|
||||
) {
|
||||
fail('QL3_POSTGRES_CA_INVALID_SIZE');
|
||||
}
|
||||
|
||||
const bytes = Buffer.alloc(stat.size + 1);
|
||||
let offset = 0;
|
||||
while (offset < bytes.byteLength) {
|
||||
const count = readSync(
|
||||
descriptor,
|
||||
bytes,
|
||||
offset,
|
||||
bytes.byteLength - offset,
|
||||
null,
|
||||
);
|
||||
if (count === 0) break;
|
||||
offset += count;
|
||||
}
|
||||
if (offset !== stat.size) {
|
||||
fail('QL3_POSTGRES_CA_CHANGED_DURING_READ');
|
||||
}
|
||||
return bytes.subarray(0, offset);
|
||||
} finally {
|
||||
closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads one immutable PostgreSQL trust bundle. Kubernetes projected Secret
|
||||
* symlinks are accepted because the opened target is validated with fstat.
|
||||
*/
|
||||
export function inspectPostgresCertificateAuthorityFile(
|
||||
filePath: string,
|
||||
): PostgresCertificateAuthorityFileInspection {
|
||||
const bytes = readBoundedRegularFile(filePath);
|
||||
let source: string;
|
||||
try {
|
||||
source = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
||||
} catch {
|
||||
fail('QL3_POSTGRES_CA_INVALID_ENCODING');
|
||||
} finally {
|
||||
bytes.fill(0);
|
||||
}
|
||||
|
||||
const pattern =
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
||||
const blocks = source.match(pattern) ?? [];
|
||||
if (blocks.length < 1 || source.replace(pattern, '').trim().length !== 0) {
|
||||
fail('QL3_POSTGRES_CA_INVALID_PEM');
|
||||
}
|
||||
if (blocks.length > POSTGRES_CA_MAX_CERTIFICATES) {
|
||||
fail('QL3_POSTGRES_CA_TOO_MANY_CERTIFICATES');
|
||||
}
|
||||
|
||||
const fingerprints = new Set<string>();
|
||||
const normalized: string[] = [];
|
||||
for (const block of blocks) {
|
||||
let certificate: X509Certificate;
|
||||
try {
|
||||
certificate = new X509Certificate(block);
|
||||
} catch {
|
||||
fail('QL3_POSTGRES_CA_INVALID_PEM');
|
||||
}
|
||||
if (!certificate.ca) fail('QL3_POSTGRES_CA_NOT_CA');
|
||||
if (fingerprints.has(certificate.fingerprint256)) {
|
||||
fail('QL3_POSTGRES_CA_DUPLICATE_CERTIFICATE');
|
||||
}
|
||||
fingerprints.add(certificate.fingerprint256);
|
||||
normalized.push(certificate.toString().trimEnd());
|
||||
}
|
||||
return Object.freeze({
|
||||
bundle: `${normalized.join('\n')}\n`,
|
||||
fingerprints256: Object.freeze([...fingerprints].sort()),
|
||||
});
|
||||
}
|
||||
|
||||
export function loadPostgresCertificateAuthorityFile(filePath: string): string {
|
||||
return inspectPostgresCertificateAuthorityFile(filePath).bundle;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { PostgresConnectionOptions } from './pool';
|
||||
|
||||
export type PostgresConnectionEnvironment = Readonly<
|
||||
Record<string, string | undefined>
|
||||
>;
|
||||
|
||||
export interface PostgresConnectionEnvironmentKeys {
|
||||
readonly connectionString: string;
|
||||
readonly host: string;
|
||||
readonly port: string;
|
||||
readonly database: string;
|
||||
readonly user: string;
|
||||
readonly password: string;
|
||||
}
|
||||
|
||||
export class PostgresConnectionEnvironmentError extends TypeError {
|
||||
readonly code = 'QL3_POSTGRES_CONNECTION_ENVIRONMENT_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`PostgreSQL connection environment is invalid: ${message}`);
|
||||
this.name = 'PostgresConnectionEnvironmentError';
|
||||
}
|
||||
}
|
||||
|
||||
function present(value: string | undefined): boolean {
|
||||
return value !== undefined && value !== '';
|
||||
}
|
||||
|
||||
function bounded(
|
||||
environment: PostgresConnectionEnvironment,
|
||||
name: string,
|
||||
maximumLength: number,
|
||||
): string | undefined {
|
||||
const value = environment[name];
|
||||
if (!present(value)) return undefined;
|
||||
if (value!.length > maximumLength || /[\0\r\n]/.test(value!)) {
|
||||
throw new PostgresConnectionEnvironmentError(`${name} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function assertConnectionString(value: string, name: string): void {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
throw new PostgresConnectionEnvironmentError(`${name} must be a valid URL`);
|
||||
}
|
||||
if (url.protocol !== 'postgres:' && url.protocol !== 'postgresql:') {
|
||||
throw new PostgresConnectionEnvironmentError(
|
||||
`${name} must use postgres or postgresql`,
|
||||
);
|
||||
}
|
||||
for (const parameter of url.searchParams.keys()) {
|
||||
if (parameter.toLowerCase().startsWith('ssl')) {
|
||||
throw new PostgresConnectionEnvironmentError(
|
||||
'PostgreSQL TLS query parameters are forbidden',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function port(value: string | undefined, name: string): number {
|
||||
if (value === undefined) return 5432;
|
||||
if (!/^\d+$/.test(value)) {
|
||||
throw new PostgresConnectionEnvironmentError(`${name} must be an integer`);
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65_535) {
|
||||
throw new PostgresConnectionEnvironmentError(
|
||||
`${name} must be between 1 and 65535`,
|
||||
);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function identifier(value: string | undefined, name: string): string {
|
||||
if (value === undefined || !/^[A-Za-z_][A-Za-z0-9_$]{0,62}$/.test(value)) {
|
||||
throw new PostgresConnectionEnvironmentError(
|
||||
`${name} must be a safe PostgreSQL identifier`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads either one legacy connection URL or an exact discrete credential set.
|
||||
*
|
||||
* Discrete values avoid duplicating Kubernetes Secret passwords inside a DSN
|
||||
* and let an operator-managed DatabaseRole Secret remain the sole password
|
||||
* authority. The two forms are deliberately mutually exclusive.
|
||||
*/
|
||||
export function loadPostgresConnectionEnvironment(
|
||||
environment: PostgresConnectionEnvironment,
|
||||
keys: PostgresConnectionEnvironmentKeys,
|
||||
): PostgresConnectionOptions {
|
||||
if (
|
||||
!environment ||
|
||||
typeof environment !== 'object' ||
|
||||
Array.isArray(environment)
|
||||
) {
|
||||
throw new PostgresConnectionEnvironmentError(
|
||||
'environment must be an object',
|
||||
);
|
||||
}
|
||||
const connectionString = bounded(environment, keys.connectionString, 4096);
|
||||
const host = bounded(environment, keys.host, 253);
|
||||
const portValue = bounded(environment, keys.port, 5);
|
||||
const database = bounded(environment, keys.database, 63);
|
||||
const user = bounded(environment, keys.user, 63);
|
||||
const password = bounded(environment, keys.password, 1024);
|
||||
const discreteValues = [host, portValue, database, user, password];
|
||||
const hasDiscreteValue = discreteValues.some((value) => value !== undefined);
|
||||
|
||||
if (connectionString !== undefined) {
|
||||
if (hasDiscreteValue) {
|
||||
throw new PostgresConnectionEnvironmentError(
|
||||
`${keys.connectionString} cannot be combined with discrete PostgreSQL connection keys`,
|
||||
);
|
||||
}
|
||||
assertConnectionString(connectionString, keys.connectionString);
|
||||
return Object.freeze({ connectionString });
|
||||
}
|
||||
|
||||
if (
|
||||
host === undefined ||
|
||||
database === undefined ||
|
||||
user === undefined ||
|
||||
password === undefined
|
||||
) {
|
||||
throw new PostgresConnectionEnvironmentError(
|
||||
`use ${keys.connectionString} or provide ${keys.host}, ${keys.database}, ${keys.user}, and ${keys.password}`,
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
host,
|
||||
port: port(portValue, keys.port),
|
||||
database: identifier(database, keys.database),
|
||||
user: identifier(user, keys.user),
|
||||
password,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
import {
|
||||
Pool,
|
||||
type PoolClient,
|
||||
type PoolConfig,
|
||||
type QueryResultRow,
|
||||
} from 'pg';
|
||||
import { isIP } from 'node:net';
|
||||
import type {
|
||||
PostgresClient,
|
||||
PostgresDatabaseResource,
|
||||
PostgresPool,
|
||||
PostgresQueryResult,
|
||||
} from '@qinglong/runtime-core';
|
||||
|
||||
const DEFAULT_RUNTIME_APPLICATION_NAME = 'qinglong-cluster-runtime';
|
||||
const DEFAULT_ADMIN_APPLICATION_NAME = 'qinglong-cluster-admin';
|
||||
const DEFAULT_AI_CREDENTIAL_TESTER_APPLICATION_NAME =
|
||||
'qinglong-ai-credential-tester';
|
||||
const DEFAULT_AUTOMATION_MANAGER_APPLICATION_NAME =
|
||||
'qinglong-automation-manager';
|
||||
const DEFAULT_APPROVAL_MANAGER_APPLICATION_NAME = 'qinglong-approval-manager';
|
||||
const DEFAULT_PACKAGE_MANAGER_APPLICATION_NAME = 'qinglong-package-manager';
|
||||
const DEFAULT_PACKAGE_EXECUTOR_APPLICATION_NAME = 'qinglong-package-executor';
|
||||
const DEFAULT_WORKER_CREDENTIAL_MANAGER_APPLICATION_NAME =
|
||||
'qinglong-worker-credential-manager';
|
||||
const DEFAULT_WORKER_CREDENTIAL_EXECUTOR_APPLICATION_NAME =
|
||||
'qinglong-worker-credential-executor';
|
||||
const DEFAULT_WORKER_INGRESS_APPLICATION_NAME = 'qinglong-worker-ingress';
|
||||
const DEFAULT_MIGRATION_APPLICATION_NAME = 'qinglong-cluster-migration';
|
||||
const DEFAULT_RUNTIME_POOL_SIZE = 8;
|
||||
const DEFAULT_ADMIN_POOL_SIZE = 2;
|
||||
const DEFAULT_WORKER_INGRESS_POOL_SIZE = 4;
|
||||
const DEFAULT_MIGRATION_POOL_SIZE = 1;
|
||||
const MAX_RUNTIME_POOL_SIZE = 64;
|
||||
const MAX_ADMIN_POOL_SIZE = 4;
|
||||
const MAX_WORKER_INGRESS_POOL_SIZE = 16;
|
||||
const MAX_MIGRATION_POOL_SIZE = 4;
|
||||
const DEFAULT_CONNECTION_TIMEOUT_MS = 5_000;
|
||||
const DEFAULT_IDLE_TIMEOUT_MS = 30_000;
|
||||
const DEFAULT_MAX_LIFETIME_SECONDS = 300;
|
||||
const DEFAULT_RUNTIME_STATEMENT_TIMEOUT_MS = 5_000;
|
||||
const DEFAULT_MIGRATION_STATEMENT_TIMEOUT_MS = 15_000;
|
||||
const DEFAULT_RUNTIME_LOCK_TIMEOUT_MS = 1_000;
|
||||
const DEFAULT_MIGRATION_LOCK_TIMEOUT_MS = 5_000;
|
||||
const DEFAULT_RUNTIME_IDLE_TRANSACTION_TIMEOUT_MS = 10_000;
|
||||
const DEFAULT_MIGRATION_IDLE_TRANSACTION_TIMEOUT_MS = 15_000;
|
||||
const FIXED_SEARCH_PATH = '-c search_path=ql3,pg_catalog';
|
||||
|
||||
export const POSTGRES_AVAILABILITY_SQLSTATE_CLASSES = Object.freeze([
|
||||
'08',
|
||||
] as const);
|
||||
|
||||
export const POSTGRES_AVAILABILITY_SQLSTATES = Object.freeze([
|
||||
'08000',
|
||||
'08001',
|
||||
'08003',
|
||||
'08004',
|
||||
'08006',
|
||||
'08007',
|
||||
'08P01',
|
||||
'25006',
|
||||
'57P01',
|
||||
'57P02',
|
||||
'57P03',
|
||||
'57P04',
|
||||
] as const);
|
||||
|
||||
export const POSTGRES_AVAILABILITY_SYSTEM_ERROR_CODES = Object.freeze([
|
||||
'EAI_AGAIN',
|
||||
'ECONNREFUSED',
|
||||
'ECONNRESET',
|
||||
'EHOSTUNREACH',
|
||||
'ENETUNREACH',
|
||||
'ENOTFOUND',
|
||||
'EPIPE',
|
||||
'ETIMEDOUT',
|
||||
] as const);
|
||||
|
||||
const POSTGRES_AVAILABILITY_CODES = new Set<string>([
|
||||
...POSTGRES_AVAILABILITY_SQLSTATES,
|
||||
...POSTGRES_AVAILABILITY_SYSTEM_ERROR_CODES,
|
||||
]);
|
||||
|
||||
export type PostgresDatabaseRole =
|
||||
| 'runtime'
|
||||
| 'ai-maintenance'
|
||||
| 'ai-credential-manager'
|
||||
| 'ai-credential-tester'
|
||||
| 'admin'
|
||||
| 'automation-manager'
|
||||
| 'approval-manager'
|
||||
| 'package-manager'
|
||||
| 'package-executor'
|
||||
| 'worker-credential-manager'
|
||||
| 'worker-credential-executor'
|
||||
| 'worker-ingress'
|
||||
| 'migration';
|
||||
|
||||
export type PostgresTlsOptions =
|
||||
| Readonly<{
|
||||
mode: 'verify-full';
|
||||
ca?: string;
|
||||
servername: string;
|
||||
}>
|
||||
| Readonly<{
|
||||
mode: 'disable';
|
||||
}>;
|
||||
|
||||
export interface PostgresConnectionOptions {
|
||||
readonly connectionString?: string;
|
||||
readonly host?: string;
|
||||
readonly port?: number;
|
||||
readonly database?: string;
|
||||
readonly user?: string;
|
||||
readonly password?: string | (() => string | Promise<string>);
|
||||
readonly tls?: PostgresTlsOptions;
|
||||
}
|
||||
|
||||
export interface PostgresPoolOptions {
|
||||
readonly applicationName?: string;
|
||||
readonly maxConnections?: number;
|
||||
readonly connectionTimeoutMs?: number;
|
||||
readonly idleTimeoutMs?: number;
|
||||
readonly maxLifetimeSeconds?: number;
|
||||
readonly keepAliveInitialDelayMs?: number;
|
||||
}
|
||||
|
||||
export interface OpenPostgresDatabaseOptions {
|
||||
readonly role: PostgresDatabaseRole;
|
||||
readonly connection: PostgresConnectionOptions;
|
||||
readonly pool?: PostgresPoolOptions;
|
||||
readonly onPoolError: (error: Error) => void;
|
||||
}
|
||||
|
||||
export function isPostgresTlsDnsServername(value: unknown): value is string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length < 3 ||
|
||||
value.length > 253 ||
|
||||
isIP(value) !== 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const labels = value.split('.');
|
||||
return (
|
||||
labels.length >= 2 &&
|
||||
labels.every((label) =>
|
||||
/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(label),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export type QingLongPostgresQueryResult<
|
||||
TRow extends Record<string, unknown> = Record<string, unknown>,
|
||||
> = PostgresQueryResult<TRow>;
|
||||
|
||||
export type QingLongPostgresClient = PostgresClient;
|
||||
|
||||
export type QingLongPostgresPool = PostgresPool;
|
||||
|
||||
export type QingLongPostgresDatabaseResource = PostgresDatabaseResource;
|
||||
|
||||
export function isPostgresAvailabilityError(error: unknown): error is Error {
|
||||
if (!(error instanceof Error)) return false;
|
||||
const code = (error as Error & { readonly code?: unknown }).code;
|
||||
if (typeof code !== 'string') return false;
|
||||
return (
|
||||
POSTGRES_AVAILABILITY_CODES.has(code) ||
|
||||
(code.length === 5 &&
|
||||
POSTGRES_AVAILABILITY_SQLSTATE_CLASSES.some((prefix) =>
|
||||
code.startsWith(prefix),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
function reportPostgresAvailabilityError(
|
||||
error: unknown,
|
||||
listener: (error: Error) => void,
|
||||
force = false,
|
||||
): void {
|
||||
if (!(error instanceof Error)) return;
|
||||
if (!force && !isPostgresAvailabilityError(error)) return;
|
||||
try {
|
||||
listener(error);
|
||||
} catch {
|
||||
// Availability reporting must never replace the original query/Pool error.
|
||||
}
|
||||
}
|
||||
|
||||
function requireIntegerInRange(
|
||||
name: string,
|
||||
value: number,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): number {
|
||||
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
||||
throw new RangeError(
|
||||
`${name} must be an integer between ${minimum} and ${maximum}`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function validateApplicationName(value: string): string {
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,62}$/.test(value)) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL applicationName must contain 1-63 safe identifier characters',
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function assertNoConnectionStringTlsOverrides(connectionString: string): void {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(connectionString);
|
||||
} catch {
|
||||
throw new TypeError('PostgreSQL connectionString must be a valid URL');
|
||||
}
|
||||
if (parsed.protocol !== 'postgres:' && parsed.protocol !== 'postgresql:') {
|
||||
throw new TypeError(
|
||||
'PostgreSQL connectionString must use postgres or postgresql',
|
||||
);
|
||||
}
|
||||
for (const key of parsed.searchParams.keys()) {
|
||||
if (key.toLowerCase().startsWith('ssl')) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL TLS settings must use the explicit tls option',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tlsConfig(tls: PostgresTlsOptions | undefined): PoolConfig['ssl'] {
|
||||
if (tls?.mode === 'disable') return false;
|
||||
if (tls !== undefined && !isPostgresTlsDnsServername(tls.servername)) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL verify-full TLS requires an explicit DNS servername',
|
||||
);
|
||||
}
|
||||
return {
|
||||
rejectUnauthorized: true,
|
||||
...(tls?.ca === undefined ? {} : { ca: tls.ca }),
|
||||
...(tls === undefined ? {} : { servername: tls.servername }),
|
||||
};
|
||||
}
|
||||
|
||||
function buildPoolConfig(options: OpenPostgresDatabaseOptions): PoolConfig {
|
||||
if (
|
||||
![
|
||||
'runtime',
|
||||
'ai-maintenance',
|
||||
'ai-credential-manager',
|
||||
'ai-credential-tester',
|
||||
'admin',
|
||||
'automation-manager',
|
||||
'approval-manager',
|
||||
'package-manager',
|
||||
'package-executor',
|
||||
'worker-credential-manager',
|
||||
'worker-credential-executor',
|
||||
'worker-ingress',
|
||||
'migration',
|
||||
].includes(options.role)
|
||||
) {
|
||||
throw new TypeError('PostgreSQL database role is invalid');
|
||||
}
|
||||
const connection = options.connection;
|
||||
const hasConnectionString = connection.connectionString !== undefined;
|
||||
const hasDiscreteAddress =
|
||||
connection.host !== undefined ||
|
||||
connection.port !== undefined ||
|
||||
connection.database !== undefined ||
|
||||
connection.user !== undefined;
|
||||
if (hasConnectionString && hasDiscreteAddress) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL connectionString cannot be combined with host, port, database, or user',
|
||||
);
|
||||
}
|
||||
if (hasConnectionString) {
|
||||
assertNoConnectionStringTlsOverrides(connection.connectionString!);
|
||||
} else if (
|
||||
connection.host === undefined ||
|
||||
connection.database === undefined ||
|
||||
connection.user === undefined
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL connection requires connectionString or host, database, and user',
|
||||
);
|
||||
}
|
||||
|
||||
const isRuntime = options.role === 'runtime';
|
||||
const isAiMaintenance = options.role === 'ai-maintenance';
|
||||
const isAiCredentialManager = options.role === 'ai-credential-manager';
|
||||
const isAiCredentialTester = options.role === 'ai-credential-tester';
|
||||
const isAdmin = options.role === 'admin';
|
||||
const isAutomationManager = options.role === 'automation-manager';
|
||||
const isApprovalManager = options.role === 'approval-manager';
|
||||
const isPackageManager = options.role === 'package-manager';
|
||||
const isPackageExecutor = options.role === 'package-executor';
|
||||
const isWorkerCredentialManager =
|
||||
options.role === 'worker-credential-manager';
|
||||
const isWorkerCredentialExecutor =
|
||||
options.role === 'worker-credential-executor';
|
||||
const isShortLivedAuthority =
|
||||
isAiMaintenance ||
|
||||
isAiCredentialManager ||
|
||||
isAiCredentialTester ||
|
||||
isAdmin ||
|
||||
isAutomationManager ||
|
||||
isApprovalManager ||
|
||||
isPackageManager ||
|
||||
isPackageExecutor ||
|
||||
isWorkerCredentialManager ||
|
||||
isWorkerCredentialExecutor;
|
||||
const isWorkerIngress = options.role === 'worker-ingress';
|
||||
const maximumPoolSize = isRuntime
|
||||
? MAX_RUNTIME_POOL_SIZE
|
||||
: isShortLivedAuthority
|
||||
? MAX_ADMIN_POOL_SIZE
|
||||
: isWorkerIngress
|
||||
? MAX_WORKER_INGRESS_POOL_SIZE
|
||||
: MAX_MIGRATION_POOL_SIZE;
|
||||
const maxConnections = requireIntegerInRange(
|
||||
'PostgreSQL maxConnections',
|
||||
options.pool?.maxConnections ??
|
||||
(isRuntime
|
||||
? DEFAULT_RUNTIME_POOL_SIZE
|
||||
: isShortLivedAuthority
|
||||
? DEFAULT_ADMIN_POOL_SIZE
|
||||
: isWorkerIngress
|
||||
? DEFAULT_WORKER_INGRESS_POOL_SIZE
|
||||
: DEFAULT_MIGRATION_POOL_SIZE),
|
||||
1,
|
||||
maximumPoolSize,
|
||||
);
|
||||
const connectionTimeoutMs = requireIntegerInRange(
|
||||
'PostgreSQL connectionTimeoutMs',
|
||||
options.pool?.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS,
|
||||
100,
|
||||
60_000,
|
||||
);
|
||||
const idleTimeoutMs = requireIntegerInRange(
|
||||
'PostgreSQL idleTimeoutMs',
|
||||
options.pool?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
|
||||
1_000,
|
||||
300_000,
|
||||
);
|
||||
const maxLifetimeSeconds = requireIntegerInRange(
|
||||
'PostgreSQL maxLifetimeSeconds',
|
||||
options.pool?.maxLifetimeSeconds ?? DEFAULT_MAX_LIFETIME_SECONDS,
|
||||
30,
|
||||
3_600,
|
||||
);
|
||||
const keepAliveInitialDelayMs = requireIntegerInRange(
|
||||
'PostgreSQL keepAliveInitialDelayMs',
|
||||
options.pool?.keepAliveInitialDelayMs ?? 1_000,
|
||||
0,
|
||||
60_000,
|
||||
);
|
||||
const applicationName = validateApplicationName(
|
||||
options.pool?.applicationName ??
|
||||
(isRuntime
|
||||
? DEFAULT_RUNTIME_APPLICATION_NAME
|
||||
: isAdmin
|
||||
? DEFAULT_ADMIN_APPLICATION_NAME
|
||||
: isAiCredentialTester
|
||||
? DEFAULT_AI_CREDENTIAL_TESTER_APPLICATION_NAME
|
||||
: isAutomationManager
|
||||
? DEFAULT_AUTOMATION_MANAGER_APPLICATION_NAME
|
||||
: isApprovalManager
|
||||
? DEFAULT_APPROVAL_MANAGER_APPLICATION_NAME
|
||||
: isPackageManager
|
||||
? DEFAULT_PACKAGE_MANAGER_APPLICATION_NAME
|
||||
: isPackageExecutor
|
||||
? DEFAULT_PACKAGE_EXECUTOR_APPLICATION_NAME
|
||||
: isWorkerCredentialManager
|
||||
? DEFAULT_WORKER_CREDENTIAL_MANAGER_APPLICATION_NAME
|
||||
: isWorkerCredentialExecutor
|
||||
? DEFAULT_WORKER_CREDENTIAL_EXECUTOR_APPLICATION_NAME
|
||||
: isWorkerIngress
|
||||
? DEFAULT_WORKER_INGRESS_APPLICATION_NAME
|
||||
: DEFAULT_MIGRATION_APPLICATION_NAME),
|
||||
);
|
||||
|
||||
return {
|
||||
...(connection.connectionString === undefined
|
||||
? {
|
||||
host: connection.host,
|
||||
...(connection.port === undefined
|
||||
? {}
|
||||
: {
|
||||
port: requireIntegerInRange(
|
||||
'PostgreSQL port',
|
||||
connection.port,
|
||||
1,
|
||||
65_535,
|
||||
),
|
||||
}),
|
||||
database: connection.database,
|
||||
user: connection.user,
|
||||
}
|
||||
: { connectionString: connection.connectionString }),
|
||||
...(connection.password === undefined
|
||||
? {}
|
||||
: { password: connection.password }),
|
||||
ssl: tlsConfig(connection.tls),
|
||||
application_name: applicationName,
|
||||
options: FIXED_SEARCH_PATH,
|
||||
max: maxConnections,
|
||||
min: 0,
|
||||
connectionTimeoutMillis: connectionTimeoutMs,
|
||||
idleTimeoutMillis: idleTimeoutMs,
|
||||
maxLifetimeSeconds,
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelayMillis: keepAliveInitialDelayMs,
|
||||
allowExitOnIdle: false,
|
||||
statement_timeout:
|
||||
isRuntime || isWorkerIngress
|
||||
? DEFAULT_RUNTIME_STATEMENT_TIMEOUT_MS
|
||||
: DEFAULT_MIGRATION_STATEMENT_TIMEOUT_MS,
|
||||
query_timeout:
|
||||
isRuntime || isWorkerIngress
|
||||
? DEFAULT_RUNTIME_STATEMENT_TIMEOUT_MS + 1_000
|
||||
: DEFAULT_MIGRATION_STATEMENT_TIMEOUT_MS + 1_000,
|
||||
lock_timeout:
|
||||
isRuntime || isWorkerIngress
|
||||
? DEFAULT_RUNTIME_LOCK_TIMEOUT_MS
|
||||
: DEFAULT_MIGRATION_LOCK_TIMEOUT_MS,
|
||||
idle_in_transaction_session_timeout:
|
||||
isRuntime || isWorkerIngress
|
||||
? DEFAULT_RUNTIME_IDLE_TRANSACTION_TIMEOUT_MS
|
||||
: DEFAULT_MIGRATION_IDLE_TRANSACTION_TIMEOUT_MS,
|
||||
};
|
||||
}
|
||||
|
||||
async function query<TRow extends Record<string, unknown>>(
|
||||
queryable: Pick<Pool | PoolClient, 'query'>,
|
||||
text: string,
|
||||
values?: readonly unknown[],
|
||||
onAvailabilityError: (error: Error) => void = () => {},
|
||||
): Promise<PostgresQueryResult<TRow>> {
|
||||
try {
|
||||
const result = await queryable.query<TRow & QueryResultRow>(
|
||||
text,
|
||||
values === undefined ? undefined : [...values],
|
||||
);
|
||||
return { rows: result.rows, rowCount: result.rowCount };
|
||||
} catch (error) {
|
||||
reportPostgresAvailabilityError(error, onAvailabilityError);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
class PgClientBinding implements PostgresClient {
|
||||
private readonly onClientError: (error: Error) => void;
|
||||
|
||||
constructor(
|
||||
private readonly client: PoolClient,
|
||||
private readonly onAvailabilityError: (error: Error) => void,
|
||||
) {
|
||||
this.onClientError = (error) =>
|
||||
reportPostgresAvailabilityError(error, this.onAvailabilityError, true);
|
||||
this.client.on('error', this.onClientError);
|
||||
}
|
||||
|
||||
query<TRow extends Record<string, unknown> = Record<string, unknown>>(
|
||||
text: string,
|
||||
values?: readonly unknown[],
|
||||
): Promise<PostgresQueryResult<TRow>> {
|
||||
return query<TRow>(this.client, text, values, this.onAvailabilityError);
|
||||
}
|
||||
|
||||
release(): void {
|
||||
this.client.removeListener('error', this.onClientError);
|
||||
this.client.release();
|
||||
}
|
||||
}
|
||||
|
||||
export class PgPoolBinding implements PostgresPool {
|
||||
constructor(
|
||||
private readonly driverPool: Pool,
|
||||
private readonly onAvailabilityError: (error: Error) => void = () => {},
|
||||
) {}
|
||||
|
||||
query<TRow extends Record<string, unknown> = Record<string, unknown>>(
|
||||
text: string,
|
||||
values?: readonly unknown[],
|
||||
): Promise<PostgresQueryResult<TRow>> {
|
||||
return query<TRow>(this.driverPool, text, values, this.onAvailabilityError);
|
||||
}
|
||||
|
||||
async connect(): Promise<PostgresClient> {
|
||||
try {
|
||||
return new PgClientBinding(
|
||||
await this.driverPool.connect(),
|
||||
this.onAvailabilityError,
|
||||
);
|
||||
} catch (error) {
|
||||
reportPostgresAvailabilityError(error, this.onAvailabilityError);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PgDatabaseResource implements PostgresDatabaseResource {
|
||||
readonly pool: PostgresPool;
|
||||
private closePromise: Promise<void> | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly driverPool: Pool,
|
||||
onAvailabilityError: (error: Error) => void,
|
||||
) {
|
||||
this.pool = new PgPoolBinding(driverPool, onAvailabilityError);
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
this.closePromise ??= this.driverPool.end();
|
||||
return this.closePromise;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a lazy database opener for the core cluster bootstrap. Constructing
|
||||
* the opener, and invoking it, do not acquire a PostgreSQL connection; the
|
||||
* first readiness or repository query triggers pg.Pool connection creation.
|
||||
*/
|
||||
export function createPostgresDatabaseOpener(
|
||||
options: OpenPostgresDatabaseOptions,
|
||||
): () => Promise<PostgresDatabaseResource> {
|
||||
const config = buildPoolConfig(options);
|
||||
return async () => {
|
||||
const driverPool = new Pool(config);
|
||||
driverPool.on('connect', (client) => {
|
||||
// Keep one listener for the physical connection lifetime. pg swaps its
|
||||
// own idle listener while a client is checked out, and a failover can
|
||||
// otherwise emit a second socket error after a checkout binding has
|
||||
// released but before Pool teardown has completed.
|
||||
client.on('error', (error) =>
|
||||
reportPostgresAvailabilityError(error, options.onPoolError, true),
|
||||
);
|
||||
});
|
||||
driverPool.on('error', (error) =>
|
||||
reportPostgresAvailabilityError(error, options.onPoolError, true),
|
||||
);
|
||||
return new PgDatabaseResource(driverPool, options.onPoolError);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
export {
|
||||
PgPoolBinding,
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresTlsDnsServername,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
type PostgresTlsOptions,
|
||||
type QingLongPostgresClient,
|
||||
type QingLongPostgresDatabaseResource,
|
||||
type QingLongPostgresPool,
|
||||
type QingLongPostgresQueryResult,
|
||||
} from '../connection/pool';
|
||||
|
||||
export {
|
||||
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
|
||||
PostgresSchemaReadinessError,
|
||||
assertPostgresAdminSchemaReady,
|
||||
type PostgresSchemaReadinessErrorCode,
|
||||
type PostgresSchemaReadinessReport,
|
||||
} from '../schema/schemaReadiness';
|
||||
|
||||
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
|
||||
export {
|
||||
loadPostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironmentKeys,
|
||||
} from '../connection/connectionEnvironment';
|
||||
export {
|
||||
loadPostgresCertificateAuthorityFile,
|
||||
type PostgresCertificateAuthorityFileInspection,
|
||||
} from '../connection/certificateAuthority';
|
||||
export * from '../security/identityAdministrationRepository';
|
||||
export * from '../security/apiCredentialAdministrationRepository';
|
||||
export * from '../security/securityAuditQueryRepository';
|
||||
export * from '../worker-credential/workerCredentialAdministrationRepository';
|
||||
export * from '../automation/automationAdministrationRepository';
|
||||
export { PostgresTaskDefinitionRepository } from '../automation/taskDefinitionRepository';
|
||||
export { PostgresTriggerRepository } from '../scheduling/triggerRepository';
|
||||
export { PostgresToolResultKeyCatalogRepository } from '../tool-execution/toolResultKeyCatalogRepository';
|
||||
export { PostgresToolResultRekeyRepository } from '../tool-execution/toolResultRekeyRepository';
|
||||
@@ -0,0 +1,21 @@
|
||||
export {
|
||||
PgPoolBinding,
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresTlsDnsServername,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
type PostgresTlsOptions,
|
||||
} from '../connection/pool';
|
||||
export {
|
||||
PostgresConnectionEnvironmentError,
|
||||
loadPostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironmentKeys,
|
||||
} from '../connection/connectionEnvironment';
|
||||
export {
|
||||
loadPostgresCertificateAuthorityFile,
|
||||
type PostgresCertificateAuthorityFileInspection,
|
||||
} from '../connection/certificateAuthority';
|
||||
export { PostgresProjectPolicyRepository } from '../security/projectPolicyRepository';
|
||||
@@ -0,0 +1,20 @@
|
||||
export {
|
||||
PgPoolBinding,
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresTlsDnsServername,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
type PostgresTlsOptions,
|
||||
} from '../connection/pool';
|
||||
export {
|
||||
PostgresConnectionEnvironmentError,
|
||||
loadPostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironmentKeys,
|
||||
} from '../connection/connectionEnvironment';
|
||||
export {
|
||||
loadPostgresCertificateAuthorityFile,
|
||||
type PostgresCertificateAuthorityFileInspection,
|
||||
} from '../connection/certificateAuthority';
|
||||
@@ -0,0 +1,21 @@
|
||||
export {
|
||||
PgPoolBinding,
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresTlsDnsServername,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresPoolOptions,
|
||||
type QingLongPostgresDatabaseResource,
|
||||
type QingLongPostgresPool,
|
||||
} from '../connection/pool';
|
||||
|
||||
export {
|
||||
PostgresConnectionEnvironmentError,
|
||||
loadPostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironment,
|
||||
} from '../connection/connectionEnvironment';
|
||||
|
||||
export {
|
||||
PostgresCertificateAuthorityFileError,
|
||||
loadPostgresCertificateAuthorityFile,
|
||||
} from '../connection/certificateAuthority';
|
||||
@@ -0,0 +1,36 @@
|
||||
export * from '../automation/automationAdministrationRepository';
|
||||
export {
|
||||
PgPoolBinding,
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresTlsDnsServername,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
type PostgresTlsOptions,
|
||||
} from '../connection/pool';
|
||||
export {
|
||||
PostgresConnectionEnvironmentError,
|
||||
loadPostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironmentKeys,
|
||||
} from '../connection/connectionEnvironment';
|
||||
export {
|
||||
loadPostgresCertificateAuthorityFile,
|
||||
type PostgresCertificateAuthorityFileInspection,
|
||||
} from '../connection/certificateAuthority';
|
||||
export {
|
||||
PostgresTaskDefinitionSource,
|
||||
PostgresTaskExecutionRevisionSource,
|
||||
} from '../automation/taskDefinitionRepository';
|
||||
export { PostgresTriggerSource } from '../scheduling/triggerRepository';
|
||||
export { PostgresProjectPolicyRepository } from '../security/projectPolicyRepository';
|
||||
export {
|
||||
PostgresPluginPackageIdentityKeysetLedgerRepository as PostgresAutomationManagementIdentityKeysetLedgerRepository,
|
||||
type ClusterManagementIdentityAuthority,
|
||||
} from '../management/pluginPackageIdentityKeysetLedgerRepository';
|
||||
export {
|
||||
assertPostgresAutomationManagerSchemaReady,
|
||||
PostgresSchemaReadinessError,
|
||||
type PostgresSchemaReadinessReport,
|
||||
} from '../schema/schemaReadiness';
|
||||
@@ -0,0 +1,76 @@
|
||||
export {
|
||||
PgPoolBinding,
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresTlsDnsServername,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
type PostgresTlsOptions,
|
||||
type QingLongPostgresClient,
|
||||
type QingLongPostgresDatabaseResource,
|
||||
type QingLongPostgresPool,
|
||||
type QingLongPostgresQueryResult,
|
||||
} from '../connection/pool';
|
||||
|
||||
export {
|
||||
PostgresConnectionEnvironmentError,
|
||||
loadPostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironmentKeys,
|
||||
} from '../connection/connectionEnvironment';
|
||||
|
||||
export {
|
||||
POSTGRES_CA_FILE_ERROR_CODES,
|
||||
POSTGRES_CA_MAX_CERTIFICATES,
|
||||
POSTGRES_CA_MAX_FILE_BYTES,
|
||||
PostgresCertificateAuthorityFileError,
|
||||
inspectPostgresCertificateAuthorityFile,
|
||||
loadPostgresCertificateAuthorityFile,
|
||||
type PostgresCertificateAuthorityFileErrorCode,
|
||||
type PostgresCertificateAuthorityFileInspection,
|
||||
} from '../connection/certificateAuthority';
|
||||
|
||||
export {
|
||||
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
|
||||
PostgresSchemaReadinessError,
|
||||
assertPostgresPackageExecutorSchemaReady,
|
||||
type PostgresSchemaReadinessErrorCode,
|
||||
type PostgresSchemaReadinessReport,
|
||||
} from '../schema/schemaReadiness';
|
||||
|
||||
export { PostgresPluginPackageMaterializedRevisionRepository } from '../plugin-package/installation/pluginPackageMaterializedRevisionRepository';
|
||||
export { PostgresPluginPackageAutomationPublicationRepository } from '../plugin-package/publication/pluginPackageAutomationPublicationRepository';
|
||||
export {
|
||||
CLUSTER_PLUGIN_PACKAGE_QUARANTINE_TARGET_LIMIT,
|
||||
PostgresPluginPackageQuarantineRepository,
|
||||
} from '../plugin-package/lifecycle/pluginPackageQuarantineRepository';
|
||||
export { PostgresPluginPackageLifecycleRepository } from '../plugin-package/lifecycle/pluginPackageLifecycleRepository';
|
||||
export {
|
||||
PostgresPluginPackageLifecyclePlanReader,
|
||||
PostgresPluginPackageLifecyclePlanRepository,
|
||||
} from '../plugin-package/lifecycle/pluginPackageLifecyclePlanRepository';
|
||||
export {
|
||||
POSTGRES_PLUGIN_PACKAGE_PROVENANCE_RECOVERY_PAGE_LIMIT,
|
||||
PostgresPluginPackagePublisherProvenanceRepository,
|
||||
type PluginPackagePublisherProvenanceRecoveryCursor,
|
||||
type PluginPackagePublisherProvenanceRecoveryPage,
|
||||
} from '../plugin-package/publisher/pluginPackagePublisherProvenanceRepository';
|
||||
export { PostgresPluginPackagePublisherTrustAuthorityRepository } from '../plugin-package/publisher/pluginPackagePublisherTrustAuthorityRepository';
|
||||
export {
|
||||
PostgresPluginPackagePublisherRevocationProposalRepository,
|
||||
findPostgresPluginPackagePublisherRevocationProposal,
|
||||
} from '../plugin-package/publisher/pluginPackagePublisherRevocationProposalRepository';
|
||||
export {
|
||||
PostgresPluginPackagePublisherTrustTransitionProposalRepository,
|
||||
findPostgresPluginPackagePublisherTrustTransitionProposal,
|
||||
} from '../plugin-package/publisher/pluginPackagePublisherTrustTransitionProposalRepository';
|
||||
export {
|
||||
PostgresPluginPackagePublisherTrustTransitionRepository,
|
||||
type ApplyPostgresPluginPackagePublisherTrustTransitionInput,
|
||||
type ApplyPostgresPluginPackagePublisherTrustTransitionResult,
|
||||
} from '../plugin-package/publisher/pluginPackagePublisherTrustTransitionRepository';
|
||||
export { PostgresPluginPackageTaskReconciliationRepository } from '../plugin-package/publication/pluginPackageTaskReconciliationRepository';
|
||||
export { PostgresProjectToolDefinitionSnapshotRepository } from '../tool-execution/projectToolDefinitionSnapshotRepository';
|
||||
|
||||
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
|
||||
@@ -0,0 +1,61 @@
|
||||
export {
|
||||
PgPoolBinding,
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresTlsDnsServername,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
type PostgresTlsOptions,
|
||||
type QingLongPostgresClient,
|
||||
type QingLongPostgresDatabaseResource,
|
||||
type QingLongPostgresPool,
|
||||
type QingLongPostgresQueryResult,
|
||||
} from '../connection/pool';
|
||||
|
||||
export {
|
||||
PostgresConnectionEnvironmentError,
|
||||
loadPostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironmentKeys,
|
||||
} from '../connection/connectionEnvironment';
|
||||
|
||||
export {
|
||||
POSTGRES_CA_FILE_ERROR_CODES,
|
||||
POSTGRES_CA_MAX_CERTIFICATES,
|
||||
POSTGRES_CA_MAX_FILE_BYTES,
|
||||
PostgresCertificateAuthorityFileError,
|
||||
inspectPostgresCertificateAuthorityFile,
|
||||
loadPostgresCertificateAuthorityFile,
|
||||
type PostgresCertificateAuthorityFileErrorCode,
|
||||
type PostgresCertificateAuthorityFileInspection,
|
||||
} from '../connection/certificateAuthority';
|
||||
|
||||
export {
|
||||
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
|
||||
PostgresSchemaReadinessError,
|
||||
assertPostgresPackageManagerSchemaReady,
|
||||
type PostgresSchemaReadinessErrorCode,
|
||||
type PostgresSchemaReadinessReport,
|
||||
} from '../schema/schemaReadiness';
|
||||
|
||||
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
|
||||
|
||||
export {
|
||||
PostgresPluginPackageManagementQuotaRepository,
|
||||
type PostgresPluginPackageManagementQuotaOptions,
|
||||
} from '../management/pluginPackageManagementQuotaRepository';
|
||||
|
||||
export {
|
||||
PostgresPluginPackageIdentityKeysetLedgerConflictError,
|
||||
PostgresPluginPackageIdentityKeysetLedgerRepository,
|
||||
PostgresPluginPackageIdentityKeysetLedgerUnavailableError,
|
||||
type PluginPackageIdentityKeysetLedgerPort,
|
||||
type PluginPackageIdentityKeysetLedgerSnapshot,
|
||||
} from '../management/pluginPackageIdentityKeysetLedgerRepository';
|
||||
|
||||
export { PostgresPluginPackagePublisherTrustAuthorityRepository } from '../plugin-package/publisher/pluginPackagePublisherTrustAuthorityRepository';
|
||||
export { PostgresPluginPackageLifecyclePlanReader } from '../plugin-package/lifecycle/pluginPackageLifecyclePlanRepository';
|
||||
export { PostgresPluginPackageInstallInventoryReader } from '../plugin-package/installation/pluginPackageInstallRepository';
|
||||
export { PostgresPluginPackagePublisherRevocationProposalRepository } from '../plugin-package/publisher/pluginPackagePublisherRevocationProposalRepository';
|
||||
export { PostgresPluginPackagePublisherTrustTransitionProposalRepository } from '../plugin-package/publisher/pluginPackagePublisherTrustTransitionProposalRepository';
|
||||
@@ -0,0 +1,82 @@
|
||||
export {
|
||||
POSTGRES_CA_FILE_ERROR_CODES,
|
||||
POSTGRES_CA_MAX_CERTIFICATES,
|
||||
POSTGRES_CA_MAX_FILE_BYTES,
|
||||
PostgresCertificateAuthorityFileError,
|
||||
inspectPostgresCertificateAuthorityFile,
|
||||
loadPostgresCertificateAuthorityFile,
|
||||
type PostgresCertificateAuthorityFileErrorCode,
|
||||
type PostgresCertificateAuthorityFileInspection,
|
||||
} from '../connection/certificateAuthority';
|
||||
|
||||
export {
|
||||
PgPoolBinding,
|
||||
POSTGRES_AVAILABILITY_SQLSTATE_CLASSES,
|
||||
POSTGRES_AVAILABILITY_SQLSTATES,
|
||||
POSTGRES_AVAILABILITY_SYSTEM_ERROR_CODES,
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresTlsDnsServername,
|
||||
isPostgresAvailabilityError,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
type PostgresTlsOptions,
|
||||
type QingLongPostgresClient,
|
||||
type QingLongPostgresDatabaseResource,
|
||||
type QingLongPostgresPool,
|
||||
type QingLongPostgresQueryResult,
|
||||
} from '../connection/pool';
|
||||
|
||||
export {
|
||||
PostgresConnectionEnvironmentError,
|
||||
loadPostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironmentKeys,
|
||||
} from '../connection/connectionEnvironment';
|
||||
|
||||
export {
|
||||
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
|
||||
PostgresSchemaReadinessError,
|
||||
assertPostgresSchemaReady,
|
||||
type PostgresSchemaReadinessErrorCode,
|
||||
type PostgresSchemaReadinessReport,
|
||||
} from '../schema/schemaReadiness';
|
||||
|
||||
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
|
||||
export { PostgresToolInvocationArtifactRepository } from '../tool-execution/toolInvocationArtifactRepository';
|
||||
export { PostgresProjectToolDefinitionSnapshotRepository } from '../tool-execution/projectToolDefinitionSnapshotRepository';
|
||||
export { PostgresStepRunRepository } from '../run/stepRunRepository';
|
||||
export { PostgresToolExecutionStartBarrierRepository } from '../tool-execution/toolExecutionStartBarrierRepository';
|
||||
export { PostgresToolExecutionCompletionRepository } from '../tool-execution/toolExecutionCompletionRepository';
|
||||
export { PostgresToolExecutionFailureCompletionRepository } from '../tool-execution/toolExecutionFailureCompletionRepository';
|
||||
export { PostgresToolResultKeyCatalogReader } from '../tool-execution/toolResultKeyCatalogRepository';
|
||||
export { PostgresToolResultRekeyReader } from '../tool-execution/toolResultRekeyRepository';
|
||||
|
||||
export * from '../run/runRepository';
|
||||
export * from '../security/projectPolicyRepository';
|
||||
export * from '../security/apiCredentialRepository';
|
||||
export * from '../security/securityAuditRepository';
|
||||
export * from '../run-recovery/clusterRecoverySource';
|
||||
export * from '../run-recovery/clusterRecoveryClaimRepository';
|
||||
export * from '../run-recovery/clusterRecoveryResolutionRepository';
|
||||
export * from '../run-recovery/clusterRuntimeRecoverySource';
|
||||
export * from '../run-recovery/clusterRunLostRetryRepository';
|
||||
export * from '../remote-execution/workerSessionRepository';
|
||||
export * from '../remote-execution/runDispatchLeaseRepository';
|
||||
export * from '../remote-execution/remoteRunActivationRepository';
|
||||
export * from '../worker-credential/remoteWorkerSecretDeliveryRepository';
|
||||
export * from '../remote-execution/remoteWorkerCompletionRepository';
|
||||
export * from '../remote-execution/remoteWorkerLeaseControlRepository';
|
||||
export * from '../run-recovery/clusterRunCancellationRepository';
|
||||
export * from '../run-recovery/clusterRunCancellationConvergenceRepository';
|
||||
export * from '../remote-execution/clusterDispatchRepository';
|
||||
export * from '../worker-credential/workerCredentialRepository';
|
||||
export * from '../remote-execution/workerExecutionAttestationRepository';
|
||||
export * from '../remote-execution/remoteWorkerAttestationEvidenceProvider';
|
||||
export * from '../scheduling/clusterScheduleRepository';
|
||||
export {
|
||||
PostgresTaskDefinitionSource,
|
||||
PostgresTaskExecutionRevisionSource,
|
||||
} from '../automation/taskDefinitionRepository';
|
||||
export { PostgresTriggerSource } from '../scheduling/triggerRepository';
|
||||
@@ -0,0 +1,27 @@
|
||||
export {
|
||||
PgPoolBinding,
|
||||
createPostgresDatabaseOpener,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
type PostgresTlsOptions,
|
||||
type QingLongPostgresClient,
|
||||
type QingLongPostgresDatabaseResource,
|
||||
type QingLongPostgresPool,
|
||||
type QingLongPostgresQueryResult,
|
||||
} from '../connection/pool';
|
||||
|
||||
export {
|
||||
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
|
||||
PostgresSchemaReadinessError,
|
||||
assertPostgresWorkerIngressSchemaReady,
|
||||
type PostgresSchemaReadinessErrorCode,
|
||||
type PostgresSchemaReadinessReport,
|
||||
} from '../schema/schemaReadiness';
|
||||
|
||||
export { postgresqlMainMigrationManifest } from '../migration/migrationManifest';
|
||||
export * from '../worker-credential/workerCredentialRepository';
|
||||
export * from '../remote-execution/workerSessionRepository';
|
||||
export * from '../remote-execution/workerExecutionAttestationRepository';
|
||||
export * from '../security/securityAuditRepository';
|
||||
@@ -0,0 +1,124 @@
|
||||
export {
|
||||
PgPoolBinding,
|
||||
POSTGRES_AVAILABILITY_SQLSTATE_CLASSES,
|
||||
POSTGRES_AVAILABILITY_SQLSTATES,
|
||||
POSTGRES_AVAILABILITY_SYSTEM_ERROR_CODES,
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresAvailabilityError,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
type PostgresTlsOptions,
|
||||
type QingLongPostgresClient,
|
||||
type QingLongPostgresDatabaseResource,
|
||||
type QingLongPostgresPool,
|
||||
type QingLongPostgresQueryResult,
|
||||
} from './connection/pool';
|
||||
|
||||
export {
|
||||
ql3PostgresTables,
|
||||
ql3Schema,
|
||||
apiCredentialMutations,
|
||||
approvedActionExecutions,
|
||||
pluginPackageInstallProposals,
|
||||
pluginPackageIdentityKeysetLedger,
|
||||
pluginPackageManagementQuotaBuckets,
|
||||
pluginPackageMaterializedRevisions,
|
||||
pluginPackageQuarantineEvents,
|
||||
pluginPackageWorkflowAdmissions,
|
||||
pluginPackageWorkflowAdmissionSteps,
|
||||
pluginPackageWithdrawalReceipts,
|
||||
pluginPackageWithdrawalTasks,
|
||||
identitySubjectMutations,
|
||||
apiCredentials,
|
||||
identitySubjects,
|
||||
projectRoleBindings,
|
||||
projects,
|
||||
taskDefinitions,
|
||||
taskDefinitionRevisions,
|
||||
taskExecutionRevisions,
|
||||
triggers,
|
||||
triggerRevisions,
|
||||
triggerSchedules,
|
||||
runAttempts,
|
||||
runDispatchLeases,
|
||||
runRecoveryControls,
|
||||
runEvents,
|
||||
runRetryPolicies,
|
||||
runs,
|
||||
schemaCapabilities,
|
||||
schemaMigrations,
|
||||
securityAuditEvents,
|
||||
toolExecutionAuditReceipts,
|
||||
toolExecutionTraceAnchors,
|
||||
workerSessions,
|
||||
workerCredentials,
|
||||
workerCredentialMutations,
|
||||
workerExecutionAttestations,
|
||||
} from './schema/schema';
|
||||
|
||||
export {
|
||||
runPostgresMigrations,
|
||||
type RunPostgresMigrationsOptions,
|
||||
} from './migration/migrate';
|
||||
|
||||
export {
|
||||
POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
|
||||
POSTGRESQL_MIGRATION_HISTORY_TABLE,
|
||||
POSTGRESQL_MIGRATION_SCHEMA,
|
||||
PostgresMigrationLeaderUnavailableError,
|
||||
PostgresMigrationStreamStore,
|
||||
readPostgresMigrationHistory,
|
||||
type PostgresMigrationClient,
|
||||
type PostgresMigrationContext,
|
||||
type PostgresMigrationPool,
|
||||
type PostgresMigrationQueryable,
|
||||
type PostgresMigrationQueryResult,
|
||||
} from './migrations/postgresMigrationStreamStore';
|
||||
|
||||
export { postgresqlMainMigrationStream } from './migrations';
|
||||
export { postgresqlMainMigrationManifest } from './migration/migrationManifest';
|
||||
export { PostgresToolInvocationArtifactRepository } from './tool-execution/toolInvocationArtifactRepository';
|
||||
|
||||
export {
|
||||
postgresqlControlSchemaContract,
|
||||
type PostgresSchemaContract,
|
||||
type PostgresSchemaContractTable,
|
||||
} from './schema/schemaContract';
|
||||
|
||||
export {
|
||||
POSTGRES_SCHEMA_READINESS_ERROR_CODES,
|
||||
PostgresSchemaReadinessError,
|
||||
assertPostgresSchemaReady,
|
||||
assertPostgresWorkerIngressSchemaReady,
|
||||
type PostgresSchemaReadinessErrorCode,
|
||||
type PostgresSchemaReadinessReport,
|
||||
} from './schema/schemaReadiness';
|
||||
|
||||
export * from './run/runRepository';
|
||||
export * from './security/projectPolicyRepository';
|
||||
export * from './security/apiCredentialRepository';
|
||||
export * from './security/securityAuditRepository';
|
||||
export * from './run-recovery/clusterRecoverySource';
|
||||
export * from './run-recovery/clusterRecoveryClaimRepository';
|
||||
export * from './run-recovery/clusterRecoveryResolutionRepository';
|
||||
export * from './run-recovery/clusterRuntimeRecoverySource';
|
||||
export * from './run-recovery/clusterRunLostRetryRepository';
|
||||
export * from './remote-execution/workerSessionRepository';
|
||||
export * from './remote-execution/runDispatchLeaseRepository';
|
||||
export * from './remote-execution/remoteRunActivationRepository';
|
||||
export * from './worker-credential/remoteWorkerSecretDeliveryRepository';
|
||||
export * from './remote-execution/remoteWorkerCompletionRepository';
|
||||
export * from './remote-execution/remoteWorkerLeaseControlRepository';
|
||||
export * from './run-recovery/clusterRunCancellationRepository';
|
||||
export * from './run-recovery/clusterRunCancellationConvergenceRepository';
|
||||
export * from './remote-execution/clusterDispatchRepository';
|
||||
export * from './worker-credential/workerCredentialRepository';
|
||||
export * from './remote-execution/workerExecutionAttestationRepository';
|
||||
export * from './remote-execution/remoteWorkerAttestationEvidenceProvider';
|
||||
export * from './scheduling/clusterScheduleRepository';
|
||||
export * from './worker-credential/workerCredentialAdministrationRepository';
|
||||
export * from './automation/taskDefinitionRepository';
|
||||
export * from './scheduling/triggerRepository';
|
||||
export * from './automation/automationAdministrationRepository';
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
// PostgreSQL anti-rollback ledger shared by bounded Cluster management authorities.
|
||||
import type { PostgresClient, PostgresPool } from '@qinglong/runtime-core';
|
||||
|
||||
export type ClusterManagementIdentityAuthority =
|
||||
| 'plugin-package-management'
|
||||
| 'worker-credential-management'
|
||||
| 'automation-management'
|
||||
| 'approval-management';
|
||||
const DIGEST_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
||||
const KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
export interface PluginPackageIdentityKeysetLedgerSnapshot {
|
||||
readonly schemaVersion: 1;
|
||||
readonly generation: number;
|
||||
readonly digest: string;
|
||||
readonly issuer: string;
|
||||
readonly audience: string;
|
||||
readonly activeKeyIds: readonly string[];
|
||||
readonly revokedKeyIds: readonly string[];
|
||||
}
|
||||
|
||||
export interface PluginPackageIdentityKeysetLedgerPort {
|
||||
observe(
|
||||
snapshot: Readonly<PluginPackageIdentityKeysetLedgerSnapshot>,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export class PostgresPluginPackageIdentityKeysetLedgerConflictError extends Error {
|
||||
readonly code = 'POSTGRES_PLUGIN_PACKAGE_IDENTITY_KEYSET_LEDGER_CONFLICT';
|
||||
|
||||
constructor() {
|
||||
super('PostgreSQL Plugin Package identity keyset ledger conflicts');
|
||||
this.name = 'PostgresPluginPackageIdentityKeysetLedgerConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class PostgresPluginPackageIdentityKeysetLedgerUnavailableError extends Error {
|
||||
readonly code = 'POSTGRES_PLUGIN_PACKAGE_IDENTITY_KEYSET_LEDGER_UNAVAILABLE';
|
||||
|
||||
constructor(options?: ErrorOptions) {
|
||||
super(
|
||||
'PostgreSQL Plugin Package identity keyset ledger is unavailable',
|
||||
options,
|
||||
);
|
||||
this.name = 'PostgresPluginPackageIdentityKeysetLedgerUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function reviewedKeyIds(
|
||||
value: readonly string[],
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): readonly string[] {
|
||||
if (
|
||||
!Array.isArray(value) ||
|
||||
value.length < minimum ||
|
||||
value.length > maximum
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Plugin Package identity key ids are invalid',
|
||||
);
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
for (const keyId of value) {
|
||||
if (
|
||||
typeof keyId !== 'string' ||
|
||||
!KEY_ID_PATTERN.test(keyId) ||
|
||||
seen.has(keyId)
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Plugin Package identity key ids are invalid',
|
||||
);
|
||||
}
|
||||
seen.add(keyId);
|
||||
}
|
||||
return Object.freeze([...value].sort());
|
||||
}
|
||||
|
||||
function reviewedSnapshot(
|
||||
value: Readonly<PluginPackageIdentityKeysetLedgerSnapshot>,
|
||||
): Readonly<PluginPackageIdentityKeysetLedgerSnapshot> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).length !== 7 ||
|
||||
Object.keys(value).some(
|
||||
(key) =>
|
||||
![
|
||||
'schemaVersion',
|
||||
'generation',
|
||||
'digest',
|
||||
'issuer',
|
||||
'audience',
|
||||
'activeKeyIds',
|
||||
'revokedKeyIds',
|
||||
].includes(key),
|
||||
) ||
|
||||
value.schemaVersion !== 1 ||
|
||||
!Number.isSafeInteger(value.generation) ||
|
||||
value.generation < 1 ||
|
||||
typeof value.digest !== 'string' ||
|
||||
!DIGEST_PATTERN.test(value.digest) ||
|
||||
typeof value.issuer !== 'string' ||
|
||||
value.issuer.length < 1 ||
|
||||
value.issuer.length > 512 ||
|
||||
CONTROL_PATTERN.test(value.issuer) ||
|
||||
typeof value.audience !== 'string' ||
|
||||
value.audience.length < 1 ||
|
||||
value.audience.length > 256 ||
|
||||
CONTROL_PATTERN.test(value.audience)
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Plugin Package identity keyset snapshot is invalid',
|
||||
);
|
||||
}
|
||||
const activeKeyIds = reviewedKeyIds(value.activeKeyIds, 1, 8);
|
||||
const revokedKeyIds = reviewedKeyIds(value.revokedKeyIds, 0, 64);
|
||||
if (activeKeyIds.some((keyId) => revokedKeyIds.includes(keyId))) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Plugin Package identity keyset snapshot is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({ ...value, activeKeyIds, revokedKeyIds });
|
||||
}
|
||||
|
||||
function integer(row: Row, name: string): number {
|
||||
const value = Number(row[name]);
|
||||
if (!Number.isSafeInteger(value) || value < 1) {
|
||||
throw new Error(`invalid ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function text(row: Row, name: string): string {
|
||||
const value = row[name];
|
||||
if (typeof value !== 'string') throw new Error(`invalid ${name}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function textArray(row: Row, name: string): readonly string[] {
|
||||
const value = row[name];
|
||||
if (
|
||||
!Array.isArray(value) ||
|
||||
value.some((candidate) => typeof candidate !== 'string')
|
||||
) {
|
||||
throw new Error(`invalid ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sameArray(left: readonly string[], right: readonly string[]): boolean {
|
||||
return (
|
||||
left.length === right.length &&
|
||||
left.every((value, index) => value === right[index])
|
||||
);
|
||||
}
|
||||
|
||||
function includesAll(
|
||||
candidate: readonly string[],
|
||||
required: readonly string[],
|
||||
): boolean {
|
||||
const values = new Set(candidate);
|
||||
return required.every((value) => values.has(value));
|
||||
}
|
||||
|
||||
async function rollback(client: PostgresClient): Promise<void> {
|
||||
await client.query('ROLLBACK').catch(() => undefined);
|
||||
}
|
||||
|
||||
export class PostgresPluginPackageIdentityKeysetLedgerRepository
|
||||
implements PluginPackageIdentityKeysetLedgerPort
|
||||
{
|
||||
readonly #authority: ClusterManagementIdentityAuthority;
|
||||
|
||||
constructor(
|
||||
private readonly pool: PostgresPool,
|
||||
authority: ClusterManagementIdentityAuthority = 'plugin-package-management',
|
||||
) {
|
||||
if (!pool || typeof pool.connect !== 'function') {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Plugin Package identity keyset ledger pool is invalid',
|
||||
);
|
||||
}
|
||||
if (
|
||||
authority !== 'plugin-package-management' &&
|
||||
authority !== 'worker-credential-management' &&
|
||||
authority !== 'automation-management' &&
|
||||
authority !== 'approval-management'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL management identity keyset authority is invalid',
|
||||
);
|
||||
}
|
||||
this.#authority = authority;
|
||||
}
|
||||
|
||||
async observe(
|
||||
value: Readonly<PluginPackageIdentityKeysetLedgerSnapshot>,
|
||||
): Promise<void> {
|
||||
const candidate = reviewedSnapshot(value);
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch (error) {
|
||||
throw new PostgresPluginPackageIdentityKeysetLedgerUnavailableError({
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."plugin_package_identity_keyset_ledger" (
|
||||
authority, generation, digest, issuer, audience,
|
||||
active_key_ids, revoked_key_ids, updated_at_ms
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6::jsonb, $7::jsonb,
|
||||
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
|
||||
)
|
||||
ON CONFLICT (authority) DO NOTHING`,
|
||||
[
|
||||
this.#authority,
|
||||
candidate.generation,
|
||||
candidate.digest,
|
||||
candidate.issuer,
|
||||
candidate.audience,
|
||||
JSON.stringify(candidate.activeKeyIds),
|
||||
JSON.stringify(candidate.revokedKeyIds),
|
||||
],
|
||||
);
|
||||
const selected = await client.query<Row>(
|
||||
`SELECT generation, digest, issuer, audience,
|
||||
active_key_ids AS "activeKeyIds",
|
||||
revoked_key_ids AS "revokedKeyIds"
|
||||
FROM "ql3"."plugin_package_identity_keyset_ledger"
|
||||
WHERE authority = $1
|
||||
FOR UPDATE`,
|
||||
[this.#authority],
|
||||
);
|
||||
if (selected.rows.length !== 1) throw new Error('ledger row is missing');
|
||||
const current = selected.rows[0]!;
|
||||
const generation = integer(current, 'generation');
|
||||
const digest = text(current, 'digest');
|
||||
const issuer = text(current, 'issuer');
|
||||
const audience = text(current, 'audience');
|
||||
const activeKeyIds = textArray(current, 'activeKeyIds');
|
||||
const revokedKeyIds = textArray(current, 'revokedKeyIds');
|
||||
const exact =
|
||||
generation === candidate.generation &&
|
||||
digest === candidate.digest &&
|
||||
issuer === candidate.issuer &&
|
||||
audience === candidate.audience &&
|
||||
sameArray(activeKeyIds, candidate.activeKeyIds) &&
|
||||
sameArray(revokedKeyIds, candidate.revokedKeyIds);
|
||||
if (!exact) {
|
||||
const retained = [
|
||||
...candidate.activeKeyIds,
|
||||
...candidate.revokedKeyIds,
|
||||
];
|
||||
if (
|
||||
candidate.generation <= generation ||
|
||||
candidate.issuer !== issuer ||
|
||||
candidate.audience !== audience ||
|
||||
!includesAll(candidate.revokedKeyIds, revokedKeyIds) ||
|
||||
!includesAll(retained, activeKeyIds)
|
||||
) {
|
||||
throw new PostgresPluginPackageIdentityKeysetLedgerConflictError();
|
||||
}
|
||||
await client.query(
|
||||
`UPDATE "ql3"."plugin_package_identity_keyset_ledger"
|
||||
SET generation = $2,
|
||||
digest = $3,
|
||||
active_key_ids = $4::jsonb,
|
||||
revoked_key_ids = $5::jsonb,
|
||||
updated_at_ms =
|
||||
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
|
||||
WHERE authority = $1`,
|
||||
[
|
||||
this.#authority,
|
||||
candidate.generation,
|
||||
candidate.digest,
|
||||
JSON.stringify(candidate.activeKeyIds),
|
||||
JSON.stringify(candidate.revokedKeyIds),
|
||||
],
|
||||
);
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
} catch (error) {
|
||||
await rollback(client);
|
||||
if (
|
||||
error instanceof PostgresPluginPackageIdentityKeysetLedgerConflictError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new PostgresPluginPackageIdentityKeysetLedgerUnavailableError({
|
||||
cause: error,
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
// PostgreSQL distributed quota authority for authenticated management operations.
|
||||
import type { PostgresPool } from '@qinglong/runtime-core';
|
||||
import {
|
||||
PLUGIN_PACKAGE_MANAGEMENT_QUOTA_OPERATIONS,
|
||||
PluginPackageManagementQuotaExceededError,
|
||||
PluginPackageManagementUnavailableError,
|
||||
type ConsumePluginPackageManagementQuotaCommand,
|
||||
type PluginPackageManagementQuotaOperation,
|
||||
type PluginPackageManagementQuotaPort,
|
||||
type PluginPackageManagementQuotaResult,
|
||||
} from '@qinglong/runtime-core/plugin-package-management';
|
||||
|
||||
import {
|
||||
postgresRequiredBoolean,
|
||||
postgresRequiredInteger,
|
||||
} from '../repository/definitionRepositorySupport';
|
||||
|
||||
const DEFAULT_WINDOW_MS = 60_000;
|
||||
const DEFAULT_LIMITS = Object.freeze({
|
||||
'plugin-package.propose': 30,
|
||||
'plugin-package.decide': 60,
|
||||
'plugin-package.inspect': 600,
|
||||
});
|
||||
const PROJECT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
export interface PostgresPluginPackageManagementQuotaOptions {
|
||||
readonly windowMs?: number;
|
||||
readonly limits?: Partial<
|
||||
Readonly<Record<PluginPackageManagementQuotaOperation, number>>
|
||||
>;
|
||||
}
|
||||
|
||||
function unavailable(cause?: unknown): PluginPackageManagementUnavailableError {
|
||||
return new PluginPackageManagementUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function integer(
|
||||
value: number | undefined,
|
||||
fallback: number,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
label: string,
|
||||
): number {
|
||||
const candidate = value ?? fallback;
|
||||
if (
|
||||
!Number.isSafeInteger(candidate) ||
|
||||
candidate < minimum ||
|
||||
candidate > maximum
|
||||
) {
|
||||
throw new TypeError(`PostgreSQL Package management ${label} is invalid`);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function reviewedOptions(
|
||||
value: PostgresPluginPackageManagementQuotaOptions | undefined,
|
||||
): Readonly<{
|
||||
windowMs: number;
|
||||
limits: Readonly<Record<PluginPackageManagementQuotaOperation, number>>;
|
||||
}> {
|
||||
if (
|
||||
value !== undefined &&
|
||||
(!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).some((key) => key !== 'windowMs' && key !== 'limits'))
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Package management quota options are invalid',
|
||||
);
|
||||
}
|
||||
const limits = value?.limits;
|
||||
if (
|
||||
limits !== undefined &&
|
||||
(!limits ||
|
||||
typeof limits !== 'object' ||
|
||||
Array.isArray(limits) ||
|
||||
Object.keys(limits).some(
|
||||
(key) =>
|
||||
!PLUGIN_PACKAGE_MANAGEMENT_QUOTA_OPERATIONS.includes(
|
||||
key as PluginPackageManagementQuotaOperation,
|
||||
),
|
||||
))
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Package management quota limits are invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
windowMs: integer(
|
||||
value?.windowMs,
|
||||
DEFAULT_WINDOW_MS,
|
||||
1_000,
|
||||
5 * 60_000,
|
||||
'quota window',
|
||||
),
|
||||
limits: Object.freeze({
|
||||
'plugin-package.propose': integer(
|
||||
limits?.['plugin-package.propose'],
|
||||
DEFAULT_LIMITS['plugin-package.propose'],
|
||||
1,
|
||||
1_000,
|
||||
'proposal quota',
|
||||
),
|
||||
'plugin-package.decide': integer(
|
||||
limits?.['plugin-package.decide'],
|
||||
DEFAULT_LIMITS['plugin-package.decide'],
|
||||
1,
|
||||
1_000,
|
||||
'decision quota',
|
||||
),
|
||||
'plugin-package.inspect': integer(
|
||||
limits?.['plugin-package.inspect'],
|
||||
DEFAULT_LIMITS['plugin-package.inspect'],
|
||||
1,
|
||||
1_000,
|
||||
'inspection quota',
|
||||
),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function validateCommand(
|
||||
command: ConsumePluginPackageManagementQuotaCommand,
|
||||
): Readonly<ConsumePluginPackageManagementQuotaCommand> {
|
||||
if (
|
||||
!command ||
|
||||
typeof command !== 'object' ||
|
||||
Array.isArray(command) ||
|
||||
Object.keys(command).some(
|
||||
(key) =>
|
||||
!['projectId', 'subject', 'operation', 'idempotencyKey'].includes(key),
|
||||
) ||
|
||||
Object.keys(command).length !== 4 ||
|
||||
typeof command.projectId !== 'string' ||
|
||||
!PROJECT_PATTERN.test(command.projectId) ||
|
||||
!command.subject ||
|
||||
typeof command.subject !== 'object' ||
|
||||
Array.isArray(command.subject) ||
|
||||
Object.keys(command.subject).length !== 2 ||
|
||||
command.subject.type !== 'user' ||
|
||||
typeof command.subject.id !== 'string' ||
|
||||
command.subject.id.length < 1 ||
|
||||
command.subject.id.length > 255 ||
|
||||
/[\u0000-\u001f\u007f]/.test(command.subject.id) ||
|
||||
!PLUGIN_PACKAGE_MANAGEMENT_QUOTA_OPERATIONS.includes(command.operation) ||
|
||||
typeof command.idempotencyKey !== 'string' ||
|
||||
!IDENTIFIER_PATTERN.test(command.idempotencyKey)
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Package management quota command is invalid',
|
||||
);
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
export class PostgresPluginPackageManagementQuotaRepository
|
||||
implements PluginPackageManagementQuotaPort
|
||||
{
|
||||
readonly #windowMs: number;
|
||||
readonly #limits: Readonly<
|
||||
Record<PluginPackageManagementQuotaOperation, number>
|
||||
>;
|
||||
|
||||
constructor(
|
||||
private readonly pool: PostgresPool,
|
||||
options?: PostgresPluginPackageManagementQuotaOptions,
|
||||
) {
|
||||
if (!pool || typeof pool.query !== 'function') {
|
||||
throw new TypeError(
|
||||
'PostgreSQL Package management quota pool is invalid',
|
||||
);
|
||||
}
|
||||
const reviewed = reviewedOptions(options);
|
||||
this.#windowMs = reviewed.windowMs;
|
||||
this.#limits = reviewed.limits;
|
||||
}
|
||||
|
||||
async consume(
|
||||
commandValue: ConsumePluginPackageManagementQuotaCommand,
|
||||
): Promise<Readonly<PluginPackageManagementQuotaResult>> {
|
||||
const command = validateCommand(commandValue);
|
||||
const limit = this.#limits[command.operation];
|
||||
let result;
|
||||
try {
|
||||
result = await this.pool.query<Row>(
|
||||
`
|
||||
WITH database_clock AS (
|
||||
SELECT floor(
|
||||
extract(epoch FROM clock_timestamp()) * 1000
|
||||
)::bigint AS now_ms
|
||||
)
|
||||
INSERT INTO "ql3"."plugin_package_management_quota_buckets" (
|
||||
project_id, subject_type, subject_id, operation,
|
||||
window_started_at_ms, consumed_count, receipt_ids, updated_at_ms
|
||||
)
|
||||
SELECT
|
||||
$1, $2, $3, $4,
|
||||
(now_ms / $6::bigint) * $6::bigint,
|
||||
1,
|
||||
jsonb_build_array($5::text),
|
||||
now_ms
|
||||
FROM database_clock
|
||||
ON CONFLICT (project_id, subject_type, subject_id, operation)
|
||||
DO UPDATE SET
|
||||
window_started_at_ms = CASE
|
||||
WHEN "plugin_package_management_quota_buckets".window_started_at_ms
|
||||
+ $6::bigint <= EXCLUDED.updated_at_ms
|
||||
THEN (EXCLUDED.updated_at_ms / $6::bigint) * $6::bigint
|
||||
ELSE "plugin_package_management_quota_buckets".window_started_at_ms
|
||||
END,
|
||||
consumed_count = CASE
|
||||
WHEN "plugin_package_management_quota_buckets".window_started_at_ms
|
||||
+ $6::bigint <= EXCLUDED.updated_at_ms
|
||||
THEN 1
|
||||
WHEN "plugin_package_management_quota_buckets".receipt_ids ? $5::text
|
||||
THEN "plugin_package_management_quota_buckets".consumed_count
|
||||
ELSE "plugin_package_management_quota_buckets".consumed_count + 1
|
||||
END,
|
||||
receipt_ids = CASE
|
||||
WHEN "plugin_package_management_quota_buckets".window_started_at_ms
|
||||
+ $6::bigint <= EXCLUDED.updated_at_ms
|
||||
THEN jsonb_build_array($5::text)
|
||||
WHEN "plugin_package_management_quota_buckets".receipt_ids ? $5::text
|
||||
THEN "plugin_package_management_quota_buckets".receipt_ids
|
||||
ELSE "plugin_package_management_quota_buckets".receipt_ids
|
||||
|| jsonb_build_array($5::text)
|
||||
END,
|
||||
updated_at_ms = EXCLUDED.updated_at_ms
|
||||
WHERE
|
||||
"plugin_package_management_quota_buckets".window_started_at_ms
|
||||
+ $6::bigint <= EXCLUDED.updated_at_ms
|
||||
OR "plugin_package_management_quota_buckets".receipt_ids ? $5::text
|
||||
OR "plugin_package_management_quota_buckets".consumed_count < $7::integer
|
||||
RETURNING
|
||||
true AS admitted,
|
||||
consumed_count AS "consumedCount",
|
||||
window_started_at_ms + $6::bigint AS "resetAtMs",
|
||||
updated_at_ms AS "observedAtMs"
|
||||
`.trim(),
|
||||
[
|
||||
command.projectId,
|
||||
command.subject.type,
|
||||
command.subject.id,
|
||||
command.operation,
|
||||
command.idempotencyKey,
|
||||
this.#windowMs,
|
||||
limit,
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
throw unavailable(error);
|
||||
}
|
||||
if (result.rows.length === 0) {
|
||||
try {
|
||||
result = await this.pool.query<Row>(
|
||||
`
|
||||
WITH database_clock AS (
|
||||
SELECT floor(
|
||||
extract(epoch FROM clock_timestamp()) * 1000
|
||||
)::bigint AS now_ms
|
||||
)
|
||||
SELECT
|
||||
false AS admitted,
|
||||
buckets.consumed_count AS "consumedCount",
|
||||
buckets.window_started_at_ms + $5::bigint AS "resetAtMs",
|
||||
database_clock.now_ms AS "observedAtMs"
|
||||
FROM "ql3"."plugin_package_management_quota_buckets" AS buckets
|
||||
CROSS JOIN database_clock
|
||||
WHERE buckets.project_id = $1
|
||||
AND buckets.subject_type = $2
|
||||
AND buckets.subject_id = $3
|
||||
AND buckets.operation = $4
|
||||
LIMIT 2
|
||||
`.trim(),
|
||||
[
|
||||
command.projectId,
|
||||
command.subject.type,
|
||||
command.subject.id,
|
||||
command.operation,
|
||||
this.#windowMs,
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
throw unavailable(error);
|
||||
}
|
||||
}
|
||||
if (result.rows.length !== 1) throw unavailable();
|
||||
const row = result.rows[0]!;
|
||||
const admitted = postgresRequiredBoolean(row.admitted, unavailable);
|
||||
const consumedCount = postgresRequiredInteger(
|
||||
row.consumedCount,
|
||||
unavailable,
|
||||
);
|
||||
const resetAtMs = postgresRequiredInteger(row.resetAtMs, unavailable);
|
||||
const observedAtMs = postgresRequiredInteger(row.observedAtMs, unavailable);
|
||||
if (
|
||||
consumedCount < 1 ||
|
||||
consumedCount > limit ||
|
||||
resetAtMs <= observedAtMs ||
|
||||
resetAtMs > observedAtMs + this.#windowMs
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
if (!admitted) {
|
||||
throw new PluginPackageManagementQuotaExceededError(
|
||||
Math.max(1, resetAtMs - observedAtMs),
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
remaining: limit - consumedCount,
|
||||
resetAtMs,
|
||||
observedAtMs,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
runMigrationStream,
|
||||
type PostgresPool,
|
||||
type RunMigrationStreamOptions,
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
PostgresMigrationStreamStore,
|
||||
type PostgresMigrationContext,
|
||||
} from '../migrations/postgresMigrationStreamStore';
|
||||
import { postgresqlMainMigrationStream } from '../migrations';
|
||||
|
||||
export interface RunPostgresMigrationsOptions {
|
||||
readonly pool: PostgresPool;
|
||||
readonly clock?: () => number;
|
||||
readonly logger?: RunMigrationStreamOptions<PostgresMigrationContext>['logger'];
|
||||
}
|
||||
|
||||
/** Executes the reviewed pg-* stream through a migration-role pool. */
|
||||
export function runPostgresMigrations(
|
||||
options: RunPostgresMigrationsOptions,
|
||||
): Promise<void> {
|
||||
return runMigrationStream({
|
||||
stream: postgresqlMainMigrationStream,
|
||||
store: new PostgresMigrationStreamStore(options.pool),
|
||||
...(options.clock === undefined ? {} : { clock: options.clock }),
|
||||
...(options.logger === undefined ? {} : { logger: options.logger }),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
export {
|
||||
PgPoolBinding,
|
||||
createPostgresDatabaseOpener,
|
||||
type OpenPostgresDatabaseOptions,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresDatabaseRole,
|
||||
type PostgresPoolOptions,
|
||||
type PostgresTlsOptions,
|
||||
type QingLongPostgresClient,
|
||||
type QingLongPostgresDatabaseResource,
|
||||
type QingLongPostgresPool,
|
||||
type QingLongPostgresQueryResult,
|
||||
} from '../connection/pool';
|
||||
|
||||
export {
|
||||
PostgresConnectionEnvironmentError,
|
||||
loadPostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironment,
|
||||
type PostgresConnectionEnvironmentKeys,
|
||||
} from '../connection/connectionEnvironment';
|
||||
|
||||
export {
|
||||
runPostgresMigrations,
|
||||
type RunPostgresMigrationsOptions,
|
||||
} from './migrate';
|
||||
|
||||
export {
|
||||
POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
|
||||
POSTGRESQL_MIGRATION_HISTORY_TABLE,
|
||||
POSTGRESQL_MIGRATION_SCHEMA,
|
||||
PostgresMigrationLeaderUnavailableError,
|
||||
PostgresMigrationStreamStore,
|
||||
readPostgresMigrationHistory,
|
||||
type PostgresMigrationClient,
|
||||
type PostgresMigrationContext,
|
||||
type PostgresMigrationPool,
|
||||
type PostgresMigrationQueryable,
|
||||
type PostgresMigrationQueryResult,
|
||||
} from '../migrations/postgresMigrationStreamStore';
|
||||
|
||||
export { postgresqlMainMigrationStream } from '../migrations';
|
||||
export { postgresqlMainMigrationManifest } from './migrationManifest';
|
||||
|
||||
export {
|
||||
ql3PostgresTables,
|
||||
ql3Schema,
|
||||
runAttempts,
|
||||
runEvents,
|
||||
runRetryPolicies,
|
||||
runs,
|
||||
schemaCapabilities,
|
||||
schemaMigrations,
|
||||
} from '../schema/schema';
|
||||
|
||||
export {
|
||||
postgresqlControlSchemaContract,
|
||||
type PostgresSchemaContract,
|
||||
type PostgresSchemaContractTable,
|
||||
} from '../schema/schemaContract';
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// One-shot migration product entrypoint owned by the migration domain.
|
||||
import { runPostgresMigrationProcess } from './migrationProcess';
|
||||
|
||||
const USAGE = 'Usage: ql3-cluster-migrate';
|
||||
|
||||
function failureFact(error: unknown): Readonly<Record<string, unknown>> {
|
||||
const candidate = error as {
|
||||
readonly name?: unknown;
|
||||
readonly code?: unknown;
|
||||
};
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-cluster-migration',
|
||||
event: 'migration_failed',
|
||||
name:
|
||||
typeof candidate?.name === 'string' && candidate.name.length <= 128
|
||||
? candidate.name
|
||||
: 'Error',
|
||||
...(typeof candidate?.code === 'string' && candidate.code.length <= 128
|
||||
? { code: candidate.code }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
async function main(argv: readonly string[]): Promise<void> {
|
||||
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
||||
process.stdout.write(`${USAGE}\n`);
|
||||
return;
|
||||
}
|
||||
if (argv.length !== 0) {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code: 'QL3_CLUSTER_MIGRATION_CLI_USAGE_INVALID',
|
||||
message: USAGE,
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 64;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await runPostgresMigrationProcess({
|
||||
environment: process.env,
|
||||
emit(record) {
|
||||
process.stdout.write(`${JSON.stringify(record)}\n`);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void main(process.argv.slice(2));
|
||||
@@ -0,0 +1,282 @@
|
||||
import type { MigrationStreamManifest } from '@qinglong/runtime-core';
|
||||
import { POSTGRESQL_MAIN_MIGRATION_STREAM_ID } from '../migrations/postgresMigrationStreamStore';
|
||||
|
||||
export const postgresqlMainMigrationManifest: MigrationStreamManifest =
|
||||
Object.freeze({
|
||||
id: POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
|
||||
dialect: 'postgresql',
|
||||
migrationIdScheme: 'postgres-prefixed',
|
||||
checksumScheme: 'sha256',
|
||||
migrations: Object.freeze([
|
||||
Object.freeze({
|
||||
id: 'pg-0001-schema-capability',
|
||||
checksum:
|
||||
'9e3499e3bcdfe3d7b2559e64ea7bbf236a8a11ba32d6a45af131034887d5a8ab',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0002-run-core',
|
||||
checksum:
|
||||
'5b59a7f9323746e49c6c321e89007f553a0751f25d16ffd23c3ae37dd87f76e4',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0003-run-retry-policy',
|
||||
checksum:
|
||||
'621792cde917cc86809bbebff389443e790bdba60f73d04f7a1dc97a0ebf72db',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0004-project-policy',
|
||||
checksum:
|
||||
'715675d1725687438106c01193f9b87368705c89793d1553c7cbfa7ddefc2343',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0005-api-credential-security-audit',
|
||||
checksum:
|
||||
'd19134de8639df125a8ee4ae4056d6493bf9be4bb2bd28ba3ea1db2a4bf119df',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0006-identity-credential-administration',
|
||||
checksum:
|
||||
'fe2e87ad191a185a6187d1ef350b7fb49bc13cb56bef610669a59838103b2f87',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0007-cluster-recovery-indexes',
|
||||
checksum:
|
||||
'e7e20ee7789a90ddf68eacbb25a9a4e7f4101358b4fb7fd10fa1d4f256e4b5dd',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0008-run-recovery-claims',
|
||||
checksum:
|
||||
'1f32c1dd83107eb1881e1b0e968c59fb33e0c63bb8d526b6cde6f350c07652db',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0009-worker-session-run-lease',
|
||||
checksum:
|
||||
'6be7e5380ca4d71aa6fcbe9170e0c9beb67aa976800ce75167aab44c41ae335b',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0010-worker-ingress-attestation',
|
||||
checksum:
|
||||
'bbc1c36d3d8d3d162073988c8abcf94795fded80277a56b7ac6aa48544caf367',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0011-api-credential-pepper-binding',
|
||||
checksum:
|
||||
'12e76002c42f8409ca417fbb562e29d407eaa57324567d80605471eb9e0fa7a4',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0012-task-trigger-definitions',
|
||||
checksum:
|
||||
'963e99d1aec9de46fd8ec034480f6c78dc904e30fe85d1b3790224c66f628055',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0013-task-execution-revisions',
|
||||
checksum:
|
||||
'a09b34cfda9102c1c573479f61a70f39a786ccd8b45bb82b640935ede4cb6301',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0014-cluster-scheduler-admission',
|
||||
checksum:
|
||||
'5f58a214fb2321c2193bf1f3c4e231d9bc2116cd56fd8550bbc23c81cdbf566d',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0015-worker-credential-delivery-ledger',
|
||||
checksum:
|
||||
'e8bcef07055a748a4858ae502233138da4c0b4f9f2ea3d3a02bd9264da61f4a4',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0016-worker-credential-stage-discard-ledger',
|
||||
checksum:
|
||||
'8e749a3c16fc9c995124bb80fbe67bb4de30ad20c0a1591f283a0dbf1ce243a0',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0017-database-role-grants',
|
||||
checksum:
|
||||
'3bb01c0f0dcab152c01b6ba374e60f0aaae7e7464d589fc76626d5d8191fe9f0',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0018-plugin-package-installs',
|
||||
checksum:
|
||||
'300bd91155c6480f4397f903645022fa980d71fabe592d42584b1402328f5348',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0019-approved-actions',
|
||||
checksum:
|
||||
'96fd50dc1e42f7b2f54af670460d7b486f2fa7c6a234ea1debad120436fcd40c',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0020-plugin-package-admission-receipts',
|
||||
checksum:
|
||||
'b1ae4af68d274eb4324e5fd521f42bfb99c1a81478cc9f57c9b604cd393ea8cf',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0021-approved-action-executions-and-package-proposals',
|
||||
checksum:
|
||||
'6f65ca0dcb25ea56b32e82d40a5a7a6b2be6cf9ed4a48d04107ef01e44f1cd6a',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0022-plugin-package-authority-split',
|
||||
checksum:
|
||||
'431c7c454583629a46a45bb88107765ef600185d3606bfedabb0ca0e40138bf6',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0023-plugin-package-management-quota',
|
||||
checksum:
|
||||
'9e3d1bd16bcb2712885de8f0a094289a60c20dc0e4ed9d90ed5adcf1b0240380',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0024-plugin-package-identity-keyset-ledger',
|
||||
checksum:
|
||||
'0c764e9427632a79033ecd75a600e3788b9f7b9b39f55899d2c9d1e2d033105f',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0025-plugin-package-materialized-revisions',
|
||||
checksum:
|
||||
'da6413393e26f369a9f2639dbd7a0b25bbd079f5de3c61e96e74f0fb10953a54',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0026-plugin-package-task-reconciliations',
|
||||
checksum:
|
||||
'b46c241d958cbc34c2e6052708dbc13e040abc3db92c58b0d5915f05c322a32f',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0027-project-tool-definition-snapshots',
|
||||
checksum:
|
||||
'147b413aa7eee0469c23d1895f4b9136ffa90da999370c9ca9d293e94afe0960',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0028-step-runs',
|
||||
checksum:
|
||||
'a5fd40534f582d07ff5851aa3b856a872a3685e8c975725e8eacf0f6a44ec04a',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0029-tool-execution-evidence',
|
||||
checksum:
|
||||
'c61683499c5d65ad319b2ce18ec94b001131381e9d118ed56f05626f6f83d5c6',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0030-tool-execution-start-barriers',
|
||||
checksum:
|
||||
'dba5dd522bc9cc7d7cce1432d14262c0195637c9f407d21bb6dc93b24a5018a7',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0031-tool-invocation-artifacts',
|
||||
checksum:
|
||||
'b95d9953315aed36a204caa26f2aba5fe9b2893f67f036c8c118bd5551a0a641',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0032-tool-execution-artifact-bindings',
|
||||
checksum:
|
||||
'9dd8c3ce052124370bfef691e21cc582331ce527b0d69740549d3d440bec4946',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0033-tool-execution-completions',
|
||||
checksum:
|
||||
'1ada1837e4473c3e4f27437ede826832cc590aa47f8e8bfa512fa0fadd8dd291',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0034-tool-execution-failure-completions',
|
||||
checksum:
|
||||
'77aa1e86f748f59b84cb4546f0c376ca2bf6ee424bf1b5b3b7fe4970b8056c2f',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0035-tool-result-key-catalog',
|
||||
checksum:
|
||||
'64af556669d93e5cf20a29f5df3842280e56029188e1c719f30325b4b75b84ff',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0036-tool-result-rekey-overlays',
|
||||
checksum:
|
||||
'3d4fb20c52502b8a5609a8242adb17eb0348f7dc7fa65f793d61136d54b5c851',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0037-plugin-package-quarantine',
|
||||
checksum:
|
||||
'a388c7e49a9fe6e417fb343e4ce30dd1b774513c726696ac387d6a1f9884bcb2',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0038-plugin-package-publisher-provenance',
|
||||
checksum:
|
||||
'faa63ec953c41fa75c25c69fce017c07539080a9e8c321b107d067405db6a4ec',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0039-plugin-package-publisher-trust-authority',
|
||||
checksum:
|
||||
'e0ee11727cbc5342bf480e2cc3142971ff256182d56fa960fe4a35f5c3333afe',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0040-plugin-package-publisher-trust-transitions',
|
||||
checksum:
|
||||
'c45ab5c687b62555530035a11370e7094f7b5608505b02d3325804cc680c23fb',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0041-plugin-package-lifecycle',
|
||||
checksum:
|
||||
'99669c63c891124aa0741586d5e92bd40d7a3ddf322ec7da970790958f554101',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0042-plugin-package-lifecycle-plans',
|
||||
checksum:
|
||||
'a242067854f4ee5231c75874fa77dda364da962a2dcaf570b5015ee069818c4b',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0043-plugin-package-automation-publications',
|
||||
checksum:
|
||||
'f93d3a1205c12b252ae31da08b401cad570c1d7f1dea48e83901eff4afe214b9',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0044-plugin-package-automation-start-guard',
|
||||
checksum:
|
||||
'd4f436769e4845c220196f9a0ee4845cc9dae6b2fcd229486f42e4d87e11effd',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0045-plugin-package-workflow-admissions',
|
||||
checksum:
|
||||
'e31520b150eb8991004f773ccd8f4fb9ec2bb559ef0750391f28038aeae43b7e',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0046-plugin-package-workflow-task-attempt-admissions',
|
||||
checksum:
|
||||
'2c5316c9b4e60a1601d9ad0346f56261e6f15ee9368044e632154adb6fe45b7b',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0047-worker-credential-management-plans',
|
||||
checksum:
|
||||
'2004c64019beb6971d43507160a7bd488baba12eb299fde3681a731800145d6a',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0048-worker-credential-preapproved-activation',
|
||||
checksum:
|
||||
'cf37300e40a127b08c118c0bbb352035937af935455709fe5569d458ef91df8b',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0049-worker-credential-execution-receipts',
|
||||
checksum:
|
||||
'd606e51a326b50f8969942c83b3d06d3ed4aa94618fbc5b25005006016918af9',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0050-worker-credential-management-boundary',
|
||||
checksum:
|
||||
'9555dd93ab076bf450cfdf16e2178586a2c58738359266fa5d5345c938913555',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0051-automation-management-boundary',
|
||||
checksum:
|
||||
'1f5e2beff59570163cee7ba2f798a149b5108522bc9db05ecb387d99cc0f96f5',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0052-automation-management-identity-keyset-ledger',
|
||||
checksum:
|
||||
'8594427ad2d53caf61891d21296c140f43ca8204cf728c66784c6db48b7743cd',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0053-plugin-package-workflow-run-list-index',
|
||||
checksum:
|
||||
'1848d8ffcf930462a0beab1220860ac6a626228511a0f55e997077bcb6ef4b63',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0054-approval-management-boundary',
|
||||
checksum:
|
||||
'5e3e6b222269f095e0d7a985fdeb0ea154510e59dfe15873192af8c8d603fca3',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
import type {
|
||||
OpenPostgresDatabase,
|
||||
PostgresDatabaseResource,
|
||||
PostgresPool,
|
||||
} from '@qinglong/runtime-core';
|
||||
import {
|
||||
createPostgresDatabaseOpener,
|
||||
isPostgresTlsDnsServername,
|
||||
type PostgresConnectionOptions,
|
||||
type PostgresPoolOptions,
|
||||
} from '../connection/pool';
|
||||
import { loadPostgresConnectionEnvironment } from '../connection/connectionEnvironment';
|
||||
import { loadPostgresCertificateAuthorityFile } from '../connection/certificateAuthority';
|
||||
import { runPostgresMigrations } from './migrate';
|
||||
|
||||
export type PostgresMigrationProcessEnvironment = Readonly<
|
||||
Record<string, string | undefined>
|
||||
>;
|
||||
|
||||
export interface PostgresMigrationProcessConfig {
|
||||
readonly connection: PostgresConnectionOptions;
|
||||
readonly pool: PostgresPoolOptions;
|
||||
}
|
||||
|
||||
export interface PostgresMigrationProcessEvent {
|
||||
readonly schemaVersion: 1;
|
||||
readonly component: 'qinglong3-cluster-migration';
|
||||
readonly event: 'migration_started' | 'migration_completed';
|
||||
}
|
||||
|
||||
export interface RunPostgresMigrationProcessOptions {
|
||||
readonly environment: PostgresMigrationProcessEnvironment;
|
||||
readonly openDatabase?: OpenPostgresDatabase;
|
||||
readonly migrate?: (options: {
|
||||
readonly pool: PostgresPool;
|
||||
}) => Promise<void>;
|
||||
readonly emit?: (
|
||||
event: PostgresMigrationProcessEvent,
|
||||
) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export class PostgresMigrationProcessConfigError extends TypeError {
|
||||
readonly code = 'QL3_POSTGRES_MIGRATION_PROCESS_CONFIG_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`PostgreSQL migration process configuration is invalid: ${message}`);
|
||||
this.name = 'PostgresMigrationProcessConfigError';
|
||||
}
|
||||
}
|
||||
|
||||
function boundedValue(
|
||||
environment: PostgresMigrationProcessEnvironment,
|
||||
name: string,
|
||||
maximumLength: number,
|
||||
required = false,
|
||||
): string | undefined {
|
||||
const value = environment[name];
|
||||
if (value === undefined || value === '') {
|
||||
if (required) {
|
||||
throw new PostgresMigrationProcessConfigError(`${name} is required`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (value.length > maximumLength || /[\0\r\n]/.test(value)) {
|
||||
throw new PostgresMigrationProcessConfigError(`${name} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function booleanValue(
|
||||
environment: PostgresMigrationProcessEnvironment,
|
||||
name: string,
|
||||
): boolean {
|
||||
const value = environment[name];
|
||||
if (value === undefined || value === '') return false;
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
throw new PostgresMigrationProcessConfigError(
|
||||
`${name} must be true or false`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Parses a migration-only PostgreSQL connection without reading runtime keys. */
|
||||
export function loadPostgresMigrationProcessConfig(
|
||||
environment: PostgresMigrationProcessEnvironment,
|
||||
): PostgresMigrationProcessConfig {
|
||||
if (
|
||||
!environment ||
|
||||
typeof environment !== 'object' ||
|
||||
Array.isArray(environment)
|
||||
) {
|
||||
throw new PostgresMigrationProcessConfigError(
|
||||
'environment must be an object',
|
||||
);
|
||||
}
|
||||
let connection: PostgresConnectionOptions;
|
||||
try {
|
||||
connection = loadPostgresConnectionEnvironment(environment, {
|
||||
connectionString: 'QL3_POSTGRES_MIGRATION_URL',
|
||||
host: 'QL3_POSTGRES_MIGRATION_HOST',
|
||||
port: 'QL3_POSTGRES_MIGRATION_PORT',
|
||||
database: 'QL3_POSTGRES_MIGRATION_DATABASE',
|
||||
user: 'QL3_POSTGRES_MIGRATION_USER',
|
||||
password: 'QL3_POSTGRES_MIGRATION_PASSWORD',
|
||||
});
|
||||
} catch (error) {
|
||||
throw new PostgresMigrationProcessConfigError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'PostgreSQL migration connection is invalid',
|
||||
);
|
||||
}
|
||||
|
||||
const mode = environment.QL3_POSTGRES_TLS_MODE ?? 'verify-full';
|
||||
if (mode !== 'verify-full' && mode !== 'disable') {
|
||||
throw new PostgresMigrationProcessConfigError(
|
||||
'QL3_POSTGRES_TLS_MODE must be verify-full or disable',
|
||||
);
|
||||
}
|
||||
if (
|
||||
mode === 'disable' &&
|
||||
!booleanValue(environment, 'QL3_POSTGRES_ALLOW_INSECURE')
|
||||
) {
|
||||
throw new PostgresMigrationProcessConfigError(
|
||||
'disabling PostgreSQL TLS requires QL3_POSTGRES_ALLOW_INSECURE=true',
|
||||
);
|
||||
}
|
||||
const servername = boundedValue(
|
||||
environment,
|
||||
'QL3_POSTGRES_TLS_SERVERNAME',
|
||||
253,
|
||||
);
|
||||
if (mode === 'verify-full' && !isPostgresTlsDnsServername(servername)) {
|
||||
throw new PostgresMigrationProcessConfigError(
|
||||
'QL3_POSTGRES_TLS_SERVERNAME must be an explicit DNS name for verify-full',
|
||||
);
|
||||
}
|
||||
const certificateAuthorityFile = boundedValue(
|
||||
environment,
|
||||
'QL3_POSTGRES_TLS_CA_FILE',
|
||||
4096,
|
||||
);
|
||||
if (mode === 'disable' && certificateAuthorityFile !== undefined) {
|
||||
throw new PostgresMigrationProcessConfigError(
|
||||
'QL3_POSTGRES_TLS_CA_FILE cannot be used when TLS is disabled',
|
||||
);
|
||||
}
|
||||
let certificateAuthority: string | undefined;
|
||||
if (certificateAuthorityFile !== undefined) {
|
||||
try {
|
||||
certificateAuthority = loadPostgresCertificateAuthorityFile(
|
||||
certificateAuthorityFile,
|
||||
);
|
||||
} catch {
|
||||
throw new PostgresMigrationProcessConfigError(
|
||||
'QL3_POSTGRES_TLS_CA_FILE must contain a bounded trusted CA bundle',
|
||||
);
|
||||
}
|
||||
}
|
||||
const applicationName =
|
||||
boundedValue(environment, 'QL3_POSTGRES_APPLICATION_NAME', 63) ??
|
||||
'qinglong3-cluster-migration';
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,62}$/.test(applicationName)) {
|
||||
throw new PostgresMigrationProcessConfigError(
|
||||
'QL3_POSTGRES_APPLICATION_NAME is invalid',
|
||||
);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
connection: Object.freeze({
|
||||
...connection,
|
||||
tls:
|
||||
mode === 'disable'
|
||||
? Object.freeze({ mode: 'disable' as const })
|
||||
: Object.freeze({
|
||||
mode: 'verify-full' as const,
|
||||
...(certificateAuthority === undefined
|
||||
? {}
|
||||
: { ca: certificateAuthority }),
|
||||
servername: servername!,
|
||||
}),
|
||||
}),
|
||||
pool: Object.freeze({
|
||||
applicationName,
|
||||
maxConnections: 1,
|
||||
connectionTimeoutMs: 15_000,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function processEvent(
|
||||
event: PostgresMigrationProcessEvent['event'],
|
||||
): PostgresMigrationProcessEvent {
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
component: 'qinglong3-cluster-migration',
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
async function emit(
|
||||
sink: RunPostgresMigrationProcessOptions['emit'],
|
||||
value: PostgresMigrationProcessEvent,
|
||||
): Promise<void> {
|
||||
if (!sink) return;
|
||||
try {
|
||||
await sink(value);
|
||||
} catch {
|
||||
// Diagnostics cannot replace migration or close outcomes.
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens exactly one migration-role Pool, runs the reviewed stream and closes. */
|
||||
export async function runPostgresMigrationProcess(
|
||||
options: RunPostgresMigrationProcessOptions,
|
||||
): Promise<'migrated'> {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new TypeError('PostgreSQL migration process options are invalid');
|
||||
}
|
||||
const config = loadPostgresMigrationProcessConfig(options.environment);
|
||||
const openDatabase =
|
||||
options.openDatabase ??
|
||||
createPostgresDatabaseOpener({
|
||||
role: 'migration',
|
||||
connection: config.connection,
|
||||
pool: config.pool,
|
||||
onPoolError() {
|
||||
// Awaited migration statements remain authoritative. An idle Pool
|
||||
// error will be observed by the migration or final close.
|
||||
},
|
||||
});
|
||||
const migrate = options.migrate ?? runPostgresMigrations;
|
||||
if (
|
||||
typeof openDatabase !== 'function' ||
|
||||
typeof migrate !== 'function' ||
|
||||
(options.emit !== undefined && typeof options.emit !== 'function')
|
||||
) {
|
||||
throw new TypeError('PostgreSQL migration process adapters are invalid');
|
||||
}
|
||||
|
||||
let database: PostgresDatabaseResource | undefined;
|
||||
let primaryError: unknown;
|
||||
try {
|
||||
database = await openDatabase();
|
||||
await emit(options.emit, processEvent('migration_started'));
|
||||
await migrate({ pool: database.pool });
|
||||
await emit(options.emit, processEvent('migration_completed'));
|
||||
} catch (error) {
|
||||
primaryError = error;
|
||||
}
|
||||
if (database) {
|
||||
try {
|
||||
await database.close();
|
||||
} catch (error) {
|
||||
primaryError ??= error;
|
||||
}
|
||||
}
|
||||
if (primaryError) throw primaryError;
|
||||
return 'migrated';
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
|
||||
type PostgresMigrationContext,
|
||||
} from './postgresMigrationStreamStore';
|
||||
import type { MigrationStreamDefinition } from '@qinglong/runtime-core';
|
||||
import { pg0001SchemaCapabilityMigration } from './pg-0001-schema-capability';
|
||||
import { pg0002RunCoreMigration } from './pg-0002-run-core';
|
||||
import { pg0003RunRetryPolicyMigration } from './pg-0003-run-retry-policy';
|
||||
import { pg0004ProjectPolicyMigration } from './pg-0004-project-policy';
|
||||
import { pg0005ApiCredentialSecurityAuditMigration } from './pg-0005-api-credential-security-audit';
|
||||
import { pg0006IdentityCredentialAdministrationMigration } from './pg-0006-identity-credential-administration';
|
||||
import { pg0007ClusterRecoveryIndexesMigration } from './pg-0007-cluster-recovery-indexes';
|
||||
import { pg0008RunRecoveryClaimsMigration } from './pg-0008-run-recovery-claims';
|
||||
import { pg0009WorkerSessionRunLeaseMigration } from './pg-0009-worker-session-run-lease';
|
||||
import { pg0010WorkerIngressAttestationMigration } from './pg-0010-worker-ingress-attestation';
|
||||
import { pg0011ApiCredentialPepperBindingMigration } from './pg-0011-api-credential-pepper-binding';
|
||||
import { pg0012TaskTriggerDefinitionsMigration } from './pg-0012-task-trigger-definitions';
|
||||
import { pg0013TaskExecutionRevisionsMigration } from './pg-0013-task-execution-revisions';
|
||||
import { pg0014ClusterSchedulerAdmissionMigration } from './pg-0014-cluster-scheduler-admission';
|
||||
import { pg0015WorkerCredentialDeliveryLedgerMigration } from './pg-0015-worker-credential-delivery-ledger';
|
||||
import { pg0016WorkerCredentialStageDiscardLedgerMigration } from './pg-0016-worker-credential-stage-discard-ledger';
|
||||
import { pg0017DatabaseRoleGrantsMigration } from './pg-0017-database-role-grants';
|
||||
import { pg0018PluginPackageInstallsMigration } from './pg-0018-plugin-package-installs';
|
||||
import { pg0019ApprovedActionsMigration } from './pg-0019-approved-actions';
|
||||
import { pg0020PluginPackageAdmissionReceiptsMigration } from './pg-0020-plugin-package-admission-receipts';
|
||||
import { pg0021ApprovedActionExecutionsAndPackageProposalsMigration } from './pg-0021-approved-action-executions-and-package-proposals';
|
||||
import { pg0022PluginPackageAuthoritySplitMigration } from './pg-0022-plugin-package-authority-split';
|
||||
import { pg0023PluginPackageManagementQuotaMigration } from './pg-0023-plugin-package-management-quota';
|
||||
import { pg0024PluginPackageIdentityKeysetLedgerMigration } from './pg-0024-plugin-package-identity-keyset-ledger';
|
||||
import { pg0025PluginPackageMaterializedRevisionsMigration } from './pg-0025-plugin-package-materialized-revisions';
|
||||
import { pg0026PluginPackageTaskReconciliationsMigration } from './pg-0026-plugin-package-task-reconciliations';
|
||||
import { pg0027ProjectToolDefinitionSnapshotsMigration } from './pg-0027-project-tool-definition-snapshots';
|
||||
import { pg0028StepRunsMigration } from './pg-0028-step-runs';
|
||||
import { pg0029ToolExecutionEvidenceMigration } from './pg-0029-tool-execution-evidence';
|
||||
import { pg0030ToolExecutionStartBarriersMigration } from './pg-0030-tool-execution-start-barriers';
|
||||
import { pg0031ToolInvocationArtifactsMigration } from './pg-0031-tool-invocation-artifacts';
|
||||
import { pg0032ToolExecutionArtifactBindingsMigration } from './pg-0032-tool-execution-artifact-bindings';
|
||||
import { pg0033ToolExecutionCompletionsMigration } from './pg-0033-tool-execution-completions';
|
||||
import { pg0034ToolExecutionFailureCompletionsMigration } from './pg-0034-tool-execution-failure-completions';
|
||||
import { pg0035ToolResultKeyCatalogMigration } from './pg-0035-tool-result-key-catalog';
|
||||
import { pg0036ToolResultRekeyOverlaysMigration } from './pg-0036-tool-result-rekey-overlays';
|
||||
import { pg0037PluginPackageQuarantineMigration } from './pg-0037-plugin-package-quarantine';
|
||||
import { pg0038PluginPackagePublisherProvenanceMigration } from './pg-0038-plugin-package-publisher-provenance';
|
||||
import { pg0039PluginPackagePublisherTrustAuthorityMigration } from './pg-0039-plugin-package-publisher-trust-authority';
|
||||
import { pg0040PluginPackagePublisherTrustTransitionsMigration } from './pg-0040-plugin-package-publisher-trust-transitions';
|
||||
import { pg0041PluginPackageLifecycleMigration } from './pg-0041-plugin-package-lifecycle';
|
||||
import { pg0042PluginPackageLifecyclePlansMigration } from './pg-0042-plugin-package-lifecycle-plans';
|
||||
import { pg0043PluginPackageAutomationPublicationsMigration } from './pg-0043-plugin-package-automation-publications';
|
||||
import { pg0044PluginPackageAutomationStartGuardMigration } from './pg-0044-plugin-package-automation-start-guard';
|
||||
import { pg0045PluginPackageWorkflowAdmissionsMigration } from './pg-0045-plugin-package-workflow-admissions';
|
||||
import { pg0046PluginPackageWorkflowTaskAttemptAdmissionsMigration } from './pg-0046-plugin-package-workflow-task-attempt-admissions';
|
||||
import { pg0047WorkerCredentialManagementPlansMigration } from './pg-0047-worker-credential-management-plans';
|
||||
import { pg0048WorkerCredentialPreapprovedActivationMigration } from './pg-0048-worker-credential-preapproved-activation';
|
||||
import { pg0049WorkerCredentialExecutionReceiptsMigration } from './pg-0049-worker-credential-execution-receipts';
|
||||
import { pg0050WorkerCredentialManagementBoundaryMigration } from './pg-0050-worker-credential-management-boundary';
|
||||
import { pg0051AutomationManagementBoundaryMigration } from './pg-0051-automation-management-boundary';
|
||||
import { pg0052AutomationManagementIdentityKeysetLedgerMigration } from './pg-0052-automation-management-identity-keyset-ledger';
|
||||
import { pg0053PluginPackageWorkflowRunListIndexMigration } from './pg-0053-plugin-package-workflow-run-list-index';
|
||||
import { pg0054ApprovalManagementBoundaryMigration } from './pg-0054-approval-management-boundary';
|
||||
|
||||
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
|
||||
Object.freeze({
|
||||
id: POSTGRESQL_MAIN_MIGRATION_STREAM_ID,
|
||||
dialect: 'postgresql',
|
||||
migrationIdScheme: 'postgres-prefixed',
|
||||
checksumScheme: 'sha256',
|
||||
migrations: Object.freeze([
|
||||
pg0001SchemaCapabilityMigration,
|
||||
pg0002RunCoreMigration,
|
||||
pg0003RunRetryPolicyMigration,
|
||||
pg0004ProjectPolicyMigration,
|
||||
pg0005ApiCredentialSecurityAuditMigration,
|
||||
pg0006IdentityCredentialAdministrationMigration,
|
||||
pg0007ClusterRecoveryIndexesMigration,
|
||||
pg0008RunRecoveryClaimsMigration,
|
||||
pg0009WorkerSessionRunLeaseMigration,
|
||||
pg0010WorkerIngressAttestationMigration,
|
||||
pg0011ApiCredentialPepperBindingMigration,
|
||||
pg0012TaskTriggerDefinitionsMigration,
|
||||
pg0013TaskExecutionRevisionsMigration,
|
||||
pg0014ClusterSchedulerAdmissionMigration,
|
||||
pg0015WorkerCredentialDeliveryLedgerMigration,
|
||||
pg0016WorkerCredentialStageDiscardLedgerMigration,
|
||||
pg0017DatabaseRoleGrantsMigration,
|
||||
pg0018PluginPackageInstallsMigration,
|
||||
pg0019ApprovedActionsMigration,
|
||||
pg0020PluginPackageAdmissionReceiptsMigration,
|
||||
pg0021ApprovedActionExecutionsAndPackageProposalsMigration,
|
||||
pg0022PluginPackageAuthoritySplitMigration,
|
||||
pg0023PluginPackageManagementQuotaMigration,
|
||||
pg0024PluginPackageIdentityKeysetLedgerMigration,
|
||||
pg0025PluginPackageMaterializedRevisionsMigration,
|
||||
pg0026PluginPackageTaskReconciliationsMigration,
|
||||
pg0027ProjectToolDefinitionSnapshotsMigration,
|
||||
pg0028StepRunsMigration,
|
||||
pg0029ToolExecutionEvidenceMigration,
|
||||
pg0030ToolExecutionStartBarriersMigration,
|
||||
pg0031ToolInvocationArtifactsMigration,
|
||||
pg0032ToolExecutionArtifactBindingsMigration,
|
||||
pg0033ToolExecutionCompletionsMigration,
|
||||
pg0034ToolExecutionFailureCompletionsMigration,
|
||||
pg0035ToolResultKeyCatalogMigration,
|
||||
pg0036ToolResultRekeyOverlaysMigration,
|
||||
pg0037PluginPackageQuarantineMigration,
|
||||
pg0038PluginPackagePublisherProvenanceMigration,
|
||||
pg0039PluginPackagePublisherTrustAuthorityMigration,
|
||||
pg0040PluginPackagePublisherTrustTransitionsMigration,
|
||||
pg0041PluginPackageLifecycleMigration,
|
||||
pg0042PluginPackageLifecyclePlansMigration,
|
||||
pg0043PluginPackageAutomationPublicationsMigration,
|
||||
pg0044PluginPackageAutomationStartGuardMigration,
|
||||
pg0045PluginPackageWorkflowAdmissionsMigration,
|
||||
pg0046PluginPackageWorkflowTaskAttemptAdmissionsMigration,
|
||||
pg0047WorkerCredentialManagementPlansMigration,
|
||||
pg0048WorkerCredentialPreapprovedActivationMigration,
|
||||
pg0049WorkerCredentialExecutionReceiptsMigration,
|
||||
pg0050WorkerCredentialManagementBoundaryMigration,
|
||||
pg0051AutomationManagementBoundaryMigration,
|
||||
pg0052AutomationManagementIdentityKeysetLedgerMigration,
|
||||
pg0053PluginPackageWorkflowRunListIndexMigration,
|
||||
pg0054ApprovalManagementBoundaryMigration,
|
||||
]),
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const POSTGRESQL_SCHEMA_CAPABILITY_TABLE = 'schema_capabilities';
|
||||
|
||||
export const pg0001SchemaCapabilityMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0001-schema-capability',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_SCHEMA_CAPABILITY_TABLE}" (
|
||||
contract_name varchar(64) PRIMARY KEY,
|
||||
contract_version integer NOT NULL
|
||||
CONSTRAINT ql3_schema_capabilities_version_check
|
||||
CHECK (contract_version >= 0),
|
||||
migration_id varchar(128) NOT NULL,
|
||||
capabilities jsonb NOT NULL
|
||||
CONSTRAINT ql3_schema_capabilities_payload_check
|
||||
CHECK (jsonb_typeof(capabilities) = 'object'),
|
||||
updated_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_schema_capabilities_updated_at_check
|
||||
CHECK (updated_at_ms >= 0),
|
||||
CONSTRAINT ql3_schema_capabilities_migration_fk
|
||||
FOREIGN KEY (migration_id)
|
||||
REFERENCES "ql3"."schema_migrations" (migration_id)
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
)
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,224 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const POSTGRESQL_RUN_TABLE = 'runs';
|
||||
export const POSTGRESQL_RUN_ATTEMPT_TABLE = 'run_attempts';
|
||||
export const POSTGRESQL_RUN_EVENT_TABLE = 'run_events';
|
||||
|
||||
export const pg0002RunCoreMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0002-run-core',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_RUN_TABLE}" (
|
||||
id varchar(36) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
task_id varchar(255) NOT NULL,
|
||||
task_revision varchar(128) NOT NULL,
|
||||
task_name varchar(255),
|
||||
task_snapshot_ref varchar(512),
|
||||
parent_run_id varchar(36),
|
||||
retry_of_run_id varchar(36),
|
||||
trigger_id varchar(36),
|
||||
trigger_type varchar(64) NOT NULL,
|
||||
execution_origin varchar(64) NOT NULL,
|
||||
execution_owner varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_runs_execution_owner_check
|
||||
CHECK (execution_owner = 'runtime'),
|
||||
triggered_by varchar(255),
|
||||
request_id varchar(128),
|
||||
scheduled_for_ms bigint
|
||||
CONSTRAINT ql3_runs_scheduled_for_check
|
||||
CHECK (scheduled_for_ms IS NULL OR scheduled_for_ms >= 0),
|
||||
status varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_runs_status_check
|
||||
CHECK (status IN (
|
||||
'created', 'queued', 'dispatching', 'running', 'waiting_approval',
|
||||
'retry_wait', 'lost', 'succeeded', 'failed', 'cancelled', 'timed_out'
|
||||
)),
|
||||
version integer NOT NULL DEFAULT 0
|
||||
CONSTRAINT ql3_runs_version_check
|
||||
CHECK (version >= 0),
|
||||
event_sequence integer NOT NULL DEFAULT 0
|
||||
CONSTRAINT ql3_runs_event_sequence_check
|
||||
CHECK (event_sequence >= 0),
|
||||
priority integer NOT NULL DEFAULT 0,
|
||||
idempotency_key varchar(255),
|
||||
input_ref varchar(512),
|
||||
output_ref varchar(512),
|
||||
created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_runs_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
queued_at_ms bigint
|
||||
CONSTRAINT ql3_runs_queued_at_check
|
||||
CHECK (queued_at_ms IS NULL OR queued_at_ms >= 0),
|
||||
started_at_ms bigint
|
||||
CONSTRAINT ql3_runs_started_at_check
|
||||
CHECK (started_at_ms IS NULL OR started_at_ms >= 0),
|
||||
finished_at_ms bigint
|
||||
CONSTRAINT ql3_runs_finished_at_check
|
||||
CHECK (finished_at_ms IS NULL OR finished_at_ms >= 0),
|
||||
cancel_requested_at_ms bigint
|
||||
CONSTRAINT ql3_runs_cancel_requested_at_check
|
||||
CHECK (cancel_requested_at_ms IS NULL OR cancel_requested_at_ms >= 0),
|
||||
cancel_reason varchar(16)
|
||||
CONSTRAINT ql3_runs_cancel_reason_check
|
||||
CHECK (cancel_reason IS NULL OR cancel_reason IN (
|
||||
'user', 'policy', 'shutdown', 'reconcile', 'timeout'
|
||||
)),
|
||||
error_code varchar(128),
|
||||
error_summary varchar(1024),
|
||||
CONSTRAINT ql3_runs_parent_fk
|
||||
FOREIGN KEY (parent_run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id),
|
||||
CONSTRAINT ql3_runs_retry_of_fk
|
||||
FOREIGN KEY (retry_of_run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_runs_project_idempotency_uidx
|
||||
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (project_id, idempotency_key)
|
||||
WHERE idempotency_key IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_runs_project_created_idx
|
||||
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (project_id, created_at_ms, id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_runs_task_created_idx
|
||||
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (task_id, created_at_ms, id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_runs_dispatch_candidates_idx
|
||||
ON "ql3"."${POSTGRESQL_RUN_TABLE}" (priority DESC, queued_at_ms, id)
|
||||
WHERE execution_owner = 'runtime'
|
||||
AND status IN ('queued', 'dispatching')
|
||||
AND cancel_requested_at_ms IS NULL
|
||||
AND queued_at_ms IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (
|
||||
id varchar(36) PRIMARY KEY,
|
||||
run_id varchar(36) NOT NULL,
|
||||
step_run_id varchar(36),
|
||||
attempt integer NOT NULL
|
||||
CONSTRAINT ql3_run_attempts_attempt_check
|
||||
CHECK (attempt >= 1),
|
||||
status varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_run_attempts_status_check
|
||||
CHECK (status IN (
|
||||
'claimed', 'starting', 'running', 'succeeded', 'failed',
|
||||
'cancelled', 'timed_out', 'lost'
|
||||
)),
|
||||
executor_type varchar(64) NOT NULL,
|
||||
worker_id varchar(128),
|
||||
executor_handle varchar(2048),
|
||||
pid integer
|
||||
CONSTRAINT ql3_run_attempts_pid_check
|
||||
CHECK (pid IS NULL OR pid >= 1),
|
||||
log_artifact_id varchar(36),
|
||||
lease_token varchar(128),
|
||||
lease_expires_at_ms bigint
|
||||
CONSTRAINT ql3_run_attempts_lease_expiry_check
|
||||
CHECK (lease_expires_at_ms IS NULL OR lease_expires_at_ms >= 0),
|
||||
deadline_at_ms bigint
|
||||
CONSTRAINT ql3_run_attempts_deadline_check
|
||||
CHECK (deadline_at_ms IS NULL OR deadline_at_ms >= 0),
|
||||
callback_token_hash varchar(128),
|
||||
callback_sequence integer NOT NULL DEFAULT 0
|
||||
CONSTRAINT ql3_run_attempts_callback_sequence_check
|
||||
CHECK (callback_sequence >= 0),
|
||||
created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_run_attempts_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
started_at_ms bigint
|
||||
CONSTRAINT ql3_run_attempts_started_at_check
|
||||
CHECK (started_at_ms IS NULL OR started_at_ms >= 0),
|
||||
finished_at_ms bigint
|
||||
CONSTRAINT ql3_run_attempts_finished_at_check
|
||||
CHECK (finished_at_ms IS NULL OR finished_at_ms >= 0),
|
||||
exit_code integer,
|
||||
error_code varchar(128),
|
||||
error_summary varchar(1024),
|
||||
CONSTRAINT ql3_run_attempts_run_fk
|
||||
FOREIGN KEY (run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_run_attempts_run_attempt_uidx
|
||||
ON "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (run_id, attempt)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_run_attempts_dispatch_candidates_idx
|
||||
ON "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (status, run_id, created_at_ms, id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_run_attempts_lease_idx
|
||||
ON "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (lease_expires_at_ms, id)
|
||||
WHERE lease_expires_at_ms IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (
|
||||
id varchar(36) PRIMARY KEY,
|
||||
run_id varchar(36) NOT NULL,
|
||||
sequence integer NOT NULL
|
||||
CONSTRAINT ql3_run_events_sequence_check
|
||||
CHECK (sequence >= 1),
|
||||
type varchar(128) NOT NULL,
|
||||
dedupe_key varchar(255),
|
||||
actor_type varchar(64) NOT NULL
|
||||
CONSTRAINT ql3_run_events_actor_type_check
|
||||
CHECK (actor_type IN (
|
||||
'user', 'api_app', 'trigger', 'agent', 'mcp_client', 'worker',
|
||||
'executor', 'system', 'legacy_shell', 'scheduler', 'reconciler',
|
||||
'compatibility'
|
||||
)),
|
||||
actor_id varchar(255),
|
||||
attempt_id varchar(36),
|
||||
step_run_id varchar(36),
|
||||
payload jsonb NOT NULL
|
||||
CONSTRAINT ql3_run_events_payload_check
|
||||
CHECK (jsonb_typeof(payload) = 'object'),
|
||||
created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_run_events_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
CONSTRAINT ql3_run_events_run_fk
|
||||
FOREIGN KEY (run_id) REFERENCES "ql3"."${POSTGRESQL_RUN_TABLE}" (id),
|
||||
CONSTRAINT ql3_run_events_attempt_fk
|
||||
FOREIGN KEY (attempt_id) REFERENCES "ql3"."${POSTGRESQL_RUN_ATTEMPT_TABLE}" (id)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_run_events_run_sequence_uidx
|
||||
ON "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (run_id, sequence)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_run_events_run_dedupe_uidx
|
||||
ON "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (run_id, dedupe_key)
|
||||
WHERE dedupe_key IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_run_events_run_created_idx
|
||||
ON "ql3"."${POSTGRESQL_RUN_EVENT_TABLE}" (run_id, created_at_ms, id)
|
||||
`.trim(),
|
||||
`
|
||||
INSERT INTO "ql3"."schema_capabilities" (
|
||||
contract_name,
|
||||
contract_version,
|
||||
migration_id,
|
||||
capabilities,
|
||||
updated_at_ms
|
||||
)
|
||||
VALUES (
|
||||
'control-core',
|
||||
1,
|
||||
'pg-0002-run-core',
|
||||
'{"run_core":1}'::jsonb,
|
||||
floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
|
||||
)
|
||||
ON CONFLICT (contract_name) DO UPDATE
|
||||
SET contract_version = EXCLUDED.contract_version,
|
||||
migration_id = EXCLUDED.migration_id,
|
||||
capabilities = EXCLUDED.capabilities,
|
||||
updated_at_ms = EXCLUDED.updated_at_ms
|
||||
WHERE "schema_capabilities".contract_version < EXCLUDED.contract_version
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const POSTGRESQL_RUN_RETRY_POLICY_TABLE = 'run_retry_policies';
|
||||
|
||||
export const pg0003RunRetryPolicyMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0003-run-retry-policy',
|
||||
statements: [
|
||||
`
|
||||
ALTER TABLE "ql3"."runs"
|
||||
ADD COLUMN legacy_cron_id integer
|
||||
CONSTRAINT ql3_runs_legacy_cron_id_check
|
||||
CHECK (legacy_cron_id IS NULL OR legacy_cron_id >= 1)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_RUN_RETRY_POLICY_TABLE}" (
|
||||
run_id varchar(36) PRIMARY KEY,
|
||||
max_attempts integer NOT NULL
|
||||
CONSTRAINT ql3_run_retry_policies_max_attempts_check
|
||||
CHECK (max_attempts BETWEEN 1 AND 16),
|
||||
retry_on_lost boolean NOT NULL,
|
||||
safety varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_run_retry_policies_safety_check
|
||||
CHECK (safety IN ('unknown', 'idempotent', 'deduplicated')),
|
||||
backoff_base_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_run_retry_policies_backoff_base_check
|
||||
CHECK (backoff_base_ms BETWEEN 0 AND 86400000),
|
||||
backoff_max_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_run_retry_policies_backoff_max_check
|
||||
CHECK (backoff_max_ms BETWEEN backoff_base_ms AND 86400000),
|
||||
next_attempt_at_ms bigint
|
||||
CONSTRAINT ql3_run_retry_policies_next_attempt_check
|
||||
CHECK (next_attempt_at_ms IS NULL OR next_attempt_at_ms >= 0),
|
||||
version integer NOT NULL DEFAULT 0
|
||||
CONSTRAINT ql3_run_retry_policies_version_check
|
||||
CHECK (version >= 0),
|
||||
created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_run_retry_policies_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
updated_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_run_retry_policies_updated_at_check
|
||||
CHECK (updated_at_ms >= created_at_ms),
|
||||
CONSTRAINT ql3_run_retry_policies_run_fk
|
||||
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE CASCADE
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_run_retry_policies_due_idx
|
||||
ON "ql3"."${POSTGRESQL_RUN_RETRY_POLICY_TABLE}" (next_attempt_at_ms, run_id)
|
||||
WHERE next_attempt_at_ms IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_runs_lost_retry_idx
|
||||
ON "ql3"."runs" (execution_owner, status, id)
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 2,
|
||||
migration_id = 'pg-0003-run-retry-policy',
|
||||
capabilities = '{"run_core":1,"run_retry_policy":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 1
|
||||
AND migration_id = 'pg-0002-run-core'
|
||||
AND capabilities = '{"run_core":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 1'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const POSTGRESQL_PROJECT_TABLE = 'projects';
|
||||
export const POSTGRESQL_PROJECT_ROLE_BINDING_TABLE = 'project_role_bindings';
|
||||
|
||||
export const pg0004ProjectPolicyMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0004-project-policy',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_PROJECT_TABLE}" (
|
||||
id varchar(128) PRIMARY KEY
|
||||
CONSTRAINT ql3_projects_id_check CHECK (char_length(id) >= 1),
|
||||
name varchar(255) NOT NULL
|
||||
CONSTRAINT ql3_projects_name_check CHECK (char_length(name) >= 1),
|
||||
slug varchar(128) NOT NULL
|
||||
CONSTRAINT ql3_projects_slug_check
|
||||
CHECK (slug ~ '^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$'),
|
||||
status varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_projects_status_check
|
||||
CHECK (status IN ('active', 'archived')),
|
||||
version integer NOT NULL
|
||||
CONSTRAINT ql3_projects_version_check
|
||||
CHECK (version BETWEEN 1 AND 2147483647),
|
||||
created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_projects_created_at_check CHECK (created_at_ms >= 0),
|
||||
updated_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_projects_updated_at_check
|
||||
CHECK (updated_at_ms >= created_at_ms)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_projects_slug_uidx
|
||||
ON "ql3"."${POSTGRESQL_PROJECT_TABLE}" (slug)
|
||||
`.trim(),
|
||||
`
|
||||
INSERT INTO "ql3"."${POSTGRESQL_PROJECT_TABLE}" (
|
||||
id, name, slug, status, version, created_at_ms, updated_at_ms
|
||||
)
|
||||
VALUES ('default', 'Default', 'default', 'active', 1, 0, 0)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_PROJECT_ROLE_BINDING_TABLE}" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
subject_type varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_project_role_bindings_subject_type_check
|
||||
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')),
|
||||
subject_id varchar(255) NOT NULL
|
||||
CONSTRAINT ql3_project_role_bindings_subject_id_check
|
||||
CHECK (char_length(subject_id) >= 1),
|
||||
version integer NOT NULL
|
||||
CONSTRAINT ql3_project_role_bindings_version_check
|
||||
CHECK (version BETWEEN 1 AND 2147483647),
|
||||
state varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_project_role_bindings_state_check
|
||||
CHECK (state IN ('active', 'revoked')),
|
||||
role varchar(16),
|
||||
mutation_id varchar(64) NOT NULL
|
||||
CONSTRAINT ql3_project_role_bindings_mutation_id_check
|
||||
CHECK (mutation_id ~ '^[A-Za-z0-9._:-]{1,64}$'),
|
||||
changed_by_type varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_project_role_bindings_changed_by_type_check
|
||||
CHECK (changed_by_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')),
|
||||
changed_by_id varchar(255) NOT NULL
|
||||
CONSTRAINT ql3_project_role_bindings_changed_by_id_check
|
||||
CHECK (char_length(changed_by_id) >= 1),
|
||||
created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_project_role_bindings_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
CONSTRAINT project_role_bindings_pkey
|
||||
PRIMARY KEY (project_id, subject_type, subject_id, version),
|
||||
CONSTRAINT ql3_project_role_bindings_role_state_check
|
||||
CHECK (
|
||||
(state = 'active' AND role IN ('owner', 'admin', 'operator', 'viewer'))
|
||||
OR (state = 'revoked' AND role IS NULL)
|
||||
),
|
||||
CONSTRAINT ql3_project_role_bindings_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."${POSTGRESQL_PROJECT_TABLE}" (id)
|
||||
ON DELETE CASCADE
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_project_role_bindings_current_idx
|
||||
ON "ql3"."${POSTGRESQL_PROJECT_ROLE_BINDING_TABLE}"
|
||||
(project_id, subject_type, subject_id, version DESC)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_project_role_bindings_mutation_uidx
|
||||
ON "ql3"."${POSTGRESQL_PROJECT_ROLE_BINDING_TABLE}"
|
||||
(project_id, mutation_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_project_role_bindings_subject_idx
|
||||
ON "ql3"."${POSTGRESQL_PROJECT_ROLE_BINDING_TABLE}"
|
||||
(subject_type, subject_id, project_id, version DESC)
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 3,
|
||||
migration_id = 'pg-0004-project-policy',
|
||||
capabilities = '{"project_policy":1,"run_core":1,"run_retry_policy":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 2
|
||||
AND migration_id = 'pg-0003-run-retry-policy'
|
||||
AND capabilities = '{"run_core":1,"run_retry_policy":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 2'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const POSTGRESQL_IDENTITY_SUBJECT_TABLE = 'identity_subjects';
|
||||
export const POSTGRESQL_API_CREDENTIAL_TABLE = 'api_credentials';
|
||||
export const POSTGRESQL_SECURITY_AUDIT_EVENT_TABLE = 'security_audit_events';
|
||||
|
||||
export const pg0005ApiCredentialSecurityAuditMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0005-api-credential-security-audit',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_TABLE}" (
|
||||
subject_type varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_identity_subjects_type_check
|
||||
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')),
|
||||
subject_id varchar(255) NOT NULL
|
||||
CONSTRAINT ql3_identity_subjects_id_check
|
||||
CHECK (char_length(subject_id) >= 1),
|
||||
status varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_identity_subjects_status_check
|
||||
CHECK (status IN ('active', 'disabled')),
|
||||
version integer NOT NULL
|
||||
CONSTRAINT ql3_identity_subjects_version_check
|
||||
CHECK (version BETWEEN 1 AND 2147483647),
|
||||
created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_identity_subjects_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
updated_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_identity_subjects_updated_at_check
|
||||
CHECK (updated_at_ms >= created_at_ms),
|
||||
CONSTRAINT identity_subjects_pkey
|
||||
PRIMARY KEY (subject_type, subject_id)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_identity_subjects_status_idx
|
||||
ON "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_TABLE}"
|
||||
(status, subject_type, subject_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_API_CREDENTIAL_TABLE}" (
|
||||
credential_id varchar(64) NOT NULL
|
||||
CONSTRAINT ql3_api_credentials_id_check
|
||||
CHECK (credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'),
|
||||
version integer NOT NULL
|
||||
CONSTRAINT ql3_api_credentials_version_check
|
||||
CHECK (version BETWEEN 1 AND 2147483647),
|
||||
state varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_api_credentials_state_check
|
||||
CHECK (state IN ('active', 'revoked')),
|
||||
subject_type varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_api_credentials_subject_type_check
|
||||
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent')),
|
||||
subject_id varchar(255) NOT NULL
|
||||
CONSTRAINT ql3_api_credentials_subject_id_check
|
||||
CHECK (char_length(subject_id) >= 1),
|
||||
secret_digest char(64) NOT NULL
|
||||
CONSTRAINT ql3_api_credentials_secret_digest_check
|
||||
CHECK (secret_digest ~ '^[a-f0-9]{64}$'),
|
||||
created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_api_credentials_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
not_before_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_api_credentials_not_before_check
|
||||
CHECK (not_before_at_ms >= created_at_ms),
|
||||
expires_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_api_credentials_expires_at_check
|
||||
CHECK (expires_at_ms > not_before_at_ms),
|
||||
CONSTRAINT api_credentials_pkey
|
||||
PRIMARY KEY (credential_id, version),
|
||||
CONSTRAINT ql3_api_credentials_subject_fk
|
||||
FOREIGN KEY (subject_type, subject_id)
|
||||
REFERENCES "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_TABLE}"
|
||||
(subject_type, subject_id)
|
||||
ON DELETE RESTRICT
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_api_credentials_current_idx
|
||||
ON "ql3"."${POSTGRESQL_API_CREDENTIAL_TABLE}"
|
||||
(credential_id, version DESC)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_api_credentials_subject_idx
|
||||
ON "ql3"."${POSTGRESQL_API_CREDENTIAL_TABLE}"
|
||||
(subject_type, subject_id, credential_id, version DESC)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_SECURITY_AUDIT_EVENT_TABLE}" (
|
||||
event_id uuid PRIMARY KEY,
|
||||
request_id varchar(128) NOT NULL
|
||||
CONSTRAINT ql3_security_audit_events_request_id_check
|
||||
CHECK (request_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'),
|
||||
operation_id varchar(128) NOT NULL
|
||||
CONSTRAINT ql3_security_audit_events_operation_id_check
|
||||
CHECK (operation_id ~ '^[a-z][a-z0-9_.:-]{0,127}$'),
|
||||
project_id varchar(128)
|
||||
CONSTRAINT ql3_security_audit_events_project_id_check
|
||||
CHECK (
|
||||
project_id IS NULL
|
||||
OR project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
subject_type varchar(32)
|
||||
CONSTRAINT ql3_security_audit_events_subject_type_check
|
||||
CHECK (
|
||||
subject_type IS NULL
|
||||
OR subject_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')
|
||||
),
|
||||
subject_id varchar(255)
|
||||
CONSTRAINT ql3_security_audit_events_subject_id_check
|
||||
CHECK (subject_id IS NULL OR char_length(subject_id) >= 1),
|
||||
authentication_id varchar(128)
|
||||
CONSTRAINT ql3_security_audit_events_authentication_id_check
|
||||
CHECK (
|
||||
authentication_id IS NULL
|
||||
OR authentication_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
outcome varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_security_audit_events_outcome_check
|
||||
CHECK (
|
||||
outcome IN (
|
||||
'authentication_rejected',
|
||||
'authentication_unavailable',
|
||||
'authorization_unavailable',
|
||||
'denied',
|
||||
'approval_required',
|
||||
'allowed'
|
||||
)
|
||||
),
|
||||
reasons jsonb NOT NULL
|
||||
CONSTRAINT ql3_security_audit_events_reasons_check
|
||||
CHECK (
|
||||
jsonb_typeof(reasons) = 'array'
|
||||
AND jsonb_array_length(reasons) BETWEEN 1 AND 8
|
||||
AND octet_length(reasons::text) <= 1024
|
||||
),
|
||||
project_version integer
|
||||
CONSTRAINT ql3_security_audit_events_project_version_check
|
||||
CHECK (project_version IS NULL OR project_version >= 1),
|
||||
binding_version integer
|
||||
CONSTRAINT ql3_security_audit_events_binding_version_check
|
||||
CHECK (binding_version IS NULL OR binding_version >= 1),
|
||||
occurred_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_security_audit_events_occurred_at_check
|
||||
CHECK (occurred_at_ms >= 0),
|
||||
CONSTRAINT ql3_security_audit_events_identity_check
|
||||
CHECK (
|
||||
(
|
||||
outcome IN ('authentication_rejected', 'authentication_unavailable')
|
||||
AND subject_type IS NULL
|
||||
AND subject_id IS NULL
|
||||
AND authentication_id IS NULL
|
||||
)
|
||||
OR (
|
||||
outcome NOT IN ('authentication_rejected', 'authentication_unavailable')
|
||||
AND subject_type IS NOT NULL
|
||||
AND subject_id IS NOT NULL
|
||||
AND authentication_id IS NOT NULL
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_security_audit_events_fence_check
|
||||
CHECK (project_version IS NOT NULL OR binding_version IS NULL)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_security_audit_events_occurred_idx
|
||||
ON "ql3"."${POSTGRESQL_SECURITY_AUDIT_EVENT_TABLE}"
|
||||
(occurred_at_ms DESC, event_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_security_audit_events_subject_idx
|
||||
ON "ql3"."${POSTGRESQL_SECURITY_AUDIT_EVENT_TABLE}"
|
||||
(subject_type, subject_id, occurred_at_ms DESC, event_id)
|
||||
WHERE subject_type IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_security_audit_events_project_idx
|
||||
ON "ql3"."${POSTGRESQL_SECURITY_AUDIT_EVENT_TABLE}"
|
||||
(project_id, occurred_at_ms DESC, event_id)
|
||||
WHERE project_id IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 4,
|
||||
migration_id = 'pg-0005-api-credential-security-audit',
|
||||
capabilities = '{"api_credential":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 3
|
||||
AND migration_id = 'pg-0004-project-policy'
|
||||
AND capabilities = '{"project_policy":1,"run_core":1,"run_retry_policy":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 3'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE =
|
||||
'identity_subject_mutations';
|
||||
export const POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE =
|
||||
'api_credential_mutations';
|
||||
|
||||
export const pg0006IdentityCredentialAdministrationMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0006-identity-credential-administration',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE}" (
|
||||
mutation_id uuid PRIMARY KEY,
|
||||
operation varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_identity_subject_mutations_operation_check
|
||||
CHECK (operation IN ('import', 'register', 'enable', 'disable')),
|
||||
subject_type varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_identity_subject_mutations_subject_type_check
|
||||
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')),
|
||||
subject_id varchar(255) NOT NULL
|
||||
CONSTRAINT ql3_identity_subject_mutations_subject_id_check
|
||||
CHECK (char_length(subject_id) >= 1),
|
||||
subject_version integer NOT NULL
|
||||
CONSTRAINT ql3_identity_subject_mutations_version_check
|
||||
CHECK (subject_version BETWEEN 1 AND 2147483647),
|
||||
expected_previous_version integer NOT NULL
|
||||
CONSTRAINT ql3_identity_subject_mutations_previous_version_check
|
||||
CHECK (expected_previous_version BETWEEN 0 AND 2147483646),
|
||||
status varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_identity_subject_mutations_status_check
|
||||
CHECK (status IN ('active', 'disabled')),
|
||||
changed_by_type varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_identity_subject_mutations_changed_by_type_check
|
||||
CHECK (changed_by_type IN ('user', 'system')),
|
||||
changed_by_id varchar(255) NOT NULL
|
||||
CONSTRAINT ql3_identity_subject_mutations_changed_by_id_check
|
||||
CHECK (char_length(changed_by_id) >= 1),
|
||||
audit_event_id uuid NOT NULL,
|
||||
identity_created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_identity_subject_mutations_identity_created_at_check
|
||||
CHECK (identity_created_at_ms >= 0),
|
||||
created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_identity_subject_mutations_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
CONSTRAINT ql3_identity_subject_mutations_version_fence_check
|
||||
CHECK (subject_version = expected_previous_version + 1),
|
||||
CONSTRAINT ql3_identity_subject_mutations_transition_check
|
||||
CHECK (
|
||||
operation = 'import'
|
||||
OR (operation = 'register' AND expected_previous_version = 0 AND status = 'active')
|
||||
OR (operation = 'enable' AND expected_previous_version >= 1 AND status = 'active')
|
||||
OR (operation = 'disable' AND expected_previous_version >= 1 AND status = 'disabled')
|
||||
),
|
||||
CONSTRAINT ql3_identity_subject_mutations_audit_identity_check
|
||||
CHECK (mutation_id = audit_event_id),
|
||||
CONSTRAINT ql3_identity_subject_mutations_subject_fk
|
||||
FOREIGN KEY (subject_type, subject_id)
|
||||
REFERENCES "ql3"."identity_subjects" (subject_type, subject_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_identity_subject_mutations_audit_fk
|
||||
FOREIGN KEY (audit_event_id)
|
||||
REFERENCES "ql3"."security_audit_events" (event_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_identity_subject_mutations_subject_version_uidx
|
||||
UNIQUE (subject_type, subject_id, subject_version)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_identity_subject_mutations_actor_idx
|
||||
ON "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE}"
|
||||
(changed_by_type, changed_by_id, created_at_ms DESC, mutation_id)
|
||||
`.trim(),
|
||||
`
|
||||
WITH source AS (
|
||||
SELECT
|
||||
subject.*,
|
||||
md5('ql3-identity-import:' || subject.subject_type || ':' || subject.subject_id) AS digest
|
||||
FROM "ql3"."identity_subjects" AS subject
|
||||
), imported AS (
|
||||
SELECT
|
||||
source.*,
|
||||
(
|
||||
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
|
||||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
|
||||
substr(digest, 21, 12)
|
||||
)::uuid AS mutation_id
|
||||
FROM source
|
||||
)
|
||||
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
|
||||
)
|
||||
SELECT
|
||||
mutation_id,
|
||||
'migration:' || mutation_id::text,
|
||||
'identity.import',
|
||||
NULL,
|
||||
'system',
|
||||
'pg-0006',
|
||||
'migration:pg-0006',
|
||||
'allowed',
|
||||
'["identity_admin"]'::jsonb,
|
||||
NULL,
|
||||
NULL,
|
||||
created_at_ms
|
||||
FROM imported
|
||||
`.trim(),
|
||||
`
|
||||
WITH source AS (
|
||||
SELECT
|
||||
subject.*,
|
||||
md5('ql3-identity-import:' || subject.subject_type || ':' || subject.subject_id) AS digest
|
||||
FROM "ql3"."identity_subjects" AS subject
|
||||
), imported AS (
|
||||
SELECT
|
||||
source.*,
|
||||
(
|
||||
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
|
||||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
|
||||
substr(digest, 21, 12)
|
||||
)::uuid AS mutation_id
|
||||
FROM source
|
||||
)
|
||||
INSERT INTO "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE}" (
|
||||
mutation_id, operation, subject_type, subject_id, subject_version,
|
||||
expected_previous_version, status, changed_by_type, changed_by_id,
|
||||
audit_event_id, identity_created_at_ms, created_at_ms
|
||||
)
|
||||
SELECT
|
||||
mutation_id,
|
||||
'import',
|
||||
subject_type,
|
||||
subject_id,
|
||||
version,
|
||||
version - 1,
|
||||
status,
|
||||
'system',
|
||||
'pg-0006',
|
||||
mutation_id,
|
||||
created_at_ms,
|
||||
created_at_ms
|
||||
FROM imported
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."${POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE}" (
|
||||
mutation_id uuid PRIMARY KEY,
|
||||
operation varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_operation_check
|
||||
CHECK (operation IN ('import', 'issue', 'rotate', 'revoke')),
|
||||
credential_id varchar(64) NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_id_check
|
||||
CHECK (credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'),
|
||||
credential_version integer NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_version_check
|
||||
CHECK (credential_version BETWEEN 1 AND 2147483647),
|
||||
expected_previous_version integer NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_previous_version_check
|
||||
CHECK (expected_previous_version BETWEEN 0 AND 2147483646),
|
||||
state varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_state_check
|
||||
CHECK (state IN ('active', 'revoked')),
|
||||
subject_type varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_subject_type_check
|
||||
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent')),
|
||||
subject_id varchar(255) NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_subject_id_check
|
||||
CHECK (char_length(subject_id) >= 1),
|
||||
subject_status varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_subject_status_check
|
||||
CHECK (subject_status IN ('active', 'disabled')),
|
||||
changed_by_type varchar(32) NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_changed_by_type_check
|
||||
CHECK (changed_by_type IN ('user', 'system')),
|
||||
changed_by_id varchar(255) NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_changed_by_id_check
|
||||
CHECK (char_length(changed_by_id) >= 1),
|
||||
audit_event_id uuid NOT NULL,
|
||||
created_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_api_credential_mutations_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
CONSTRAINT ql3_api_credential_mutations_version_fence_check
|
||||
CHECK (credential_version = expected_previous_version + 1),
|
||||
CONSTRAINT ql3_api_credential_mutations_transition_check
|
||||
CHECK (
|
||||
operation = 'import'
|
||||
OR (operation = 'issue' AND expected_previous_version = 0 AND state = 'active')
|
||||
OR (operation = 'rotate' AND expected_previous_version >= 1 AND state = 'active')
|
||||
OR (operation = 'revoke' AND expected_previous_version >= 1 AND state = 'revoked')
|
||||
),
|
||||
CONSTRAINT ql3_api_credential_mutations_audit_identity_check
|
||||
CHECK (mutation_id = audit_event_id),
|
||||
CONSTRAINT ql3_api_credential_mutations_credential_fk
|
||||
FOREIGN KEY (credential_id, credential_version)
|
||||
REFERENCES "ql3"."api_credentials" (credential_id, version)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_api_credential_mutations_subject_fk
|
||||
FOREIGN KEY (subject_type, subject_id)
|
||||
REFERENCES "ql3"."identity_subjects" (subject_type, subject_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_api_credential_mutations_audit_fk
|
||||
FOREIGN KEY (audit_event_id)
|
||||
REFERENCES "ql3"."security_audit_events" (event_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_api_credential_mutations_credential_version_uidx
|
||||
UNIQUE (credential_id, credential_version)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_api_credential_mutations_actor_idx
|
||||
ON "ql3"."${POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE}"
|
||||
(changed_by_type, changed_by_id, created_at_ms DESC, mutation_id)
|
||||
`.trim(),
|
||||
`
|
||||
WITH source AS (
|
||||
SELECT
|
||||
credential.*,
|
||||
md5('ql3-credential-import:' || credential.credential_id || ':' || credential.version::text) AS digest
|
||||
FROM "ql3"."api_credentials" AS credential
|
||||
), imported AS (
|
||||
SELECT
|
||||
source.*,
|
||||
(
|
||||
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
|
||||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
|
||||
substr(digest, 21, 12)
|
||||
)::uuid AS mutation_id
|
||||
FROM source
|
||||
)
|
||||
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
|
||||
)
|
||||
SELECT
|
||||
mutation_id,
|
||||
'migration:' || mutation_id::text,
|
||||
'credential.import',
|
||||
NULL,
|
||||
'system',
|
||||
'pg-0006',
|
||||
'migration:pg-0006',
|
||||
'allowed',
|
||||
'["credential_admin"]'::jsonb,
|
||||
NULL,
|
||||
NULL,
|
||||
created_at_ms
|
||||
FROM imported
|
||||
`.trim(),
|
||||
`
|
||||
WITH source AS (
|
||||
SELECT
|
||||
credential.*,
|
||||
md5('ql3-credential-import:' || credential.credential_id || ':' || credential.version::text) AS digest
|
||||
FROM "ql3"."api_credentials" AS credential
|
||||
), imported AS (
|
||||
SELECT
|
||||
source.*,
|
||||
(
|
||||
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
|
||||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
|
||||
substr(digest, 21, 12)
|
||||
)::uuid AS mutation_id
|
||||
FROM source
|
||||
)
|
||||
INSERT INTO "ql3"."${POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE}" (
|
||||
mutation_id, operation, credential_id, credential_version,
|
||||
expected_previous_version, state, subject_type, subject_id,
|
||||
subject_status, changed_by_type, changed_by_id, audit_event_id, created_at_ms
|
||||
)
|
||||
SELECT
|
||||
mutation_id,
|
||||
'import',
|
||||
credential_id,
|
||||
version,
|
||||
version - 1,
|
||||
state,
|
||||
subject_type,
|
||||
subject_id,
|
||||
(
|
||||
SELECT identity.status
|
||||
FROM "ql3"."identity_subjects" AS identity
|
||||
WHERE identity.subject_type = imported.subject_type
|
||||
AND identity.subject_id = imported.subject_id
|
||||
),
|
||||
'system',
|
||||
'pg-0006',
|
||||
mutation_id,
|
||||
created_at_ms
|
||||
FROM imported
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 5,
|
||||
migration_id = 'pg-0006-identity-credential-administration',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 4
|
||||
AND migration_id = 'pg-0005-api-credential-security-audit'
|
||||
AND capabilities = '{"api_credential":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 4'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0007ClusterRecoveryIndexesMigration = definePostgresSqlMigration(
|
||||
{
|
||||
id: 'pg-0007-cluster-recovery-indexes',
|
||||
statements: [
|
||||
`
|
||||
CREATE INDEX ql3_runs_runtime_recovery_idx
|
||||
ON "ql3"."runs" (created_at_ms, id)
|
||||
WHERE execution_owner = 'runtime'
|
||||
AND status IN ('created', 'dispatching', 'running')
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_run_attempts_recovery_idx
|
||||
ON "ql3"."run_attempts" (lease_expires_at_ms, created_at_ms, id)
|
||||
WHERE status IN ('claimed', 'starting', 'running')
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 6,
|
||||
migration_id = 'pg-0007-cluster-recovery-indexes',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 5
|
||||
AND migration_id = 'pg-0006-identity-credential-administration'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 5'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,148 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0008RunRecoveryClaimsMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0008-run-recovery-claims',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."run_recovery_controls" (
|
||||
target_kind varchar(16) NOT NULL,
|
||||
target_id varchar(36) NOT NULL,
|
||||
run_id varchar(36) NOT NULL,
|
||||
attempt_id varchar(36),
|
||||
target_status varchar(32) NOT NULL,
|
||||
target_created_at_ms bigint NOT NULL,
|
||||
observed_at_ms bigint NOT NULL,
|
||||
state varchar(16) NOT NULL,
|
||||
claim_owner varchar(128),
|
||||
claim_token varchar(64),
|
||||
claim_version integer NOT NULL DEFAULT 0,
|
||||
claim_expires_at_ms bigint,
|
||||
next_claim_at_ms bigint,
|
||||
failure_count integer NOT NULL DEFAULT 0,
|
||||
created_at_ms bigint NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT run_recovery_controls_pkey
|
||||
PRIMARY KEY (target_kind, target_id),
|
||||
CONSTRAINT ql3_run_recovery_controls_target_kind_check
|
||||
CHECK (target_kind IN ('run', 'attempt')),
|
||||
CONSTRAINT ql3_run_recovery_controls_target_id_check
|
||||
CHECK (char_length(target_id) >= 1),
|
||||
CONSTRAINT ql3_run_recovery_controls_target_shape_check
|
||||
CHECK (
|
||||
(
|
||||
target_kind = 'run'
|
||||
AND target_id = run_id
|
||||
AND attempt_id IS NULL
|
||||
AND target_status IN ('created', 'dispatching', 'running')
|
||||
)
|
||||
OR (
|
||||
target_kind = 'attempt'
|
||||
AND target_id = attempt_id
|
||||
AND attempt_id IS NOT NULL
|
||||
AND target_status IN ('claimed', 'starting', 'running')
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_run_recovery_controls_target_created_at_check
|
||||
CHECK (target_created_at_ms >= 0),
|
||||
CONSTRAINT ql3_run_recovery_controls_observed_at_check
|
||||
CHECK (observed_at_ms >= 0),
|
||||
CONSTRAINT ql3_run_recovery_controls_state_check
|
||||
CHECK (state IN ('available', 'claimed', 'retry', 'manual', 'resolved')),
|
||||
CONSTRAINT ql3_run_recovery_controls_claim_owner_check
|
||||
CHECK (
|
||||
claim_owner IS NULL
|
||||
OR claim_owner ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_run_recovery_controls_claim_token_check
|
||||
CHECK (
|
||||
claim_token IS NULL
|
||||
OR claim_token ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{15,63}$'
|
||||
),
|
||||
CONSTRAINT ql3_run_recovery_controls_claim_version_check
|
||||
CHECK (claim_version BETWEEN 0 AND 2147483647),
|
||||
CONSTRAINT ql3_run_recovery_controls_claim_expires_at_check
|
||||
CHECK (claim_expires_at_ms IS NULL OR claim_expires_at_ms >= 0),
|
||||
CONSTRAINT ql3_run_recovery_controls_next_claim_at_check
|
||||
CHECK (next_claim_at_ms IS NULL OR next_claim_at_ms >= 0),
|
||||
CONSTRAINT ql3_run_recovery_controls_failure_count_check
|
||||
CHECK (failure_count BETWEEN 0 AND 2147483647),
|
||||
CONSTRAINT ql3_run_recovery_controls_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
CONSTRAINT ql3_run_recovery_controls_updated_at_check
|
||||
CHECK (updated_at_ms >= created_at_ms),
|
||||
CONSTRAINT ql3_run_recovery_controls_state_shape_check
|
||||
CHECK (
|
||||
(
|
||||
state = 'claimed'
|
||||
AND claim_owner IS NOT NULL
|
||||
AND claim_token IS NOT NULL
|
||||
AND claim_expires_at_ms IS NOT NULL
|
||||
AND next_claim_at_ms IS NULL
|
||||
)
|
||||
OR (
|
||||
state = 'retry'
|
||||
AND claim_owner IS NULL
|
||||
AND claim_token IS NULL
|
||||
AND claim_expires_at_ms IS NULL
|
||||
AND next_claim_at_ms IS NOT NULL
|
||||
)
|
||||
OR (
|
||||
state IN ('available', 'manual', 'resolved')
|
||||
AND claim_owner IS NULL
|
||||
AND claim_token IS NULL
|
||||
AND claim_expires_at_ms IS NULL
|
||||
AND next_claim_at_ms IS NULL
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_run_recovery_controls_run_fk
|
||||
FOREIGN KEY (run_id)
|
||||
REFERENCES "ql3"."runs" (id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_run_recovery_controls_attempt_fk
|
||||
FOREIGN KEY (attempt_id)
|
||||
REFERENCES "ql3"."run_attempts" (id)
|
||||
ON DELETE CASCADE
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_run_recovery_controls_available_idx
|
||||
ON "ql3"."run_recovery_controls"
|
||||
(target_created_at_ms, target_kind, target_id)
|
||||
WHERE state IN ('available', 'resolved')
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_run_recovery_controls_retry_idx
|
||||
ON "ql3"."run_recovery_controls"
|
||||
(next_claim_at_ms, target_created_at_ms, target_kind, target_id)
|
||||
WHERE state = 'retry'
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_run_recovery_controls_claim_expiry_idx
|
||||
ON "ql3"."run_recovery_controls"
|
||||
(claim_expires_at_ms, target_created_at_ms, target_kind, target_id)
|
||||
WHERE state = 'claimed'
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 7,
|
||||
migration_id = 'pg-0008-run-recovery-claims',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 6
|
||||
AND migration_id = 'pg-0007-cluster-recovery-indexes'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 6'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0009WorkerSessionRunLeaseMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0009-worker-session-run-lease',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."worker_sessions" (
|
||||
worker_id varchar(128) PRIMARY KEY,
|
||||
session_id varchar(36) NOT NULL,
|
||||
generation integer NOT NULL,
|
||||
status varchar(16) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
capabilities_json varchar(16384) NOT NULL,
|
||||
capabilities_hash char(64) NOT NULL,
|
||||
max_concurrent_runs integer NOT NULL,
|
||||
available_slots integer NOT NULL,
|
||||
registered_at_ms bigint NOT NULL,
|
||||
last_heartbeat_at_ms bigint NOT NULL,
|
||||
lease_expires_at_ms bigint NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT ql3_worker_sessions_worker_id_check
|
||||
CHECK (worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'),
|
||||
CONSTRAINT ql3_worker_sessions_session_id_check
|
||||
CHECK (session_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
|
||||
CONSTRAINT ql3_worker_sessions_generation_check
|
||||
CHECK (generation BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_worker_sessions_status_check
|
||||
CHECK (status IN ('online', 'draining', 'offline')),
|
||||
CONSTRAINT ql3_worker_sessions_version_check
|
||||
CHECK (version BETWEEN 0 AND 2147483647),
|
||||
CONSTRAINT ql3_worker_sessions_capabilities_check
|
||||
CHECK (octet_length(capabilities_json) BETWEEN 2 AND 16384),
|
||||
CONSTRAINT ql3_worker_sessions_capabilities_hash_check
|
||||
CHECK (capabilities_hash ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT ql3_worker_sessions_concurrency_check
|
||||
CHECK (
|
||||
max_concurrent_runs BETWEEN 1 AND 1024
|
||||
AND available_slots BETWEEN 0 AND max_concurrent_runs
|
||||
),
|
||||
CONSTRAINT ql3_worker_sessions_timestamps_check
|
||||
CHECK (
|
||||
registered_at_ms >= 0
|
||||
AND last_heartbeat_at_ms >= registered_at_ms
|
||||
AND updated_at_ms >= last_heartbeat_at_ms
|
||||
AND (
|
||||
(status = 'offline' AND lease_expires_at_ms >= last_heartbeat_at_ms)
|
||||
OR (status <> 'offline' AND lease_expires_at_ms > last_heartbeat_at_ms)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_worker_sessions_status_capacity_check
|
||||
CHECK (status = 'online' OR available_slots = 0)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_worker_sessions_available_idx
|
||||
ON "ql3"."worker_sessions" (worker_id)
|
||||
WHERE status = 'online' AND available_slots > 0
|
||||
`.trim(),
|
||||
`
|
||||
ALTER TABLE "ql3"."run_attempts"
|
||||
ADD COLUMN worker_session_id varchar(36),
|
||||
ADD COLUMN worker_generation integer,
|
||||
ADD COLUMN lease_generation integer,
|
||||
ADD COLUMN lease_version integer,
|
||||
ADD COLUMN lease_token_digest char(64),
|
||||
ADD COLUMN offer_id varchar(128),
|
||||
ADD CONSTRAINT ql3_run_attempts_worker_session_id_check
|
||||
CHECK (
|
||||
worker_session_id IS NULL
|
||||
OR worker_session_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
|
||||
),
|
||||
ADD CONSTRAINT ql3_run_attempts_worker_generation_check
|
||||
CHECK (worker_generation IS NULL OR worker_generation BETWEEN 1 AND 2147483647),
|
||||
ADD CONSTRAINT ql3_run_attempts_lease_generation_check
|
||||
CHECK (lease_generation IS NULL OR lease_generation BETWEEN 1 AND 2147483647),
|
||||
ADD CONSTRAINT ql3_run_attempts_lease_version_check
|
||||
CHECK (lease_version IS NULL OR lease_version BETWEEN 0 AND 2147483647),
|
||||
ADD CONSTRAINT ql3_run_attempts_lease_token_digest_check
|
||||
CHECK (lease_token_digest IS NULL OR lease_token_digest ~ '^[0-9a-f]{64}$'),
|
||||
ADD CONSTRAINT ql3_run_attempts_offer_id_check
|
||||
CHECK (offer_id IS NULL OR char_length(offer_id) BETWEEN 1 AND 128),
|
||||
ADD CONSTRAINT ql3_run_attempts_remote_fence_shape_check
|
||||
CHECK (
|
||||
(
|
||||
worker_session_id IS NULL
|
||||
AND worker_generation IS NULL
|
||||
AND lease_generation IS NULL
|
||||
AND lease_version IS NULL
|
||||
AND lease_token_digest IS NULL
|
||||
AND offer_id IS NULL
|
||||
)
|
||||
OR (
|
||||
worker_id IS NOT NULL
|
||||
AND worker_session_id IS NOT NULL
|
||||
AND worker_generation IS NOT NULL
|
||||
AND lease_generation IS NOT NULL
|
||||
AND lease_version IS NOT NULL
|
||||
AND lease_token_digest IS NOT NULL
|
||||
AND offer_id IS NOT NULL
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."run_dispatch_leases" (
|
||||
attempt_id varchar(36) PRIMARY KEY,
|
||||
run_id varchar(36) NOT NULL,
|
||||
status varchar(16) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
lease_generation integer NOT NULL,
|
||||
worker_id varchar(128) NOT NULL,
|
||||
worker_session_id varchar(36) NOT NULL,
|
||||
worker_generation integer NOT NULL,
|
||||
lease_token_digest char(64) NOT NULL,
|
||||
offer_id varchar(128) NOT NULL,
|
||||
acquired_at_ms bigint NOT NULL,
|
||||
renewed_at_ms bigint NOT NULL,
|
||||
expires_at_ms bigint NOT NULL,
|
||||
released_at_ms bigint,
|
||||
release_reason varchar(32),
|
||||
completed_at_ms bigint,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT ql3_run_dispatch_leases_status_check
|
||||
CHECK (status IN ('leased', 'released', 'completed')),
|
||||
CONSTRAINT ql3_run_dispatch_leases_version_check
|
||||
CHECK (version BETWEEN 0 AND 2147483647),
|
||||
CONSTRAINT ql3_run_dispatch_leases_generation_check
|
||||
CHECK (lease_generation BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_run_dispatch_leases_worker_generation_check
|
||||
CHECK (worker_generation BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_run_dispatch_leases_token_digest_check
|
||||
CHECK (lease_token_digest ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT ql3_run_dispatch_leases_offer_id_check
|
||||
CHECK (char_length(offer_id) BETWEEN 1 AND 128),
|
||||
CONSTRAINT ql3_run_dispatch_leases_timestamps_check
|
||||
CHECK (
|
||||
acquired_at_ms >= 0
|
||||
AND renewed_at_ms >= acquired_at_ms
|
||||
AND expires_at_ms > renewed_at_ms
|
||||
AND updated_at_ms >= acquired_at_ms
|
||||
),
|
||||
CONSTRAINT ql3_run_dispatch_leases_state_shape_check
|
||||
CHECK (
|
||||
(
|
||||
status = 'leased'
|
||||
AND released_at_ms IS NULL
|
||||
AND release_reason IS NULL
|
||||
AND completed_at_ms IS NULL
|
||||
)
|
||||
OR (
|
||||
status = 'released'
|
||||
AND released_at_ms IS NOT NULL
|
||||
AND release_reason IN (
|
||||
'declined', 'shutdown', 'start_failed', 'capacity_changed', 'lease_expired'
|
||||
)
|
||||
AND completed_at_ms IS NULL
|
||||
)
|
||||
OR (
|
||||
status = 'completed'
|
||||
AND released_at_ms IS NULL
|
||||
AND release_reason IS NULL
|
||||
AND completed_at_ms IS NOT NULL
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_run_dispatch_leases_attempt_fk
|
||||
FOREIGN KEY (attempt_id) REFERENCES "ql3"."run_attempts" (id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_run_dispatch_leases_run_fk
|
||||
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_run_dispatch_leases_worker_fk
|
||||
FOREIGN KEY (worker_id) REFERENCES "ql3"."worker_sessions" (worker_id)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_run_dispatch_leases_offer_uidx
|
||||
ON "ql3"."run_dispatch_leases" (offer_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_run_dispatch_leases_worker_active_idx
|
||||
ON "ql3"."run_dispatch_leases"
|
||||
(worker_id, worker_session_id, worker_generation, expires_at_ms, attempt_id)
|
||||
WHERE status = 'leased'
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_run_dispatch_leases_expiry_idx
|
||||
ON "ql3"."run_dispatch_leases" (expires_at_ms, attempt_id)
|
||||
WHERE status = 'leased'
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 8,
|
||||
migration_id = 'pg-0009-worker-session-run-lease',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 7
|
||||
AND migration_id = 'pg-0008-run-recovery-claims'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 7'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0010WorkerIngressAttestationMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0010-worker-ingress-attestation',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."worker_credentials" (
|
||||
credential_id varchar(64) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
state varchar(16) NOT NULL,
|
||||
worker_id varchar(128) NOT NULL,
|
||||
secret_digest char(64) NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
not_before_at_ms bigint NOT NULL,
|
||||
expires_at_ms bigint NOT NULL,
|
||||
PRIMARY KEY (credential_id, version),
|
||||
CONSTRAINT ql3_worker_credentials_id_check
|
||||
CHECK (credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'),
|
||||
CONSTRAINT ql3_worker_credentials_version_check
|
||||
CHECK (version BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_worker_credentials_state_check
|
||||
CHECK (state IN ('active', 'revoked')),
|
||||
CONSTRAINT ql3_worker_credentials_worker_id_check
|
||||
CHECK (worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'),
|
||||
CONSTRAINT ql3_worker_credentials_digest_check
|
||||
CHECK (secret_digest ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT ql3_worker_credentials_lifetime_check
|
||||
CHECK (
|
||||
created_at_ms >= 0
|
||||
AND not_before_at_ms >= created_at_ms
|
||||
AND expires_at_ms > not_before_at_ms
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_worker_credentials_latest_idx
|
||||
ON "ql3"."worker_credentials" (credential_id, version DESC)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."worker_credential_mutations" (
|
||||
mutation_id varchar(36) PRIMARY KEY,
|
||||
operation varchar(16) NOT NULL,
|
||||
credential_id varchar(64) NOT NULL,
|
||||
credential_version integer NOT NULL,
|
||||
expected_previous_version integer NOT NULL,
|
||||
changed_by_type varchar(32) NOT NULL,
|
||||
changed_by_id varchar(255) NOT NULL,
|
||||
audit_event_id uuid NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
CONSTRAINT ql3_worker_credential_mutations_id_check
|
||||
CHECK (mutation_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
|
||||
CONSTRAINT ql3_worker_credential_mutations_operation_check
|
||||
CHECK (operation IN ('issue', 'rotate', 'revoke')),
|
||||
CONSTRAINT ql3_worker_credential_mutations_version_check
|
||||
CHECK (
|
||||
credential_version BETWEEN 1 AND 2147483647
|
||||
AND expected_previous_version BETWEEN 0 AND 2147483646
|
||||
AND credential_version = expected_previous_version + 1
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_mutations_actor_check
|
||||
CHECK (
|
||||
changed_by_type IN ('user', 'system')
|
||||
AND char_length(changed_by_id) BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_mutations_created_at_check
|
||||
CHECK (created_at_ms >= 0),
|
||||
CONSTRAINT ql3_worker_credential_mutations_credential_fk
|
||||
FOREIGN KEY (credential_id, credential_version)
|
||||
REFERENCES "ql3"."worker_credentials" (credential_id, version),
|
||||
CONSTRAINT ql3_worker_credential_mutations_audit_fk
|
||||
FOREIGN KEY (audit_event_id)
|
||||
REFERENCES "ql3"."security_audit_events" (event_id)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_worker_credential_mutations_audit_uidx
|
||||
ON "ql3"."worker_credential_mutations" (audit_event_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."worker_execution_attestations" (
|
||||
attestation_id varchar(36) PRIMARY KEY,
|
||||
run_id varchar(36) NOT NULL,
|
||||
attempt_id varchar(36) NOT NULL,
|
||||
sequence integer NOT NULL,
|
||||
state varchar(16) NOT NULL,
|
||||
worker_id varchar(128) NOT NULL,
|
||||
worker_session_id varchar(36) NOT NULL,
|
||||
worker_generation integer NOT NULL,
|
||||
lease_token_digest char(64) NOT NULL,
|
||||
lease_generation integer NOT NULL,
|
||||
lease_version integer NOT NULL,
|
||||
offer_id varchar(128) NOT NULL,
|
||||
callback_sequence integer NOT NULL,
|
||||
executor_handle varchar(512) NOT NULL,
|
||||
journal_revision integer NOT NULL,
|
||||
received_at_ms bigint NOT NULL,
|
||||
CONSTRAINT ql3_worker_execution_attestations_id_check
|
||||
CHECK (attestation_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
|
||||
CONSTRAINT ql3_worker_execution_attestations_sequence_check
|
||||
CHECK (sequence BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_worker_execution_attestations_state_check
|
||||
CHECK (state IN ('running', 'stopped')),
|
||||
CONSTRAINT ql3_worker_execution_attestations_worker_id_check
|
||||
CHECK (worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'),
|
||||
CONSTRAINT ql3_worker_execution_attestations_session_id_check
|
||||
CHECK (worker_session_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'),
|
||||
CONSTRAINT ql3_worker_execution_attestations_generation_check
|
||||
CHECK (
|
||||
worker_generation BETWEEN 1 AND 2147483647
|
||||
AND lease_generation BETWEEN 1 AND 2147483647
|
||||
AND lease_version BETWEEN 0 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_worker_execution_attestations_digest_check
|
||||
CHECK (lease_token_digest ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT ql3_worker_execution_attestations_offer_check
|
||||
CHECK (char_length(offer_id) BETWEEN 1 AND 128),
|
||||
CONSTRAINT ql3_worker_execution_attestations_callback_check
|
||||
CHECK (callback_sequence BETWEEN 0 AND 2147483647),
|
||||
CONSTRAINT ql3_worker_execution_attestations_handle_check
|
||||
CHECK (char_length(executor_handle) BETWEEN 1 AND 512),
|
||||
CONSTRAINT ql3_worker_execution_attestations_journal_check
|
||||
CHECK (journal_revision BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_worker_execution_attestations_received_at_check
|
||||
CHECK (received_at_ms >= 0),
|
||||
CONSTRAINT ql3_worker_execution_attestations_run_fk
|
||||
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_worker_execution_attestations_attempt_fk
|
||||
FOREIGN KEY (attempt_id) REFERENCES "ql3"."run_attempts" (id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_worker_execution_attestations_worker_fk
|
||||
FOREIGN KEY (worker_id) REFERENCES "ql3"."worker_sessions" (worker_id)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_worker_execution_attestations_sequence_uidx
|
||||
ON "ql3"."worker_execution_attestations"
|
||||
(attempt_id, lease_generation, sequence)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_worker_execution_attestations_exact_idx
|
||||
ON "ql3"."worker_execution_attestations"
|
||||
(attempt_id, lease_generation, sequence DESC)
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 9,
|
||||
migration_id = 'pg-0010-worker-ingress-attestation',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 8
|
||||
AND migration_id = 'pg-0009-worker-session-run-lease'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 8'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0011ApiCredentialPepperBindingMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0011-api-credential-pepper-binding',
|
||||
statements: [
|
||||
`
|
||||
ALTER TABLE "ql3"."api_credentials"
|
||||
ADD COLUMN pepper_key_id varchar(64) NOT NULL DEFAULT 'legacy-v1'
|
||||
`.trim(),
|
||||
`
|
||||
ALTER TABLE "ql3"."api_credentials"
|
||||
ADD CONSTRAINT ql3_api_credentials_pepper_key_id_check
|
||||
CHECK (pepper_key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$')
|
||||
`.trim(),
|
||||
`
|
||||
ALTER TABLE "ql3"."api_credentials"
|
||||
ALTER COLUMN pepper_key_id DROP DEFAULT
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 10,
|
||||
migration_id = 'pg-0011-api-credential-pepper-binding',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 9
|
||||
AND migration_id = 'pg-0010-worker-ingress-attestation'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 9'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0012TaskTriggerDefinitionsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0012-task-trigger-definitions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."task_definitions" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
current_revision integer NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT task_definitions_pkey PRIMARY KEY (project_id, task_id),
|
||||
CONSTRAINT ql3_task_definitions_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_task_definitions_id_check CHECK (
|
||||
char_length(project_id) BETWEEN 1 AND 128
|
||||
AND char_length(task_id) BETWEEN 1 AND 128
|
||||
AND project_id !~ '[[:cntrl:]]'
|
||||
AND task_id !~ '[[:cntrl:]]'
|
||||
),
|
||||
CONSTRAINT ql3_task_definitions_revision_check
|
||||
CHECK (current_revision BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_task_definitions_time_check
|
||||
CHECK (created_at_ms >= 0 AND updated_at_ms >= created_at_ms)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."task_definition_revisions" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
revision integer NOT NULL,
|
||||
mutation_id uuid NOT NULL,
|
||||
name varchar(255) NOT NULL,
|
||||
description varchar(4096),
|
||||
kind varchar(16) NOT NULL,
|
||||
spec_json jsonb NOT NULL,
|
||||
labels_json jsonb NOT NULL,
|
||||
enabled boolean NOT NULL,
|
||||
content_digest char(64) NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
CONSTRAINT task_definition_revisions_pkey
|
||||
PRIMARY KEY (project_id, task_id, revision),
|
||||
CONSTRAINT ql3_task_definition_revisions_head_fk
|
||||
FOREIGN KEY (project_id, task_id)
|
||||
REFERENCES "ql3"."task_definitions" (project_id, task_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_task_definition_revisions_revision_check
|
||||
CHECK (revision BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_task_definition_revisions_name_check CHECK (
|
||||
char_length(name) BETWEEN 1 AND 255
|
||||
AND (description IS NULL OR char_length(description) BETWEEN 1 AND 4096)
|
||||
),
|
||||
CONSTRAINT ql3_task_definition_revisions_kind_check
|
||||
CHECK (kind IN ('script', 'command', 'workflow', 'agent', 'tool')),
|
||||
CONSTRAINT ql3_task_definition_revisions_spec_check CHECK (
|
||||
jsonb_typeof(spec_json) = 'object'
|
||||
AND octet_length(spec_json::text) BETWEEN 2 AND 65536
|
||||
),
|
||||
CONSTRAINT ql3_task_definition_revisions_labels_check CHECK (
|
||||
jsonb_typeof(labels_json) = 'object'
|
||||
AND octet_length(labels_json::text) BETWEEN 2 AND 16384
|
||||
),
|
||||
CONSTRAINT ql3_task_definition_revisions_digest_check
|
||||
CHECK (content_digest ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT ql3_task_definition_revisions_created_check
|
||||
CHECK (created_at_ms >= 0)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_task_definition_revisions_mutation_uidx
|
||||
ON "ql3"."task_definition_revisions" (mutation_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_task_definition_revisions_project_kind_idx
|
||||
ON "ql3"."task_definition_revisions"
|
||||
(project_id, kind, enabled, task_id, revision)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."triggers" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
trigger_id varchar(128) NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
current_revision integer NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT triggers_pkey PRIMARY KEY (project_id, trigger_id),
|
||||
CONSTRAINT ql3_triggers_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_triggers_task_fk
|
||||
FOREIGN KEY (project_id, task_id)
|
||||
REFERENCES "ql3"."task_definitions" (project_id, task_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_triggers_id_check CHECK (
|
||||
char_length(project_id) BETWEEN 1 AND 128
|
||||
AND char_length(trigger_id) BETWEEN 1 AND 128
|
||||
AND char_length(task_id) BETWEEN 1 AND 128
|
||||
AND project_id !~ '[[:cntrl:]]'
|
||||
AND trigger_id !~ '[[:cntrl:]]'
|
||||
AND task_id !~ '[[:cntrl:]]'
|
||||
),
|
||||
CONSTRAINT ql3_triggers_revision_check
|
||||
CHECK (current_revision BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_triggers_time_check
|
||||
CHECK (created_at_ms >= 0 AND updated_at_ms >= created_at_ms)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_triggers_task_uidx
|
||||
ON "ql3"."triggers" (project_id, trigger_id, task_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."trigger_revisions" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
trigger_id varchar(128) NOT NULL,
|
||||
revision integer NOT NULL,
|
||||
mutation_id uuid NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
task_revision integer NOT NULL,
|
||||
task_content_digest char(64) NOT NULL,
|
||||
spec_json jsonb NOT NULL,
|
||||
enabled boolean NOT NULL,
|
||||
content_digest char(64) NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
CONSTRAINT trigger_revisions_pkey
|
||||
PRIMARY KEY (project_id, trigger_id, revision),
|
||||
CONSTRAINT ql3_trigger_revisions_head_fk
|
||||
FOREIGN KEY (project_id, trigger_id, task_id)
|
||||
REFERENCES "ql3"."triggers" (project_id, trigger_id, task_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_trigger_revisions_task_revision_fk
|
||||
FOREIGN KEY (project_id, task_id, task_revision)
|
||||
REFERENCES "ql3"."task_definition_revisions" (project_id, task_id, revision)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_trigger_revisions_revision_check CHECK (
|
||||
revision BETWEEN 1 AND 2147483647
|
||||
AND task_revision BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_trigger_revisions_task_digest_check
|
||||
CHECK (task_content_digest ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT ql3_trigger_revisions_spec_check CHECK (
|
||||
jsonb_typeof(spec_json) = 'object'
|
||||
AND octet_length(spec_json::text) BETWEEN 2 AND 16384
|
||||
),
|
||||
CONSTRAINT ql3_trigger_revisions_digest_check
|
||||
CHECK (content_digest ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT ql3_trigger_revisions_created_check
|
||||
CHECK (created_at_ms >= 0)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_trigger_revisions_mutation_uidx
|
||||
ON "ql3"."trigger_revisions" (mutation_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_trigger_revisions_project_enabled_idx
|
||||
ON "ql3"."trigger_revisions"
|
||||
(project_id, enabled, trigger_id, revision)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_trigger_revisions_task_idx
|
||||
ON "ql3"."trigger_revisions"
|
||||
(project_id, task_id, task_revision, trigger_id, revision)
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 11,
|
||||
migration_id = 'pg-0012-task-trigger-definitions',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 10
|
||||
AND migration_id = 'pg-0011-api-credential-pepper-binding'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 10'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0013TaskExecutionRevisionsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0013-task-execution-revisions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."task_execution_revisions" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
source_revision integer NOT NULL,
|
||||
task_revision varchar(96) NOT NULL,
|
||||
source_content_digest char(64) NOT NULL,
|
||||
executor_type varchar(32) NOT NULL,
|
||||
plan_schema varchar(64) NOT NULL,
|
||||
plan_json jsonb NOT NULL,
|
||||
content_digest char(64) NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
CONSTRAINT task_execution_revisions_pkey
|
||||
PRIMARY KEY (project_id, task_id, source_revision, executor_type),
|
||||
CONSTRAINT ql3_task_execution_revisions_source_fk
|
||||
FOREIGN KEY (project_id, task_id, source_revision)
|
||||
REFERENCES "ql3"."task_definition_revisions"
|
||||
(project_id, task_id, revision)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_task_execution_revisions_identity_check CHECK (
|
||||
task_revision = concat(
|
||||
'qltd:v1:', source_revision::text, ':', source_content_digest
|
||||
)
|
||||
AND source_content_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_task_execution_revisions_executor_check
|
||||
CHECK (executor_type = 'remote_worker'),
|
||||
CONSTRAINT ql3_task_execution_revisions_plan_check CHECK (
|
||||
plan_schema = 'qinglong/command-execution@v1'
|
||||
AND jsonb_typeof(plan_json) = 'object'
|
||||
AND octet_length(plan_json::text) BETWEEN 2 AND 98304
|
||||
),
|
||||
CONSTRAINT ql3_task_execution_revisions_digest_check
|
||||
CHECK (content_digest ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT ql3_task_execution_revisions_created_check
|
||||
CHECK (created_at_ms >= 0)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_task_execution_revisions_ref_uidx
|
||||
ON "ql3"."task_execution_revisions"
|
||||
(project_id, task_id, task_revision, executor_type)
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 12,
|
||||
migration_id = 'pg-0013-task-execution-revisions',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 11
|
||||
AND migration_id = 'pg-0012-task-trigger-definitions'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 11'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0014ClusterSchedulerAdmissionMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0014-cluster-scheduler-admission',
|
||||
statements: [
|
||||
`
|
||||
ALTER TABLE "ql3"."runs"
|
||||
ALTER COLUMN trigger_id TYPE varchar(128)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."trigger_schedules" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
trigger_id varchar(128) NOT NULL,
|
||||
trigger_revision integer NOT NULL,
|
||||
next_fire_at_ms bigint,
|
||||
last_scheduled_at_ms bigint,
|
||||
state_version integer NOT NULL,
|
||||
claim_owner varchar(128),
|
||||
claim_token uuid,
|
||||
claim_version integer NOT NULL,
|
||||
claim_expires_at_ms bigint,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT trigger_schedules_pkey PRIMARY KEY (project_id, trigger_id),
|
||||
CONSTRAINT ql3_trigger_schedules_revision_fk
|
||||
FOREIGN KEY (project_id, trigger_id, trigger_revision)
|
||||
REFERENCES "ql3"."trigger_revisions" (project_id, trigger_id, revision)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_trigger_schedules_revision_check
|
||||
CHECK (trigger_revision BETWEEN 1 AND 2147483647),
|
||||
CONSTRAINT ql3_trigger_schedules_cursor_check CHECK (
|
||||
next_fire_at_ms IS NULL OR next_fire_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_trigger_schedules_last_check CHECK (
|
||||
last_scheduled_at_ms IS NULL OR last_scheduled_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_trigger_schedules_version_check CHECK (
|
||||
state_version BETWEEN 0 AND 2147483647
|
||||
AND claim_version BETWEEN 0 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_trigger_schedules_claim_owner_check CHECK (
|
||||
claim_owner IS NULL
|
||||
OR (
|
||||
char_length(claim_owner) BETWEEN 1 AND 128
|
||||
AND claim_owner ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_trigger_schedules_claim_shape_check CHECK (
|
||||
(
|
||||
claim_owner IS NULL
|
||||
AND claim_token IS NULL
|
||||
AND claim_expires_at_ms IS NULL
|
||||
)
|
||||
OR (
|
||||
claim_owner IS NOT NULL
|
||||
AND claim_token IS NOT NULL
|
||||
AND claim_version >= 1
|
||||
AND claim_expires_at_ms IS NOT NULL
|
||||
AND claim_expires_at_ms > updated_at_ms
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_trigger_schedules_updated_check CHECK (updated_at_ms >= 0)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
INSERT INTO "ql3"."trigger_schedules" (
|
||||
project_id, trigger_id, trigger_revision, next_fire_at_ms,
|
||||
last_scheduled_at_ms, state_version, claim_owner, claim_token,
|
||||
claim_version, claim_expires_at_ms, updated_at_ms
|
||||
)
|
||||
SELECT
|
||||
project_id, trigger_id, current_revision, NULL, NULL, 0,
|
||||
NULL, NULL, 0, NULL, updated_at_ms
|
||||
FROM "ql3"."triggers"
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_trigger_schedules_due_idx
|
||||
ON "ql3"."trigger_schedules"
|
||||
(next_fire_at_ms NULLS FIRST, claim_expires_at_ms, project_id, trigger_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_trigger_schedules_claim_expiry_idx
|
||||
ON "ql3"."trigger_schedules"
|
||||
(claim_expires_at_ms, project_id, trigger_id)
|
||||
WHERE claim_token IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 13,
|
||||
migration_id = 'pg-0014-cluster-scheduler-admission',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 12
|
||||
AND migration_id = 'pg-0013-task-execution-revisions'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 12'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0015WorkerCredentialDeliveryLedgerMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0015-worker-credential-delivery-ledger',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."worker_credential_deliveries" (
|
||||
delivery_id varchar(36) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
state varchar(32) NOT NULL,
|
||||
worker_id varchar(128) NOT NULL,
|
||||
credential_id varchar(64) NOT NULL,
|
||||
credential_version integer NOT NULL,
|
||||
previous_credential_id varchar(64),
|
||||
secret_digest char(64) NOT NULL,
|
||||
token_digest char(64) NOT NULL,
|
||||
deployment_target_digest char(64) NOT NULL,
|
||||
deployment_generation varchar(128) NOT NULL,
|
||||
staged_at_ms bigint NOT NULL,
|
||||
credential_committed_at_ms bigint NOT NULL,
|
||||
published_at_ms bigint,
|
||||
publication_digest char(64),
|
||||
observed_at_ms bigint,
|
||||
observed_session_id varchar(36),
|
||||
observed_session_version integer,
|
||||
previous_revoked_at_ms bigint,
|
||||
CONSTRAINT worker_credential_deliveries_pkey
|
||||
PRIMARY KEY (delivery_id, version),
|
||||
CONSTRAINT ql3_worker_credential_deliveries_mutation_fk
|
||||
FOREIGN KEY (delivery_id)
|
||||
REFERENCES "ql3"."worker_credential_mutations" (mutation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_worker_credential_deliveries_credential_fk
|
||||
FOREIGN KEY (credential_id, credential_version)
|
||||
REFERENCES "ql3"."worker_credentials" (credential_id, version)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_worker_credential_deliveries_version_check CHECK (
|
||||
version BETWEEN 1 AND 4
|
||||
AND credential_version = 1
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_deliveries_id_check CHECK (
|
||||
delivery_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_deliveries_state_check CHECK (
|
||||
state IN (
|
||||
'credential_committed', 'published', 'observed', 'previous_revoked'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_deliveries_worker_check CHECK (
|
||||
char_length(worker_id) BETWEEN 1 AND 128
|
||||
AND worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_deliveries_credential_check CHECK (
|
||||
char_length(credential_id) BETWEEN 1 AND 64
|
||||
AND credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'
|
||||
AND (
|
||||
previous_credential_id IS NULL
|
||||
OR (
|
||||
char_length(previous_credential_id) BETWEEN 1 AND 64
|
||||
AND previous_credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'
|
||||
AND previous_credential_id <> credential_id
|
||||
)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_deliveries_digest_check CHECK (
|
||||
secret_digest ~ '^[0-9a-f]{64}$'
|
||||
AND token_digest ~ '^[0-9a-f]{64}$'
|
||||
AND deployment_target_digest ~ '^[0-9a-f]{64}$'
|
||||
AND (
|
||||
publication_digest IS NULL
|
||||
OR publication_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_deliveries_generation_check CHECK (
|
||||
char_length(deployment_generation) BETWEEN 1 AND 128
|
||||
AND deployment_generation ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_deliveries_session_check CHECK (
|
||||
observed_session_id IS NULL
|
||||
OR observed_session_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_deliveries_time_check CHECK (
|
||||
staged_at_ms >= 0
|
||||
AND credential_committed_at_ms >= staged_at_ms
|
||||
AND (published_at_ms IS NULL OR published_at_ms >= credential_committed_at_ms)
|
||||
AND (observed_at_ms IS NULL OR observed_at_ms >= published_at_ms)
|
||||
AND (
|
||||
previous_revoked_at_ms IS NULL
|
||||
OR previous_revoked_at_ms >= observed_at_ms
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_deliveries_state_shape_check CHECK (
|
||||
(
|
||||
version = 1
|
||||
AND state = 'credential_committed'
|
||||
AND published_at_ms IS NULL
|
||||
AND publication_digest IS NULL
|
||||
AND observed_at_ms IS NULL
|
||||
AND observed_session_id IS NULL
|
||||
AND observed_session_version IS NULL
|
||||
AND previous_revoked_at_ms IS NULL
|
||||
)
|
||||
OR (
|
||||
version = 2
|
||||
AND state = 'published'
|
||||
AND published_at_ms IS NOT NULL
|
||||
AND publication_digest IS NOT NULL
|
||||
AND observed_at_ms IS NULL
|
||||
AND observed_session_id IS NULL
|
||||
AND observed_session_version IS NULL
|
||||
AND previous_revoked_at_ms IS NULL
|
||||
)
|
||||
OR (
|
||||
version = 3
|
||||
AND state = 'observed'
|
||||
AND published_at_ms IS NOT NULL
|
||||
AND publication_digest IS NOT NULL
|
||||
AND observed_at_ms IS NOT NULL
|
||||
AND observed_session_id IS NOT NULL
|
||||
AND observed_session_version BETWEEN 1 AND 2147483647
|
||||
AND previous_revoked_at_ms IS NULL
|
||||
)
|
||||
OR (
|
||||
version = 4
|
||||
AND state = 'previous_revoked'
|
||||
AND previous_credential_id IS NOT NULL
|
||||
AND published_at_ms IS NOT NULL
|
||||
AND publication_digest IS NOT NULL
|
||||
AND observed_at_ms IS NOT NULL
|
||||
AND observed_session_id IS NOT NULL
|
||||
AND observed_session_version BETWEEN 1 AND 2147483647
|
||||
AND previous_revoked_at_ms IS NOT NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_worker_credential_deliveries_recovery_idx
|
||||
ON "ql3"."worker_credential_deliveries"
|
||||
(state, delivery_id, version DESC)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_worker_credential_deliveries_credential_uidx
|
||||
ON "ql3"."worker_credential_deliveries"
|
||||
(credential_id, credential_version)
|
||||
WHERE version = 1
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_worker_credential_deliveries_credential_idx
|
||||
ON "ql3"."worker_credential_deliveries"
|
||||
(credential_id, credential_version, delivery_id, version)
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 14,
|
||||
migration_id = 'pg-0015-worker-credential-delivery-ledger',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 13
|
||||
AND migration_id = 'pg-0014-cluster-scheduler-admission'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 13'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0016WorkerCredentialStageDiscardLedgerMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0016-worker-credential-stage-discard-ledger',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."worker_credential_stage_discards" (
|
||||
delivery_id varchar(36) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
state varchar(32) NOT NULL,
|
||||
worker_id varchar(128) NOT NULL,
|
||||
credential_id varchar(64) NOT NULL,
|
||||
credential_version integer NOT NULL,
|
||||
previous_credential_id varchar(64),
|
||||
secret_digest char(64) NOT NULL,
|
||||
token_digest char(64) NOT NULL,
|
||||
deployment_target_digest char(64) NOT NULL,
|
||||
deployment_generation varchar(128) NOT NULL,
|
||||
staged_at_ms bigint NOT NULL,
|
||||
authorized_at_ms bigint NOT NULL,
|
||||
discarded_at_ms bigint,
|
||||
CONSTRAINT worker_credential_stage_discards_pkey
|
||||
PRIMARY KEY (delivery_id, version),
|
||||
CONSTRAINT ql3_worker_credential_stage_discards_version_check CHECK (
|
||||
version BETWEEN 1 AND 2
|
||||
AND credential_version = 1
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_stage_discards_id_check CHECK (
|
||||
delivery_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_stage_discards_state_check CHECK (
|
||||
state IN ('discard_authorized', 'discarded')
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_stage_discards_worker_check CHECK (
|
||||
char_length(worker_id) BETWEEN 1 AND 128
|
||||
AND worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_stage_discards_credential_check CHECK (
|
||||
char_length(credential_id) BETWEEN 1 AND 64
|
||||
AND credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'
|
||||
AND (
|
||||
previous_credential_id IS NULL
|
||||
OR (
|
||||
char_length(previous_credential_id) BETWEEN 1 AND 64
|
||||
AND previous_credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'
|
||||
AND previous_credential_id <> credential_id
|
||||
)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_stage_discards_digest_check CHECK (
|
||||
secret_digest ~ '^[0-9a-f]{64}$'
|
||||
AND token_digest ~ '^[0-9a-f]{64}$'
|
||||
AND deployment_target_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_stage_discards_generation_check CHECK (
|
||||
char_length(deployment_generation) BETWEEN 1 AND 128
|
||||
AND deployment_generation ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_stage_discards_time_check CHECK (
|
||||
staged_at_ms >= 0
|
||||
AND authorized_at_ms >= 0
|
||||
AND (discarded_at_ms IS NULL OR discarded_at_ms >= authorized_at_ms)
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_stage_discards_state_shape_check CHECK (
|
||||
(
|
||||
version = 1
|
||||
AND state = 'discard_authorized'
|
||||
AND discarded_at_ms IS NULL
|
||||
)
|
||||
OR (
|
||||
version = 2
|
||||
AND state = 'discarded'
|
||||
AND discarded_at_ms IS NOT NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_worker_credential_stage_discards_recovery_idx
|
||||
ON "ql3"."worker_credential_stage_discards"
|
||||
(state, delivery_id, version DESC)
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 15,
|
||||
migration_id = 'pg-0016-worker-credential-stage-discard-ledger',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 14
|
||||
AND migration_id = 'pg-0015-worker-credential-delivery-ledger'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 14'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0017DatabaseRoleGrantsMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0017-database-role-grants',
|
||||
statements: [
|
||||
`
|
||||
DO $ql3$
|
||||
DECLARE
|
||||
invalid_roles text[];
|
||||
BEGIN
|
||||
SELECT array_agg(expected.role_name ORDER BY expected.role_name)
|
||||
INTO invalid_roles
|
||||
FROM (
|
||||
VALUES
|
||||
('ql3_migration'),
|
||||
('ql3_runtime'),
|
||||
('ql3_admin'),
|
||||
('ql3_worker_ingress')
|
||||
) AS expected(role_name)
|
||||
LEFT JOIN pg_catalog.pg_roles roles
|
||||
ON roles.rolname = expected.role_name
|
||||
WHERE roles.rolname IS NULL
|
||||
OR roles.rolcanlogin IS NOT TRUE
|
||||
OR roles.rolsuper IS NOT FALSE
|
||||
OR roles.rolcreatedb IS NOT FALSE
|
||||
OR roles.rolcreaterole IS NOT FALSE
|
||||
OR roles.rolreplication IS NOT FALSE
|
||||
OR roles.rolbypassrls IS NOT FALSE;
|
||||
|
||||
IF invalid_roles IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'required QingLong database roles are missing or privileged: %',
|
||||
invalid_roles
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
'REVOKE ALL ON SCHEMA "ql3" FROM PUBLIC',
|
||||
'REVOKE ALL ON ALL TABLES IN SCHEMA "ql3" FROM PUBLIC',
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
EXECUTE format(
|
||||
'REVOKE CONNECT ON DATABASE %I FROM PUBLIC',
|
||||
current_database()
|
||||
);
|
||||
EXECUTE format(
|
||||
'GRANT CONNECT ON DATABASE %I TO ql3_migration, ql3_runtime, ql3_admin, ql3_worker_ingress',
|
||||
current_database()
|
||||
);
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
'GRANT USAGE ON SCHEMA "ql3" TO ql3_runtime',
|
||||
'GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities" TO ql3_runtime',
|
||||
'GRANT SELECT, INSERT, UPDATE ON "ql3"."runs", "ql3"."run_attempts", "ql3"."worker_sessions", "ql3"."run_dispatch_leases", "ql3"."run_recovery_controls", "ql3"."run_retry_policies" TO ql3_runtime',
|
||||
'GRANT SELECT, INSERT ON "ql3"."run_events" TO ql3_runtime',
|
||||
'GRANT SELECT, INSERT, UPDATE ON "ql3"."projects" TO ql3_runtime',
|
||||
'GRANT SELECT, INSERT ON "ql3"."project_role_bindings" TO ql3_runtime',
|
||||
'GRANT SELECT ON "ql3"."identity_subjects", "ql3"."api_credentials", "ql3"."worker_execution_attestations", "ql3"."task_definitions", "ql3"."task_definition_revisions", "ql3"."task_execution_revisions", "ql3"."triggers", "ql3"."trigger_revisions" TO ql3_runtime',
|
||||
'GRANT SELECT, UPDATE ON "ql3"."trigger_schedules" TO ql3_runtime',
|
||||
'GRANT INSERT ON "ql3"."security_audit_events" TO ql3_runtime',
|
||||
'GRANT USAGE ON SCHEMA "ql3" TO ql3_admin',
|
||||
'GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities" TO ql3_admin',
|
||||
'GRANT SELECT ON "ql3"."projects" TO ql3_admin',
|
||||
'GRANT SELECT, INSERT, UPDATE ON "ql3"."identity_subjects" TO ql3_admin',
|
||||
'GRANT SELECT, INSERT ON "ql3"."api_credentials", "ql3"."security_audit_events", "ql3"."identity_subject_mutations", "ql3"."api_credential_mutations" TO ql3_admin',
|
||||
'GRANT SELECT, INSERT, UPDATE ON "ql3"."worker_credentials" TO ql3_admin',
|
||||
'GRANT SELECT, INSERT ON "ql3"."worker_credential_mutations" TO ql3_admin',
|
||||
'GRANT SELECT, INSERT ON "ql3"."worker_credential_deliveries", "ql3"."worker_credential_stage_discards" TO ql3_admin',
|
||||
'GRANT SELECT, INSERT, UPDATE ON "ql3"."task_definitions", "ql3"."triggers", "ql3"."trigger_schedules" TO ql3_admin',
|
||||
'GRANT SELECT, INSERT ON "ql3"."task_definition_revisions", "ql3"."task_execution_revisions", "ql3"."trigger_revisions" TO ql3_admin',
|
||||
'GRANT USAGE ON SCHEMA "ql3" TO ql3_worker_ingress',
|
||||
'GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities", "ql3"."run_attempts", "ql3"."run_dispatch_leases", "ql3"."worker_credentials" TO ql3_worker_ingress',
|
||||
'GRANT SELECT, INSERT, UPDATE ON "ql3"."worker_sessions" TO ql3_worker_ingress',
|
||||
'GRANT SELECT, INSERT ON "ql3"."worker_credential_deliveries" TO ql3_worker_ingress',
|
||||
'GRANT SELECT, INSERT ON "ql3"."worker_execution_attestations" TO ql3_worker_ingress',
|
||||
'GRANT INSERT ON "ql3"."security_audit_events" TO ql3_worker_ingress',
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 16,
|
||||
migration_id = 'pg-0017-database-role-grants',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 15
|
||||
AND migration_id = 'pg-0016-worker-credential-stage-discard-ledger'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 15'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0018PluginPackageInstallsMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0018-plugin-package-installs',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_installs" (
|
||||
installation_id varchar(128) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
package_version varchar(128) NOT NULL,
|
||||
operation varchar(16) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
target_generation integer NOT NULL,
|
||||
previous_active_lock_digest char(64),
|
||||
active_lock_digest char(64),
|
||||
state varchar(16) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
last_mutation_id varchar(128) NOT NULL,
|
||||
last_mutation_digest char(64) NOT NULL,
|
||||
lock_json jsonb NOT NULL,
|
||||
record_json jsonb NOT NULL,
|
||||
record_digest char(64) NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_installs_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_installs_identity_check CHECK (
|
||||
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
AND package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'
|
||||
AND char_length(package_version) BETWEEN 1 AND 128
|
||||
AND last_mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_installs_operation_check CHECK (
|
||||
operation IN ('install', 'reinstall', 'upgrade', 'rollback')
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_installs_state_check CHECK (
|
||||
state IN ('queued', 'staged', 'activating', 'active', 'failed')
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_installs_version_check CHECK (
|
||||
target_generation BETWEEN 1 AND 2147483647
|
||||
AND version BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_installs_digest_check CHECK (
|
||||
lock_digest ~ '^[0-9a-f]{64}$'
|
||||
AND (
|
||||
previous_active_lock_digest IS NULL
|
||||
OR previous_active_lock_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
AND (
|
||||
active_lock_digest IS NULL
|
||||
OR active_lock_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
AND last_mutation_digest ~ '^[0-9a-f]{64}$'
|
||||
AND record_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_installs_record_check CHECK (
|
||||
jsonb_typeof(lock_json) = 'object'
|
||||
AND octet_length(lock_json::text) BETWEEN 2 AND 262144
|
||||
AND lock_json @> jsonb_build_object(
|
||||
'lockDigest', lock_digest,
|
||||
'projectId', project_id,
|
||||
'packageName', package_name
|
||||
)
|
||||
AND jsonb_typeof(record_json) = 'object'
|
||||
AND octet_length(record_json::text) BETWEEN 2 AND 262144
|
||||
AND record_json @> jsonb_build_object(
|
||||
'installationId', installation_id,
|
||||
'projectId', project_id,
|
||||
'packageName', package_name,
|
||||
'lockDigest', lock_digest,
|
||||
'state', state,
|
||||
'version', version,
|
||||
'recordDigest', record_digest
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_installs_time_check CHECK (
|
||||
created_at_ms >= 0
|
||||
AND updated_at_ms >= created_at_ms
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_install_heads" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
installation_id varchar(128) NOT NULL,
|
||||
CONSTRAINT plugin_package_install_heads_pkey
|
||||
PRIMARY KEY (project_id, package_name),
|
||||
CONSTRAINT ql3_plugin_package_install_heads_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_install_heads_install_fk
|
||||
FOREIGN KEY (installation_id)
|
||||
REFERENCES "ql3"."plugin_package_installs" (installation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_install_heads_identity_check CHECK (
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'
|
||||
AND installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_install_heads_install_uidx
|
||||
ON "ql3"."plugin_package_install_heads" (installation_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_install_mutations" (
|
||||
installation_id varchar(128) NOT NULL,
|
||||
mutation_id varchar(128) NOT NULL,
|
||||
mutation_digest char(64) NOT NULL,
|
||||
resulting_record_digest char(64) NOT NULL,
|
||||
occurred_at_ms bigint NOT NULL,
|
||||
CONSTRAINT plugin_package_install_mutations_pkey
|
||||
PRIMARY KEY (installation_id, mutation_id),
|
||||
CONSTRAINT ql3_plugin_package_install_mutations_install_fk
|
||||
FOREIGN KEY (installation_id)
|
||||
REFERENCES "ql3"."plugin_package_installs" (installation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_install_mutations_identity_check CHECK (
|
||||
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_install_mutations_digest_check CHECK (
|
||||
mutation_digest ~ '^[0-9a-f]{64}$'
|
||||
AND resulting_record_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_install_mutations_time_check CHECK (
|
||||
occurred_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_plugin_package_installs_recovery_idx
|
||||
ON "ql3"."plugin_package_installs" (state, package_name, installation_id)
|
||||
WHERE state IN ('queued', 'staged', 'activating')
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_plugin_package_installs_project_history_idx
|
||||
ON "ql3"."plugin_package_installs"
|
||||
(project_id, package_name, created_at_ms, installation_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_plugin_package_install_mutations_result_idx
|
||||
ON "ql3"."plugin_package_install_mutations"
|
||||
(installation_id, resulting_record_digest)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE FUNCTION "ql3"."lock_active_plugin_package_project"(varchar)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
DECLARE
|
||||
project_is_active boolean;
|
||||
BEGIN
|
||||
SELECT projects.status = 'active'
|
||||
INTO project_is_active
|
||||
FROM "ql3"."projects" AS projects
|
||||
WHERE projects.id = $1
|
||||
FOR SHARE;
|
||||
|
||||
RETURN COALESCE(project_is_active, false);
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON FUNCTION "ql3"."lock_active_plugin_package_project"(varchar)
|
||||
FROM PUBLIC
|
||||
`.trim(),
|
||||
`
|
||||
GRANT EXECUTE
|
||||
ON FUNCTION "ql3"."lock_active_plugin_package_project"(varchar)
|
||||
TO ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT, UPDATE
|
||||
ON "ql3"."plugin_package_installs", "ql3"."plugin_package_install_heads"
|
||||
TO ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."plugin_package_install_mutations"
|
||||
TO ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 17,
|
||||
migration_id = 'pg-0018-plugin-package-installs',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 16
|
||||
AND migration_id = 'pg-0017-database-role-grants'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 16'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0019ApprovedActionsMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0019-approved-actions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."approval_requests" (
|
||||
request_id varchar(128) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
state varchar(16) NOT NULL,
|
||||
action_type varchar(128) NOT NULL,
|
||||
action_ref varchar(255) NOT NULL,
|
||||
action_digest char(64) NOT NULL,
|
||||
preview_digest char(64) NOT NULL,
|
||||
requested_by_type varchar(32) NOT NULL,
|
||||
requested_by_id varchar(255) NOT NULL,
|
||||
decision_id varchar(128),
|
||||
consumption_id varchar(128),
|
||||
dispatch_id varchar(128),
|
||||
expires_at_ms bigint NOT NULL,
|
||||
request_json jsonb NOT NULL,
|
||||
request_digest char(64) NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT ql3_approval_requests_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_approval_requests_identity_check CHECK (
|
||||
request_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
AND action_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
AND action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
|
||||
),
|
||||
CONSTRAINT ql3_approval_requests_state_version_check CHECK (
|
||||
(state = 'pending' AND version = 1)
|
||||
OR (state IN ('approved', 'rejected') AND version = 2)
|
||||
OR (state = 'consumed' AND version = 3)
|
||||
),
|
||||
CONSTRAINT ql3_approval_requests_digest_check CHECK (
|
||||
action_digest ~ '^[0-9a-f]{64}$'
|
||||
AND preview_digest ~ '^[0-9a-f]{64}$'
|
||||
AND request_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_approval_requests_subject_check CHECK (
|
||||
requested_by_type IN
|
||||
('user','api_app','mcp_client','agent','system','worker')
|
||||
AND char_length(requested_by_id) BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_approval_requests_mutation_tuple_check CHECK (
|
||||
(
|
||||
version = 1 AND decision_id IS NULL
|
||||
AND consumption_id IS NULL AND dispatch_id IS NULL
|
||||
)
|
||||
OR (
|
||||
version = 2 AND decision_id IS NOT NULL
|
||||
AND consumption_id IS NULL AND dispatch_id IS NULL
|
||||
)
|
||||
OR (
|
||||
version = 3 AND decision_id IS NOT NULL
|
||||
AND consumption_id IS NOT NULL AND dispatch_id IS NOT NULL
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_approval_requests_json_check CHECK (
|
||||
jsonb_typeof(request_json) = 'object'
|
||||
AND octet_length(request_json::text) BETWEEN 2 AND 65536
|
||||
AND request_json @> jsonb_build_object(
|
||||
'id', request_id,
|
||||
'projectId', project_id,
|
||||
'version', version,
|
||||
'state', state,
|
||||
'action', jsonb_build_object(
|
||||
'actionType', action_type,
|
||||
'actionRef', action_ref,
|
||||
'actionDigest', action_digest,
|
||||
'previewDigest', preview_digest
|
||||
)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_approval_requests_time_check CHECK (
|
||||
expires_at_ms > 0 AND updated_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_approval_requests_decision_uidx
|
||||
ON "ql3"."approval_requests" (decision_id)
|
||||
WHERE decision_id IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_approval_requests_consumption_uidx
|
||||
ON "ql3"."approval_requests" (consumption_id)
|
||||
WHERE consumption_id IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_approval_requests_dispatch_uidx
|
||||
ON "ql3"."approval_requests" (dispatch_id)
|
||||
WHERE dispatch_id IS NOT NULL
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_approval_requests_pending_idx
|
||||
ON "ql3"."approval_requests" (expires_at_ms, request_id)
|
||||
WHERE state = 'pending'
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_approval_requests_project_idx
|
||||
ON "ql3"."approval_requests" (project_id, updated_at_ms, request_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."approved_action_dispatches" (
|
||||
dispatch_id varchar(128) PRIMARY KEY,
|
||||
approval_request_id varchar(128) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
action_type varchar(128) NOT NULL,
|
||||
action_ref varchar(255) NOT NULL,
|
||||
action_digest char(64) NOT NULL,
|
||||
preview_digest char(64) NOT NULL,
|
||||
dispatch_json jsonb NOT NULL,
|
||||
dispatch_digest char(64) NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
CONSTRAINT ql3_approved_action_dispatch_request_fk
|
||||
FOREIGN KEY (approval_request_id)
|
||||
REFERENCES "ql3"."approval_requests" (request_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_approved_action_dispatch_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_approved_action_dispatch_identity_check CHECK (
|
||||
dispatch_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
AND action_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
AND action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_dispatch_digest_check CHECK (
|
||||
action_digest ~ '^[0-9a-f]{64}$'
|
||||
AND preview_digest ~ '^[0-9a-f]{64}$'
|
||||
AND dispatch_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_dispatch_json_check CHECK (
|
||||
jsonb_typeof(dispatch_json) = 'object'
|
||||
AND octet_length(dispatch_json::text) BETWEEN 2 AND 65536
|
||||
AND dispatch_json @> jsonb_build_object(
|
||||
'id', dispatch_id,
|
||||
'approvalRequestId', approval_request_id,
|
||||
'projectId', project_id,
|
||||
'action', jsonb_build_object(
|
||||
'actionType', action_type,
|
||||
'actionRef', action_ref,
|
||||
'actionDigest', action_digest,
|
||||
'previewDigest', preview_digest
|
||||
)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_dispatch_time_check CHECK (
|
||||
created_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_approved_action_dispatch_request_uidx
|
||||
ON "ql3"."approved_action_dispatches" (approval_request_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_approved_action_dispatch_project_idx
|
||||
ON "ql3"."approved_action_dispatches"
|
||||
(project_id, created_at_ms, dispatch_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE FUNCTION "ql3"."lock_approval_policy_fence"(
|
||||
varchar,
|
||||
varchar,
|
||||
varchar,
|
||||
integer,
|
||||
integer
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
DECLARE
|
||||
current_project_version integer;
|
||||
current_project_status varchar;
|
||||
current_binding_version integer;
|
||||
BEGIN
|
||||
SELECT project.version, project.status
|
||||
INTO current_project_version, current_project_status
|
||||
FROM "ql3"."projects" AS project
|
||||
WHERE project.id = $1
|
||||
FOR SHARE;
|
||||
|
||||
SELECT binding.version
|
||||
INTO current_binding_version
|
||||
FROM "ql3"."project_role_bindings" AS binding
|
||||
WHERE binding.project_id = $1
|
||||
AND binding.subject_type = $2
|
||||
AND binding.subject_id = $3
|
||||
ORDER BY binding.version DESC
|
||||
LIMIT 1
|
||||
FOR SHARE;
|
||||
|
||||
RETURN current_project_status = 'active'
|
||||
AND current_project_version = $4
|
||||
AND current_binding_version IS NOT DISTINCT FROM $5;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON FUNCTION "ql3"."lock_approval_policy_fence"(
|
||||
varchar, varchar, varchar, integer, integer
|
||||
)
|
||||
FROM PUBLIC
|
||||
`.trim(),
|
||||
`
|
||||
GRANT EXECUTE
|
||||
ON FUNCTION "ql3"."lock_approval_policy_fence"(
|
||||
varchar, varchar, varchar, integer, integer
|
||||
)
|
||||
TO ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT, UPDATE
|
||||
ON "ql3"."approval_requests"
|
||||
TO ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."approved_action_dispatches"
|
||||
TO ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 18,
|
||||
migration_id = 'pg-0019-approved-actions',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 17
|
||||
AND migration_id = 'pg-0018-plugin-package-installs'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 17'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0020PluginPackageAdmissionReceiptsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0020-plugin-package-admission-receipts',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_admission_receipts" (
|
||||
dispatch_id varchar(128) PRIMARY KEY,
|
||||
dispatch_digest char(64) NOT NULL,
|
||||
approval_request_id varchar(128) NOT NULL,
|
||||
action_ref varchar(255) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(64) NOT NULL,
|
||||
installation_id varchar(128) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
record_digest char(64) NOT NULL,
|
||||
mutation_id varchar(128) NOT NULL,
|
||||
mutation_digest char(64) NOT NULL,
|
||||
audit_event_id uuid NOT NULL,
|
||||
admitted_at_ms bigint NOT NULL,
|
||||
receipt_json jsonb NOT NULL,
|
||||
receipt_digest char(64) NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_admission_dispatch_fk
|
||||
FOREIGN KEY (dispatch_id)
|
||||
REFERENCES "ql3"."approved_action_dispatches" (dispatch_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_admission_request_fk
|
||||
FOREIGN KEY (approval_request_id)
|
||||
REFERENCES "ql3"."approval_requests" (request_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_admission_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_admission_install_fk
|
||||
FOREIGN KEY (installation_id)
|
||||
REFERENCES "ql3"."plugin_package_installs" (installation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_admission_audit_fk
|
||||
FOREIGN KEY (audit_event_id)
|
||||
REFERENCES "ql3"."security_audit_events" (event_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_admission_identity_check CHECK (
|
||||
dispatch_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
AND approval_request_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
AND action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
|
||||
AND project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
AND package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'
|
||||
AND installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
AND mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_admission_digest_check CHECK (
|
||||
dispatch_digest ~ '^[0-9a-f]{64}$'
|
||||
AND lock_digest ~ '^[0-9a-f]{64}$'
|
||||
AND record_digest ~ '^[0-9a-f]{64}$'
|
||||
AND mutation_digest ~ '^[0-9a-f]{64}$'
|
||||
AND receipt_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_admission_json_check CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object'
|
||||
AND octet_length(receipt_json::text) BETWEEN 2 AND 65536
|
||||
AND receipt_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-admission-receipt@v1',
|
||||
'dispatchId', dispatch_id,
|
||||
'dispatchDigest', dispatch_digest,
|
||||
'approvalRequestId', approval_request_id,
|
||||
'actionRef', action_ref,
|
||||
'projectId', project_id,
|
||||
'packageName', package_name,
|
||||
'installationId', installation_id,
|
||||
'lockDigest', lock_digest,
|
||||
'recordDigest', record_digest,
|
||||
'mutationId', mutation_id,
|
||||
'mutationDigest', mutation_digest,
|
||||
'auditEventId', audit_event_id::text,
|
||||
'admittedAtMs', admitted_at_ms,
|
||||
'receiptDigest', receipt_digest
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_admission_time_check CHECK (
|
||||
admitted_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_admission_install_uidx
|
||||
ON "ql3"."plugin_package_admission_receipts" (installation_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_admission_audit_uidx
|
||||
ON "ql3"."plugin_package_admission_receipts" (audit_event_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_plugin_package_admission_project_idx
|
||||
ON "ql3"."plugin_package_admission_receipts"
|
||||
(project_id, admitted_at_ms, dispatch_id)
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."plugin_package_admission_receipts"
|
||||
TO ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 19,
|
||||
migration_id = 'pg-0020-plugin-package-admission-receipts',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 18
|
||||
AND migration_id = 'pg-0019-approved-actions'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 18'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const pg0021ApprovedActionExecutionsAndPackageProposalsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0021-approved-action-executions-and-package-proposals',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_install_proposals" (
|
||||
action_ref varchar(255) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
action_type varchar(128) NOT NULL,
|
||||
permission varchar(128) NOT NULL,
|
||||
action_digest char(64) NOT NULL,
|
||||
preview_digest char(64) NOT NULL,
|
||||
proposed_by_type varchar(32) NOT NULL,
|
||||
proposed_by_id varchar(255) NOT NULL,
|
||||
fence_project_version integer NOT NULL,
|
||||
fence_binding_version integer,
|
||||
created_at_ms bigint NOT NULL,
|
||||
proposal_json jsonb NOT NULL,
|
||||
proposal_digest char(64) NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_proposal_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_proposal_identity_check CHECK (
|
||||
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
|
||||
AND action_type = 'plugin_package.install'
|
||||
AND permission = 'package.manage'
|
||||
AND proposed_by_type IN
|
||||
('user','api_app','mcp_client','agent','system','worker')
|
||||
AND char_length(proposed_by_id) BETWEEN 1 AND 255
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_proposal_digest_check CHECK (
|
||||
action_digest ~ '^[0-9a-f]{64}$'
|
||||
AND preview_digest ~ '^[0-9a-f]{64}$'
|
||||
AND proposal_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_proposal_json_check CHECK (
|
||||
jsonb_typeof(proposal_json) = 'object'
|
||||
AND octet_length(proposal_json::text) BETWEEN 2 AND 262144
|
||||
AND proposal_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-install-proposal@v1',
|
||||
'actionRef', action_ref,
|
||||
'projectId', project_id,
|
||||
'actionType', action_type,
|
||||
'permission', permission,
|
||||
'actionDigest', action_digest,
|
||||
'previewDigest', preview_digest,
|
||||
'proposedBy', jsonb_build_object(
|
||||
'type', proposed_by_type,
|
||||
'id', proposed_by_id
|
||||
),
|
||||
'proposalFence', jsonb_build_object(
|
||||
'projectVersion', fence_project_version,
|
||||
'bindingVersion', fence_binding_version
|
||||
),
|
||||
'createdAtMs', created_at_ms,
|
||||
'proposalDigest', proposal_digest
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_proposal_time_check CHECK (
|
||||
fence_project_version > 0
|
||||
AND (fence_binding_version IS NULL OR fence_binding_version > 0)
|
||||
AND created_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_plugin_package_proposal_project_idx
|
||||
ON "ql3"."plugin_package_install_proposals"
|
||||
(project_id, created_at_ms, action_ref)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."approved_action_executions" (
|
||||
dispatch_id varchar(128) PRIMARY KEY,
|
||||
dispatch_digest char(64) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
status varchar(16) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
attempt_count integer NOT NULL,
|
||||
max_attempts integer NOT NULL,
|
||||
eligible_at_ms bigint,
|
||||
next_attempt_at_ms bigint,
|
||||
lease_owner varchar(128),
|
||||
lease_token varchar(128),
|
||||
lease_expires_at_ms bigint,
|
||||
started_at_ms bigint,
|
||||
result_mutation_id varchar(128),
|
||||
result_code varchar(64),
|
||||
result_digest char(64),
|
||||
completed_at_ms bigint,
|
||||
created_at_ms bigint NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
execution_json jsonb NOT NULL,
|
||||
execution_digest char(64) NOT NULL,
|
||||
CONSTRAINT ql3_approved_action_execution_dispatch_fk
|
||||
FOREIGN KEY (dispatch_id)
|
||||
REFERENCES "ql3"."approved_action_dispatches" (dispatch_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_approved_action_execution_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_approved_action_execution_state_check CHECK (
|
||||
status IN
|
||||
('pending','leased','executing','retry_wait','succeeded','failed','blocked')
|
||||
AND version BETWEEN 0 AND 2147483647
|
||||
AND attempt_count BETWEEN 0 AND 16
|
||||
AND max_attempts BETWEEN 1 AND 16
|
||||
AND attempt_count <= max_attempts
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_execution_lease_check CHECK (
|
||||
(
|
||||
lease_owner IS NULL AND lease_token IS NULL
|
||||
AND lease_expires_at_ms IS NULL
|
||||
)
|
||||
OR (
|
||||
char_length(lease_owner) BETWEEN 1 AND 128
|
||||
AND char_length(lease_token) BETWEEN 1 AND 128
|
||||
AND lease_expires_at_ms > updated_at_ms
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_execution_result_check CHECK (
|
||||
(
|
||||
result_mutation_id IS NULL AND result_code IS NULL
|
||||
)
|
||||
OR (
|
||||
char_length(result_mutation_id) BETWEEN 1 AND 128
|
||||
AND char_length(result_code) BETWEEN 1 AND 64
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_execution_digest_check CHECK (
|
||||
dispatch_digest ~ '^[0-9a-f]{64}$'
|
||||
AND (result_digest IS NULL OR result_digest ~ '^[0-9a-f]{64}$')
|
||||
AND execution_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_execution_json_check CHECK (
|
||||
jsonb_typeof(execution_json) = 'object'
|
||||
AND octet_length(execution_json::text) BETWEEN 2 AND 65536
|
||||
AND execution_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/approved-action-execution@v1',
|
||||
'dispatchId', dispatch_id,
|
||||
'dispatchDigest', dispatch_digest,
|
||||
'projectId', project_id,
|
||||
'status', status,
|
||||
'version', version,
|
||||
'attemptCount', attempt_count,
|
||||
'maxAttempts', max_attempts,
|
||||
'executionDigest', execution_digest
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_approved_action_execution_time_check CHECK (
|
||||
created_at_ms >= 0 AND updated_at_ms >= created_at_ms
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_approved_action_execution_due_idx
|
||||
ON "ql3"."approved_action_executions" (eligible_at_ms, dispatch_id)
|
||||
WHERE status IN ('pending','leased','retry_wait')
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_approved_action_execution_recovery_idx
|
||||
ON "ql3"."approved_action_executions" (lease_expires_at_ms, dispatch_id)
|
||||
WHERE status = 'executing'
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_approved_action_execution_project_idx
|
||||
ON "ql3"."approved_action_executions"
|
||||
(project_id, updated_at_ms, dispatch_id)
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."plugin_package_install_proposals"
|
||||
TO ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT, UPDATE
|
||||
ON "ql3"."approved_action_executions"
|
||||
TO ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 20,
|
||||
migration_id =
|
||||
'pg-0021-approved-action-executions-and-package-proposals',
|
||||
capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_install":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 19
|
||||
AND migration_id = 'pg-0020-plugin-package-admission-receipts'
|
||||
AND capabilities = '{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_install":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 19'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V20 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_install":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V21 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_install":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0022PluginPackageAuthoritySplitMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0022-plugin-package-authority-split',
|
||||
statements: [
|
||||
`
|
||||
DO $ql3$
|
||||
DECLARE
|
||||
invalid_roles text[];
|
||||
BEGIN
|
||||
SELECT array_agg(expected.role_name ORDER BY expected.role_name)
|
||||
INTO invalid_roles
|
||||
FROM (
|
||||
VALUES
|
||||
('ql3_package_manager'),
|
||||
('ql3_package_executor')
|
||||
) AS expected(role_name)
|
||||
LEFT JOIN pg_catalog.pg_roles roles
|
||||
ON roles.rolname = expected.role_name
|
||||
WHERE roles.rolname IS NULL
|
||||
OR roles.rolcanlogin IS NOT TRUE
|
||||
OR roles.rolsuper IS NOT FALSE
|
||||
OR roles.rolcreatedb IS NOT FALSE
|
||||
OR roles.rolcreaterole IS NOT FALSE
|
||||
OR roles.rolreplication IS NOT FALSE
|
||||
OR roles.rolbypassrls IS NOT FALSE;
|
||||
|
||||
IF invalid_roles IS NOT NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'required QingLong Package database roles are missing or privileged: %',
|
||||
invalid_roles
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
EXECUTE format(
|
||||
'GRANT CONNECT ON DATABASE %I TO ql3_package_manager, ql3_package_executor',
|
||||
current_database()
|
||||
);
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
'GRANT USAGE ON SCHEMA "ql3" TO ql3_package_manager, ql3_package_executor',
|
||||
'GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities" TO ql3_package_manager, ql3_package_executor',
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."plugin_package_install_proposals",
|
||||
"ql3"."approval_requests",
|
||||
"ql3"."approved_action_dispatches",
|
||||
"ql3"."approved_action_executions",
|
||||
"ql3"."plugin_package_installs",
|
||||
"ql3"."plugin_package_install_heads",
|
||||
"ql3"."plugin_package_install_mutations",
|
||||
"ql3"."plugin_package_admission_receipts"
|
||||
FROM ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE EXECUTE
|
||||
ON FUNCTION "ql3"."lock_approval_policy_fence"(
|
||||
varchar, varchar, varchar, integer, integer
|
||||
)
|
||||
FROM ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE EXECUTE
|
||||
ON FUNCTION "ql3"."lock_active_plugin_package_project"(varchar)
|
||||
FROM ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT
|
||||
ON "ql3"."projects", "ql3"."project_role_bindings"
|
||||
TO ql3_package_manager, ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."security_audit_events"
|
||||
TO ql3_package_manager, ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."plugin_package_install_proposals"
|
||||
TO ql3_package_manager
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT, UPDATE
|
||||
ON "ql3"."approval_requests"
|
||||
TO ql3_package_manager
|
||||
`.trim(),
|
||||
`
|
||||
GRANT EXECUTE
|
||||
ON FUNCTION "ql3"."lock_approval_policy_fence"(
|
||||
varchar, varchar, varchar, integer, integer
|
||||
)
|
||||
TO ql3_package_manager, ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT
|
||||
ON "ql3"."plugin_package_install_proposals"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, UPDATE
|
||||
ON "ql3"."approval_requests"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."approved_action_dispatches"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT, UPDATE
|
||||
ON "ql3"."approved_action_executions",
|
||||
"ql3"."plugin_package_installs",
|
||||
"ql3"."plugin_package_install_heads"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."plugin_package_install_mutations",
|
||||
"ql3"."plugin_package_admission_receipts"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT EXECUTE
|
||||
ON FUNCTION "ql3"."lock_active_plugin_package_project"(varchar)
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 21,
|
||||
migration_id = 'pg-0022-plugin-package-authority-split',
|
||||
capabilities = '${CAPABILITIES_V21}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 20
|
||||
AND migration_id =
|
||||
'pg-0021-approved-action-executions-and-package-proposals'
|
||||
AND capabilities = '${CAPABILITIES_V20}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 20'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V21 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_install":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V22 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0023PluginPackageManagementQuotaMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0023-plugin-package-management-quota',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_management_quota_buckets" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
subject_type varchar(32) NOT NULL,
|
||||
subject_id varchar(255) NOT NULL,
|
||||
operation varchar(64) NOT NULL,
|
||||
window_started_at_ms bigint NOT NULL,
|
||||
consumed_count integer NOT NULL,
|
||||
receipt_ids jsonb NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT plugin_package_management_quota_buckets_pkey
|
||||
PRIMARY KEY (project_id, subject_type, subject_id, operation),
|
||||
CONSTRAINT ql3_plugin_package_management_quota_project_fk
|
||||
FOREIGN KEY (project_id)
|
||||
REFERENCES "ql3"."projects"(id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_management_quota_identity_check
|
||||
CHECK (
|
||||
subject_type = 'user'
|
||||
AND char_length(subject_id) BETWEEN 1 AND 255
|
||||
AND subject_id !~ '[[:cntrl:]]'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_management_quota_operation_check
|
||||
CHECK (
|
||||
operation IN (
|
||||
'plugin-package.propose',
|
||||
'plugin-package.decide',
|
||||
'plugin-package.inspect'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_management_quota_window_check
|
||||
CHECK (
|
||||
window_started_at_ms >= 0
|
||||
AND consumed_count BETWEEN 1 AND 1000
|
||||
AND updated_at_ms >= window_started_at_ms
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_management_quota_receipts_check
|
||||
CHECK (
|
||||
jsonb_typeof(receipt_ids) = 'array'
|
||||
AND jsonb_array_length(receipt_ids) = consumed_count
|
||||
AND octet_length(receipt_ids::text) BETWEEN 3 AND 262144
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."plugin_package_management_quota_buckets"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT, UPDATE
|
||||
ON "ql3"."plugin_package_management_quota_buckets"
|
||||
TO ql3_package_manager
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 22,
|
||||
migration_id = 'pg-0023-plugin-package-management-quota',
|
||||
capabilities = '${CAPABILITIES_V22}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 21
|
||||
AND migration_id = 'pg-0022-plugin-package-authority-split'
|
||||
AND capabilities = '${CAPABILITIES_V21}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 21'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V22 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V23 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0024PluginPackageIdentityKeysetLedgerMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0024-plugin-package-identity-keyset-ledger',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_identity_keyset_ledger" (
|
||||
authority varchar(64) NOT NULL,
|
||||
generation bigint NOT NULL,
|
||||
digest varchar(43) NOT NULL,
|
||||
issuer varchar(512) NOT NULL,
|
||||
audience varchar(256) NOT NULL,
|
||||
active_key_ids jsonb NOT NULL,
|
||||
revoked_key_ids jsonb NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT plugin_package_identity_keyset_ledger_pkey
|
||||
PRIMARY KEY (authority),
|
||||
CONSTRAINT ql3_plugin_package_identity_keyset_authority_check
|
||||
CHECK (authority = 'plugin-package-management'),
|
||||
CONSTRAINT ql3_plugin_package_identity_keyset_generation_check
|
||||
CHECK (generation >= 1 AND updated_at_ms >= 0),
|
||||
CONSTRAINT ql3_plugin_package_identity_keyset_digest_check
|
||||
CHECK (digest ~ '^[A-Za-z0-9_-]{43}$'),
|
||||
CONSTRAINT ql3_plugin_package_identity_keyset_trust_domain_check
|
||||
CHECK (
|
||||
char_length(issuer) BETWEEN 1 AND 512
|
||||
AND issuer !~ '[[:cntrl:]]'
|
||||
AND char_length(audience) BETWEEN 1 AND 256
|
||||
AND audience !~ '[[:cntrl:]]'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_identity_keyset_keys_check
|
||||
CHECK (
|
||||
jsonb_typeof(active_key_ids) = 'array'
|
||||
AND jsonb_array_length(active_key_ids) BETWEEN 1 AND 8
|
||||
AND octet_length(active_key_ids::text) BETWEEN 3 AND 8192
|
||||
AND jsonb_typeof(revoked_key_ids) = 'array'
|
||||
AND jsonb_array_length(revoked_key_ids) BETWEEN 0 AND 64
|
||||
AND octet_length(revoked_key_ids::text) BETWEEN 2 AND 16384
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."plugin_package_identity_keyset_ledger"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT, UPDATE
|
||||
ON "ql3"."plugin_package_identity_keyset_ledger"
|
||||
TO ql3_package_manager
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 23,
|
||||
migration_id = 'pg-0024-plugin-package-identity-keyset-ledger',
|
||||
capabilities = '${CAPABILITIES_V23}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 22
|
||||
AND migration_id = 'pg-0023-plugin-package-management-quota'
|
||||
AND capabilities = '${CAPABILITIES_V22}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 22'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V23 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V24 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0025PluginPackageMaterializedRevisionsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0025-plugin-package-materialized-revisions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_materialized_revisions" (
|
||||
generation_digest char(64) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
generation integer NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
manifest_digest char(64) NOT NULL,
|
||||
revision_digest char(64) NOT NULL,
|
||||
revision_json jsonb NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
CONSTRAINT plugin_package_materialized_revisions_pkey
|
||||
PRIMARY KEY (generation_digest),
|
||||
CONSTRAINT ql3_plugin_package_materialized_revision_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_materialized_revision_identity_check CHECK (
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
|
||||
generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_materialized_revision_digest_check CHECK (
|
||||
generation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
lock_digest ~ '^[0-9a-f]{64}$' AND
|
||||
manifest_digest ~ '^[0-9a-f]{64}$' AND
|
||||
revision_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_materialized_revision_json_check CHECK (
|
||||
jsonb_typeof(revision_json) = 'object' AND
|
||||
octet_length(revision_json::text) BETWEEN 2 AND 25165824 AND
|
||||
revision_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-materialized-revision@v1',
|
||||
'generation', jsonb_build_object(
|
||||
'generationDigest', generation_digest,
|
||||
'projectId', project_id,
|
||||
'packageName', package_name,
|
||||
'generation', generation,
|
||||
'lockDigest', lock_digest
|
||||
),
|
||||
'manifestDigest', manifest_digest,
|
||||
'revisionDigest', revision_digest
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_materialized_revision_time_check CHECK (
|
||||
created_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_materialized_revision_generation_uidx
|
||||
ON "ql3"."plugin_package_materialized_revisions"
|
||||
(project_id, package_name, generation)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_plugin_package_materialized_revision_lock_idx
|
||||
ON "ql3"."plugin_package_materialized_revisions"
|
||||
(lock_digest, generation_digest)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."plugin_package_materialized_revisions"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."plugin_package_materialized_revisions"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 24,
|
||||
migration_id = 'pg-0025-plugin-package-materialized-revisions',
|
||||
capabilities = '${CAPABILITIES_V24}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 23
|
||||
AND migration_id = 'pg-0024-plugin-package-identity-keyset-ledger'
|
||||
AND capabilities = '${CAPABILITIES_V23}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 23'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+622
@@ -0,0 +1,622 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V24 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V25 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0026PluginPackageTaskReconciliationsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0026-plugin-package-task-reconciliations',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_task_ownerships" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
claimed_generation_digest char(64) NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
CONSTRAINT plugin_package_task_ownerships_pkey
|
||||
PRIMARY KEY (project_id, task_id),
|
||||
CONSTRAINT ql3_plugin_package_task_ownership_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_task_ownership_task_fk
|
||||
FOREIGN KEY (project_id, task_id)
|
||||
REFERENCES "ql3"."task_definitions" (project_id, task_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_task_ownership_identity_check CHECK (
|
||||
char_length(task_id) BETWEEN 1 AND 128 AND
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_ownership_digest_check CHECK (
|
||||
claimed_generation_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_ownership_namespace_check CHECK (
|
||||
task_id LIKE 'pkg:' || package_name || ':%'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_ownership_time_check CHECK (
|
||||
created_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_plugin_package_task_ownership_package_idx
|
||||
ON "ql3"."plugin_package_task_ownerships"
|
||||
(project_id, package_name, task_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_task_reconciliations" (
|
||||
generation_digest char(64) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
generation integer NOT NULL,
|
||||
materialized_revision_digest char(64) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
previous_lock_digest char(64),
|
||||
receipt_digest char(64) NOT NULL,
|
||||
receipt_json jsonb NOT NULL,
|
||||
committed_at_ms bigint NOT NULL,
|
||||
CONSTRAINT plugin_package_task_reconciliations_pkey
|
||||
PRIMARY KEY (generation_digest),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_materialized_fk
|
||||
FOREIGN KEY (generation_digest)
|
||||
REFERENCES "ql3"."plugin_package_materialized_revisions"
|
||||
(generation_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_identity_check CHECK (
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
|
||||
generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_digest_check CHECK (
|
||||
generation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
materialized_revision_digest ~ '^[0-9a-f]{64}$' AND
|
||||
lock_digest ~ '^[0-9a-f]{64}$' AND
|
||||
(previous_lock_digest IS NULL OR
|
||||
previous_lock_digest ~ '^[0-9a-f]{64}$') AND
|
||||
receipt_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_json_check CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
octet_length(receipt_json::text) BETWEEN 2 AND 8388608 AND
|
||||
receipt_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-task-reconciliation@v1',
|
||||
'generationDigest', generation_digest,
|
||||
'projectId', project_id,
|
||||
'packageName', package_name,
|
||||
'generation', generation,
|
||||
'materializedRevisionDigest', materialized_revision_digest,
|
||||
'lockDigest', lock_digest,
|
||||
'receiptDigest', receipt_digest
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_time_check CHECK (
|
||||
committed_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_task_reconciliation_generation_uidx
|
||||
ON "ql3"."plugin_package_task_reconciliations"
|
||||
(project_id, package_name, generation)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_plugin_package_task_reconciliation_lock_idx
|
||||
ON "ql3"."plugin_package_task_reconciliations"
|
||||
(lock_digest, generation_digest)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_task_reconciliation_items" (
|
||||
generation_digest char(64) NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
revision integer NOT NULL,
|
||||
disposition varchar(24) NOT NULL,
|
||||
content_digest char(64) NOT NULL,
|
||||
CONSTRAINT plugin_package_task_reconciliation_items_pkey
|
||||
PRIMARY KEY (generation_digest, task_id),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_item_reconciliation_fk
|
||||
FOREIGN KEY (generation_digest)
|
||||
REFERENCES "ql3"."plugin_package_task_reconciliations"
|
||||
(generation_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_item_identity_check CHECK (
|
||||
char_length(task_id) BETWEEN 1 AND 128 AND
|
||||
revision BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_item_disposition_check
|
||||
CHECK (disposition IN (
|
||||
'already_disabled','created','disabled','retained','updated'
|
||||
)),
|
||||
CONSTRAINT ql3_plugin_package_task_reconciliation_item_digest_check CHECK (
|
||||
content_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_plugin_package_task_reconciliation_item_task_idx
|
||||
ON "ql3"."plugin_package_task_reconciliation_items"
|
||||
(task_id, generation_digest)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE FUNCTION "ql3"."commit_plugin_package_task_reconciliation"(
|
||||
p_generation_digest char(64),
|
||||
p_materialized_revision_digest char(64),
|
||||
p_receipt jsonb,
|
||||
p_writes jsonb,
|
||||
p_execution_revisions jsonb
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
DECLARE
|
||||
materialized "ql3"."plugin_package_materialized_revisions"%ROWTYPE;
|
||||
install_record "ql3"."plugin_package_installs"%ROWTYPE;
|
||||
previous_receipt jsonb;
|
||||
expected_ids text[];
|
||||
receipt_ids text[];
|
||||
write_ids text[];
|
||||
execution_ids text[];
|
||||
task_id_value text;
|
||||
item jsonb;
|
||||
write_value jsonb;
|
||||
definition jsonb;
|
||||
execution_value jsonb;
|
||||
resource_value jsonb;
|
||||
current_head "ql3"."task_definitions"%ROWTYPE;
|
||||
current_revision "ql3"."task_definition_revisions"%ROWTYPE;
|
||||
owner_package varchar(63);
|
||||
now_ms bigint;
|
||||
BEGIN
|
||||
IF NOT pg_has_role(session_user, 'ql3_package_executor', 'member') THEN
|
||||
RAISE EXCEPTION 'Package Task reconciliation authority is required'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
IF p_generation_digest !~ '^[0-9a-f]{64}$'
|
||||
OR p_materialized_revision_digest !~ '^[0-9a-f]{64}$'
|
||||
OR jsonb_typeof(p_receipt) <> 'object'
|
||||
OR jsonb_typeof(p_writes) <> 'array'
|
||||
OR jsonb_typeof(p_execution_revisions) <> 'array'
|
||||
OR jsonb_array_length(p_writes) > 512
|
||||
OR jsonb_array_length(p_execution_revisions) > 512 THEN
|
||||
RAISE EXCEPTION 'invalid Package Task reconciliation input'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
SELECT * INTO materialized
|
||||
FROM "ql3"."plugin_package_materialized_revisions"
|
||||
WHERE generation_digest = p_generation_digest
|
||||
FOR KEY SHARE;
|
||||
IF NOT FOUND
|
||||
OR materialized.revision_digest <> p_materialized_revision_digest THEN
|
||||
RAISE EXCEPTION 'materialized revision fence changed'
|
||||
USING ERRCODE = 'serialization_failure';
|
||||
END IF;
|
||||
|
||||
SELECT install.* INTO install_record
|
||||
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 = materialized.project_id
|
||||
AND head.package_name = materialized.package_name
|
||||
FOR UPDATE OF install;
|
||||
IF NOT FOUND
|
||||
OR install_record.state <> 'active'
|
||||
OR install_record.installation_id <>
|
||||
materialized.revision_json #>> '{generation,installationId}'
|
||||
OR install_record.target_generation <> materialized.generation
|
||||
OR install_record.lock_digest <> materialized.lock_digest
|
||||
OR install_record.previous_active_lock_digest IS DISTINCT FROM
|
||||
NULLIF(materialized.revision_json #>> '{generation,previousActiveLockDigest}', '')
|
||||
THEN
|
||||
RAISE EXCEPTION 'active Package install fence changed'
|
||||
USING ERRCODE = 'serialization_failure';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM "ql3"."plugin_package_task_reconciliations"
|
||||
WHERE generation_digest = p_generation_digest
|
||||
AND receipt_json = p_receipt
|
||||
) THEN
|
||||
RETURN false;
|
||||
ELSIF EXISTS (
|
||||
SELECT 1 FROM "ql3"."plugin_package_task_reconciliations"
|
||||
WHERE generation_digest = p_generation_digest
|
||||
) THEN
|
||||
RAISE EXCEPTION 'generation receipt identity is already bound'
|
||||
USING ERRCODE = 'unique_violation';
|
||||
END IF;
|
||||
|
||||
IF materialized.generation = 1 THEN
|
||||
IF materialized.revision_json #>> '{generation,previousActiveLockDigest}'
|
||||
IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'first generation has a previous lock'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
previous_receipt := NULL;
|
||||
ELSE
|
||||
SELECT receipt_json INTO previous_receipt
|
||||
FROM "ql3"."plugin_package_task_reconciliations"
|
||||
WHERE project_id = materialized.project_id
|
||||
AND package_name = materialized.package_name
|
||||
AND generation = materialized.generation - 1
|
||||
FOR KEY SHARE;
|
||||
IF NOT FOUND
|
||||
OR previous_receipt ->> 'lockDigest' <>
|
||||
materialized.revision_json #>>
|
||||
'{generation,previousActiveLockDigest}' THEN
|
||||
RAISE EXCEPTION 'previous generation receipt is missing'
|
||||
USING ERRCODE = 'serialization_failure';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF p_receipt ->> 'schema' <>
|
||||
'qinglong/plugin-package-task-reconciliation@v1'
|
||||
OR p_receipt ->> 'generationDigest' <> p_generation_digest
|
||||
OR p_receipt ->> 'materializedRevisionDigest' <>
|
||||
p_materialized_revision_digest
|
||||
OR p_receipt ->> 'projectId' <> materialized.project_id
|
||||
OR p_receipt ->> 'packageName' <> materialized.package_name
|
||||
OR (p_receipt ->> 'generation')::integer <> materialized.generation
|
||||
OR p_receipt ->> 'lockDigest' <> materialized.lock_digest
|
||||
OR jsonb_typeof(p_receipt -> 'items') <> 'array'
|
||||
OR jsonb_array_length(p_receipt -> 'items') > 512 THEN
|
||||
RAISE EXCEPTION 'receipt does not match materialized generation'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
SELECT array_agg(value ORDER BY value) INTO expected_ids
|
||||
FROM (
|
||||
SELECT 'pkg:' || materialized.package_name || ':' ||
|
||||
(resource_json -> 'value' ->> 'id') AS value
|
||||
FROM jsonb_array_elements(materialized.revision_json -> 'resources')
|
||||
AS resources(resource_json)
|
||||
WHERE resource_json ->> 'kind' = 'task'
|
||||
UNION
|
||||
SELECT previous_item_json ->> 'taskId'
|
||||
FROM jsonb_array_elements(COALESCE(previous_receipt -> 'items', '[]'::jsonb))
|
||||
AS previous_items(previous_item_json)
|
||||
) AS expected;
|
||||
expected_ids := COALESCE(expected_ids, ARRAY[]::text[]);
|
||||
|
||||
SELECT array_agg(value ORDER BY value), count(DISTINCT value)
|
||||
INTO receipt_ids, now_ms
|
||||
FROM (
|
||||
SELECT receipt_item_json ->> 'taskId' AS value
|
||||
FROM jsonb_array_elements(p_receipt -> 'items')
|
||||
AS receipt_items(receipt_item_json)
|
||||
) AS receipt_values;
|
||||
receipt_ids := COALESCE(receipt_ids, ARRAY[]::text[]);
|
||||
IF receipt_ids <> expected_ids
|
||||
OR now_ms <> cardinality(receipt_ids) THEN
|
||||
RAISE EXCEPTION 'receipt does not exactly cover the generation task set'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
SELECT array_agg(value ORDER BY value), count(DISTINCT value)
|
||||
INTO write_ids, now_ms
|
||||
FROM (
|
||||
SELECT write_entry_json -> 'definition' ->> 'taskId' AS value
|
||||
FROM jsonb_array_elements(p_writes) AS writes(write_entry_json)
|
||||
) AS write_values;
|
||||
write_ids := COALESCE(write_ids, ARRAY[]::text[]);
|
||||
IF now_ms <> cardinality(write_ids)
|
||||
OR NOT write_ids <@ expected_ids THEN
|
||||
RAISE EXCEPTION 'writes are duplicated or outside the generation task set'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
SELECT array_agg(value ORDER BY value), count(DISTINCT value)
|
||||
INTO execution_ids, now_ms
|
||||
FROM (
|
||||
SELECT execution_entry_json ->> 'taskId' AS value
|
||||
FROM jsonb_array_elements(p_execution_revisions)
|
||||
AS executions(execution_entry_json)
|
||||
) AS execution_values;
|
||||
execution_ids := COALESCE(execution_ids, ARRAY[]::text[]);
|
||||
IF now_ms <> cardinality(execution_ids)
|
||||
OR NOT execution_ids <@ write_ids THEN
|
||||
RAISE EXCEPTION 'execution revisions are duplicated or unbound'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
FOREACH task_id_value IN ARRAY expected_ids LOOP
|
||||
SELECT receipt_item_json INTO item
|
||||
FROM jsonb_array_elements(p_receipt -> 'items')
|
||||
AS receipt_items(receipt_item_json)
|
||||
WHERE receipt_item_json ->> 'taskId' = task_id_value;
|
||||
SELECT write_entry_json INTO write_value
|
||||
FROM jsonb_array_elements(p_writes) AS writes(write_entry_json)
|
||||
WHERE write_entry_json -> 'definition' ->> 'taskId' = task_id_value;
|
||||
definition := write_value -> 'definition';
|
||||
SELECT resource_json -> 'value' INTO resource_value
|
||||
FROM jsonb_array_elements(materialized.revision_json -> 'resources')
|
||||
AS resources(resource_json)
|
||||
WHERE resource_json ->> 'kind' = 'task'
|
||||
AND 'pkg:' || materialized.package_name || ':' ||
|
||||
(resource_json -> 'value' ->> 'id') = task_id_value;
|
||||
|
||||
SELECT * INTO current_head
|
||||
FROM "ql3"."task_definitions"
|
||||
WHERE project_id = materialized.project_id AND task_id = task_id_value
|
||||
FOR UPDATE;
|
||||
IF FOUND THEN
|
||||
SELECT * INTO current_revision
|
||||
FROM "ql3"."task_definition_revisions"
|
||||
WHERE project_id = current_head.project_id
|
||||
AND task_id = current_head.task_id
|
||||
AND revision = current_head.current_revision;
|
||||
SELECT package_name INTO owner_package
|
||||
FROM "ql3"."plugin_package_task_ownerships"
|
||||
WHERE project_id = current_head.project_id
|
||||
AND task_id = current_head.task_id;
|
||||
IF owner_package IS DISTINCT FROM materialized.package_name THEN
|
||||
RAISE EXCEPTION 'TaskDefinition ownership collision'
|
||||
USING ERRCODE = 'unique_violation';
|
||||
END IF;
|
||||
ELSE
|
||||
current_revision := NULL;
|
||||
owner_package := NULL;
|
||||
END IF;
|
||||
|
||||
IF item ->> 'disposition' IN ('created','updated','disabled') THEN
|
||||
IF write_value IS NULL
|
||||
OR item ->> 'contentDigest' <> definition ->> 'contentDigest'
|
||||
OR (item ->> 'revision')::integer <>
|
||||
(definition ->> 'revision')::integer
|
||||
OR write_value -> 'command' ->> 'projectId' <>
|
||||
materialized.project_id
|
||||
OR write_value -> 'command' ->> 'taskId' <> task_id_value
|
||||
OR write_value -> 'command' ->> 'mutationId' <>
|
||||
definition ->> 'mutationId' THEN
|
||||
RAISE EXCEPTION 'receipt write binding is invalid'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
ELSIF write_value IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'non-mutating receipt item has a write'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
IF resource_value IS NOT NULL THEN
|
||||
IF item ->> 'disposition' NOT IN ('created','retained','updated') THEN
|
||||
RAISE EXCEPTION 'active materialized Task has an invalid disposition'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
IF write_value IS NOT NULL AND (
|
||||
definition ->> 'projectId' <> materialized.project_id OR
|
||||
definition ->> 'taskId' <> task_id_value OR
|
||||
definition ->> 'name' <> resource_value ->> 'name' OR
|
||||
definition -> 'description' IS DISTINCT FROM
|
||||
resource_value -> 'description' OR
|
||||
definition ->> 'kind' <> resource_value ->> 'kind' OR
|
||||
definition -> 'spec' IS DISTINCT FROM resource_value -> 'spec' OR
|
||||
definition -> 'labels' IS DISTINCT FROM resource_value -> 'labels' OR
|
||||
definition -> 'enabled' IS DISTINCT FROM resource_value -> 'enabled'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Task write differs from materialized resource'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
ELSE
|
||||
IF item ->> 'disposition' NOT IN ('already_disabled','disabled') THEN
|
||||
RAISE EXCEPTION 'removed Task has an invalid disposition'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
IF item ->> 'disposition' = 'disabled' AND (
|
||||
current_revision IS NULL OR
|
||||
definition ->> 'name' <> current_revision.name OR
|
||||
definition -> 'description' IS DISTINCT FROM
|
||||
to_jsonb(current_revision.description) OR
|
||||
definition ->> 'kind' <> current_revision.kind OR
|
||||
definition -> 'spec' IS DISTINCT FROM current_revision.spec_json OR
|
||||
definition -> 'labels' IS DISTINCT FROM current_revision.labels_json OR
|
||||
definition -> 'enabled' <> 'false'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'removed Task disable write is invalid'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF write_value IS NULL THEN
|
||||
IF current_revision IS NULL
|
||||
OR (item ->> 'revision')::integer <> current_revision.revision
|
||||
OR item ->> 'contentDigest' <> current_revision.content_digest THEN
|
||||
RAISE EXCEPTION 'retained Task head changed'
|
||||
USING ERRCODE = 'serialization_failure';
|
||||
END IF;
|
||||
ELSIF current_revision IS NULL THEN
|
||||
IF item ->> 'disposition' <> 'created'
|
||||
OR write_value -> 'command' -> 'expectedRevision' <> 'null'::jsonb
|
||||
OR (definition ->> 'revision')::integer <> 1 THEN
|
||||
RAISE EXCEPTION 'Task create CAS is invalid'
|
||||
USING ERRCODE = 'serialization_failure';
|
||||
END IF;
|
||||
ELSIF item ->> 'disposition' = 'created'
|
||||
OR (write_value -> 'command' ->> 'expectedRevision')::integer <>
|
||||
current_revision.revision
|
||||
OR (definition ->> 'revision')::integer <>
|
||||
current_revision.revision + 1 THEN
|
||||
RAISE EXCEPTION 'Task update CAS is invalid'
|
||||
USING ERRCODE = 'serialization_failure';
|
||||
END IF;
|
||||
|
||||
execution_value := NULL;
|
||||
SELECT execution_entry_json INTO execution_value
|
||||
FROM jsonb_array_elements(p_execution_revisions)
|
||||
AS executions(execution_entry_json)
|
||||
WHERE execution_entry_json ->> 'taskId' = task_id_value;
|
||||
IF write_value IS NOT NULL
|
||||
AND definition -> 'enabled' = 'true'::jsonb
|
||||
AND definition ->> 'kind' = 'command'
|
||||
AND definition #>> '{spec,schema}' = 'qinglong/command@v1' THEN
|
||||
IF execution_value IS NULL
|
||||
OR (execution_value ->> 'sourceRevision')::integer <>
|
||||
(definition ->> 'revision')::integer
|
||||
OR execution_value ->> 'sourceContentDigest' <>
|
||||
definition ->> 'contentDigest' THEN
|
||||
RAISE EXCEPTION 'required execution revision is missing'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
ELSIF execution_value IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'unexpected execution revision'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
now_ms := floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint;
|
||||
FOREACH task_id_value IN ARRAY write_ids LOOP
|
||||
SELECT write_entry_json INTO write_value
|
||||
FROM jsonb_array_elements(p_writes) AS writes(write_entry_json)
|
||||
WHERE write_entry_json -> 'definition' ->> 'taskId' = task_id_value;
|
||||
definition := write_value -> 'definition';
|
||||
IF write_value -> 'command' -> 'expectedRevision' = 'null'::jsonb THEN
|
||||
INSERT INTO "ql3"."task_definitions" (
|
||||
project_id, task_id, current_revision, created_at_ms, updated_at_ms
|
||||
) VALUES (
|
||||
definition ->> 'projectId', definition ->> 'taskId', 1,
|
||||
(definition ->> 'createdAtMs')::bigint,
|
||||
(definition ->> 'updatedAtMs')::bigint
|
||||
);
|
||||
INSERT INTO "ql3"."plugin_package_task_ownerships" (
|
||||
project_id, task_id, package_name,
|
||||
claimed_generation_digest, created_at_ms
|
||||
) VALUES (
|
||||
materialized.project_id, task_id_value, materialized.package_name,
|
||||
p_generation_digest, now_ms
|
||||
);
|
||||
END IF;
|
||||
INSERT INTO "ql3"."task_definition_revisions" (
|
||||
project_id, task_id, revision, mutation_id, name, description, kind,
|
||||
spec_json, labels_json, enabled, content_digest, created_at_ms
|
||||
) VALUES (
|
||||
definition ->> 'projectId', definition ->> 'taskId',
|
||||
(definition ->> 'revision')::integer,
|
||||
(definition ->> 'mutationId')::uuid,
|
||||
definition ->> 'name', definition ->> 'description',
|
||||
definition ->> 'kind', definition -> 'spec', definition -> 'labels',
|
||||
(definition ->> 'enabled')::boolean, definition ->> 'contentDigest',
|
||||
(definition ->> 'updatedAtMs')::bigint
|
||||
);
|
||||
SELECT execution_entry_json INTO execution_value
|
||||
FROM jsonb_array_elements(p_execution_revisions)
|
||||
AS executions(execution_entry_json)
|
||||
WHERE execution_entry_json ->> 'taskId' = task_id_value;
|
||||
IF execution_value IS NOT NULL THEN
|
||||
INSERT INTO "ql3"."task_execution_revisions" (
|
||||
project_id, task_id, source_revision, task_revision,
|
||||
source_content_digest, executor_type, plan_schema, plan_json,
|
||||
content_digest, created_at_ms
|
||||
) VALUES (
|
||||
execution_value ->> 'projectId', execution_value ->> 'taskId',
|
||||
(execution_value ->> 'sourceRevision')::integer,
|
||||
execution_value ->> 'taskRevision',
|
||||
execution_value ->> 'sourceContentDigest',
|
||||
execution_value ->> 'executorType',
|
||||
execution_value ->> 'planSchema',
|
||||
execution_value -> 'planJson',
|
||||
execution_value ->> 'contentDigest',
|
||||
(execution_value ->> 'createdAtMs')::bigint
|
||||
);
|
||||
END IF;
|
||||
IF write_value -> 'command' -> 'expectedRevision' <> 'null'::jsonb THEN
|
||||
UPDATE "ql3"."task_definitions" AS task_head
|
||||
SET current_revision = (definition ->> 'revision')::integer,
|
||||
updated_at_ms = (definition ->> 'updatedAtMs')::bigint
|
||||
WHERE task_head.project_id = materialized.project_id
|
||||
AND task_head.task_id = task_id_value
|
||||
AND task_head.current_revision =
|
||||
(write_value -> 'command' ->> 'expectedRevision')::integer;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Task head changed while committing'
|
||||
USING ERRCODE = 'serialization_failure';
|
||||
END IF;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
INSERT INTO "ql3"."plugin_package_task_reconciliations" (
|
||||
generation_digest, project_id, package_name, generation,
|
||||
materialized_revision_digest, lock_digest, previous_lock_digest,
|
||||
receipt_digest, receipt_json, committed_at_ms
|
||||
) VALUES (
|
||||
p_generation_digest, materialized.project_id, materialized.package_name,
|
||||
materialized.generation, p_materialized_revision_digest,
|
||||
materialized.lock_digest,
|
||||
NULLIF(p_receipt ->> 'previousLockDigest', ''),
|
||||
p_receipt ->> 'receiptDigest', p_receipt,
|
||||
(p_receipt ->> 'committedAtMs')::bigint
|
||||
);
|
||||
INSERT INTO "ql3"."plugin_package_task_reconciliation_items" (
|
||||
generation_digest, task_id, revision, disposition, content_digest
|
||||
)
|
||||
SELECT p_generation_digest, receipt_item_json ->> 'taskId',
|
||||
(receipt_item_json ->> 'revision')::integer,
|
||||
receipt_item_json ->> 'disposition',
|
||||
receipt_item_json ->> 'contentDigest'
|
||||
FROM jsonb_array_elements(p_receipt -> 'items')
|
||||
AS receipt_items(receipt_item_json);
|
||||
RETURN true;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."plugin_package_task_ownerships",
|
||||
"ql3"."plugin_package_task_reconciliations",
|
||||
"ql3"."plugin_package_task_reconciliation_items"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT
|
||||
ON "ql3"."plugin_package_task_ownerships"
|
||||
TO ql3_admin, ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT
|
||||
ON "ql3"."plugin_package_task_reconciliations",
|
||||
"ql3"."plugin_package_task_reconciliation_items",
|
||||
"ql3"."task_definitions",
|
||||
"ql3"."task_definition_revisions",
|
||||
"ql3"."task_execution_revisions"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON FUNCTION "ql3"."commit_plugin_package_task_reconciliation"(
|
||||
char(64), char(64), jsonb, jsonb, jsonb
|
||||
)
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT EXECUTE
|
||||
ON FUNCTION "ql3"."commit_plugin_package_task_reconciliation"(
|
||||
char(64), char(64), jsonb, jsonb, jsonb
|
||||
)
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 25,
|
||||
migration_id = 'pg-0026-plugin-package-task-reconciliations',
|
||||
capabilities = '${CAPABILITIES_V25}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 24
|
||||
AND migration_id = 'pg-0025-plugin-package-materialized-revisions'
|
||||
AND capabilities = '${CAPABILITIES_V24}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 24'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V25 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V26 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0027ProjectToolDefinitionSnapshotsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0027-project-tool-definition-snapshots',
|
||||
statements: [
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_installs_snapshot_source_uidx
|
||||
ON "ql3"."plugin_package_installs"
|
||||
(project_id, package_name, installation_id, target_generation, lock_digest)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_materialized_revision_snapshot_source_uidx
|
||||
ON "ql3"."plugin_package_materialized_revisions"
|
||||
(project_id, package_name, generation, generation_digest,
|
||||
lock_digest, revision_digest)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."project_tool_definition_snapshots" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
active_vector_digest char(64) NOT NULL,
|
||||
definitions_digest char(64) NOT NULL,
|
||||
snapshot_digest char(64) NOT NULL,
|
||||
snapshot_json jsonb NOT NULL,
|
||||
committed_at_ms bigint NOT NULL,
|
||||
CONSTRAINT project_tool_definition_snapshots_pkey
|
||||
PRIMARY KEY (project_id, active_vector_digest),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_identity_check CHECK (
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_digest_check CHECK (
|
||||
active_vector_digest ~ '^[0-9a-f]{64}$' AND
|
||||
definitions_digest ~ '^[0-9a-f]{64}$' AND
|
||||
snapshot_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_json_check CHECK (
|
||||
jsonb_typeof(snapshot_json) = 'object' AND
|
||||
octet_length(snapshot_json::text) BETWEEN 2 AND 8388608 AND
|
||||
snapshot_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/project-tool-definition-snapshot@v1',
|
||||
'projectId', project_id,
|
||||
'activeVectorDigest', active_vector_digest,
|
||||
'definitionsDigest', definitions_digest,
|
||||
'snapshotDigest', snapshot_digest
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_time_check CHECK (
|
||||
committed_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_project_tool_definition_snapshot_digest_uidx
|
||||
ON "ql3"."project_tool_definition_snapshots" (snapshot_digest)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_project_tool_definition_snapshot_current_idx
|
||||
ON "ql3"."project_tool_definition_snapshots"
|
||||
(project_id, committed_at_ms DESC, active_vector_digest)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."project_tool_definition_snapshot_sources" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
active_vector_digest char(64) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
installation_id varchar(128) NOT NULL,
|
||||
generation integer NOT NULL,
|
||||
generation_digest char(64) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
revision_digest char(64) NOT NULL,
|
||||
CONSTRAINT project_tool_definition_snapshot_sources_pkey
|
||||
PRIMARY KEY (project_id, active_vector_digest, package_name),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_source_snapshot_fk
|
||||
FOREIGN KEY (project_id, active_vector_digest)
|
||||
REFERENCES "ql3"."project_tool_definition_snapshots"
|
||||
(project_id, active_vector_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_source_install_fk
|
||||
FOREIGN KEY (
|
||||
project_id, package_name, installation_id, generation, lock_digest
|
||||
)
|
||||
REFERENCES "ql3"."plugin_package_installs" (
|
||||
project_id, package_name, installation_id, target_generation, lock_digest
|
||||
)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_source_revision_fk
|
||||
FOREIGN KEY (
|
||||
project_id, package_name, generation, generation_digest,
|
||||
lock_digest, revision_digest
|
||||
)
|
||||
REFERENCES "ql3"."plugin_package_materialized_revisions" (
|
||||
project_id, package_name, generation, generation_digest,
|
||||
lock_digest, revision_digest
|
||||
)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_source_identity_check CHECK (
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
|
||||
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_project_tool_definition_snapshot_source_digest_check CHECK (
|
||||
active_vector_digest ~ '^[0-9a-f]{64}$' AND
|
||||
generation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
lock_digest ~ '^[0-9a-f]{64}$' AND
|
||||
revision_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_project_tool_definition_snapshot_source_generation_idx
|
||||
ON "ql3"."project_tool_definition_snapshot_sources"
|
||||
(generation_digest, project_id, package_name)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_project_tool_definition_snapshot_source_install_idx
|
||||
ON "ql3"."project_tool_definition_snapshot_sources"
|
||||
(installation_id, active_vector_digest)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."project_tool_definition_snapshots",
|
||||
"ql3"."project_tool_definition_snapshot_sources"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."project_tool_definition_snapshots",
|
||||
"ql3"."project_tool_definition_snapshot_sources"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 26,
|
||||
migration_id = 'pg-0027-project-tool-definition-snapshots',
|
||||
capabilities = '${CAPABILITIES_V26}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 25
|
||||
AND migration_id = 'pg-0026-plugin-package-task-reconciliations'
|
||||
AND capabilities = '${CAPABILITIES_V25}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 25'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V26 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V27 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0028StepRunsMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0028-step-runs',
|
||||
statements: [
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM "ql3"."run_attempts" WHERE step_run_id IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT 1 FROM "ql3"."run_events" WHERE step_run_id IS NOT NULL
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'legacy step_run_id cannot be upgraded without StepRun authority'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
ALTER TABLE "ql3"."run_attempts"
|
||||
ALTER COLUMN step_run_id TYPE varchar(128)
|
||||
`.trim(),
|
||||
`
|
||||
ALTER TABLE "ql3"."run_events"
|
||||
ALTER COLUMN step_run_id TYPE varchar(128)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."step_runs" (
|
||||
id varchar(128) PRIMARY KEY,
|
||||
run_id varchar(36) NOT NULL,
|
||||
parent_step_run_id varchar(128),
|
||||
step_key varchar(128) NOT NULL,
|
||||
kind varchar(32) NOT NULL,
|
||||
definition_ref varchar(512) NOT NULL,
|
||||
definition_digest char(64) NOT NULL,
|
||||
required boolean NOT NULL,
|
||||
status varchar(32) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
attempt_count integer NOT NULL,
|
||||
input_ref varchar(512),
|
||||
output_ref varchar(512),
|
||||
approval_request_id varchar(128),
|
||||
ready_at_ms bigint,
|
||||
started_at_ms bigint,
|
||||
finished_at_ms bigint,
|
||||
result_code varchar(64),
|
||||
error_summary varchar(2048),
|
||||
created_at_ms bigint NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
last_mutation_id varchar(128) NOT NULL,
|
||||
step_run_digest char(64) NOT NULL,
|
||||
step_run_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_step_runs_run_fk
|
||||
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_step_runs_identity_check CHECK (
|
||||
id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
(parent_step_run_id IS NULL OR
|
||||
(parent_step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
parent_step_run_id <> id)) AND
|
||||
step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
octet_length(definition_ref) BETWEEN 1 AND 512
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_kind_check CHECK (
|
||||
kind IN (
|
||||
'task', 'tool', 'model', 'agent', 'condition', 'approval',
|
||||
'subworkflow'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_status_check CHECK (
|
||||
status IN (
|
||||
'pending', 'ready', 'waiting_approval', 'running', 'lost',
|
||||
'succeeded', 'failed', 'skipped', 'cancelled', 'timed_out'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_digest_check CHECK (
|
||||
definition_digest ~ '^[0-9a-f]{64}$' AND
|
||||
step_run_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_counter_check CHECK (
|
||||
version BETWEEN 1 AND 2147483647 AND
|
||||
attempt_count BETWEEN 0 AND 64
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_reference_check CHECK (
|
||||
(input_ref IS NULL OR octet_length(input_ref) BETWEEN 1 AND 512) AND
|
||||
(output_ref IS NULL OR octet_length(output_ref) BETWEEN 1 AND 512) AND
|
||||
(approval_request_id IS NULL OR
|
||||
approval_request_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$')
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_time_check CHECK (
|
||||
created_at_ms >= 0 AND updated_at_ms >= created_at_ms AND
|
||||
(ready_at_ms IS NULL OR
|
||||
ready_at_ms BETWEEN created_at_ms AND updated_at_ms) AND
|
||||
(started_at_ms IS NULL OR
|
||||
(ready_at_ms IS NOT NULL AND
|
||||
started_at_ms BETWEEN ready_at_ms AND updated_at_ms)) AND
|
||||
(finished_at_ms IS NULL OR
|
||||
(finished_at_ms BETWEEN created_at_ms AND updated_at_ms AND
|
||||
(ready_at_ms IS NULL OR finished_at_ms >= ready_at_ms) AND
|
||||
(started_at_ms IS NULL OR finished_at_ms >= started_at_ms)))
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_state_shape_check CHECK (
|
||||
(status = 'pending' AND ready_at_ms IS NULL AND
|
||||
started_at_ms IS NULL AND finished_at_ms IS NULL) OR
|
||||
(status IN ('ready', 'waiting_approval') AND ready_at_ms IS NOT NULL AND
|
||||
started_at_ms IS NULL AND finished_at_ms IS NULL) OR
|
||||
(status IN ('running', 'lost') AND ready_at_ms IS NOT NULL AND
|
||||
started_at_ms IS NOT NULL AND finished_at_ms IS NULL) OR
|
||||
(status IN ('succeeded', 'failed', 'skipped', 'cancelled', 'timed_out')
|
||||
AND finished_at_ms IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_approval_shape_check CHECK (
|
||||
status <> 'waiting_approval' OR approval_request_id IS NOT NULL
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_result_shape_check CHECK (
|
||||
(output_ref IS NULL OR status = 'succeeded') AND
|
||||
((status = 'succeeded' AND result_code IS NULL AND
|
||||
error_summary IS NULL) OR
|
||||
(status IN ('failed', 'skipped', 'cancelled', 'timed_out', 'lost')
|
||||
AND result_code IS NOT NULL) OR
|
||||
(status IN ('pending', 'ready', 'waiting_approval', 'running')
|
||||
AND result_code IS NULL AND error_summary IS NULL)) AND
|
||||
(result_code IS NULL OR result_code ~ '^[a-z][a-z0-9_]{0,63}$') AND
|
||||
(error_summary IS NULL OR octet_length(error_summary) BETWEEN 1 AND 2048)
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_mutation_identity_check CHECK (
|
||||
last_mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_step_runs_json_check CHECK (
|
||||
jsonb_typeof(step_run_json) = 'object' AND
|
||||
octet_length(step_run_json::text) BETWEEN 2 AND 16384 AND
|
||||
step_run_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/step-run@v1',
|
||||
'id', id,
|
||||
'runId', run_id,
|
||||
'parentStepRunId', parent_step_run_id,
|
||||
'stepKey', step_key,
|
||||
'kind', kind,
|
||||
'definitionRef', definition_ref,
|
||||
'definitionDigest', definition_digest,
|
||||
'required', required,
|
||||
'status', status,
|
||||
'version', version,
|
||||
'attemptCount', attempt_count,
|
||||
'inputRef', input_ref,
|
||||
'outputRef', output_ref,
|
||||
'approvalRequestId', approval_request_id,
|
||||
'readyAtMs', ready_at_ms,
|
||||
'startedAtMs', started_at_ms,
|
||||
'finishedAtMs', finished_at_ms,
|
||||
'resultCode', result_code,
|
||||
'errorSummary', error_summary,
|
||||
'createdAtMs', created_at_ms,
|
||||
'updatedAtMs', updated_at_ms,
|
||||
'lastMutationId', last_mutation_id,
|
||||
'stepRunDigest', step_run_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_step_runs_run_id_uidx
|
||||
ON "ql3"."step_runs" (run_id, id)
|
||||
`.trim(),
|
||||
`
|
||||
ALTER TABLE "ql3"."step_runs"
|
||||
ADD CONSTRAINT ql3_step_runs_parent_fk
|
||||
FOREIGN KEY (run_id, parent_step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id)
|
||||
ON DELETE RESTRICT
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_step_runs_run_step_uidx
|
||||
ON "ql3"."step_runs" (run_id, step_key)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_step_runs_run_status_idx
|
||||
ON "ql3"."step_runs" (run_id, status, id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_step_runs_recovery_idx
|
||||
ON "ql3"."step_runs" (status, updated_at_ms, id)
|
||||
WHERE status IN ('waiting_approval', 'running', 'lost')
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."step_run_mutations" (
|
||||
mutation_id varchar(128) PRIMARY KEY,
|
||||
mutation_digest char(64) NOT NULL,
|
||||
run_id varchar(36) NOT NULL,
|
||||
step_run_id varchar(128) NOT NULL,
|
||||
step_run_digest char(64) NOT NULL,
|
||||
event_id varchar(36) NOT NULL,
|
||||
event_sequence integer NOT NULL,
|
||||
run_version integer NOT NULL,
|
||||
step_run_json jsonb NOT NULL,
|
||||
committed_at_ms bigint NOT NULL,
|
||||
CONSTRAINT ql3_step_run_mutations_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_step_run_mutations_event_fk
|
||||
FOREIGN KEY (event_id) REFERENCES "ql3"."run_events" (id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_step_run_mutations_identity_check CHECK (
|
||||
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_step_run_mutations_digest_check CHECK (
|
||||
mutation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
step_run_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_step_run_mutations_counter_check CHECK (
|
||||
event_sequence BETWEEN 1 AND 2147483647 AND
|
||||
run_version BETWEEN 1 AND 2147483647 AND
|
||||
committed_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_step_run_mutations_json_check CHECK (
|
||||
jsonb_typeof(step_run_json) = 'object' AND
|
||||
octet_length(step_run_json::text) BETWEEN 2 AND 16384 AND
|
||||
step_run_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/step-run@v1',
|
||||
'id', step_run_id,
|
||||
'runId', run_id,
|
||||
'lastMutationId', mutation_id,
|
||||
'stepRunDigest', step_run_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_step_run_mutations_event_uidx
|
||||
ON "ql3"."step_run_mutations" (event_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_step_run_mutations_step_idx
|
||||
ON "ql3"."step_run_mutations"
|
||||
(run_id, step_run_id, event_sequence, mutation_id)
|
||||
`.trim(),
|
||||
`
|
||||
ALTER TABLE "ql3"."run_attempts"
|
||||
ADD CONSTRAINT ql3_run_attempts_step_run_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id)
|
||||
ON DELETE RESTRICT
|
||||
`.trim(),
|
||||
`
|
||||
ALTER TABLE "ql3"."run_events"
|
||||
ADD CONSTRAINT ql3_run_events_step_run_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id)
|
||||
ON DELETE RESTRICT
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."step_runs", "ql3"."step_run_mutations"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT, UPDATE
|
||||
ON "ql3"."step_runs"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."step_run_mutations"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 27,
|
||||
migration_id = 'pg-0028-step-runs',
|
||||
capabilities = '${CAPABILITIES_V27}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 26
|
||||
AND migration_id = 'pg-0027-project-tool-definition-snapshots'
|
||||
AND capabilities = '${CAPABILITIES_V26}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 26'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V27 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V28 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0029ToolExecutionEvidenceMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0029-tool-execution-evidence',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_execution_trace_anchors" (
|
||||
trace_id char(32) NOT NULL,
|
||||
span_id char(16) NOT NULL,
|
||||
parent_span_id char(16),
|
||||
project_id varchar(128) NOT NULL,
|
||||
run_id varchar(36) NOT NULL,
|
||||
step_run_id varchar(128) NOT NULL,
|
||||
invocation_plan_digest char(64) NOT NULL,
|
||||
binding_digest char(64) NOT NULL,
|
||||
adapter_digest char(64) NOT NULL,
|
||||
redaction_contract_digest char(64) NOT NULL,
|
||||
audit_contract_digest char(64) NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
trace_digest char(64) NOT NULL,
|
||||
trace_json jsonb NOT NULL,
|
||||
CONSTRAINT tool_execution_trace_anchors_pkey
|
||||
PRIMARY KEY (trace_id, span_id),
|
||||
CONSTRAINT ql3_tool_execution_trace_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_tool_execution_trace_identity_check CHECK (
|
||||
trace_id ~ '^[0-9a-f]{32}$' AND
|
||||
span_id ~ '^[0-9a-f]{16}$' AND
|
||||
(parent_span_id IS NULL OR (
|
||||
parent_span_id ~ '^[0-9a-f]{16}$' AND parent_span_id <> span_id
|
||||
)) AND
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_trace_digest_check CHECK (
|
||||
invocation_plan_digest ~ '^[0-9a-f]{64}$' AND
|
||||
binding_digest ~ '^[0-9a-f]{64}$' AND
|
||||
adapter_digest ~ '^[0-9a-f]{64}$' AND
|
||||
redaction_contract_digest ~ '^[0-9a-f]{64}$' AND
|
||||
audit_contract_digest ~ '^[0-9a-f]{64}$' AND
|
||||
trace_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_trace_time_check CHECK (
|
||||
created_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_trace_json_check CHECK (
|
||||
jsonb_typeof(trace_json) = 'object' AND
|
||||
octet_length(trace_json::text) BETWEEN 2 AND 16384 AND
|
||||
trace_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-execution-trace-anchor@v1',
|
||||
'traceId', trace_id,
|
||||
'spanId', span_id,
|
||||
'parentSpanId', parent_span_id,
|
||||
'projectId', project_id,
|
||||
'runId', run_id,
|
||||
'stepRunId', step_run_id,
|
||||
'invocationPlanDigest', invocation_plan_digest,
|
||||
'bindingDigest', binding_digest,
|
||||
'adapterDigest', adapter_digest,
|
||||
'redactionContractDigest', redaction_contract_digest,
|
||||
'auditContractDigest', audit_contract_digest,
|
||||
'createdAtMs', created_at_ms,
|
||||
'traceDigest', trace_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_execution_trace_run_idx
|
||||
ON "ql3"."tool_execution_trace_anchors"
|
||||
(run_id, created_at_ms, trace_id, span_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_execution_trace_step_idx
|
||||
ON "ql3"."tool_execution_trace_anchors"
|
||||
(run_id, step_run_id, created_at_ms, trace_id, span_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_execution_audit_receipts" (
|
||||
event_id uuid PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
run_id varchar(36) NOT NULL,
|
||||
step_run_id varchar(128) NOT NULL,
|
||||
trace_id char(32) NOT NULL,
|
||||
span_id char(16) NOT NULL,
|
||||
trace_digest char(64) NOT NULL,
|
||||
invocation_plan_digest char(64) NOT NULL,
|
||||
binding_digest char(64) NOT NULL,
|
||||
audit_record_digest char(64) NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
receipt_digest char(64) NOT NULL,
|
||||
audit_json jsonb NOT NULL,
|
||||
receipt_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_tool_execution_audit_event_fk
|
||||
FOREIGN KEY (event_id)
|
||||
REFERENCES "ql3"."security_audit_events" (event_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_execution_audit_trace_fk
|
||||
FOREIGN KEY (trace_id, span_id)
|
||||
REFERENCES "ql3"."tool_execution_trace_anchors" (trace_id, span_id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_tool_execution_audit_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE CASCADE,
|
||||
CONSTRAINT ql3_tool_execution_audit_identity_check CHECK (
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
trace_id ~ '^[0-9a-f]{32}$' AND
|
||||
span_id ~ '^[0-9a-f]{16}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_audit_digest_check CHECK (
|
||||
trace_digest ~ '^[0-9a-f]{64}$' AND
|
||||
invocation_plan_digest ~ '^[0-9a-f]{64}$' AND
|
||||
binding_digest ~ '^[0-9a-f]{64}$' AND
|
||||
audit_record_digest ~ '^[0-9a-f]{64}$' AND
|
||||
receipt_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_audit_time_check CHECK (
|
||||
created_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_audit_json_check CHECK (
|
||||
jsonb_typeof(audit_json) = 'object' AND
|
||||
octet_length(audit_json::text) BETWEEN 2 AND 8192 AND
|
||||
audit_json @> jsonb_build_object(
|
||||
'eventId', event_id,
|
||||
'projectId', project_id,
|
||||
'operationId', 'tool.invoke.start',
|
||||
'outcome', 'allowed',
|
||||
'occurredAtMs', created_at_ms
|
||||
) AND
|
||||
jsonb_typeof(audit_json -> 'fence') = 'object'
|
||||
),
|
||||
CONSTRAINT ql3_tool_execution_audit_receipt_json_check CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
octet_length(receipt_json::text) BETWEEN 2 AND 16384 AND
|
||||
receipt_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-execution-audit-receipt@v1',
|
||||
'eventId', event_id,
|
||||
'projectId', project_id,
|
||||
'runId', run_id,
|
||||
'stepRunId', step_run_id,
|
||||
'traceId', trace_id,
|
||||
'spanId', span_id,
|
||||
'traceDigest', trace_digest,
|
||||
'invocationPlanDigest', invocation_plan_digest,
|
||||
'bindingDigest', binding_digest,
|
||||
'auditRecordDigest', audit_record_digest,
|
||||
'createdAtMs', created_at_ms,
|
||||
'receiptDigest', receipt_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_execution_audit_trace_uidx
|
||||
ON "ql3"."tool_execution_audit_receipts" (trace_id, span_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_execution_audit_run_idx
|
||||
ON "ql3"."tool_execution_audit_receipts"
|
||||
(run_id, created_at_ms, trace_id, span_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_execution_audit_step_idx
|
||||
ON "ql3"."tool_execution_audit_receipts"
|
||||
(run_id, step_run_id, created_at_ms, event_id)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."tool_execution_trace_anchors",
|
||||
"ql3"."tool_execution_audit_receipts"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."tool_execution_trace_anchors",
|
||||
"ql3"."tool_execution_audit_receipts"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 28,
|
||||
migration_id = 'pg-0029-tool-execution-evidence',
|
||||
capabilities = '${CAPABILITIES_V28}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 27
|
||||
AND migration_id = 'pg-0028-step-runs'
|
||||
AND capabilities = '${CAPABILITIES_V27}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 27'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V28 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V29 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0030ToolExecutionStartBarriersMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0030-tool-execution-start-barriers',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_execution_start_barriers" (
|
||||
start_id varchar(128) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
run_id varchar(36) NOT NULL,
|
||||
step_run_id varchar(128) NOT NULL,
|
||||
started_step_run_version integer NOT NULL,
|
||||
step_run_mutation_id varchar(128) NOT NULL,
|
||||
run_event_id varchar(128) NOT NULL,
|
||||
trace_id char(32) NOT NULL,
|
||||
span_id char(16) NOT NULL,
|
||||
audit_event_id uuid NOT NULL,
|
||||
command_digest char(64) NOT NULL,
|
||||
barrier_digest char(64) NOT NULL,
|
||||
started_at_ms bigint NOT NULL,
|
||||
barrier_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_tool_start_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_mutation_fk
|
||||
FOREIGN KEY (step_run_mutation_id)
|
||||
REFERENCES "ql3"."step_run_mutations" (mutation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_event_fk
|
||||
FOREIGN KEY (run_event_id)
|
||||
REFERENCES "ql3"."run_events" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_trace_fk
|
||||
FOREIGN KEY (trace_id, span_id)
|
||||
REFERENCES "ql3"."tool_execution_trace_anchors" (trace_id, span_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_audit_fk
|
||||
FOREIGN KEY (audit_event_id)
|
||||
REFERENCES "ql3"."tool_execution_audit_receipts" (event_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_identity_check CHECK (
|
||||
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
step_run_mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_event_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
trace_id ~ '^[0-9a-f]{32}$' AND span_id ~ '^[0-9a-f]{16}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_start_version_time_check CHECK (
|
||||
started_step_run_version BETWEEN 2 AND 2147483647 AND
|
||||
started_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_tool_start_digest_check CHECK (
|
||||
command_digest ~ '^[0-9a-f]{64}$' AND
|
||||
barrier_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_start_json_check CHECK (
|
||||
jsonb_typeof(barrier_json) = 'object' AND
|
||||
octet_length(barrier_json::text) BETWEEN 2 AND 16384 AND
|
||||
barrier_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-execution-start-barrier@v1',
|
||||
'startId', start_id,
|
||||
'projectId', project_id,
|
||||
'runId', run_id,
|
||||
'stepRunId', step_run_id,
|
||||
'startedStepRunVersion', started_step_run_version,
|
||||
'stepRunMutationId', step_run_mutation_id,
|
||||
'runEventId', run_event_id,
|
||||
'traceId', trace_id,
|
||||
'spanId', span_id,
|
||||
'auditEventId', audit_event_id,
|
||||
'commandDigest', command_digest,
|
||||
'barrierDigest', barrier_digest,
|
||||
'startedAtMs', started_at_ms
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_start_step_version_uidx
|
||||
ON "ql3"."tool_execution_start_barriers"
|
||||
(run_id, step_run_id, started_step_run_version)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_start_mutation_uidx
|
||||
ON "ql3"."tool_execution_start_barriers" (step_run_mutation_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_start_event_uidx
|
||||
ON "ql3"."tool_execution_start_barriers" (run_event_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_start_trace_uidx
|
||||
ON "ql3"."tool_execution_start_barriers" (trace_id, span_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_start_audit_uidx
|
||||
ON "ql3"."tool_execution_start_barriers" (audit_event_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_start_run_time_idx
|
||||
ON "ql3"."tool_execution_start_barriers"
|
||||
(run_id, started_at_ms, start_id)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."tool_execution_start_barriers"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."tool_execution_start_barriers"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 29,
|
||||
migration_id = 'pg-0030-tool-execution-start-barriers',
|
||||
capabilities = '${CAPABILITIES_V29}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 28
|
||||
AND migration_id = 'pg-0029-tool-execution-evidence'
|
||||
AND capabilities = '${CAPABILITIES_V28}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 28'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V29 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V30 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0031ToolInvocationArtifactsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0031-tool-invocation-artifacts',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_invocation_input_artifacts" (
|
||||
artifact_id varchar(128) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
action_ref varchar(255) NOT NULL,
|
||||
input_digest char(64) NOT NULL,
|
||||
invocation_action_digest char(64) NOT NULL,
|
||||
artifact_digest char(64) NOT NULL,
|
||||
key_id varchar(128) NOT NULL,
|
||||
algorithm varchar(32) NOT NULL,
|
||||
plaintext_bytes integer NOT NULL,
|
||||
sealed_at_ms bigint NOT NULL,
|
||||
artifact_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_tool_input_artifact_project_fk
|
||||
FOREIGN KEY (project_id)
|
||||
REFERENCES "ql3"."projects" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_input_artifact_identity_check CHECK (
|
||||
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$' AND
|
||||
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
|
||||
algorithm = 'aes-256-gcm'
|
||||
),
|
||||
CONSTRAINT ql3_tool_input_artifact_digest_check CHECK (
|
||||
input_digest ~ '^[0-9a-f]{64}$' AND
|
||||
invocation_action_digest ~ '^[0-9a-f]{64}$' AND
|
||||
artifact_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_input_artifact_budget_check CHECK (
|
||||
plaintext_bytes BETWEEN 0 AND 65536 AND sealed_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_tool_input_artifact_json_check CHECK (
|
||||
jsonb_typeof(artifact_json) = 'object' AND
|
||||
octet_length(artifact_json::text) BETWEEN 2 AND 98304 AND
|
||||
artifact_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-invocation-input-artifact@v1',
|
||||
'artifactId', artifact_id,
|
||||
'projectId', project_id,
|
||||
'actionRef', action_ref,
|
||||
'inputDigest', input_digest,
|
||||
'invocationActionDigest', invocation_action_digest,
|
||||
'artifactDigest', artifact_digest,
|
||||
'keyId', key_id,
|
||||
'algorithm', algorithm,
|
||||
'plaintextBytes', plaintext_bytes,
|
||||
'sealedAtMs', sealed_at_ms
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_input_artifact_action_uidx
|
||||
ON "ql3"."tool_invocation_input_artifacts" (project_id, action_ref)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_input_artifact_project_time_idx
|
||||
ON "ql3"."tool_invocation_input_artifacts"
|
||||
(project_id, sealed_at_ms, artifact_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_invocation_preview_artifacts" (
|
||||
artifact_id varchar(128) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
action_ref varchar(255) NOT NULL,
|
||||
action_digest char(64) NOT NULL,
|
||||
preview_digest char(64) NOT NULL,
|
||||
redaction_contract_digest char(64) NOT NULL,
|
||||
artifact_digest char(64) NOT NULL,
|
||||
byte_length integer NOT NULL,
|
||||
sealed_at_ms bigint NOT NULL,
|
||||
artifact_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_tool_preview_artifact_project_fk
|
||||
FOREIGN KEY (project_id)
|
||||
REFERENCES "ql3"."projects" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_preview_artifact_identity_check CHECK (
|
||||
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_preview_artifact_digest_check CHECK (
|
||||
action_digest ~ '^[0-9a-f]{64}$' AND
|
||||
preview_digest ~ '^[0-9a-f]{64}$' AND
|
||||
redaction_contract_digest ~ '^[0-9a-f]{64}$' AND
|
||||
artifact_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_preview_artifact_budget_check CHECK (
|
||||
byte_length BETWEEN 2 AND 8192 AND sealed_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_tool_preview_artifact_json_check CHECK (
|
||||
jsonb_typeof(artifact_json) = 'object' AND
|
||||
octet_length(artifact_json::text) BETWEEN 2 AND 16384 AND
|
||||
artifact_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-invocation-preview-artifact@v1',
|
||||
'artifactId', artifact_id,
|
||||
'projectId', project_id,
|
||||
'actionRef', action_ref,
|
||||
'actionDigest', action_digest,
|
||||
'previewDigest', preview_digest,
|
||||
'redactionContractDigest', redaction_contract_digest,
|
||||
'artifactDigest', artifact_digest,
|
||||
'byteLength', byte_length,
|
||||
'sealedAtMs', sealed_at_ms
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_preview_artifact_action_uidx
|
||||
ON "ql3"."tool_invocation_preview_artifacts" (project_id, action_ref)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_preview_artifact_action_digest_uidx
|
||||
ON "ql3"."tool_invocation_preview_artifacts" (action_digest)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_preview_artifact_project_time_idx
|
||||
ON "ql3"."tool_invocation_preview_artifacts"
|
||||
(project_id, sealed_at_ms, artifact_id)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."tool_invocation_input_artifacts",
|
||||
"ql3"."tool_invocation_preview_artifacts"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."tool_invocation_input_artifacts",
|
||||
"ql3"."tool_invocation_preview_artifacts"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 30,
|
||||
migration_id = 'pg-0031-tool-invocation-artifacts',
|
||||
capabilities = '${CAPABILITIES_V30}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 29
|
||||
AND migration_id = 'pg-0030-tool-execution-start-barriers'
|
||||
AND capabilities = '${CAPABILITIES_V29}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 29'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V30 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V31 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0032ToolExecutionArtifactBindingsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0032-tool-execution-artifact-bindings',
|
||||
statements: [
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_input_artifact_start_binding_uidx
|
||||
ON "ql3"."tool_invocation_input_artifacts" (
|
||||
artifact_id, artifact_digest, project_id, action_ref, input_digest
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_preview_artifact_start_binding_uidx
|
||||
ON "ql3"."tool_invocation_preview_artifacts" (
|
||||
artifact_id, artifact_digest, project_id, action_ref, action_digest,
|
||||
preview_digest, redaction_contract_digest
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_execution_start_artifact_bindings" (
|
||||
start_id varchar(128) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
action_ref varchar(255) NOT NULL,
|
||||
input_artifact_id varchar(128) NOT NULL,
|
||||
input_artifact_digest char(64) NOT NULL,
|
||||
input_digest char(64) NOT NULL,
|
||||
preview_artifact_id varchar(128) NOT NULL,
|
||||
preview_artifact_digest char(64) NOT NULL,
|
||||
action_digest char(64) NOT NULL,
|
||||
preview_digest char(64) NOT NULL,
|
||||
redaction_contract_digest char(64) NOT NULL,
|
||||
bound_at_ms bigint NOT NULL,
|
||||
CONSTRAINT ql3_tool_start_artifact_barrier_fk
|
||||
FOREIGN KEY (start_id)
|
||||
REFERENCES "ql3"."tool_execution_start_barriers" (start_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_input_artifact_fk
|
||||
FOREIGN KEY (
|
||||
input_artifact_id, input_artifact_digest, project_id, action_ref,
|
||||
input_digest
|
||||
)
|
||||
REFERENCES "ql3"."tool_invocation_input_artifacts" (
|
||||
artifact_id, artifact_digest, project_id, action_ref, input_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_preview_artifact_fk
|
||||
FOREIGN KEY (
|
||||
preview_artifact_id, preview_artifact_digest, project_id, action_ref,
|
||||
action_digest, preview_digest, redaction_contract_digest
|
||||
)
|
||||
REFERENCES "ql3"."tool_invocation_preview_artifacts" (
|
||||
artifact_id, artifact_digest, project_id, action_ref, action_digest,
|
||||
preview_digest, redaction_contract_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_start_artifact_identity_check CHECK (
|
||||
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$' AND
|
||||
input_artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
preview_artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_start_artifact_digest_check CHECK (
|
||||
input_artifact_digest ~ '^[0-9a-f]{64}$' AND
|
||||
input_digest ~ '^[0-9a-f]{64}$' AND
|
||||
preview_artifact_digest ~ '^[0-9a-f]{64}$' AND
|
||||
action_digest ~ '^[0-9a-f]{64}$' AND
|
||||
preview_digest ~ '^[0-9a-f]{64}$' AND
|
||||
redaction_contract_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_start_artifact_time_check CHECK (bound_at_ms >= 0)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_start_artifact_input_idx
|
||||
ON "ql3"."tool_execution_start_artifact_bindings"
|
||||
(input_artifact_id, start_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_start_artifact_preview_idx
|
||||
ON "ql3"."tool_execution_start_artifact_bindings"
|
||||
(preview_artifact_id, start_id)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."tool_execution_start_artifact_bindings"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."tool_execution_start_artifact_bindings"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 31,
|
||||
migration_id = 'pg-0032-tool-execution-artifact-bindings',
|
||||
capabilities = '${CAPABILITIES_V31}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 30
|
||||
AND migration_id = 'pg-0031-tool-invocation-artifacts'
|
||||
AND capabilities = '${CAPABILITIES_V30}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 30'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V31 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V32 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0033ToolExecutionCompletionsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0033-tool-execution-completions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_execution_completions" (
|
||||
start_id varchar(128) PRIMARY KEY,
|
||||
artifact_id varchar(128) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
run_id varchar(128) NOT NULL,
|
||||
step_run_id varchar(128) NOT NULL,
|
||||
started_step_run_version integer NOT NULL,
|
||||
completed_step_run_version integer NOT NULL,
|
||||
barrier_digest char(64) NOT NULL,
|
||||
adapter_digest char(64) NOT NULL,
|
||||
output_digest char(64) NOT NULL,
|
||||
execution_result_digest char(64) NOT NULL,
|
||||
artifact_digest char(64) NOT NULL,
|
||||
key_id varchar(128) NOT NULL,
|
||||
algorithm varchar(32) NOT NULL,
|
||||
plaintext_bytes integer NOT NULL,
|
||||
step_run_mutation_id varchar(128) NOT NULL,
|
||||
step_run_mutation_digest char(64) NOT NULL,
|
||||
completed_step_run_digest char(64) NOT NULL,
|
||||
run_event_id varchar(128) NOT NULL,
|
||||
completed_at_ms bigint NOT NULL,
|
||||
completion_digest char(64) NOT NULL,
|
||||
artifact_json jsonb NOT NULL,
|
||||
completion_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_tool_completion_start_fk
|
||||
FOREIGN KEY (start_id)
|
||||
REFERENCES "ql3"."tool_execution_start_barriers" (start_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_completion_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_completion_mutation_fk
|
||||
FOREIGN KEY (step_run_mutation_id)
|
||||
REFERENCES "ql3"."step_run_mutations" (mutation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_completion_event_fk
|
||||
FOREIGN KEY (run_event_id)
|
||||
REFERENCES "ql3"."run_events" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_completion_identity_check CHECK (
|
||||
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
|
||||
step_run_mutation_id ~
|
||||
'^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_event_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_completion_version_check CHECK (
|
||||
started_step_run_version BETWEEN 2 AND 2147483646 AND
|
||||
completed_step_run_version = started_step_run_version + 1
|
||||
),
|
||||
CONSTRAINT ql3_tool_completion_digest_check CHECK (
|
||||
barrier_digest ~ '^[0-9a-f]{64}$' AND
|
||||
adapter_digest ~ '^[0-9a-f]{64}$' AND
|
||||
output_digest ~ '^[0-9a-f]{64}$' AND
|
||||
execution_result_digest ~ '^[0-9a-f]{64}$' AND
|
||||
artifact_digest ~ '^[0-9a-f]{64}$' AND
|
||||
step_run_mutation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
completed_step_run_digest ~ '^[0-9a-f]{64}$' AND
|
||||
completion_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_completion_budget_check CHECK (
|
||||
algorithm = 'aes-256-gcm' AND
|
||||
plaintext_bytes BETWEEN 0 AND 262144 AND
|
||||
completed_at_ms >= 0 AND
|
||||
octet_length(artifact_json::text) BETWEEN 2 AND 393216 AND
|
||||
octet_length(completion_json::text) BETWEEN 2 AND 24576
|
||||
),
|
||||
CONSTRAINT ql3_tool_completion_json_check CHECK (
|
||||
jsonb_typeof(artifact_json) = 'object' AND
|
||||
artifact_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-execution-result-artifact@v1',
|
||||
'artifactId', artifact_id,
|
||||
'projectId', project_id,
|
||||
'startId', start_id,
|
||||
'runId', run_id,
|
||||
'stepRunId', step_run_id,
|
||||
'barrierDigest', barrier_digest,
|
||||
'adapterDigest', adapter_digest,
|
||||
'outputDigest', output_digest,
|
||||
'executionResultDigest', execution_result_digest,
|
||||
'artifactDigest', artifact_digest,
|
||||
'keyId', key_id,
|
||||
'algorithm', algorithm,
|
||||
'plaintextBytes', plaintext_bytes,
|
||||
'sealedAtMs', completed_at_ms
|
||||
) AND
|
||||
jsonb_typeof(completion_json) = 'object' AND
|
||||
completion_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-execution-completion@v1',
|
||||
'startId', start_id,
|
||||
'projectId', project_id,
|
||||
'runId', run_id,
|
||||
'stepRunId', step_run_id,
|
||||
'startedStepRunVersion', started_step_run_version,
|
||||
'completedStepRunVersion', completed_step_run_version,
|
||||
'barrierDigest', barrier_digest,
|
||||
'adapterDigest', adapter_digest,
|
||||
'resultArtifact', jsonb_build_object(
|
||||
'artifactId', artifact_id,
|
||||
'artifactDigest', artifact_digest,
|
||||
'outputDigest', output_digest,
|
||||
'executionResultDigest', execution_result_digest
|
||||
),
|
||||
'stepRunMutationId', step_run_mutation_id,
|
||||
'stepRunMutationDigest', step_run_mutation_digest,
|
||||
'completedStepRunDigest', completed_step_run_digest,
|
||||
'runEventId', run_event_id,
|
||||
'completedAtMs', completed_at_ms,
|
||||
'completionDigest', completion_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_completion_artifact_uidx
|
||||
ON "ql3"."tool_execution_completions" (artifact_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_completion_mutation_uidx
|
||||
ON "ql3"."tool_execution_completions" (step_run_mutation_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_completion_event_uidx
|
||||
ON "ql3"."tool_execution_completions" (run_event_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_completion_step_version_uidx
|
||||
ON "ql3"."tool_execution_completions"
|
||||
(run_id, step_run_id, completed_step_run_version)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_completion_project_time_idx
|
||||
ON "ql3"."tool_execution_completions"
|
||||
(project_id, completed_at_ms, start_id)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."tool_execution_completions"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."tool_execution_completions"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 32,
|
||||
migration_id = 'pg-0033-tool-execution-completions',
|
||||
capabilities = '${CAPABILITIES_V32}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 31
|
||||
AND migration_id = 'pg-0032-tool-execution-artifact-bindings'
|
||||
AND capabilities = '${CAPABILITIES_V31}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 31'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V32 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V33 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0034ToolExecutionFailureCompletionsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0034-tool-execution-failure-completions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_execution_failure_completions" (
|
||||
start_id varchar(128) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
run_id varchar(128) NOT NULL,
|
||||
step_run_id varchar(128) NOT NULL,
|
||||
started_step_run_version integer NOT NULL,
|
||||
completed_step_run_version integer NOT NULL,
|
||||
barrier_digest char(64) NOT NULL,
|
||||
adapter_digest char(64) NOT NULL,
|
||||
outcome varchar(16) NOT NULL,
|
||||
result_code varchar(64) NOT NULL,
|
||||
error_summary varchar(128) NOT NULL,
|
||||
step_run_mutation_id varchar(128) NOT NULL,
|
||||
step_run_mutation_digest char(64) NOT NULL,
|
||||
completed_step_run_digest char(64) NOT NULL,
|
||||
run_event_id varchar(128) NOT NULL,
|
||||
completed_at_ms bigint NOT NULL,
|
||||
completion_digest char(64) NOT NULL,
|
||||
completion_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_tool_failure_completion_start_fk
|
||||
FOREIGN KEY (start_id)
|
||||
REFERENCES "ql3"."tool_execution_start_barriers" (start_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_failure_completion_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_failure_completion_mutation_fk
|
||||
FOREIGN KEY (step_run_mutation_id)
|
||||
REFERENCES "ql3"."step_run_mutations" (mutation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_failure_completion_event_fk
|
||||
FOREIGN KEY (run_event_id)
|
||||
REFERENCES "ql3"."run_events" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_failure_completion_identity_check CHECK (
|
||||
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
step_run_mutation_id ~
|
||||
'^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_event_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_failure_completion_version_check CHECK (
|
||||
started_step_run_version BETWEEN 2 AND 2147483646 AND
|
||||
completed_step_run_version = started_step_run_version + 1
|
||||
),
|
||||
CONSTRAINT ql3_tool_failure_completion_digest_check CHECK (
|
||||
barrier_digest ~ '^[0-9a-f]{64}$' AND
|
||||
adapter_digest ~ '^[0-9a-f]{64}$' AND
|
||||
step_run_mutation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
completed_step_run_digest ~ '^[0-9a-f]{64}$' AND
|
||||
completion_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_failure_completion_fact_check CHECK (
|
||||
(
|
||||
outcome = 'failed' AND
|
||||
result_code = 'tool_adapter_failed' AND
|
||||
error_summary = 'Trusted Tool execution failed'
|
||||
) OR (
|
||||
outcome = 'timed_out' AND
|
||||
result_code = 'tool_deadline_exceeded' AND
|
||||
error_summary = 'Trusted Tool execution deadline exceeded'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_tool_failure_completion_budget_check CHECK (
|
||||
completed_at_ms >= 0 AND
|
||||
octet_length(completion_json::text) BETWEEN 2 AND 24576
|
||||
),
|
||||
CONSTRAINT ql3_tool_failure_completion_json_check CHECK (
|
||||
jsonb_typeof(completion_json) = 'object' AND
|
||||
completion_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-execution-failure-completion@v1',
|
||||
'startId', start_id,
|
||||
'projectId', project_id,
|
||||
'runId', run_id,
|
||||
'stepRunId', step_run_id,
|
||||
'startedStepRunVersion', started_step_run_version,
|
||||
'completedStepRunVersion', completed_step_run_version,
|
||||
'barrierDigest', barrier_digest,
|
||||
'adapterDigest', adapter_digest,
|
||||
'outcome', outcome,
|
||||
'resultCode', result_code,
|
||||
'errorSummary', error_summary,
|
||||
'stepRunMutationId', step_run_mutation_id,
|
||||
'stepRunMutationDigest', step_run_mutation_digest,
|
||||
'completedStepRunDigest', completed_step_run_digest,
|
||||
'runEventId', run_event_id,
|
||||
'completedAtMs', completed_at_ms,
|
||||
'completionDigest', completion_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_failure_completion_mutation_uidx
|
||||
ON "ql3"."tool_execution_failure_completions" (step_run_mutation_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_failure_completion_event_uidx
|
||||
ON "ql3"."tool_execution_failure_completions" (run_event_id)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_tool_failure_completion_step_version_uidx
|
||||
ON "ql3"."tool_execution_failure_completions"
|
||||
(run_id, step_run_id, completed_step_run_version)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_failure_completion_project_time_idx
|
||||
ON "ql3"."tool_execution_failure_completions"
|
||||
(project_id, completed_at_ms, start_id)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."tool_execution_failure_completions"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."tool_execution_failure_completions"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 33,
|
||||
migration_id = 'pg-0034-tool-execution-failure-completions',
|
||||
capabilities = '${CAPABILITIES_V33}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 32
|
||||
AND migration_id = 'pg-0033-tool-execution-completions'
|
||||
AND capabilities = '${CAPABILITIES_V32}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 32'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V33 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V34 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0035ToolResultKeyCatalogMigration = definePostgresSqlMigration({
|
||||
id: 'pg-0035-tool-result-key-catalog',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_result_key_catalog_generations" (
|
||||
authority varchar(64) NOT NULL,
|
||||
generation integer NOT NULL,
|
||||
previous_generation integer,
|
||||
previous_catalog_digest char(64),
|
||||
active_key_id varchar(128),
|
||||
mutation_kind varchar(16) NOT NULL,
|
||||
mutation_id varchar(128) NOT NULL,
|
||||
catalog_digest char(64) NOT NULL,
|
||||
command_digest char(64) NOT NULL,
|
||||
committed_at_ms bigint NOT NULL,
|
||||
catalog_json jsonb NOT NULL,
|
||||
PRIMARY KEY (authority, generation),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_generation_digest_key
|
||||
UNIQUE (authority, generation, catalog_digest),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_mutation_key
|
||||
UNIQUE (mutation_id),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_digest_key
|
||||
UNIQUE (catalog_digest),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_previous_fk
|
||||
FOREIGN KEY (
|
||||
authority, previous_generation, previous_catalog_digest
|
||||
)
|
||||
REFERENCES "ql3"."tool_result_key_catalog_generations" (
|
||||
authority, generation, catalog_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_key_catalog_authority_check CHECK (
|
||||
authority = 'trusted-tool-results'
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_generation_check CHECK (
|
||||
generation BETWEEN 1 AND 2147483647 AND
|
||||
(
|
||||
(
|
||||
generation = 1 AND
|
||||
previous_generation IS NULL AND
|
||||
previous_catalog_digest IS NULL AND
|
||||
mutation_kind = 'bootstrap'
|
||||
) OR (
|
||||
generation > 1 AND
|
||||
previous_generation = generation - 1 AND
|
||||
previous_catalog_digest IS NOT NULL AND
|
||||
mutation_kind IN (
|
||||
'rotate', 'retire', 'mark_lost', 'restore'
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_identity_check CHECK (
|
||||
active_key_id IS NULL OR
|
||||
active_key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_digest_check CHECK (
|
||||
(
|
||||
previous_catalog_digest IS NULL OR
|
||||
previous_catalog_digest ~ '^[0-9a-f]{64}$'
|
||||
) AND
|
||||
catalog_digest ~ '^[0-9a-f]{64}$' AND
|
||||
command_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_budget_check CHECK (
|
||||
committed_at_ms >= 0 AND
|
||||
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
octet_length(catalog_json::text) BETWEEN 2 AND 65536
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_catalog_json_check CHECK (
|
||||
jsonb_typeof(catalog_json) = 'object' AND
|
||||
catalog_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-result-key-catalog@v1',
|
||||
'generation', generation,
|
||||
'previousCatalogDigest', previous_catalog_digest,
|
||||
'activeKeyId', active_key_id,
|
||||
'mutationKind', mutation_kind,
|
||||
'mutationId', mutation_id,
|
||||
'catalogDigest', catalog_digest,
|
||||
'committedAtMs', committed_at_ms
|
||||
) AND
|
||||
jsonb_typeof(catalog_json -> 'keys') = 'array' AND
|
||||
jsonb_array_length(catalog_json -> 'keys') BETWEEN 1 AND 64
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_result_key_catalog_current_idx
|
||||
ON "ql3"."tool_result_key_catalog_generations"
|
||||
(authority, generation DESC)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_execution_result_key_bindings" (
|
||||
start_id varchar(128) PRIMARY KEY,
|
||||
artifact_id varchar(128) NOT NULL,
|
||||
artifact_digest char(64) NOT NULL,
|
||||
catalog_authority varchar(64) NOT NULL,
|
||||
catalog_generation integer NOT NULL,
|
||||
catalog_digest char(64) NOT NULL,
|
||||
key_id varchar(128) NOT NULL,
|
||||
material_proof char(64) NOT NULL,
|
||||
binding_digest char(64) NOT NULL,
|
||||
CONSTRAINT ql3_tool_result_key_binding_artifact_key
|
||||
UNIQUE (artifact_id),
|
||||
CONSTRAINT ql3_tool_result_key_binding_digest_key
|
||||
UNIQUE (binding_digest),
|
||||
CONSTRAINT ql3_tool_result_key_binding_completion_fk
|
||||
FOREIGN KEY (start_id)
|
||||
REFERENCES "ql3"."tool_execution_completions" (start_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_key_binding_artifact_fk
|
||||
FOREIGN KEY (artifact_id)
|
||||
REFERENCES "ql3"."tool_execution_completions" (artifact_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_key_binding_catalog_fk
|
||||
FOREIGN KEY (
|
||||
catalog_authority, catalog_generation, catalog_digest
|
||||
)
|
||||
REFERENCES "ql3"."tool_result_key_catalog_generations" (
|
||||
authority, generation, catalog_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_tool_result_key_binding_authority_check CHECK (
|
||||
catalog_authority = 'trusted-tool-results'
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_binding_identity_check CHECK (
|
||||
start_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
|
||||
catalog_generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_tool_result_key_binding_digest_check CHECK (
|
||||
artifact_digest ~ '^[0-9a-f]{64}$' AND
|
||||
catalog_digest ~ '^[0-9a-f]{64}$' AND
|
||||
material_proof ~ '^[0-9a-f]{64}$' AND
|
||||
binding_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_tool_result_key_binding_catalog_idx
|
||||
ON "ql3"."tool_execution_result_key_bindings"
|
||||
(catalog_generation, key_id, start_id)
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."tool_result_key_catalog_generations",
|
||||
"ql3"."tool_execution_result_key_bindings"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT
|
||||
ON "ql3"."tool_result_key_catalog_generations"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."tool_result_key_catalog_generations"
|
||||
TO ql3_admin
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."tool_execution_result_key_bindings"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 34,
|
||||
migration_id = 'pg-0035-tool-result-key-catalog',
|
||||
capabilities = '${CAPABILITIES_V34}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 33
|
||||
AND migration_id = 'pg-0034-tool-execution-failure-completions'
|
||||
AND capabilities = '${CAPABILITIES_V33}'::jsonb;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 33'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V34 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V35 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0036ToolResultRekeyOverlaysMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0036-tool-result-rekey-overlays',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_execution_result_rekey_overlays" (
|
||||
overlay_id varchar(128) PRIMARY KEY,
|
||||
artifact_id varchar(128) NOT NULL,
|
||||
source_binding_digest char(64) NOT NULL,
|
||||
revision integer NOT NULL,
|
||||
previous_overlay_digest char(64),
|
||||
from_key_id varchar(128) NOT NULL,
|
||||
target_catalog_authority varchar(64) NOT NULL,
|
||||
target_catalog_generation integer NOT NULL,
|
||||
target_catalog_digest char(64) NOT NULL,
|
||||
target_key_id varchar(128) NOT NULL,
|
||||
target_material_proof char(64) NOT NULL,
|
||||
mutation_id varchar(128) NOT NULL,
|
||||
command_digest char(64) NOT NULL,
|
||||
overlay_digest char(64) NOT NULL,
|
||||
rekeyed_at_ms bigint NOT NULL,
|
||||
overlay_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_result_rekey_digest_key UNIQUE (overlay_digest),
|
||||
CONSTRAINT ql3_result_rekey_artifact_fk FOREIGN KEY (artifact_id)
|
||||
REFERENCES "ql3"."tool_execution_result_key_bindings" (artifact_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_result_rekey_binding_fk FOREIGN KEY (source_binding_digest)
|
||||
REFERENCES "ql3"."tool_execution_result_key_bindings" (binding_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_result_rekey_previous_fk FOREIGN KEY (previous_overlay_digest)
|
||||
REFERENCES "ql3"."tool_execution_result_rekey_overlays" (overlay_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_result_rekey_catalog_fk FOREIGN KEY (
|
||||
target_catalog_authority, target_catalog_generation,
|
||||
target_catalog_digest
|
||||
) REFERENCES "ql3"."tool_result_key_catalog_generations" (
|
||||
authority, generation, catalog_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_result_rekey_revision_check CHECK (
|
||||
revision BETWEEN 1 AND 2147483647 AND
|
||||
((revision = 1 AND previous_overlay_digest IS NULL) OR
|
||||
(revision > 1 AND previous_overlay_digest IS NOT NULL))
|
||||
),
|
||||
CONSTRAINT ql3_result_rekey_authority_check CHECK (
|
||||
target_catalog_authority = 'trusted-tool-results'
|
||||
),
|
||||
CONSTRAINT ql3_result_rekey_identity_check CHECK (
|
||||
overlay_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
from_key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
|
||||
target_key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
|
||||
from_key_id <> target_key_id AND
|
||||
target_catalog_generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_result_rekey_digest_check CHECK (
|
||||
(previous_overlay_digest IS NULL OR
|
||||
previous_overlay_digest ~ '^[0-9a-f]{64}$') AND
|
||||
source_binding_digest ~ '^[0-9a-f]{64}$' AND
|
||||
target_catalog_digest ~ '^[0-9a-f]{64}$' AND
|
||||
target_material_proof ~ '^[0-9a-f]{64}$' AND
|
||||
command_digest ~ '^[0-9a-f]{64}$' AND
|
||||
overlay_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_result_rekey_budget_check CHECK (
|
||||
rekeyed_at_ms >= 0 AND
|
||||
octet_length(overlay_json::text) BETWEEN 2 AND 393216
|
||||
),
|
||||
CONSTRAINT ql3_result_rekey_json_check CHECK (
|
||||
jsonb_typeof(overlay_json) = 'object' AND
|
||||
overlay_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-execution-result-rekey-overlay@v1',
|
||||
'overlayId', overlay_id,
|
||||
'sourceBindingDigest', source_binding_digest,
|
||||
'revision', revision,
|
||||
'previousOverlayDigest', previous_overlay_digest,
|
||||
'fromKeyId', from_key_id,
|
||||
'rekeyedAtMs', rekeyed_at_ms,
|
||||
'overlayDigest', overlay_digest
|
||||
) AND
|
||||
overlay_json -> 'sourceArtifact' ->> 'artifactId' = artifact_id AND
|
||||
overlay_json -> 'targetCatalogFence' @> jsonb_build_object(
|
||||
'generation', target_catalog_generation,
|
||||
'catalogDigest', target_catalog_digest,
|
||||
'keyId', target_key_id,
|
||||
'materialProof', target_material_proof
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_result_rekey_mutation_key ON "ql3"."tool_execution_result_rekey_overlays" (mutation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_result_rekey_artifact_revision_key ON "ql3"."tool_execution_result_rekey_overlays" (artifact_id, revision)`,
|
||||
`CREATE UNIQUE INDEX ql3_result_rekey_artifact_revision_digest_key ON "ql3"."tool_execution_result_rekey_overlays" (artifact_id, revision, overlay_digest)`,
|
||||
`CREATE INDEX ql3_result_rekey_artifact_idx ON "ql3"."tool_execution_result_rekey_overlays" (artifact_id, revision DESC)`,
|
||||
`CREATE INDEX ql3_result_rekey_target_idx ON "ql3"."tool_execution_result_rekey_overlays" (target_key_id, artifact_id, revision DESC)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_execution_result_rekey_heads" (
|
||||
artifact_id varchar(128) PRIMARY KEY,
|
||||
revision integer NOT NULL,
|
||||
overlay_id varchar(128) NOT NULL,
|
||||
overlay_digest char(64) NOT NULL,
|
||||
target_catalog_generation integer NOT NULL,
|
||||
target_catalog_digest char(64) NOT NULL,
|
||||
target_key_id varchar(128) NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT ql3_result_rekey_head_overlay_fk FOREIGN KEY (
|
||||
artifact_id, revision, overlay_digest
|
||||
) REFERENCES "ql3"."tool_execution_result_rekey_overlays" (
|
||||
artifact_id, revision, overlay_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_result_rekey_head_identity_check CHECK (
|
||||
revision BETWEEN 1 AND 2147483647 AND
|
||||
artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
overlay_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
target_key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
|
||||
target_catalog_generation BETWEEN 1 AND 2147483647 AND
|
||||
updated_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_result_rekey_head_digest_check CHECK (
|
||||
overlay_digest ~ '^[0-9a-f]{64}$' AND
|
||||
target_catalog_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_result_rekey_head_overlay_key ON "ql3"."tool_execution_result_rekey_heads" (overlay_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_result_rekey_head_digest_key ON "ql3"."tool_execution_result_rekey_heads" (overlay_digest)`,
|
||||
`CREATE INDEX ql3_result_rekey_head_target_idx ON "ql3"."tool_execution_result_rekey_heads" (target_key_id, artifact_id)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."tool_result_key_retirement_receipts" (
|
||||
receipt_digest char(64) PRIMARY KEY,
|
||||
catalog_authority varchar(64) NOT NULL,
|
||||
catalog_generation integer NOT NULL,
|
||||
catalog_digest char(64) NOT NULL,
|
||||
key_id varchar(128) NOT NULL,
|
||||
material_proof char(64) NOT NULL,
|
||||
mutation_id varchar(128) NOT NULL,
|
||||
command_digest char(64) NOT NULL,
|
||||
binding_count integer NOT NULL,
|
||||
overlay_head_count integer NOT NULL,
|
||||
uncovered_binding_count integer NOT NULL,
|
||||
uncovered_overlay_head_count integer NOT NULL,
|
||||
coverage_digest char(64) NOT NULL,
|
||||
created_at_ms bigint NOT NULL,
|
||||
receipt_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_result_retirement_catalog_fk FOREIGN KEY (
|
||||
catalog_authority, catalog_generation, catalog_digest
|
||||
) REFERENCES "ql3"."tool_result_key_catalog_generations" (
|
||||
authority, generation, catalog_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_result_retirement_authority_check CHECK (
|
||||
catalog_authority = 'trusted-tool-results'
|
||||
),
|
||||
CONSTRAINT ql3_result_retirement_identity_check CHECK (
|
||||
catalog_generation BETWEEN 1 AND 2147483647 AND
|
||||
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' AND
|
||||
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_result_retirement_count_check CHECK (
|
||||
binding_count BETWEEN 0 AND 2147483647 AND
|
||||
overlay_head_count BETWEEN 0 AND 2147483647 AND
|
||||
uncovered_binding_count = 0 AND
|
||||
uncovered_overlay_head_count = 0 AND created_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_result_retirement_digest_check CHECK (
|
||||
receipt_digest ~ '^[0-9a-f]{64}$' AND
|
||||
catalog_digest ~ '^[0-9a-f]{64}$' AND
|
||||
material_proof ~ '^[0-9a-f]{64}$' AND
|
||||
command_digest ~ '^[0-9a-f]{64}$' AND
|
||||
coverage_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_result_retirement_json_check CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
receipt_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/tool-result-key-retirement-receipt@v1',
|
||||
'catalogGeneration', catalog_generation,
|
||||
'catalogDigest', catalog_digest,
|
||||
'keyId', key_id,
|
||||
'materialProof', material_proof,
|
||||
'mutationId', mutation_id,
|
||||
'bindingCount', binding_count,
|
||||
'overlayHeadCount', overlay_head_count,
|
||||
'uncoveredBindingCount', 0,
|
||||
'uncoveredOverlayHeadCount', 0,
|
||||
'coverageDigest', coverage_digest,
|
||||
'createdAtMs', created_at_ms,
|
||||
'receiptDigest', receipt_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_result_retirement_mutation_key ON "ql3"."tool_result_key_retirement_receipts" (mutation_id)`,
|
||||
`CREATE INDEX ql3_result_retirement_catalog_idx ON "ql3"."tool_result_key_retirement_receipts" (catalog_generation, key_id)`,
|
||||
`
|
||||
REVOKE ALL ON
|
||||
"ql3"."tool_execution_result_rekey_overlays",
|
||||
"ql3"."tool_execution_result_rekey_heads",
|
||||
"ql3"."tool_result_key_retirement_receipts"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`GRANT SELECT ON "ql3"."tool_execution_result_rekey_overlays", "ql3"."tool_execution_result_rekey_heads" TO ql3_runtime`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."tool_execution_result_rekey_overlays", "ql3"."tool_result_key_retirement_receipts" TO ql3_admin`,
|
||||
`GRANT SELECT, INSERT, UPDATE ON "ql3"."tool_execution_result_rekey_heads" TO ql3_admin`,
|
||||
`GRANT SELECT ON "ql3"."tool_execution_completions", "ql3"."tool_execution_result_key_bindings" TO ql3_admin`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 35,
|
||||
migration_id = 'pg-0036-tool-result-rekey-overlays',
|
||||
capabilities = '${CAPABILITIES_V35}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 34
|
||||
AND migration_id = 'pg-0035-tool-result-key-catalog'
|
||||
AND capabilities = '${CAPABILITIES_V34}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 34'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,779 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V35 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V36 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0037PluginPackageQuarantineMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0037-plugin-package-quarantine',
|
||||
statements: [
|
||||
`
|
||||
ALTER TABLE "ql3"."plugin_package_installs"
|
||||
ADD CONSTRAINT ql3_plugin_package_installs_quarantine_target_key
|
||||
UNIQUE (
|
||||
project_id, package_name, installation_id, lock_digest, record_digest
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
ALTER TABLE "ql3"."project_tool_definition_snapshots"
|
||||
ADD CONSTRAINT ql3_project_tool_snapshot_withdrawal_key
|
||||
UNIQUE (project_id, active_vector_digest, snapshot_digest)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_quarantine_events" (
|
||||
event_digest char(64) PRIMARY KEY,
|
||||
mutation_id varchar(128) NOT NULL,
|
||||
revocation_receipt_digest char(64) NOT NULL,
|
||||
impact_digest char(64) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
installation_id varchar(128) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
install_state varchar(16) NOT NULL,
|
||||
install_version integer NOT NULL,
|
||||
install_record_digest char(64) NOT NULL,
|
||||
active_lock_digest char(64),
|
||||
proposer_type varchar(16) NOT NULL,
|
||||
proposer_id varchar(255) NOT NULL,
|
||||
confirmer_type varchar(16) NOT NULL,
|
||||
confirmer_id varchar(255) NOT NULL,
|
||||
authorization_mode varchar(16) NOT NULL,
|
||||
reason_code varchar(32) NOT NULL,
|
||||
occurred_at_ms bigint NOT NULL,
|
||||
event_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_quarantine_install_fk FOREIGN KEY (
|
||||
project_id, package_name, installation_id, lock_digest,
|
||||
install_record_digest
|
||||
) REFERENCES "ql3"."plugin_package_installs" (
|
||||
project_id, package_name, installation_id, lock_digest, record_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_quarantine_identity_check CHECK (
|
||||
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
|
||||
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
install_state IN ('queued', 'staged', 'activating', 'active') AND
|
||||
install_version BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_quarantine_state_check CHECK (
|
||||
(install_state = 'active' AND active_lock_digest = lock_digest) OR
|
||||
(install_state <> 'active' AND
|
||||
(active_lock_digest IS NULL OR active_lock_digest <> lock_digest))
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_quarantine_subject_check CHECK (
|
||||
proposer_type IN (
|
||||
'user', 'api_app', 'mcp_client', 'agent', 'system', 'worker'
|
||||
) AND
|
||||
confirmer_type IN (
|
||||
'user', 'api_app', 'mcp_client', 'agent', 'system', 'worker'
|
||||
) AND
|
||||
octet_length(proposer_id) BETWEEN 1 AND 255 AND
|
||||
octet_length(confirmer_id) BETWEEN 1 AND 255 AND
|
||||
authorization_mode IN ('dual_control', 'break_glass') AND
|
||||
(authorization_mode = 'break_glass' OR
|
||||
proposer_type <> confirmer_type OR proposer_id <> confirmer_id) AND
|
||||
reason_code IN (
|
||||
'suspected_key_compromise', 'confirmed_key_compromise'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_quarantine_digest_check CHECK (
|
||||
event_digest ~ '^[0-9a-f]{64}$' AND
|
||||
revocation_receipt_digest ~ '^[0-9a-f]{64}$' AND
|
||||
impact_digest ~ '^[0-9a-f]{64}$' AND
|
||||
lock_digest ~ '^[0-9a-f]{64}$' AND
|
||||
install_record_digest ~ '^[0-9a-f]{64}$' AND
|
||||
(active_lock_digest IS NULL OR
|
||||
active_lock_digest ~ '^[0-9a-f]{64}$')
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_quarantine_json_check CHECK (
|
||||
jsonb_typeof(event_json) = 'object' AND
|
||||
octet_length(event_json::text) BETWEEN 2 AND 262144 AND
|
||||
event_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-quarantine-event@v1',
|
||||
'mutationId', mutation_id,
|
||||
'revocationReceiptDigest', revocation_receipt_digest,
|
||||
'impactDigest', impact_digest,
|
||||
'authorizationMode', authorization_mode,
|
||||
'reasonCode', reason_code,
|
||||
'occurredAtMs', occurred_at_ms,
|
||||
'eventDigest', event_digest
|
||||
) AND
|
||||
event_json -> 'target' @> jsonb_build_object(
|
||||
'projectId', project_id,
|
||||
'packageName', package_name,
|
||||
'installationId', installation_id,
|
||||
'lockDigest', lock_digest,
|
||||
'installState', install_state,
|
||||
'installVersion', install_version,
|
||||
'installRecordDigest', install_record_digest,
|
||||
'activeLockDigest', active_lock_digest
|
||||
) AND
|
||||
event_json -> 'proposer' @> jsonb_build_object(
|
||||
'type', proposer_type, 'id', proposer_id
|
||||
) AND
|
||||
event_json -> 'confirmer' @> jsonb_build_object(
|
||||
'type', confirmer_type, 'id', confirmer_id
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_quarantine_time_check CHECK (
|
||||
occurred_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_quarantine_mutation_key ON "ql3"."plugin_package_quarantine_events" (mutation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_quarantine_target_key ON "ql3"."plugin_package_quarantine_events" (project_id, package_name, installation_id, lock_digest)`,
|
||||
`CREATE INDEX ql3_plugin_package_quarantine_lock_idx ON "ql3"."plugin_package_quarantine_events" (lock_digest, project_id, package_name)`,
|
||||
`CREATE INDEX ql3_plugin_package_quarantine_project_idx ON "ql3"."plugin_package_quarantine_events" (project_id, package_name, occurred_at_ms, event_digest)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_withdrawal_receipts" (
|
||||
event_digest char(64) PRIMARY KEY,
|
||||
receipt_digest char(64) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
capability_status varchar(16) NOT NULL,
|
||||
task_count integer NOT NULL,
|
||||
previous_active_vector_digest char(64),
|
||||
current_active_vector_digest char(64),
|
||||
current_tool_snapshot_digest char(64),
|
||||
retained_source_count integer NOT NULL,
|
||||
committed_at_ms bigint NOT NULL,
|
||||
receipt_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_event_fk FOREIGN KEY (
|
||||
event_digest
|
||||
) REFERENCES "ql3"."plugin_package_quarantine_events" (event_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_snapshot_fk FOREIGN KEY (
|
||||
project_id, current_active_vector_digest, current_tool_snapshot_digest
|
||||
) REFERENCES "ql3"."project_tool_definition_snapshots" (
|
||||
project_id, active_vector_digest, snapshot_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_disposition_check CHECK (
|
||||
(
|
||||
capability_status = 'not_active' AND task_count = 0 AND
|
||||
previous_active_vector_digest IS NULL AND
|
||||
current_active_vector_digest IS NULL AND
|
||||
current_tool_snapshot_digest IS NULL AND retained_source_count = 0
|
||||
) OR (
|
||||
capability_status = 'withdrawn' AND task_count BETWEEN 0 AND 128 AND
|
||||
previous_active_vector_digest IS NOT NULL AND
|
||||
current_active_vector_digest IS NOT NULL AND
|
||||
previous_active_vector_digest <> current_active_vector_digest AND
|
||||
current_tool_snapshot_digest IS NOT NULL AND
|
||||
retained_source_count BETWEEN 0 AND 128
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_digest_check CHECK (
|
||||
event_digest ~ '^[0-9a-f]{64}$' AND
|
||||
receipt_digest ~ '^[0-9a-f]{64}$' AND
|
||||
(previous_active_vector_digest IS NULL OR
|
||||
previous_active_vector_digest ~ '^[0-9a-f]{64}$') AND
|
||||
(current_active_vector_digest IS NULL OR
|
||||
current_active_vector_digest ~ '^[0-9a-f]{64}$') AND
|
||||
(current_tool_snapshot_digest IS NULL OR
|
||||
current_tool_snapshot_digest ~ '^[0-9a-f]{64}$')
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_json_check CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
octet_length(receipt_json::text) BETWEEN 2 AND 8388608 AND
|
||||
receipt_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-withdrawal-receipt@v1',
|
||||
'eventDigest', event_digest,
|
||||
'committedAtMs', committed_at_ms,
|
||||
'receiptDigest', receipt_digest
|
||||
) AND
|
||||
receipt_json -> 'target' ->> 'projectId' = project_id AND
|
||||
receipt_json -> 'capability' @> jsonb_build_object(
|
||||
'status', capability_status,
|
||||
'previousActiveVectorDigest', previous_active_vector_digest,
|
||||
'currentActiveVectorDigest', current_active_vector_digest,
|
||||
'currentToolSnapshotDigest', current_tool_snapshot_digest,
|
||||
'retainedSourceCount', retained_source_count
|
||||
) AND
|
||||
jsonb_array_length(
|
||||
receipt_json -> 'capability' -> 'taskWithdrawals'
|
||||
) = task_count
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_time_check CHECK (
|
||||
committed_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_withdrawal_receipt_key ON "ql3"."plugin_package_withdrawal_receipts" (receipt_digest)`,
|
||||
`CREATE INDEX ql3_plugin_package_withdrawal_snapshot_idx ON "ql3"."plugin_package_withdrawal_receipts" (current_tool_snapshot_digest, event_digest)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_withdrawal_tasks" (
|
||||
event_digest char(64) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
previous_revision integer NOT NULL,
|
||||
disabled_revision integer NOT NULL,
|
||||
previous_content_digest char(64) NOT NULL,
|
||||
disabled_content_digest char(64) NOT NULL,
|
||||
PRIMARY KEY (event_digest, task_id),
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_task_receipt_fk FOREIGN KEY (
|
||||
event_digest
|
||||
) REFERENCES "ql3"."plugin_package_withdrawal_receipts" (event_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_task_previous_fk FOREIGN KEY (
|
||||
project_id, task_id, previous_revision
|
||||
) REFERENCES "ql3"."task_definition_revisions" (
|
||||
project_id, task_id, revision
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_task_disabled_fk FOREIGN KEY (
|
||||
project_id, task_id, disabled_revision
|
||||
) REFERENCES "ql3"."task_definition_revisions" (
|
||||
project_id, task_id, revision
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_task_identity_check CHECK (
|
||||
task_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
previous_revision BETWEEN 1 AND 2147483646 AND
|
||||
disabled_revision = previous_revision + 1
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_withdrawal_task_digest_check CHECK (
|
||||
event_digest ~ '^[0-9a-f]{64}$' AND
|
||||
previous_content_digest ~ '^[0-9a-f]{64}$' AND
|
||||
disabled_content_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE INDEX ql3_plugin_package_withdrawal_task_idx ON "ql3"."plugin_package_withdrawal_tasks" (project_id, task_id, event_digest)`,
|
||||
`
|
||||
CREATE FUNCTION "ql3"."plugin_package_run_start_allowed"(
|
||||
p_project_id varchar,
|
||||
p_task_id varchar,
|
||||
p_task_revision varchar
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
DECLARE
|
||||
project_exists boolean;
|
||||
BEGIN
|
||||
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
|
||||
RAISE EXCEPTION 'Runtime authority is required'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
|
||||
SELECT true INTO project_exists
|
||||
FROM "ql3"."projects"
|
||||
WHERE id = p_project_id
|
||||
FOR SHARE;
|
||||
|
||||
IF NOT COALESCE(project_exists, false) THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
RETURN NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
|
||||
JOIN "ql3"."plugin_package_task_reconciliations" AS reconciliation
|
||||
ON reconciliation.project_id = quarantine.project_id
|
||||
AND reconciliation.package_name = quarantine.package_name
|
||||
AND reconciliation.lock_digest = quarantine.lock_digest
|
||||
JOIN "ql3"."plugin_package_task_reconciliation_items" AS item
|
||||
ON item.generation_digest = reconciliation.generation_digest
|
||||
AND item.task_id = p_task_id
|
||||
AND 'qltd:v1:' || item.revision || ':' || item.content_digest =
|
||||
p_task_revision
|
||||
WHERE quarantine.project_id = p_project_id
|
||||
);
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
CREATE FUNCTION "ql3"."plugin_package_tool_start_allowed"(
|
||||
p_project_id varchar,
|
||||
p_definition_ref varchar,
|
||||
p_definition_digest char(64)
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
DECLARE
|
||||
project_exists boolean;
|
||||
BEGIN
|
||||
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
|
||||
RAISE EXCEPTION 'Runtime authority is required'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
|
||||
SELECT true INTO project_exists
|
||||
FROM "ql3"."projects"
|
||||
WHERE id = p_project_id
|
||||
FOR SHARE;
|
||||
|
||||
IF NOT COALESCE(project_exists, false) THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
RETURN NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
|
||||
JOIN "ql3"."project_tool_definition_snapshot_sources" AS source
|
||||
ON source.project_id = quarantine.project_id
|
||||
AND source.package_name = quarantine.package_name
|
||||
AND source.installation_id = quarantine.installation_id
|
||||
AND source.lock_digest = quarantine.lock_digest
|
||||
JOIN "ql3"."project_tool_definition_snapshots" AS snapshot
|
||||
ON snapshot.project_id = source.project_id
|
||||
AND snapshot.active_vector_digest = source.active_vector_digest
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
snapshot.snapshot_json -> 'definitions'
|
||||
) AS definition(item)
|
||||
WHERE quarantine.project_id = p_project_id
|
||||
AND definition.item ->> 'packageName' = quarantine.package_name
|
||||
AND 'tool:' ||
|
||||
(definition.item -> 'definition' ->> 'name') || '@' ||
|
||||
(definition.item -> 'definition' ->> 'version') = p_definition_ref
|
||||
AND definition.item ->> 'definitionDigest' = p_definition_digest
|
||||
);
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
CREATE FUNCTION "ql3"."commit_plugin_package_quarantine"(
|
||||
p_event jsonb,
|
||||
p_receipt jsonb,
|
||||
p_task_writes jsonb,
|
||||
p_snapshot jsonb
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
DECLARE
|
||||
event_digest_value char(64);
|
||||
project_id_value varchar(128);
|
||||
package_name_value varchar(63);
|
||||
installation_id_value varchar(128);
|
||||
lock_digest_value char(64);
|
||||
install_record "ql3"."plugin_package_installs"%ROWTYPE;
|
||||
existing_event jsonb;
|
||||
existing_receipt jsonb;
|
||||
task_write jsonb;
|
||||
previous_task "ql3"."task_definition_revisions"%ROWTYPE;
|
||||
disabled_task jsonb;
|
||||
withdrawal jsonb;
|
||||
enabled_task_count integer;
|
||||
source_count integer;
|
||||
inserted_snapshot integer;
|
||||
BEGIN
|
||||
IF NOT pg_has_role(session_user, 'ql3_package_executor', 'member') THEN
|
||||
RAISE EXCEPTION 'Package executor authority is required'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
IF jsonb_typeof(p_event) <> 'object'
|
||||
OR jsonb_typeof(p_receipt) <> 'object'
|
||||
OR jsonb_typeof(p_task_writes) <> 'array'
|
||||
OR jsonb_array_length(p_task_writes) > 128
|
||||
OR (p_snapshot <> 'null'::jsonb AND
|
||||
jsonb_typeof(p_snapshot) <> 'object') THEN
|
||||
RAISE EXCEPTION 'invalid Package quarantine input'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
event_digest_value := p_event ->> 'eventDigest';
|
||||
project_id_value := p_event -> 'target' ->> 'projectId';
|
||||
package_name_value := p_event -> 'target' ->> 'packageName';
|
||||
installation_id_value := p_event -> 'target' ->> 'installationId';
|
||||
lock_digest_value := p_event -> 'target' ->> 'lockDigest';
|
||||
|
||||
IF p_receipt ->> 'eventDigest' IS DISTINCT FROM event_digest_value
|
||||
OR p_receipt -> 'target' IS DISTINCT FROM p_event -> 'target'
|
||||
OR (p_receipt ->> 'committedAtMs')::bigint <
|
||||
(p_event ->> 'occurredAtMs')::bigint THEN
|
||||
RAISE EXCEPTION 'Package quarantine receipt does not bind the event'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
PERFORM 1 FROM "ql3"."projects"
|
||||
WHERE id = project_id_value
|
||||
FOR UPDATE;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Package quarantine Project is absent'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
SELECT event_json INTO existing_event
|
||||
FROM "ql3"."plugin_package_quarantine_events"
|
||||
WHERE event_digest = event_digest_value;
|
||||
IF FOUND THEN
|
||||
SELECT receipt_json INTO existing_receipt
|
||||
FROM "ql3"."plugin_package_withdrawal_receipts"
|
||||
WHERE event_digest = event_digest_value;
|
||||
IF NOT FOUND
|
||||
OR existing_event IS DISTINCT FROM p_event
|
||||
OR existing_receipt IS DISTINCT FROM p_receipt THEN
|
||||
RAISE EXCEPTION 'Package quarantine replay conflicts'
|
||||
USING ERRCODE = 'unique_violation';
|
||||
END IF;
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
SELECT * INTO install_record
|
||||
FROM "ql3"."plugin_package_installs"
|
||||
WHERE installation_id = installation_id_value
|
||||
FOR UPDATE;
|
||||
IF NOT FOUND
|
||||
OR install_record.project_id <> project_id_value
|
||||
OR install_record.package_name <> package_name_value
|
||||
OR install_record.lock_digest <> lock_digest_value
|
||||
OR install_record.state <> p_event -> 'target' ->> 'installState'
|
||||
OR install_record.version <>
|
||||
(p_event -> 'target' ->> 'installVersion')::integer
|
||||
OR install_record.record_digest <>
|
||||
p_event -> 'target' ->> 'installRecordDigest'
|
||||
OR install_record.active_lock_digest IS DISTINCT FROM
|
||||
p_event -> 'target' ->> 'activeLockDigest' THEN
|
||||
RAISE EXCEPTION 'Package quarantine target drifted'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
IF install_record.state = 'active' AND NOT EXISTS (
|
||||
SELECT 1
|
||||
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_materialized_revisions" AS revision
|
||||
ON revision.project_id = install_record.project_id
|
||||
AND revision.package_name = install_record.package_name
|
||||
AND revision.generation = install_record.target_generation
|
||||
AND revision.lock_digest = install_record.lock_digest
|
||||
WHERE head.project_id = install_record.project_id
|
||||
AND head.package_name = install_record.package_name
|
||||
AND head.installation_id = install_record.installation_id
|
||||
AND head_install.active_lock_digest = install_record.lock_digest
|
||||
) THEN
|
||||
RAISE EXCEPTION 'active Package quarantine target is not materialized'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
INSERT INTO "ql3"."plugin_package_quarantine_events" (
|
||||
event_digest, mutation_id, revocation_receipt_digest, impact_digest,
|
||||
project_id, package_name, installation_id, lock_digest,
|
||||
install_state, install_version, install_record_digest,
|
||||
active_lock_digest, proposer_type, proposer_id, confirmer_type,
|
||||
confirmer_id, authorization_mode, reason_code, occurred_at_ms, event_json
|
||||
) VALUES (
|
||||
event_digest_value,
|
||||
p_event ->> 'mutationId',
|
||||
p_event ->> 'revocationReceiptDigest',
|
||||
p_event ->> 'impactDigest',
|
||||
project_id_value,
|
||||
package_name_value,
|
||||
installation_id_value,
|
||||
lock_digest_value,
|
||||
p_event -> 'target' ->> 'installState',
|
||||
(p_event -> 'target' ->> 'installVersion')::integer,
|
||||
p_event -> 'target' ->> 'installRecordDigest',
|
||||
p_event -> 'target' ->> 'activeLockDigest',
|
||||
p_event -> 'proposer' ->> 'type',
|
||||
p_event -> 'proposer' ->> 'id',
|
||||
p_event -> 'confirmer' ->> 'type',
|
||||
p_event -> 'confirmer' ->> 'id',
|
||||
p_event ->> 'authorizationMode',
|
||||
p_event ->> 'reasonCode',
|
||||
(p_event ->> 'occurredAtMs')::bigint,
|
||||
p_event
|
||||
);
|
||||
|
||||
IF install_record.state <> 'active' THEN
|
||||
IF p_snapshot <> 'null'::jsonb OR jsonb_array_length(p_task_writes) <> 0
|
||||
OR p_receipt -> 'capability' ->> 'status' <> 'not_active' THEN
|
||||
RAISE EXCEPTION 'inactive Package quarantine disposition is invalid'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
ELSE
|
||||
IF p_snapshot = 'null'::jsonb
|
||||
OR p_receipt -> 'capability' ->> 'status' <> 'withdrawn' THEN
|
||||
RAISE EXCEPTION 'active Package quarantine disposition is invalid'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
SELECT count(*)::integer INTO source_count
|
||||
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 = project_id_value
|
||||
AND head_install.active_lock_digest IS NOT NULL
|
||||
AND active_install.state = 'active'
|
||||
AND quarantine.event_digest IS NULL;
|
||||
IF source_count > 128
|
||||
OR source_count <>
|
||||
jsonb_array_length(p_snapshot -> 'sources')
|
||||
OR source_count <>
|
||||
(p_receipt -> 'capability' ->> 'retainedSourceCount')::integer
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
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 = project_id_value
|
||||
AND head_install.active_lock_digest IS NOT NULL
|
||||
AND active_install.state = 'active'
|
||||
AND quarantine.event_digest IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(
|
||||
p_snapshot -> 'sources'
|
||||
) AS source
|
||||
WHERE source ->> 'packageName' = active_install.package_name
|
||||
AND source ->> 'installationId' =
|
||||
active_install.installation_id
|
||||
AND (source ->> 'generation')::integer =
|
||||
active_install.target_generation
|
||||
AND source ->> 'generationDigest' =
|
||||
revision.generation_digest
|
||||
AND source ->> 'lockDigest' = active_install.lock_digest
|
||||
AND source ->> 'revisionDigest' =
|
||||
revision.revision_digest
|
||||
)
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Package quarantine Tool source vector drifted'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
SELECT count(*)::integer INTO enabled_task_count
|
||||
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 = project_id_value
|
||||
AND ownership.package_name = package_name_value
|
||||
AND revision.enabled = true;
|
||||
IF enabled_task_count > 128
|
||||
OR enabled_task_count <> jsonb_array_length(p_task_writes)
|
||||
OR enabled_task_count <>
|
||||
jsonb_array_length(
|
||||
p_receipt -> 'capability' -> 'taskWithdrawals'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Package quarantine Task set drifted'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
FOR task_write IN SELECT value FROM jsonb_array_elements(p_task_writes)
|
||||
LOOP
|
||||
disabled_task := task_write -> 'disabled';
|
||||
withdrawal := task_write -> 'withdrawal';
|
||||
SELECT revision.* INTO previous_task
|
||||
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 = project_id_value
|
||||
AND ownership.package_name = package_name_value
|
||||
AND ownership.task_id = withdrawal ->> 'taskId'
|
||||
AND revision.enabled = true
|
||||
FOR UPDATE OF head;
|
||||
IF NOT FOUND
|
||||
OR previous_task.revision <>
|
||||
(withdrawal ->> 'previousRevision')::integer
|
||||
OR previous_task.content_digest <>
|
||||
withdrawal ->> 'previousContentDigest'
|
||||
OR disabled_task ->> 'projectId' <> project_id_value
|
||||
OR disabled_task ->> 'taskId' <> withdrawal ->> 'taskId'
|
||||
OR (disabled_task ->> 'revision')::integer <>
|
||||
previous_task.revision + 1
|
||||
OR (disabled_task ->> 'enabled')::boolean <> false
|
||||
OR disabled_task ->> 'contentDigest' <>
|
||||
withdrawal ->> 'disabledContentDigest' THEN
|
||||
RAISE EXCEPTION 'Package quarantine Task write drifted'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
INSERT INTO "ql3"."task_definition_revisions" (
|
||||
project_id, task_id, revision, mutation_id, name, description,
|
||||
kind, spec_json, labels_json, enabled, content_digest, created_at_ms
|
||||
) VALUES (
|
||||
project_id_value,
|
||||
disabled_task ->> 'taskId',
|
||||
(disabled_task ->> 'revision')::integer,
|
||||
(disabled_task ->> 'mutationId')::uuid,
|
||||
disabled_task ->> 'name',
|
||||
disabled_task ->> 'description',
|
||||
disabled_task ->> 'kind',
|
||||
disabled_task -> 'spec',
|
||||
disabled_task -> 'labels',
|
||||
false,
|
||||
disabled_task ->> 'contentDigest',
|
||||
(disabled_task ->> 'updatedAtMs')::bigint
|
||||
);
|
||||
UPDATE "ql3"."task_definitions"
|
||||
SET current_revision = (disabled_task ->> 'revision')::integer,
|
||||
updated_at_ms = (disabled_task ->> 'updatedAtMs')::bigint
|
||||
WHERE project_id = project_id_value
|
||||
AND task_id = disabled_task ->> 'taskId'
|
||||
AND current_revision = previous_task.revision;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Package quarantine Task head drifted'
|
||||
USING ERRCODE = 'serialization_failure';
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
INSERT INTO "ql3"."project_tool_definition_snapshots" (
|
||||
project_id, active_vector_digest, definitions_digest,
|
||||
snapshot_digest, snapshot_json, committed_at_ms
|
||||
) VALUES (
|
||||
project_id_value,
|
||||
p_snapshot ->> 'activeVectorDigest',
|
||||
p_snapshot ->> 'definitionsDigest',
|
||||
p_snapshot ->> 'snapshotDigest',
|
||||
p_snapshot,
|
||||
(p_receipt ->> 'committedAtMs')::bigint
|
||||
)
|
||||
ON CONFLICT (project_id, active_vector_digest) DO NOTHING;
|
||||
GET DIAGNOSTICS inserted_snapshot = ROW_COUNT;
|
||||
IF inserted_snapshot = 1 THEN
|
||||
INSERT INTO "ql3"."project_tool_definition_snapshot_sources" (
|
||||
project_id, active_vector_digest, package_name, installation_id,
|
||||
generation, generation_digest, lock_digest, revision_digest
|
||||
)
|
||||
SELECT
|
||||
project_id_value,
|
||||
p_snapshot ->> 'activeVectorDigest',
|
||||
source ->> 'packageName',
|
||||
source ->> 'installationId',
|
||||
(source ->> 'generation')::integer,
|
||||
source ->> 'generationDigest',
|
||||
source ->> 'lockDigest',
|
||||
source ->> 'revisionDigest'
|
||||
FROM jsonb_array_elements(p_snapshot -> 'sources') AS source;
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."project_tool_definition_snapshots"
|
||||
WHERE project_id = project_id_value
|
||||
AND active_vector_digest = p_snapshot ->> 'activeVectorDigest'
|
||||
AND snapshot_json = p_snapshot
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Package quarantine snapshot conflicts'
|
||||
USING ERRCODE = 'unique_violation';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
INSERT INTO "ql3"."plugin_package_withdrawal_receipts" (
|
||||
event_digest, receipt_digest, project_id, capability_status,
|
||||
task_count, previous_active_vector_digest,
|
||||
current_active_vector_digest, current_tool_snapshot_digest,
|
||||
retained_source_count, committed_at_ms, receipt_json
|
||||
) VALUES (
|
||||
event_digest_value,
|
||||
p_receipt ->> 'receiptDigest',
|
||||
project_id_value,
|
||||
p_receipt -> 'capability' ->> 'status',
|
||||
jsonb_array_length(
|
||||
p_receipt -> 'capability' -> 'taskWithdrawals'
|
||||
),
|
||||
p_receipt -> 'capability' ->> 'previousActiveVectorDigest',
|
||||
p_receipt -> 'capability' ->> 'currentActiveVectorDigest',
|
||||
p_receipt -> 'capability' ->> 'currentToolSnapshotDigest',
|
||||
(p_receipt -> 'capability' ->> 'retainedSourceCount')::integer,
|
||||
(p_receipt ->> 'committedAtMs')::bigint,
|
||||
p_receipt
|
||||
);
|
||||
|
||||
INSERT INTO "ql3"."plugin_package_withdrawal_tasks" (
|
||||
event_digest, project_id, task_id, previous_revision,
|
||||
disabled_revision, previous_content_digest, disabled_content_digest
|
||||
)
|
||||
SELECT
|
||||
event_digest_value,
|
||||
project_id_value,
|
||||
receipt_withdrawal ->> 'taskId',
|
||||
(receipt_withdrawal ->> 'previousRevision')::integer,
|
||||
(receipt_withdrawal ->> 'disabledRevision')::integer,
|
||||
receipt_withdrawal ->> 'previousContentDigest',
|
||||
receipt_withdrawal ->> 'disabledContentDigest'
|
||||
FROM jsonb_array_elements(
|
||||
p_receipt -> 'capability' -> 'taskWithdrawals'
|
||||
) AS receipt_withdrawal;
|
||||
|
||||
RETURN true;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL ON
|
||||
"ql3"."plugin_package_quarantine_events",
|
||||
"ql3"."plugin_package_withdrawal_receipts",
|
||||
"ql3"."plugin_package_withdrawal_tasks"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL ON FUNCTION
|
||||
"ql3"."plugin_package_run_start_allowed"(varchar, varchar, varchar),
|
||||
"ql3"."plugin_package_tool_start_allowed"(varchar, varchar, char(64)),
|
||||
"ql3"."commit_plugin_package_quarantine"(jsonb, jsonb, jsonb, jsonb)
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`GRANT SELECT ON "ql3"."plugin_package_quarantine_events", "ql3"."plugin_package_withdrawal_receipts", "ql3"."plugin_package_withdrawal_tasks" TO ql3_package_executor`,
|
||||
`GRANT EXECUTE ON FUNCTION "ql3"."plugin_package_run_start_allowed"(varchar, varchar, varchar), "ql3"."plugin_package_tool_start_allowed"(varchar, varchar, char(64)) TO ql3_runtime`,
|
||||
`GRANT EXECUTE ON FUNCTION "ql3"."commit_plugin_package_quarantine"(jsonb, jsonb, jsonb, jsonb) TO ql3_package_executor`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 36,
|
||||
migration_id = 'pg-0037-plugin-package-quarantine',
|
||||
capabilities = '${CAPABILITIES_V36}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 35
|
||||
AND migration_id = 'pg-0036-tool-result-rekey-overlays'
|
||||
AND capabilities = '${CAPABILITIES_V35}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 35'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V36 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V37 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0038PluginPackagePublisherProvenanceMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0038-plugin-package-publisher-provenance',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_publisher_provenance" (
|
||||
installation_id varchar(128) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
artifact_digest char(64) NOT NULL,
|
||||
manifest_digest char(64) NOT NULL,
|
||||
content_digest char(64) NOT NULL,
|
||||
stage_evidence_digest char(64) NOT NULL,
|
||||
publisher varchar(253) NOT NULL,
|
||||
key_id varchar(128) NOT NULL,
|
||||
signature_digest char(64) NOT NULL,
|
||||
key_not_before_ms bigint NOT NULL,
|
||||
key_not_after_ms bigint NOT NULL,
|
||||
verified_at_ms bigint NOT NULL,
|
||||
provenance_digest char(64) NOT NULL,
|
||||
provenance_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_publisher_provenance_install_fk
|
||||
FOREIGN KEY (installation_id)
|
||||
REFERENCES "ql3"."plugin_package_installs" (installation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_publisher_provenance_identity_check CHECK (
|
||||
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
|
||||
publisher ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$' AND
|
||||
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_provenance_digest_check CHECK (
|
||||
lock_digest ~ '^[0-9a-f]{64}$' AND
|
||||
artifact_digest ~ '^[0-9a-f]{64}$' AND
|
||||
manifest_digest ~ '^[0-9a-f]{64}$' AND
|
||||
content_digest ~ '^[0-9a-f]{64}$' AND
|
||||
stage_evidence_digest ~ '^[0-9a-f]{64}$' AND
|
||||
signature_digest ~ '^[0-9a-f]{64}$' AND
|
||||
provenance_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_provenance_time_check CHECK (
|
||||
key_not_before_ms >= 0 AND
|
||||
key_not_after_ms > key_not_before_ms AND
|
||||
verified_at_ms >= key_not_before_ms AND
|
||||
verified_at_ms < key_not_after_ms
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_provenance_json_check CHECK (
|
||||
jsonb_typeof(provenance_json) = 'object' AND
|
||||
octet_length(provenance_json::text) BETWEEN 2 AND 262144 AND
|
||||
provenance_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-publisher-provenance@v1',
|
||||
'projectId', project_id,
|
||||
'packageName', package_name,
|
||||
'installationId', installation_id,
|
||||
'lockDigest', lock_digest,
|
||||
'artifactDigest', artifact_digest,
|
||||
'manifestDigest', manifest_digest,
|
||||
'contentDigest', content_digest,
|
||||
'stageEvidenceDigest', stage_evidence_digest,
|
||||
'publisher', publisher,
|
||||
'keyId', key_id,
|
||||
'signatureDigest', signature_digest,
|
||||
'keyNotBeforeMs', key_not_before_ms,
|
||||
'keyNotAfterMs', key_not_after_ms,
|
||||
'verifiedAtMs', verified_at_ms,
|
||||
'provenanceDigest', provenance_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_provenance_digest_key ON "ql3"."plugin_package_publisher_provenance" (provenance_digest)`,
|
||||
`CREATE INDEX ql3_plugin_package_publisher_provenance_signer_idx ON "ql3"."plugin_package_publisher_provenance" (publisher, key_id, project_id, package_name, installation_id)`,
|
||||
`CREATE INDEX ql3_plugin_package_publisher_provenance_lock_idx ON "ql3"."plugin_package_publisher_provenance" (lock_digest, installation_id)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_publisher_revocation_receipts" (
|
||||
receipt_digest char(64) PRIMARY KEY,
|
||||
mutation_id varchar(128) NOT NULL,
|
||||
publisher varchar(253) NOT NULL,
|
||||
key_id varchar(128) NOT NULL,
|
||||
previous_trust_digest char(64) NOT NULL,
|
||||
current_trust_digest char(64) NOT NULL,
|
||||
proposer_type varchar(16) NOT NULL,
|
||||
proposer_id varchar(255) NOT NULL,
|
||||
confirmer_type varchar(16) NOT NULL,
|
||||
confirmer_id varchar(255) NOT NULL,
|
||||
authorization_mode varchar(16) NOT NULL,
|
||||
reason_code varchar(32) NOT NULL,
|
||||
revoked_at_ms bigint NOT NULL,
|
||||
receipt_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_publisher_revocation_identity_check CHECK (
|
||||
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
publisher ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$' AND
|
||||
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
proposer_type IN (
|
||||
'user', 'api_app', 'mcp_client', 'agent', 'system', 'worker'
|
||||
) AND
|
||||
confirmer_type IN (
|
||||
'user', 'api_app', 'mcp_client', 'agent', 'system', 'worker'
|
||||
) AND
|
||||
octet_length(proposer_id) BETWEEN 1 AND 255 AND
|
||||
octet_length(confirmer_id) BETWEEN 1 AND 255 AND
|
||||
authorization_mode IN ('dual_control', 'break_glass') AND
|
||||
(authorization_mode = 'break_glass' OR
|
||||
proposer_type <> confirmer_type OR proposer_id <> confirmer_id) AND
|
||||
reason_code IN (
|
||||
'suspected_key_compromise', 'confirmed_key_compromise'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_revocation_digest_check CHECK (
|
||||
receipt_digest ~ '^[0-9a-f]{64}$' AND
|
||||
previous_trust_digest ~ '^[0-9a-f]{64}$' AND
|
||||
current_trust_digest ~ '^[0-9a-f]{64}$' AND
|
||||
previous_trust_digest <> current_trust_digest
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_revocation_time_check CHECK (
|
||||
revoked_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_revocation_json_check CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
octet_length(receipt_json::text) BETWEEN 2 AND 262144 AND
|
||||
receipt_json @> jsonb_build_object(
|
||||
'schema',
|
||||
'qinglong/plugin-package-publisher-key-revocation-receipt@v1',
|
||||
'mutationId', mutation_id,
|
||||
'publisher', publisher,
|
||||
'keyId', key_id,
|
||||
'previousTrustDigest', previous_trust_digest,
|
||||
'currentTrustDigest', current_trust_digest,
|
||||
'authorizationMode', authorization_mode,
|
||||
'reasonCode', reason_code,
|
||||
'revokedAtMs', revoked_at_ms,
|
||||
'receiptDigest', receipt_digest
|
||||
) AND
|
||||
receipt_json -> 'proposer' @> jsonb_build_object(
|
||||
'type', proposer_type, 'id', proposer_id
|
||||
) AND
|
||||
receipt_json -> 'confirmer' @> jsonb_build_object(
|
||||
'type', confirmer_type, 'id', confirmer_id
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_revocation_mutation_key ON "ql3"."plugin_package_publisher_revocation_receipts" (mutation_id)`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_revocation_signer_key ON "ql3"."plugin_package_publisher_revocation_receipts" (publisher, key_id)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_publisher_revocation_impacts" (
|
||||
revocation_receipt_digest char(64) PRIMARY KEY,
|
||||
impact_digest char(64) NOT NULL,
|
||||
item_count integer NOT NULL,
|
||||
generated_at_ms bigint NOT NULL,
|
||||
impact_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_publisher_impact_receipt_fk
|
||||
FOREIGN KEY (revocation_receipt_digest)
|
||||
REFERENCES "ql3"."plugin_package_publisher_revocation_receipts"
|
||||
(receipt_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_publisher_impact_digest_check CHECK (
|
||||
revocation_receipt_digest ~ '^[0-9a-f]{64}$' AND
|
||||
impact_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_impact_count_check CHECK (
|
||||
item_count BETWEEN 0 AND 4096 AND generated_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_impact_json_check CHECK (
|
||||
jsonb_typeof(impact_json) = 'object' AND
|
||||
octet_length(impact_json::text) BETWEEN 2 AND 8388608 AND
|
||||
impact_json @> jsonb_build_object(
|
||||
'schema',
|
||||
'qinglong/plugin-package-publisher-key-revocation-impact@v1',
|
||||
'revocationReceiptDigest', revocation_receipt_digest,
|
||||
'generatedAtMs', generated_at_ms,
|
||||
'impactDigest', impact_digest
|
||||
) AND
|
||||
jsonb_array_length(impact_json -> 'items') = item_count
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_impact_digest_key ON "ql3"."plugin_package_publisher_revocation_impacts" (impact_digest)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_publisher_revocation_impact_items" (
|
||||
impact_digest char(64) NOT NULL,
|
||||
provenance_digest char(64) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
installation_id varchar(128) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
PRIMARY KEY (impact_digest, provenance_digest),
|
||||
CONSTRAINT ql3_plugin_package_publisher_impact_item_impact_fk
|
||||
FOREIGN KEY (impact_digest)
|
||||
REFERENCES "ql3"."plugin_package_publisher_revocation_impacts"
|
||||
(impact_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_publisher_impact_item_provenance_fk
|
||||
FOREIGN KEY (provenance_digest)
|
||||
REFERENCES "ql3"."plugin_package_publisher_provenance"
|
||||
(provenance_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_publisher_impact_item_install_fk
|
||||
FOREIGN KEY (installation_id)
|
||||
REFERENCES "ql3"."plugin_package_installs" (installation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_publisher_impact_item_identity_check CHECK (
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
|
||||
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
impact_digest ~ '^[0-9a-f]{64}$' AND
|
||||
provenance_digest ~ '^[0-9a-f]{64}$' AND
|
||||
lock_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_impact_item_install_key ON "ql3"."plugin_package_publisher_revocation_impact_items" (impact_digest, installation_id)`,
|
||||
`CREATE INDEX ql3_plugin_package_publisher_impact_item_target_idx ON "ql3"."plugin_package_publisher_revocation_impact_items" (project_id, package_name, installation_id, lock_digest)`,
|
||||
`
|
||||
CREATE FUNCTION "ql3"."enforce_plugin_package_stage_provenance"()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
BEGIN
|
||||
IF OLD.state = 'queued' AND NEW.state = 'staged' AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
|
||||
WHERE provenance.installation_id = NEW.installation_id
|
||||
AND provenance.project_id = NEW.project_id
|
||||
AND provenance.package_name = NEW.package_name
|
||||
AND provenance.lock_digest = NEW.lock_digest
|
||||
AND provenance.artifact_digest =
|
||||
NEW.lock_json -> 'source' ->> 'artifactDigest'
|
||||
AND provenance.manifest_digest = NEW.lock_json ->> 'manifestDigest'
|
||||
AND provenance.content_digest =
|
||||
NEW.lock_json -> 'source' ->> 'contentDigest'
|
||||
AND provenance.stage_evidence_digest =
|
||||
NEW.record_json -> 'stageReceipt' ->> 'evidenceDigest'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'Plugin Package stage requires exact publisher provenance'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TRIGGER ql3_plugin_package_stage_provenance_guard
|
||||
BEFORE UPDATE ON "ql3"."plugin_package_installs"
|
||||
FOR EACH ROW EXECUTE FUNCTION
|
||||
"ql3"."enforce_plugin_package_stage_provenance"()
|
||||
`.trim(),
|
||||
`
|
||||
CREATE OR REPLACE FUNCTION "ql3"."plugin_package_run_start_allowed"(
|
||||
p_project_id varchar,
|
||||
p_task_id varchar,
|
||||
p_task_revision varchar
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
DECLARE
|
||||
project_exists boolean;
|
||||
BEGIN
|
||||
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
|
||||
RAISE EXCEPTION 'Runtime authority is required'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
|
||||
SELECT true INTO project_exists
|
||||
FROM "ql3"."projects"
|
||||
WHERE id = p_project_id
|
||||
FOR SHARE;
|
||||
|
||||
IF NOT COALESCE(project_exists, false) THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
RETURN NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_task_reconciliations" AS reconciliation
|
||||
JOIN "ql3"."plugin_package_task_reconciliation_items" AS item
|
||||
ON item.generation_digest = reconciliation.generation_digest
|
||||
AND item.task_id = p_task_id
|
||||
AND 'qltd:v1:' || item.revision || ':' || item.content_digest =
|
||||
p_task_revision
|
||||
WHERE reconciliation.project_id = p_project_id
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
|
||||
WHERE quarantine.project_id = reconciliation.project_id
|
||||
AND quarantine.package_name = reconciliation.package_name
|
||||
AND quarantine.lock_digest = reconciliation.lock_digest
|
||||
) 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.lock_digest = reconciliation.lock_digest
|
||||
)
|
||||
)
|
||||
);
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
CREATE OR REPLACE FUNCTION "ql3"."plugin_package_tool_start_allowed"(
|
||||
p_project_id varchar,
|
||||
p_definition_ref varchar,
|
||||
p_definition_digest char(64)
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
DECLARE
|
||||
project_exists boolean;
|
||||
BEGIN
|
||||
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
|
||||
RAISE EXCEPTION 'Runtime authority is required'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
|
||||
SELECT true INTO project_exists
|
||||
FROM "ql3"."projects"
|
||||
WHERE id = p_project_id
|
||||
FOR SHARE;
|
||||
|
||||
IF NOT COALESCE(project_exists, false) THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
RETURN NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."project_tool_definition_snapshot_sources" AS source
|
||||
JOIN "ql3"."project_tool_definition_snapshots" AS snapshot
|
||||
ON snapshot.project_id = source.project_id
|
||||
AND snapshot.active_vector_digest = source.active_vector_digest
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
snapshot.snapshot_json -> 'definitions'
|
||||
) AS definition(item)
|
||||
WHERE source.project_id = p_project_id
|
||||
AND definition.item ->> 'packageName' = source.package_name
|
||||
AND 'tool:' ||
|
||||
(definition.item -> 'definition' ->> 'name') || '@' ||
|
||||
(definition.item -> 'definition' ->> 'version') = p_definition_ref
|
||||
AND definition.item ->> 'definitionDigest' = p_definition_digest
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
|
||||
WHERE quarantine.project_id = source.project_id
|
||||
AND quarantine.package_name = source.package_name
|
||||
AND quarantine.installation_id = source.installation_id
|
||||
AND quarantine.lock_digest = source.lock_digest
|
||||
) 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 = source.installation_id
|
||||
AND provenance.lock_digest = source.lock_digest
|
||||
)
|
||||
)
|
||||
);
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL ON
|
||||
"ql3"."plugin_package_publisher_provenance",
|
||||
"ql3"."plugin_package_publisher_revocation_receipts",
|
||||
"ql3"."plugin_package_publisher_revocation_impacts",
|
||||
"ql3"."plugin_package_publisher_revocation_impact_items"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL ON FUNCTION
|
||||
"ql3"."enforce_plugin_package_stage_provenance"()
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT ON
|
||||
"ql3"."plugin_package_publisher_provenance",
|
||||
"ql3"."plugin_package_publisher_revocation_receipts",
|
||||
"ql3"."plugin_package_publisher_revocation_impacts",
|
||||
"ql3"."plugin_package_publisher_revocation_impact_items"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 37,
|
||||
migration_id = 'pg-0038-plugin-package-publisher-provenance',
|
||||
capabilities = '${CAPABILITIES_V37}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 36
|
||||
AND migration_id = 'pg-0037-plugin-package-quarantine'
|
||||
AND capabilities = '${CAPABILITIES_V36}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 36'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V37 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V38 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0039PluginPackagePublisherTrustAuthorityMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0039-plugin-package-publisher-trust-authority',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_publisher_trust_snapshots" (
|
||||
snapshot_digest char(64) PRIMARY KEY,
|
||||
key_count integer NOT NULL,
|
||||
observed_by varchar(128) NOT NULL,
|
||||
observed_at_ms bigint NOT NULL,
|
||||
snapshot_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_publisher_trust_snapshot_identity_check CHECK (
|
||||
observed_by ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_trust_snapshot_digest_check CHECK (
|
||||
snapshot_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_trust_snapshot_count_check CHECK (
|
||||
key_count BETWEEN 0 AND 32 AND observed_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_trust_snapshot_json_check CHECK (
|
||||
jsonb_typeof(snapshot_json) = 'object' AND
|
||||
octet_length(snapshot_json::text) BETWEEN 2 AND 262144 AND
|
||||
snapshot_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-publisher-trust-snapshot@v1',
|
||||
'snapshotDigest', snapshot_digest
|
||||
) AND
|
||||
jsonb_array_length(snapshot_json -> 'keys') = key_count
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_publisher_trust_heads" (
|
||||
authority_id varchar(128) PRIMARY KEY,
|
||||
generation integer NOT NULL,
|
||||
base_snapshot_digest char(64) NOT NULL,
|
||||
effective_trust_digest char(64) NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
head_digest char(64) NOT NULL,
|
||||
head_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_publisher_trust_head_snapshot_fk
|
||||
FOREIGN KEY (base_snapshot_digest)
|
||||
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
|
||||
(snapshot_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_publisher_trust_head_effective_snapshot_fk
|
||||
FOREIGN KEY (effective_trust_digest)
|
||||
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
|
||||
(snapshot_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_publisher_trust_head_identity_check CHECK (
|
||||
authority_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
generation BETWEEN 1 AND 2147483647
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_trust_head_digest_check CHECK (
|
||||
base_snapshot_digest ~ '^[0-9a-f]{64}$' AND
|
||||
effective_trust_digest ~ '^[0-9a-f]{64}$' AND
|
||||
head_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_trust_head_time_check CHECK (
|
||||
updated_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_trust_head_json_check CHECK (
|
||||
jsonb_typeof(head_json) = 'object' AND
|
||||
octet_length(head_json::text) BETWEEN 2 AND 65536 AND
|
||||
head_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-publisher-trust-head@v1',
|
||||
'authorityId', authority_id,
|
||||
'generation', generation,
|
||||
'baseSnapshotDigest', base_snapshot_digest,
|
||||
'effectiveTrustDigest', effective_trust_digest,
|
||||
'updatedAtMs', updated_at_ms,
|
||||
'headDigest', head_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_publisher_revocation_proposals" (
|
||||
action_ref varchar(255) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
authority_id varchar(128) NOT NULL,
|
||||
trust_generation integer NOT NULL,
|
||||
publisher varchar(253) NOT NULL,
|
||||
key_id varchar(128) NOT NULL,
|
||||
previous_trust_digest char(64) NOT NULL,
|
||||
current_trust_digest char(64) NOT NULL,
|
||||
action_type varchar(128) NOT NULL,
|
||||
permission varchar(128) NOT NULL,
|
||||
action_digest char(64) NOT NULL,
|
||||
preview_digest char(64) NOT NULL,
|
||||
authorization_mode varchar(16) NOT NULL,
|
||||
reason_code varchar(32) NOT NULL,
|
||||
proposed_by_type varchar(32) NOT NULL,
|
||||
proposed_by_id varchar(255) NOT NULL,
|
||||
proposer_assurance varchar(32) NOT NULL,
|
||||
fence_project_version integer NOT NULL,
|
||||
fence_binding_version integer,
|
||||
created_at_ms bigint NOT NULL,
|
||||
proposal_json jsonb NOT NULL,
|
||||
proposal_digest char(64) NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_head_fk
|
||||
FOREIGN KEY (authority_id)
|
||||
REFERENCES "ql3"."plugin_package_publisher_trust_heads" (authority_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_identity_check
|
||||
CHECK (
|
||||
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$' AND
|
||||
authority_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
publisher ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$' AND
|
||||
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
action_type = 'plugin_package.publisher_key.revoke' AND
|
||||
permission = 'package.manage' AND
|
||||
authorization_mode IN ('dual_control', 'break_glass') AND
|
||||
reason_code IN (
|
||||
'suspected_key_compromise', 'confirmed_key_compromise'
|
||||
) AND
|
||||
proposed_by_type IN (
|
||||
'user', 'api_app', 'mcp_client', 'agent', 'system', 'worker'
|
||||
) AND
|
||||
octet_length(proposed_by_id) BETWEEN 1 AND 255 AND
|
||||
proposer_assurance IN (
|
||||
'single_factor', 'multi_factor', 'service', 'hardware',
|
||||
'local_console'
|
||||
) AND
|
||||
(authorization_mode <> 'break_glass' OR
|
||||
proposer_assurance = 'hardware')
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_digest_check
|
||||
CHECK (
|
||||
previous_trust_digest ~ '^[0-9a-f]{64}$' AND
|
||||
current_trust_digest ~ '^[0-9a-f]{64}$' AND
|
||||
previous_trust_digest <> current_trust_digest AND
|
||||
action_digest ~ '^[0-9a-f]{64}$' AND
|
||||
preview_digest ~ '^[0-9a-f]{64}$' AND
|
||||
proposal_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_time_check
|
||||
CHECK (
|
||||
trust_generation BETWEEN 1 AND 2147483647 AND
|
||||
fence_project_version BETWEEN 1 AND 2147483647 AND
|
||||
(fence_binding_version IS NULL OR
|
||||
fence_binding_version BETWEEN 1 AND 2147483647) AND
|
||||
created_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_publisher_revocation_proposal_json_check
|
||||
CHECK (
|
||||
jsonb_typeof(proposal_json) = 'object' AND
|
||||
octet_length(proposal_json::text) BETWEEN 2 AND 262144 AND
|
||||
proposal_json @> jsonb_build_object(
|
||||
'schema',
|
||||
'qinglong/plugin-package-publisher-key-revocation-proposal@v1',
|
||||
'actionRef', action_ref,
|
||||
'projectId', project_id,
|
||||
'actionType', action_type,
|
||||
'permission', permission,
|
||||
'actionDigest', action_digest,
|
||||
'previewDigest', preview_digest,
|
||||
'proposerAssurance', proposer_assurance,
|
||||
'createdAtMs', created_at_ms,
|
||||
'proposalDigest', proposal_digest
|
||||
) AND
|
||||
proposal_json -> 'actionInput' @> jsonb_build_object(
|
||||
'authorityProjectId', project_id,
|
||||
'trustAuthorityId', authority_id,
|
||||
'trustGeneration', trust_generation,
|
||||
'publisher', publisher,
|
||||
'keyId', key_id,
|
||||
'previousTrustDigest', previous_trust_digest,
|
||||
'currentTrustDigest', current_trust_digest,
|
||||
'authorizationMode', authorization_mode,
|
||||
'reasonCode', reason_code
|
||||
) AND
|
||||
proposal_json -> 'proposedBy' @> jsonb_build_object(
|
||||
'type', proposed_by_type, 'id', proposed_by_id
|
||||
) AND
|
||||
proposal_json -> 'proposalFence' @> jsonb_build_object(
|
||||
'projectVersion', fence_project_version,
|
||||
'bindingVersion', fence_binding_version
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_publisher_revocation_proposal_digest_key ON "ql3"."plugin_package_publisher_revocation_proposals" (proposal_digest)`,
|
||||
`CREATE INDEX ql3_plugin_package_publisher_revocation_proposal_project_idx ON "ql3"."plugin_package_publisher_revocation_proposals" (project_id, created_at_ms, action_ref)`,
|
||||
`CREATE INDEX ql3_plugin_package_publisher_revocation_proposal_signer_idx ON "ql3"."plugin_package_publisher_revocation_proposals" (publisher, key_id, trust_generation)`,
|
||||
`
|
||||
REVOKE ALL ON
|
||||
"ql3"."plugin_package_publisher_trust_snapshots",
|
||||
"ql3"."plugin_package_publisher_trust_heads",
|
||||
"ql3"."plugin_package_publisher_revocation_proposals"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT ON
|
||||
"ql3"."plugin_package_publisher_trust_snapshots",
|
||||
"ql3"."plugin_package_publisher_trust_heads",
|
||||
"ql3"."plugin_package_publisher_revocation_proposals"
|
||||
TO ql3_package_manager
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT ON
|
||||
"ql3"."plugin_package_publisher_trust_snapshots"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT ON
|
||||
"ql3"."plugin_package_publisher_revocation_proposals"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, UPDATE ON
|
||||
"ql3"."plugin_package_publisher_trust_heads"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 38,
|
||||
migration_id =
|
||||
'pg-0039-plugin-package-publisher-trust-authority',
|
||||
capabilities = '${CAPABILITIES_V38}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 37
|
||||
AND migration_id = 'pg-0038-plugin-package-publisher-provenance'
|
||||
AND capabilities = '${CAPABILITIES_V37}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 37'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V38 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V39 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0040PluginPackagePublisherTrustTransitionsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0040-plugin-package-publisher-trust-transitions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_publisher_trust_transition_proposals" (
|
||||
action_ref varchar(255) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
authority_id varchar(128) NOT NULL,
|
||||
trust_generation integer NOT NULL,
|
||||
mode varchar(16) NOT NULL,
|
||||
publisher varchar(253) NOT NULL,
|
||||
key_id varchar(128) NOT NULL,
|
||||
previous_trust_digest char(64) NOT NULL,
|
||||
current_trust_digest char(64) NOT NULL,
|
||||
action_type varchar(128) NOT NULL,
|
||||
permission varchar(128) NOT NULL,
|
||||
action_digest char(64) NOT NULL,
|
||||
preview_digest char(64) NOT NULL,
|
||||
proposed_by_type varchar(32) NOT NULL,
|
||||
proposed_by_id varchar(255) NOT NULL,
|
||||
proposer_assurance varchar(32) NOT NULL,
|
||||
fence_project_version integer NOT NULL,
|
||||
fence_binding_version integer,
|
||||
created_at_ms bigint NOT NULL,
|
||||
proposal_json jsonb NOT NULL,
|
||||
proposal_digest char(64) NOT NULL,
|
||||
CONSTRAINT ql3_pp_trust_transition_proposal_digest_key
|
||||
UNIQUE (proposal_digest),
|
||||
CONSTRAINT ql3_pp_trust_transition_proposal_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_trust_transition_proposal_head_fk
|
||||
FOREIGN KEY (authority_id)
|
||||
REFERENCES "ql3"."plugin_package_publisher_trust_heads" (authority_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_trust_transition_proposal_previous_fk
|
||||
FOREIGN KEY (previous_trust_digest)
|
||||
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
|
||||
(snapshot_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_trust_transition_proposal_current_fk
|
||||
FOREIGN KEY (current_trust_digest)
|
||||
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
|
||||
(snapshot_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_trust_transition_proposal_identity_check
|
||||
CHECK (
|
||||
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$' AND
|
||||
authority_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
publisher ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$' AND
|
||||
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
mode IN ('overlap_add', 'safe_retire') AND
|
||||
action_type = CASE mode
|
||||
WHEN 'overlap_add'
|
||||
THEN 'plugin_package.publisher_key.overlap_add'
|
||||
WHEN 'safe_retire'
|
||||
THEN 'plugin_package.publisher_key.safe_retire'
|
||||
END AND
|
||||
permission = 'package.manage' AND
|
||||
proposed_by_type = 'user' AND
|
||||
octet_length(proposed_by_id) BETWEEN 1 AND 255 AND
|
||||
proposer_assurance IN ('multi_factor', 'hardware')
|
||||
),
|
||||
CONSTRAINT ql3_pp_trust_transition_proposal_digest_check
|
||||
CHECK (
|
||||
previous_trust_digest ~ '^[0-9a-f]{64}$' AND
|
||||
current_trust_digest ~ '^[0-9a-f]{64}$' AND
|
||||
previous_trust_digest <> current_trust_digest AND
|
||||
action_digest ~ '^[0-9a-f]{64}$' AND
|
||||
preview_digest ~ '^[0-9a-f]{64}$' AND
|
||||
proposal_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_pp_trust_transition_proposal_time_check
|
||||
CHECK (
|
||||
trust_generation BETWEEN 1 AND 2147483647 AND
|
||||
fence_project_version BETWEEN 1 AND 2147483647 AND
|
||||
(fence_binding_version IS NULL OR
|
||||
fence_binding_version BETWEEN 1 AND 2147483647) AND
|
||||
created_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_pp_trust_transition_proposal_json_check
|
||||
CHECK (
|
||||
jsonb_typeof(proposal_json) = 'object' AND
|
||||
octet_length(proposal_json::text) BETWEEN 2 AND 262144 AND
|
||||
proposal_json @> jsonb_build_object(
|
||||
'schema',
|
||||
'qinglong/plugin-package-publisher-trust-transition-proposal@v1',
|
||||
'actionRef', action_ref,
|
||||
'projectId', project_id,
|
||||
'actionType', action_type,
|
||||
'permission', permission,
|
||||
'actionDigest', action_digest,
|
||||
'previewDigest', preview_digest,
|
||||
'proposerAssurance', proposer_assurance,
|
||||
'createdAtMs', created_at_ms,
|
||||
'proposalDigest', proposal_digest
|
||||
) AND
|
||||
proposal_json -> 'actionInput' @> jsonb_build_object(
|
||||
'authorityProjectId', project_id,
|
||||
'trustAuthorityId', authority_id,
|
||||
'trustGeneration', trust_generation,
|
||||
'mode', mode,
|
||||
'publisher', publisher,
|
||||
'keyId', key_id,
|
||||
'previousTrustDigest', previous_trust_digest,
|
||||
'currentTrustDigest', current_trust_digest
|
||||
) AND
|
||||
proposal_json -> 'proposedBy' @> jsonb_build_object(
|
||||
'type', proposed_by_type, 'id', proposed_by_id
|
||||
) AND
|
||||
proposal_json -> 'proposalFence' @> jsonb_build_object(
|
||||
'projectVersion', fence_project_version,
|
||||
'bindingVersion', fence_binding_version
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE INDEX ql3_pp_trust_transition_proposal_project_idx ON "ql3"."plugin_package_publisher_trust_transition_proposals" (project_id, created_at_ms, action_ref)`,
|
||||
`CREATE INDEX ql3_pp_trust_transition_proposal_signer_idx ON "ql3"."plugin_package_publisher_trust_transition_proposals" (publisher, key_id, trust_generation)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_publisher_trust_transition_receipts" (
|
||||
mutation_id varchar(128) PRIMARY KEY,
|
||||
proposal_digest char(64) NOT NULL,
|
||||
authority_id varchar(128) NOT NULL,
|
||||
previous_generation integer NOT NULL,
|
||||
current_generation integer NOT NULL,
|
||||
mode varchar(16) NOT NULL,
|
||||
publisher varchar(253) NOT NULL,
|
||||
key_id varchar(128) NOT NULL,
|
||||
previous_trust_digest char(64) NOT NULL,
|
||||
current_trust_digest char(64) NOT NULL,
|
||||
proposer_type varchar(32) NOT NULL,
|
||||
proposer_id varchar(255) NOT NULL,
|
||||
confirmer_type varchar(32) NOT NULL,
|
||||
confirmer_id varchar(255) NOT NULL,
|
||||
retirement_matching_installations integer,
|
||||
executed_at_ms bigint NOT NULL,
|
||||
receipt_json jsonb NOT NULL,
|
||||
receipt_digest char(64) NOT NULL,
|
||||
CONSTRAINT ql3_pp_trust_transition_receipt_proposal_key
|
||||
UNIQUE (proposal_digest),
|
||||
CONSTRAINT ql3_pp_trust_transition_receipt_digest_key
|
||||
UNIQUE (receipt_digest),
|
||||
CONSTRAINT ql3_pp_trust_transition_receipt_dispatch_fk
|
||||
FOREIGN KEY (mutation_id)
|
||||
REFERENCES "ql3"."approved_action_dispatches" (dispatch_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_trust_transition_receipt_proposal_fk
|
||||
FOREIGN KEY (proposal_digest)
|
||||
REFERENCES "ql3"."plugin_package_publisher_trust_transition_proposals"
|
||||
(proposal_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_trust_transition_receipt_head_fk
|
||||
FOREIGN KEY (authority_id)
|
||||
REFERENCES "ql3"."plugin_package_publisher_trust_heads" (authority_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_trust_transition_receipt_previous_fk
|
||||
FOREIGN KEY (previous_trust_digest)
|
||||
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
|
||||
(snapshot_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_trust_transition_receipt_current_fk
|
||||
FOREIGN KEY (current_trust_digest)
|
||||
REFERENCES "ql3"."plugin_package_publisher_trust_snapshots"
|
||||
(snapshot_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_trust_transition_receipt_identity_check
|
||||
CHECK (
|
||||
mutation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
proposal_digest ~ '^[0-9a-f]{64}$' AND
|
||||
authority_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
mode IN ('overlap_add', 'safe_retire') AND
|
||||
publisher ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$' AND
|
||||
key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
proposer_type = 'user' AND
|
||||
confirmer_type = 'user' AND
|
||||
octet_length(proposer_id) BETWEEN 1 AND 255 AND
|
||||
octet_length(confirmer_id) BETWEEN 1 AND 255 AND
|
||||
(proposer_type, proposer_id) <> (confirmer_type, confirmer_id)
|
||||
),
|
||||
CONSTRAINT ql3_pp_trust_transition_receipt_transition_check
|
||||
CHECK (
|
||||
previous_generation BETWEEN 1 AND 2147483646 AND
|
||||
current_generation = previous_generation + 1 AND
|
||||
previous_trust_digest ~ '^[0-9a-f]{64}$' AND
|
||||
current_trust_digest ~ '^[0-9a-f]{64}$' AND
|
||||
previous_trust_digest <> current_trust_digest AND
|
||||
(
|
||||
(mode = 'overlap_add' AND
|
||||
retirement_matching_installations IS NULL) OR
|
||||
(mode = 'safe_retire' AND
|
||||
retirement_matching_installations = 0)
|
||||
) AND
|
||||
executed_at_ms >= 0 AND
|
||||
receipt_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_pp_trust_transition_receipt_json_check
|
||||
CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
octet_length(receipt_json::text) BETWEEN 2 AND 262144 AND
|
||||
receipt_json @> jsonb_build_object(
|
||||
'schema',
|
||||
'qinglong/plugin-package-publisher-trust-transition-receipt@v1',
|
||||
'mutationId', mutation_id,
|
||||
'proposalDigest', proposal_digest,
|
||||
'trustAuthorityId', authority_id,
|
||||
'previousGeneration', previous_generation,
|
||||
'currentGeneration', current_generation,
|
||||
'mode', mode,
|
||||
'publisher', publisher,
|
||||
'keyId', key_id,
|
||||
'previousTrustDigest', previous_trust_digest,
|
||||
'currentTrustDigest', current_trust_digest,
|
||||
'retirementMatchingInstallations',
|
||||
retirement_matching_installations,
|
||||
'executedAtMs', executed_at_ms,
|
||||
'receiptDigest', receipt_digest
|
||||
) AND
|
||||
receipt_json -> 'proposer' @> jsonb_build_object(
|
||||
'type', proposer_type, 'id', proposer_id
|
||||
) AND
|
||||
receipt_json -> 'confirmer' @> jsonb_build_object(
|
||||
'type', confirmer_type, 'id', confirmer_id
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE INDEX ql3_pp_trust_transition_receipt_signer_idx ON "ql3"."plugin_package_publisher_trust_transition_receipts" (publisher, key_id, current_generation)`,
|
||||
`
|
||||
REVOKE ALL ON
|
||||
"ql3"."plugin_package_publisher_trust_transition_proposals",
|
||||
"ql3"."plugin_package_publisher_trust_transition_receipts"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT ON
|
||||
"ql3"."plugin_package_publisher_trust_transition_proposals"
|
||||
TO ql3_package_manager
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT ON
|
||||
"ql3"."plugin_package_publisher_trust_transition_receipts"
|
||||
TO ql3_package_manager
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT ON
|
||||
"ql3"."plugin_package_publisher_trust_transition_proposals"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT ON
|
||||
"ql3"."plugin_package_publisher_trust_transition_receipts"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 39,
|
||||
migration_id =
|
||||
'pg-0040-plugin-package-publisher-trust-transitions',
|
||||
capabilities = '${CAPABILITIES_V39}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 38
|
||||
AND migration_id =
|
||||
'pg-0039-plugin-package-publisher-trust-authority'
|
||||
AND capabilities = '${CAPABILITIES_V38}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 38'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+108
@@ -0,0 +1,108 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V40 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V41 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0042PluginPackageLifecyclePlansMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0042-plugin-package-lifecycle-plans',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_lifecycle_plans" (
|
||||
action_ref varchar(255) PRIMARY KEY,
|
||||
plan_digest char(64) NOT NULL,
|
||||
action varchar(16) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
installation_id varchar(128) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
impact_digest char(64) NOT NULL,
|
||||
requested_by_type varchar(16) NOT NULL,
|
||||
requested_by_id varchar(255) NOT NULL,
|
||||
planned_at_ms bigint NOT NULL,
|
||||
expires_at_ms bigint NOT NULL,
|
||||
plan_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_plan_install_fk
|
||||
FOREIGN KEY (installation_id)
|
||||
REFERENCES "ql3"."plugin_package_installs" (installation_id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_plan_identity_check CHECK (
|
||||
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$'
|
||||
AND action IN ('disable', 'enable', 'uninstall')
|
||||
AND package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'
|
||||
AND requested_by_type = 'user'
|
||||
AND char_length(requested_by_id) BETWEEN 1 AND 255
|
||||
AND requested_by_id !~ '[[:cntrl:]]'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_plan_digest_check CHECK (
|
||||
plan_digest ~ '^[0-9a-f]{64}$'
|
||||
AND lock_digest ~ '^[0-9a-f]{64}$'
|
||||
AND impact_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_plan_time_check CHECK (
|
||||
planned_at_ms >= 0
|
||||
AND expires_at_ms > planned_at_ms
|
||||
AND expires_at_ms - planned_at_ms <= 900000
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_lifecycle_plan_json_check CHECK (
|
||||
jsonb_typeof(plan_json) = 'object'
|
||||
AND octet_length(plan_json::text) BETWEEN 2 AND 98304
|
||||
AND plan_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-lifecycle-plan@v1',
|
||||
'actionRef', action_ref,
|
||||
'planDigest', plan_digest,
|
||||
'requestedBy', jsonb_build_object(
|
||||
'type', requested_by_type,
|
||||
'id', requested_by_id
|
||||
),
|
||||
'plannedAtMs', planned_at_ms,
|
||||
'expiresAtMs', expires_at_ms,
|
||||
'impact', jsonb_build_object(
|
||||
'action', action,
|
||||
'impactDigest', impact_digest,
|
||||
'target', jsonb_build_object(
|
||||
'projectId', project_id,
|
||||
'packageName', package_name,
|
||||
'installationId', installation_id,
|
||||
'lockDigest', lock_digest
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_lifecycle_plan_digest_key ON "ql3"."plugin_package_lifecycle_plans" (plan_digest)`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_lifecycle_plan_impact_key ON "ql3"."plugin_package_lifecycle_plans" (project_id, package_name, installation_id, lock_digest, impact_digest)`,
|
||||
`CREATE INDEX ql3_plugin_package_lifecycle_plan_expiry_idx ON "ql3"."plugin_package_lifecycle_plans" (expires_at_ms, action_ref)`,
|
||||
`
|
||||
REVOKE ALL ON "ql3"."plugin_package_lifecycle_plans"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`GRANT SELECT ON "ql3"."plugin_package_lifecycle_plans" TO ql3_package_manager, ql3_package_executor`,
|
||||
`GRANT INSERT ON "ql3"."plugin_package_lifecycle_plans" TO ql3_package_executor`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 41,
|
||||
migration_id = 'pg-0042-plugin-package-lifecycle-plans',
|
||||
capabilities = '${CAPABILITIES_V41}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 40
|
||||
AND migration_id = 'pg-0041-plugin-package-lifecycle'
|
||||
AND capabilities = '${CAPABILITIES_V40}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 40'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V41 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V42 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0043PluginPackageAutomationPublicationsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0043-plugin-package-automation-publications',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_automation_publications" (
|
||||
publication_digest char(64) PRIMARY KEY,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
installation_id varchar(128) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
generation integer NOT NULL,
|
||||
generation_digest char(64) NOT NULL,
|
||||
materialized_revision_digest char(64) NOT NULL,
|
||||
state varchar(16) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
previous_publication_digest char(64),
|
||||
lifecycle_event_digest char(64),
|
||||
published_at_ms bigint NOT NULL,
|
||||
publication_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_revision_fk
|
||||
FOREIGN KEY (generation_digest)
|
||||
REFERENCES "ql3"."plugin_package_materialized_revisions" (
|
||||
generation_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_previous_fk
|
||||
FOREIGN KEY (previous_publication_digest)
|
||||
REFERENCES "ql3"."plugin_package_automation_publications" (
|
||||
publication_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_lifecycle_fk
|
||||
FOREIGN KEY (lifecycle_event_digest)
|
||||
REFERENCES "ql3"."plugin_package_lifecycle_events" (event_digest)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_identity_check CHECK (
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
|
||||
generation BETWEEN 1 AND 2147483647 AND
|
||||
state IN ('active', 'withdrawn', 'absent') AND
|
||||
version BETWEEN 1 AND 2147483647 AND
|
||||
published_at_ms >= 0 AND
|
||||
(
|
||||
version = 1 AND state IN ('active', 'absent') AND
|
||||
previous_publication_digest IS NULL AND
|
||||
lifecycle_event_digest IS NULL
|
||||
OR
|
||||
version > 1 AND previous_publication_digest IS NOT NULL
|
||||
) AND
|
||||
(state <> 'withdrawn' OR lifecycle_event_digest IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_digest_check CHECK (
|
||||
publication_digest ~ '^[0-9a-f]{64}$' AND
|
||||
lock_digest ~ '^[0-9a-f]{64}$' AND
|
||||
generation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
materialized_revision_digest ~ '^[0-9a-f]{64}$' AND
|
||||
(
|
||||
previous_publication_digest IS NULL OR
|
||||
previous_publication_digest ~ '^[0-9a-f]{64}$'
|
||||
) AND
|
||||
(
|
||||
lifecycle_event_digest IS NULL OR
|
||||
lifecycle_event_digest ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_json_check CHECK (
|
||||
jsonb_typeof(publication_json) = 'object' AND
|
||||
octet_length(publication_json::text) BETWEEN 2 AND 12582912 AND
|
||||
publication_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-automation-publication@v1',
|
||||
'state', state,
|
||||
'version', version,
|
||||
'previousPublicationDigest', previous_publication_digest,
|
||||
'lifecycleEventDigest', lifecycle_event_digest,
|
||||
'publishedAtMs', published_at_ms,
|
||||
'publicationDigest', publication_digest,
|
||||
'target', jsonb_build_object(
|
||||
'projectId', project_id,
|
||||
'packageName', package_name,
|
||||
'installationId', installation_id,
|
||||
'lockDigest', lock_digest,
|
||||
'generation', generation,
|
||||
'generationDigest', generation_digest,
|
||||
'materializedRevisionDigest', materialized_revision_digest
|
||||
)
|
||||
) AND
|
||||
jsonb_typeof(publication_json -> 'definitions' -> 'workflows') =
|
||||
'array' AND
|
||||
jsonb_typeof(publication_json -> 'definitions' -> 'prompts') =
|
||||
'array' AND
|
||||
(
|
||||
state = 'absent' AND
|
||||
jsonb_array_length(publication_json -> 'definitions' -> 'workflows') +
|
||||
jsonb_array_length(publication_json -> 'definitions' -> 'prompts') = 0
|
||||
OR
|
||||
state <> 'absent' AND
|
||||
jsonb_array_length(publication_json -> 'definitions' -> 'workflows') +
|
||||
jsonb_array_length(publication_json -> 'definitions' -> 'prompts') > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_version_key ON "ql3"."plugin_package_automation_publications" (project_id, package_name, version)`,
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_previous_key ON "ql3"."plugin_package_automation_publications" (previous_publication_digest) WHERE previous_publication_digest IS NOT NULL`,
|
||||
`CREATE INDEX ql3_plugin_package_automation_publication_generation_idx ON "ql3"."plugin_package_automation_publications" (generation_digest, publication_digest)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_automation_publication_heads" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
publication_digest char(64) NOT NULL,
|
||||
generation_digest char(64) NOT NULL,
|
||||
state varchar(16) NOT NULL,
|
||||
version integer NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
PRIMARY KEY (project_id, package_name),
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_head_publication_fk
|
||||
FOREIGN KEY (publication_digest)
|
||||
REFERENCES "ql3"."plugin_package_automation_publications" (
|
||||
publication_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_automation_publication_head_state_check CHECK (
|
||||
publication_digest ~ '^[0-9a-f]{64}$' AND
|
||||
generation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
state IN ('active', 'withdrawn', 'absent') AND
|
||||
version BETWEEN 1 AND 2147483647 AND
|
||||
updated_at_ms >= 0
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_automation_publication_head_digest_key ON "ql3"."plugin_package_automation_publication_heads" (publication_digest)`,
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."plugin_package_automation_publications",
|
||||
"ql3"."plugin_package_automation_publication_heads"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT
|
||||
ON "ql3"."plugin_package_automation_publications",
|
||||
"ql3"."plugin_package_automation_publication_heads"
|
||||
TO ql3_runtime, ql3_package_manager, ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT INSERT
|
||||
ON "ql3"."plugin_package_automation_publications"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
GRANT INSERT, UPDATE
|
||||
ON "ql3"."plugin_package_automation_publication_heads"
|
||||
TO ql3_package_executor
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 42,
|
||||
migration_id = 'pg-0043-plugin-package-automation-publications',
|
||||
capabilities = '${CAPABILITIES_V42}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 41
|
||||
AND migration_id = 'pg-0042-plugin-package-lifecycle-plans'
|
||||
AND capabilities = '${CAPABILITIES_V41}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 41'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V42 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V43 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0044PluginPackageAutomationStartGuardMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0044-plugin-package-automation-start-guard',
|
||||
statements: [
|
||||
`
|
||||
CREATE FUNCTION "ql3"."plugin_package_automation_start_allowed"(
|
||||
p_project_id varchar,
|
||||
p_package_name varchar,
|
||||
p_publication_digest char(64)
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
DECLARE
|
||||
project_exists boolean;
|
||||
current_publication "ql3"."plugin_package_automation_publications"%ROWTYPE;
|
||||
current_lifecycle varchar(16);
|
||||
current_installation_id varchar(128);
|
||||
current_publisher varchar(253);
|
||||
current_key_id varchar(128);
|
||||
BEGIN
|
||||
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
|
||||
RAISE EXCEPTION 'Runtime authority is required'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
|
||||
SELECT true INTO project_exists
|
||||
FROM "ql3"."projects"
|
||||
WHERE id = p_project_id
|
||||
FOR SHARE;
|
||||
IF NOT COALESCE(project_exists, false) THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
SELECT publication.* INTO current_publication
|
||||
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 = p_project_id
|
||||
AND head.package_name = p_package_name
|
||||
AND head.publication_digest = p_publication_digest
|
||||
FOR SHARE OF head, publication;
|
||||
IF NOT FOUND OR current_publication.state <> 'active' THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
SELECT install.installation_id INTO current_installation_id
|
||||
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 = current_publication.project_id
|
||||
AND head.package_name = current_publication.package_name
|
||||
AND head.installation_id = current_publication.installation_id
|
||||
AND install.lock_digest = current_publication.lock_digest
|
||||
AND install.state = 'active'
|
||||
AND install.active_lock_digest = current_publication.lock_digest
|
||||
FOR SHARE OF head, install;
|
||||
IF NOT FOUND THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
SELECT disposition INTO current_lifecycle
|
||||
FROM "ql3"."plugin_package_lifecycle_heads"
|
||||
WHERE project_id = current_publication.project_id
|
||||
AND package_name = current_publication.package_name
|
||||
FOR SHARE;
|
||||
IF FOUND AND current_lifecycle <> 'active' THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
|
||||
WHERE quarantine.project_id = current_publication.project_id
|
||||
AND quarantine.package_name = current_publication.package_name
|
||||
AND quarantine.installation_id =
|
||||
current_publication.installation_id
|
||||
AND quarantine.lock_digest = current_publication.lock_digest
|
||||
) THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
SELECT provenance.publisher, provenance.key_id
|
||||
INTO current_publisher, current_key_id
|
||||
FROM "ql3"."plugin_package_publisher_provenance" AS provenance
|
||||
WHERE provenance.installation_id = current_publication.installation_id
|
||||
AND provenance.lock_digest = current_publication.lock_digest;
|
||||
IF NOT FOUND THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
PERFORM pg_advisory_xact_lock(
|
||||
hashtextextended(
|
||||
octet_length(current_publisher)::text || ':' ||
|
||||
current_publisher ||
|
||||
octet_length(current_key_id)::text || ':' || current_key_id,
|
||||
774635230
|
||||
)
|
||||
);
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_publisher_revocation_receipts" AS revoked
|
||||
WHERE revoked.publisher = current_publisher
|
||||
AND revoked.key_id = current_key_id
|
||||
) THEN
|
||||
RETURN false;
|
||||
END IF;
|
||||
|
||||
RETURN true;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL ON FUNCTION
|
||||
"ql3"."plugin_package_automation_start_allowed"(
|
||||
varchar, varchar, char(64)
|
||||
)
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT EXECUTE ON FUNCTION
|
||||
"ql3"."plugin_package_automation_start_allowed"(
|
||||
varchar, varchar, char(64)
|
||||
)
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 43,
|
||||
migration_id = 'pg-0044-plugin-package-automation-start-guard',
|
||||
capabilities = '${CAPABILITIES_V43}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 42
|
||||
AND migration_id = 'pg-0043-plugin-package-automation-publications'
|
||||
AND capabilities = '${CAPABILITIES_V42}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 42'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V43 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V44 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0045PluginPackageWorkflowAdmissionsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0045-plugin-package-workflow-admissions',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_workflow_admissions" (
|
||||
plan_digest char(64) PRIMARY KEY,
|
||||
plan_id varchar(128) NOT NULL,
|
||||
run_id varchar(36) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
package_name varchar(63) NOT NULL,
|
||||
installation_id varchar(128) NOT NULL,
|
||||
lock_digest char(64) NOT NULL,
|
||||
generation integer NOT NULL,
|
||||
generation_digest char(64) NOT NULL,
|
||||
materialized_revision_digest char(64) NOT NULL,
|
||||
publication_digest char(64) NOT NULL,
|
||||
workflow_id varchar(63) NOT NULL,
|
||||
workflow_definition_digest char(64) NOT NULL,
|
||||
step_count integer NOT NULL,
|
||||
admitted_at_ms bigint NOT NULL,
|
||||
final_run_version integer NOT NULL,
|
||||
final_run_event_sequence integer NOT NULL,
|
||||
receipt_digest char(64) NOT NULL,
|
||||
plan_json jsonb NOT NULL,
|
||||
receipt_json jsonb NOT NULL,
|
||||
CONSTRAINT plugin_package_workflow_admissions_plan_id_key
|
||||
UNIQUE (plan_id),
|
||||
CONSTRAINT plugin_package_workflow_admissions_run_id_key
|
||||
UNIQUE (run_id),
|
||||
CONSTRAINT plugin_package_workflow_admissions_receipt_digest_key
|
||||
UNIQUE (receipt_digest),
|
||||
CONSTRAINT ql3_plugin_package_workflow_admission_run_fk
|
||||
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_workflow_admission_publication_fk
|
||||
FOREIGN KEY (publication_digest)
|
||||
REFERENCES "ql3"."plugin_package_automation_publications" (
|
||||
publication_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_workflow_admission_identity_check CHECK (
|
||||
plan_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
|
||||
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
|
||||
workflow_id ~ '^[a-z][a-z0-9-]{0,62}$' AND
|
||||
generation BETWEEN 1 AND 2147483647 AND
|
||||
step_count BETWEEN 1 AND 128 AND
|
||||
admitted_at_ms >= 0 AND
|
||||
final_run_version = step_count + 1 AND
|
||||
final_run_event_sequence = step_count + 1
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_workflow_admission_digest_check CHECK (
|
||||
plan_digest ~ '^[0-9a-f]{64}$' AND
|
||||
lock_digest ~ '^[0-9a-f]{64}$' AND
|
||||
generation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
materialized_revision_digest ~ '^[0-9a-f]{64}$' AND
|
||||
publication_digest ~ '^[0-9a-f]{64}$' AND
|
||||
workflow_definition_digest ~ '^[0-9a-f]{64}$' AND
|
||||
receipt_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_plugin_package_workflow_admission_json_check CHECK (
|
||||
jsonb_typeof(plan_json) = 'object' AND
|
||||
octet_length(plan_json::text) BETWEEN 2 AND 262144 AND
|
||||
plan_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-workflow-execution-plan@v1',
|
||||
'planId', plan_id, 'planDigest', plan_digest, 'runId', run_id,
|
||||
'plannedAtMs', admitted_at_ms
|
||||
) AND
|
||||
plan_json -> 'target' @> jsonb_build_object(
|
||||
'projectId', project_id, 'packageName', package_name,
|
||||
'installationId', installation_id, 'lockDigest', lock_digest,
|
||||
'generation', generation, 'generationDigest', generation_digest,
|
||||
'materializedRevisionDigest', materialized_revision_digest,
|
||||
'publicationDigest', publication_digest, 'workflowId', workflow_id,
|
||||
'workflowDefinitionDigest', workflow_definition_digest
|
||||
) AND
|
||||
jsonb_typeof(plan_json -> 'steps') = 'array' AND
|
||||
jsonb_array_length(plan_json -> 'steps') = step_count AND
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
octet_length(receipt_json::text) BETWEEN 2 AND 262144 AND
|
||||
receipt_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/plugin-package-workflow-admission-receipt@v1',
|
||||
'planId', plan_id, 'planDigest', plan_digest, 'runId', run_id,
|
||||
'publicationDigest', publication_digest, 'workflowId', workflow_id,
|
||||
'finalRunVersion', final_run_version,
|
||||
'finalRunEventSequence', final_run_event_sequence,
|
||||
'admittedAtMs', admitted_at_ms, 'receiptDigest', receipt_digest
|
||||
) AND
|
||||
jsonb_typeof(receipt_json -> 'steps') = 'array' AND
|
||||
jsonb_array_length(receipt_json -> 'steps') = step_count
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_plugin_package_workflow_admission_plan_run_key ON "ql3"."plugin_package_workflow_admissions" (plan_digest, run_id)`,
|
||||
`CREATE INDEX ql3_plugin_package_workflow_admission_target_idx ON "ql3"."plugin_package_workflow_admissions" (project_id, package_name, admitted_at_ms, plan_digest)`,
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_workflow_admission_steps" (
|
||||
plan_digest char(64) NOT NULL,
|
||||
run_id varchar(36) NOT NULL,
|
||||
step_key varchar(63) NOT NULL,
|
||||
step_run_id varchar(128) NOT NULL,
|
||||
task_id varchar(63) NOT NULL,
|
||||
task_definition_ref varchar(512) NOT NULL,
|
||||
task_definition_digest char(64) NOT NULL,
|
||||
needs_json jsonb NOT NULL,
|
||||
initial_status varchar(16) NOT NULL,
|
||||
mutation_id varchar(128) NOT NULL,
|
||||
event_id varchar(36) NOT NULL,
|
||||
PRIMARY KEY (plan_digest, step_key),
|
||||
CONSTRAINT plugin_package_workflow_admission_steps_step_run_id_key
|
||||
UNIQUE (step_run_id),
|
||||
CONSTRAINT plugin_package_workflow_admission_steps_mutation_id_key
|
||||
UNIQUE (mutation_id),
|
||||
CONSTRAINT plugin_package_workflow_admission_steps_event_id_key
|
||||
UNIQUE (event_id),
|
||||
CONSTRAINT ql3_plugin_package_workflow_admission_step_admission_fk
|
||||
FOREIGN KEY (plan_digest, run_id)
|
||||
REFERENCES "ql3"."plugin_package_workflow_admissions" (
|
||||
plan_digest, run_id
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_workflow_admission_step_run_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_workflow_admission_step_mutation_fk
|
||||
FOREIGN KEY (mutation_id)
|
||||
REFERENCES "ql3"."step_run_mutations" (mutation_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_workflow_admission_step_event_fk
|
||||
FOREIGN KEY (event_id)
|
||||
REFERENCES "ql3"."run_events" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_workflow_admission_step_identity_check CHECK (
|
||||
step_key ~ '^[a-z][a-z0-9-]{0,62}$' AND
|
||||
task_id ~ '^[a-z][a-z0-9-]{0,62}$' AND
|
||||
initial_status IN ('pending', 'ready') AND
|
||||
task_definition_digest ~ '^[0-9a-f]{64}$' AND
|
||||
jsonb_typeof(needs_json) = 'array' AND
|
||||
jsonb_array_length(needs_json) BETWEEN 0 AND 127
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE INDEX ql3_plugin_package_workflow_admission_step_task_idx ON "ql3"."plugin_package_workflow_admission_steps" (task_id, task_definition_digest, plan_digest)`,
|
||||
`
|
||||
CREATE FUNCTION "ql3"."plugin_package_workflow_admission_snapshot"(
|
||||
p_project_id varchar,
|
||||
p_package_name varchar,
|
||||
p_publication_digest char(64)
|
||||
)
|
||||
RETURNS TABLE(publication_json jsonb, revision_json jsonb)
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
BEGIN
|
||||
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
|
||||
RAISE EXCEPTION 'Runtime authority is required'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
IF NOT "ql3"."plugin_package_automation_start_allowed"(
|
||||
p_project_id, p_package_name, p_publication_digest
|
||||
) THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
RETURN QUERY
|
||||
SELECT publication.publication_json, revision.revision_json
|
||||
FROM "ql3"."plugin_package_automation_publications" AS publication
|
||||
JOIN "ql3"."plugin_package_materialized_revisions" AS revision
|
||||
ON revision.generation_digest = publication.generation_digest
|
||||
AND revision.revision_digest =
|
||||
publication.materialized_revision_digest
|
||||
WHERE publication.project_id = p_project_id
|
||||
AND publication.package_name = p_package_name
|
||||
AND publication.publication_digest = p_publication_digest
|
||||
AND publication.state = 'active'
|
||||
FOR SHARE OF publication, revision;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."plugin_package_workflow_admissions",
|
||||
"ql3"."plugin_package_workflow_admission_steps"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."plugin_package_workflow_admissions",
|
||||
"ql3"."plugin_package_workflow_admission_steps"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL ON FUNCTION
|
||||
"ql3"."plugin_package_workflow_admission_snapshot"(
|
||||
varchar, varchar, char(64)
|
||||
)
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT EXECUTE ON FUNCTION
|
||||
"ql3"."plugin_package_workflow_admission_snapshot"(
|
||||
varchar, varchar, char(64)
|
||||
)
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 44,
|
||||
migration_id = 'pg-0045-plugin-package-workflow-admissions',
|
||||
capabilities = '${CAPABILITIES_V44}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 43
|
||||
AND migration_id = 'pg-0044-plugin-package-automation-start-guard'
|
||||
AND capabilities = '${CAPABILITIES_V43}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 43'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V44 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V45 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0046PluginPackageWorkflowTaskAttemptAdmissionsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0046-plugin-package-workflow-task-attempt-admissions',
|
||||
statements: [
|
||||
`
|
||||
CREATE UNIQUE INDEX ql3_plugin_package_task_reconciliation_receipt_uidx
|
||||
ON "ql3"."plugin_package_task_reconciliations"
|
||||
(generation_digest, receipt_digest)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE TABLE "ql3"."plugin_package_workflow_task_attempt_admissions" (
|
||||
receipt_digest char(64) PRIMARY KEY,
|
||||
attempt_id varchar(36) NOT NULL,
|
||||
plan_digest char(64) NOT NULL,
|
||||
run_id varchar(36) NOT NULL,
|
||||
step_run_id varchar(128) NOT NULL,
|
||||
step_run_version integer NOT NULL,
|
||||
step_run_digest char(64) NOT NULL,
|
||||
generation_digest char(64) NOT NULL,
|
||||
resource_task_id varchar(128) NOT NULL,
|
||||
task_reconciliation_receipt_digest char(64) NOT NULL,
|
||||
project_id varchar(128) NOT NULL,
|
||||
task_id varchar(128) NOT NULL,
|
||||
source_revision integer NOT NULL,
|
||||
task_revision varchar(96) NOT NULL,
|
||||
task_definition_digest char(64) NOT NULL,
|
||||
executor_type varchar(32) NOT NULL,
|
||||
execution_digest char(64) NOT NULL,
|
||||
attempt_number integer NOT NULL,
|
||||
event_id varchar(36) NOT NULL,
|
||||
run_version integer NOT NULL,
|
||||
run_event_sequence integer NOT NULL,
|
||||
admitted_at_ms bigint NOT NULL,
|
||||
receipt_json jsonb NOT NULL,
|
||||
CONSTRAINT plugin_package_workflow_task_attempt_admissions_attempt_id_key
|
||||
UNIQUE (attempt_id),
|
||||
CONSTRAINT plugin_package_workflow_task_attempt_admissions_event_id_key
|
||||
UNIQUE (event_id),
|
||||
CONSTRAINT plugin_package_workflow_task_attempt_admissions_epoch_key
|
||||
UNIQUE (run_id, step_run_id, step_run_version),
|
||||
CONSTRAINT plugin_package_workflow_task_attempt_admissions_number_key
|
||||
UNIQUE (run_id, attempt_number),
|
||||
CONSTRAINT ql3_plugin_package_workflow_task_attempt_admission_plan_fk
|
||||
FOREIGN KEY (plan_digest, run_id)
|
||||
REFERENCES "ql3"."plugin_package_workflow_admissions" (
|
||||
plan_digest, run_id
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_workflow_task_attempt_admission_step_fk
|
||||
FOREIGN KEY (run_id, step_run_id)
|
||||
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_workflow_task_attempt_admission_attempt_fk
|
||||
FOREIGN KEY (attempt_id)
|
||||
REFERENCES "ql3"."run_attempts" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_workflow_task_attempt_admission_event_fk
|
||||
FOREIGN KEY (event_id)
|
||||
REFERENCES "ql3"."run_events" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_workflow_task_attempt_reconciliation_fk
|
||||
FOREIGN KEY (
|
||||
generation_digest, task_reconciliation_receipt_digest
|
||||
) REFERENCES "ql3"."plugin_package_task_reconciliations" (
|
||||
generation_digest, receipt_digest
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_plugin_package_workflow_task_attempt_admission_execution_fk
|
||||
FOREIGN KEY (
|
||||
project_id, task_id, source_revision, executor_type
|
||||
) REFERENCES "ql3"."task_execution_revisions" (
|
||||
project_id, task_id, source_revision, executor_type
|
||||
) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_pp_workflow_task_attempt_identity_check
|
||||
CHECK (
|
||||
attempt_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
|
||||
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
|
||||
char_length(step_run_id) BETWEEN 1 AND 128 AND
|
||||
char_length(resource_task_id) BETWEEN 1 AND 128 AND
|
||||
char_length(project_id) BETWEEN 1 AND 128 AND
|
||||
char_length(task_id) BETWEEN 1 AND 128 AND
|
||||
event_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
|
||||
executor_type = 'remote_worker'
|
||||
),
|
||||
CONSTRAINT ql3_pp_workflow_task_attempt_counter_check
|
||||
CHECK (
|
||||
step_run_version BETWEEN 1 AND 2147483647 AND
|
||||
source_revision BETWEEN 1 AND 2147483647 AND
|
||||
attempt_number BETWEEN 1 AND 8192 AND
|
||||
run_version BETWEEN 1 AND 2147483647 AND
|
||||
run_event_sequence = run_version AND
|
||||
admitted_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT
|
||||
ql3_plugin_package_workflow_task_attempt_admission_digest_check
|
||||
CHECK (
|
||||
receipt_digest ~ '^[0-9a-f]{64}$' AND
|
||||
plan_digest ~ '^[0-9a-f]{64}$' AND
|
||||
step_run_digest ~ '^[0-9a-f]{64}$' AND
|
||||
generation_digest ~ '^[0-9a-f]{64}$' AND
|
||||
task_reconciliation_receipt_digest ~ '^[0-9a-f]{64}$' AND
|
||||
task_definition_digest ~ '^[0-9a-f]{64}$' AND
|
||||
execution_digest ~ '^[0-9a-f]{64}$' AND
|
||||
task_revision = concat(
|
||||
'qltd:v1:', source_revision::text, ':', task_definition_digest
|
||||
)
|
||||
),
|
||||
CONSTRAINT
|
||||
ql3_plugin_package_workflow_task_attempt_admission_json_check
|
||||
CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
octet_length(receipt_json::text) BETWEEN 2 AND 16384 AND
|
||||
receipt_json @> jsonb_build_object(
|
||||
'schema',
|
||||
'qinglong/plugin-package-workflow-task-attempt-admission@v1',
|
||||
'receiptDigest', receipt_digest,
|
||||
'attemptId', attempt_id,
|
||||
'planDigest', plan_digest,
|
||||
'runId', run_id,
|
||||
'stepRunId', step_run_id,
|
||||
'stepRunVersion', step_run_version,
|
||||
'stepRunDigest', step_run_digest,
|
||||
'resourceTaskId', resource_task_id,
|
||||
'taskReconciliationReceiptDigest',
|
||||
task_reconciliation_receipt_digest,
|
||||
'taskId', task_id,
|
||||
'taskRevision', task_revision,
|
||||
'taskDefinitionDigest', task_definition_digest,
|
||||
'executorType', executor_type,
|
||||
'executionDigest', execution_digest,
|
||||
'attemptNumber', attempt_number,
|
||||
'eventId', event_id,
|
||||
'runVersion', run_version,
|
||||
'runEventSequence', run_event_sequence,
|
||||
'admittedAtMs', admitted_at_ms
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE INDEX ql3_pp_workflow_task_attempt_candidate_idx
|
||||
ON "ql3"."plugin_package_workflow_task_attempt_admissions"
|
||||
(run_id, step_run_id, admitted_at_ms)
|
||||
`.trim(),
|
||||
`
|
||||
CREATE FUNCTION "ql3"."plugin_package_workflow_task_attempt_snapshot"(
|
||||
p_run_id varchar,
|
||||
p_step_run_id varchar
|
||||
)
|
||||
RETURNS TABLE(
|
||||
plan_json jsonb,
|
||||
reconciliation_json jsonb,
|
||||
execution_project_id varchar,
|
||||
execution_task_id varchar,
|
||||
execution_source_revision integer,
|
||||
execution_task_revision varchar,
|
||||
execution_source_content_digest char(64),
|
||||
execution_executor_type varchar,
|
||||
execution_plan_schema varchar,
|
||||
execution_plan_json jsonb,
|
||||
execution_content_digest char(64),
|
||||
execution_created_at_ms bigint
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
BEGIN
|
||||
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
|
||||
RAISE EXCEPTION 'Runtime authority is required'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
workflow.plan_json,
|
||||
reconciliation.receipt_json,
|
||||
execution.project_id,
|
||||
execution.task_id,
|
||||
execution.source_revision,
|
||||
execution.task_revision,
|
||||
execution.source_content_digest,
|
||||
execution.executor_type,
|
||||
execution.plan_schema,
|
||||
execution.plan_json,
|
||||
execution.content_digest,
|
||||
execution.created_at_ms
|
||||
FROM "ql3"."plugin_package_workflow_admissions" AS workflow
|
||||
JOIN "ql3"."plugin_package_workflow_admission_steps" AS source
|
||||
ON source.plan_digest = workflow.plan_digest
|
||||
AND source.run_id = workflow.run_id
|
||||
AND source.step_run_id = p_step_run_id
|
||||
JOIN "ql3"."plugin_package_task_reconciliations" AS reconciliation
|
||||
ON reconciliation.generation_digest = workflow.generation_digest
|
||||
JOIN "ql3"."plugin_package_task_reconciliation_items" AS item
|
||||
ON item.generation_digest = reconciliation.generation_digest
|
||||
AND item.task_id =
|
||||
concat('pkg:', workflow.package_name, ':', source.task_id)
|
||||
AND item.disposition IN ('created', 'retained', 'updated')
|
||||
JOIN "ql3"."task_execution_revisions" AS execution
|
||||
ON execution.project_id = workflow.project_id
|
||||
AND execution.task_id = item.task_id
|
||||
AND execution.source_revision = item.revision
|
||||
AND execution.executor_type = 'remote_worker'
|
||||
AND execution.source_content_digest = item.content_digest
|
||||
WHERE workflow.run_id = p_run_id
|
||||
FOR KEY SHARE OF workflow, source, reconciliation, item, execution;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL
|
||||
ON "ql3"."plugin_package_workflow_task_attempt_admissions"
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT SELECT, INSERT
|
||||
ON "ql3"."plugin_package_workflow_task_attempt_admissions"
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
REVOKE ALL ON FUNCTION
|
||||
"ql3"."plugin_package_workflow_task_attempt_snapshot"(varchar, varchar)
|
||||
FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager,
|
||||
ql3_package_executor, ql3_worker_ingress
|
||||
`.trim(),
|
||||
`
|
||||
GRANT EXECUTE ON FUNCTION
|
||||
"ql3"."plugin_package_workflow_task_attempt_snapshot"(varchar, varchar)
|
||||
TO ql3_runtime
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 45,
|
||||
migration_id =
|
||||
'pg-0046-plugin-package-workflow-task-attempt-admissions',
|
||||
capabilities = '${CAPABILITIES_V45}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 44
|
||||
AND migration_id = 'pg-0045-plugin-package-workflow-admissions'
|
||||
AND capabilities = '${CAPABILITIES_V44}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 44'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V45 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V46 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_management_plan":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0047WorkerCredentialManagementPlansMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0047-worker-credential-management-plans',
|
||||
statements: [
|
||||
`
|
||||
DO $ql3$
|
||||
DECLARE
|
||||
invalid_roles text[];
|
||||
BEGIN
|
||||
SELECT array_agg(expected.role_name ORDER BY expected.role_name)
|
||||
INTO invalid_roles
|
||||
FROM (
|
||||
VALUES
|
||||
('ql3_worker_credential_manager'),
|
||||
('ql3_worker_credential_executor')
|
||||
) AS expected(role_name)
|
||||
LEFT JOIN pg_catalog.pg_roles roles
|
||||
ON roles.rolname = expected.role_name
|
||||
WHERE roles.rolname IS NULL
|
||||
OR roles.rolcanlogin IS NOT TRUE
|
||||
OR roles.rolsuper IS NOT FALSE
|
||||
OR roles.rolcreatedb IS NOT FALSE
|
||||
OR roles.rolcreaterole IS NOT FALSE
|
||||
OR roles.rolreplication IS NOT FALSE
|
||||
OR roles.rolbypassrls IS NOT FALSE;
|
||||
|
||||
IF invalid_roles IS NOT NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'required QingLong Worker credential roles are missing or privileged: %',
|
||||
invalid_roles
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
EXECUTE format(
|
||||
'GRANT CONNECT ON DATABASE %I TO ql3_worker_credential_manager, ql3_worker_credential_executor',
|
||||
current_database()
|
||||
);
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`GRANT USAGE ON SCHEMA "ql3" TO ql3_worker_credential_manager, ql3_worker_credential_executor`,
|
||||
`GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities" TO ql3_worker_credential_manager, ql3_worker_credential_executor`,
|
||||
`
|
||||
CREATE TABLE "ql3"."worker_credential_management_plans" (
|
||||
action_ref varchar(255) PRIMARY KEY,
|
||||
authority_project_id varchar(128) NOT NULL,
|
||||
action varchar(16) NOT NULL,
|
||||
delivery_id varchar(36) NOT NULL,
|
||||
worker_id varchar(128) NOT NULL,
|
||||
credential_id varchar(64) NOT NULL,
|
||||
previous_credential_id varchar(64),
|
||||
credential_not_before_at_ms bigint NOT NULL,
|
||||
credential_expires_at_ms bigint NOT NULL,
|
||||
deployment_target_digest char(64) NOT NULL,
|
||||
deployment_generation varchar(128) NOT NULL,
|
||||
requested_by_type varchar(16) NOT NULL,
|
||||
requested_by_id varchar(255) NOT NULL,
|
||||
planned_at_ms bigint NOT NULL,
|
||||
expires_at_ms bigint NOT NULL,
|
||||
plan_digest char(64) NOT NULL,
|
||||
preview_digest char(64) NOT NULL,
|
||||
plan_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_worker_credential_management_plan_project_fk
|
||||
FOREIGN KEY (authority_project_id)
|
||||
REFERENCES "ql3"."projects" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_worker_credential_management_plan_identity_check CHECK (
|
||||
action_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$' AND
|
||||
action IN ('issue', 'rotate') AND
|
||||
delivery_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' AND
|
||||
worker_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$' AND
|
||||
(previous_credential_id IS NULL OR
|
||||
previous_credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$') AND
|
||||
deployment_generation ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
|
||||
requested_by_type = 'user' AND
|
||||
char_length(requested_by_id) BETWEEN 1 AND 255 AND
|
||||
requested_by_id !~ '[[:cntrl:]]'
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_management_plan_action_check CHECK (
|
||||
(action = 'issue' AND previous_credential_id IS NULL) OR
|
||||
(action = 'rotate' AND previous_credential_id IS NOT NULL AND
|
||||
previous_credential_id <> credential_id)
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_management_plan_digest_check CHECK (
|
||||
deployment_target_digest ~ '^[0-9a-f]{64}$' AND
|
||||
plan_digest ~ '^[0-9a-f]{64}$' AND
|
||||
preview_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_management_plan_time_check CHECK (
|
||||
planned_at_ms >= 0 AND
|
||||
expires_at_ms > planned_at_ms AND
|
||||
expires_at_ms - planned_at_ms <= 900000 AND
|
||||
credential_not_before_at_ms >= planned_at_ms AND
|
||||
credential_expires_at_ms > credential_not_before_at_ms AND
|
||||
credential_expires_at_ms - credential_not_before_at_ms <= 63072000000
|
||||
),
|
||||
CONSTRAINT ql3_worker_credential_management_plan_json_check CHECK (
|
||||
jsonb_typeof(plan_json) = 'object' AND
|
||||
octet_length(plan_json::text) BETWEEN 2 AND 16384 AND
|
||||
plan_json @> jsonb_build_object(
|
||||
'schema', 'qinglong/worker-credential-management-plan@v1',
|
||||
'actionRef', action_ref,
|
||||
'authorityProjectId', authority_project_id,
|
||||
'action', action,
|
||||
'planDigest', plan_digest,
|
||||
'previewDigest', preview_digest,
|
||||
'requestedBy', jsonb_build_object(
|
||||
'type', requested_by_type,
|
||||
'id', requested_by_id
|
||||
),
|
||||
'plannedAtMs', planned_at_ms,
|
||||
'expiresAtMs', expires_at_ms,
|
||||
'target', jsonb_build_object(
|
||||
'deliveryId', delivery_id,
|
||||
'workerId', worker_id,
|
||||
'credentialId', credential_id,
|
||||
'previousCredentialId', previous_credential_id,
|
||||
'credentialNotBeforeAtMs', credential_not_before_at_ms,
|
||||
'credentialExpiresAtMs', credential_expires_at_ms,
|
||||
'deploymentTargetDigest', deployment_target_digest,
|
||||
'deploymentGeneration', deployment_generation
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`.trim(),
|
||||
`CREATE UNIQUE INDEX ql3_worker_credential_management_plan_digest_key ON "ql3"."worker_credential_management_plans" (plan_digest)`,
|
||||
`CREATE UNIQUE INDEX ql3_worker_credential_management_plan_delivery_key ON "ql3"."worker_credential_management_plans" (delivery_id)`,
|
||||
`CREATE INDEX ql3_worker_credential_management_plan_expiry_idx ON "ql3"."worker_credential_management_plans" (expires_at_ms, action_ref)`,
|
||||
`REVOKE ALL ON "ql3"."worker_credential_management_plans" FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress, ql3_worker_credential_manager, ql3_worker_credential_executor`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."worker_credential_management_plans" TO ql3_worker_credential_manager`,
|
||||
`GRANT SELECT ON "ql3"."worker_credential_management_plans" TO ql3_worker_credential_executor`,
|
||||
`GRANT SELECT ON "ql3"."projects", "ql3"."project_role_bindings" TO ql3_worker_credential_manager, ql3_worker_credential_executor`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."security_audit_events" TO ql3_worker_credential_manager, ql3_worker_credential_executor`,
|
||||
`GRANT SELECT, INSERT, UPDATE ON "ql3"."approval_requests" TO ql3_worker_credential_manager`,
|
||||
`GRANT SELECT, UPDATE ON "ql3"."approval_requests" TO ql3_worker_credential_executor`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."approved_action_dispatches" TO ql3_worker_credential_executor`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."worker_credentials", "ql3"."worker_credential_mutations", "ql3"."worker_credential_deliveries", "ql3"."worker_credential_stage_discards" TO ql3_worker_credential_executor`,
|
||||
`GRANT EXECUTE ON FUNCTION "ql3"."lock_approval_policy_fence"(varchar, varchar, varchar, integer, integer) TO ql3_worker_credential_manager, ql3_worker_credential_executor`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 46,
|
||||
migration_id = 'pg-0047-worker-credential-management-plans',
|
||||
capabilities = '${CAPABILITIES_V46}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 45
|
||||
AND migration_id =
|
||||
'pg-0046-plugin-package-workflow-task-attempt-admissions'
|
||||
AND capabilities = '${CAPABILITIES_V45}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 45'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V46 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_management_plan":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V47 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_management_plan":1,"worker_credential_preapproved_activation":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0048WorkerCredentialPreapprovedActivationMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0048-worker-credential-preapproved-activation',
|
||||
statements: [
|
||||
`ALTER TABLE "ql3"."worker_credentials" DROP CONSTRAINT ql3_worker_credentials_lifetime_check`,
|
||||
`ALTER TABLE "ql3"."worker_credentials" ADD CONSTRAINT ql3_worker_credentials_lifetime_check CHECK (created_at_ms >= 0 AND not_before_at_ms >= 0 AND expires_at_ms > GREATEST(created_at_ms, not_before_at_ms))`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 47,
|
||||
migration_id = 'pg-0048-worker-credential-preapproved-activation',
|
||||
capabilities = '${CAPABILITIES_V47}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 46
|
||||
AND migration_id = 'pg-0047-worker-credential-management-plans'
|
||||
AND capabilities = '${CAPABILITIES_V46}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 46'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V47 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_management_plan":1,"worker_credential_preapproved_activation":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
const CAPABILITIES_V48 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_execution_receipt":1,"worker_credential_management_plan":1,"worker_credential_preapproved_activation":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
|
||||
export const pg0049WorkerCredentialExecutionReceiptsMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0049-worker-credential-execution-receipts',
|
||||
statements: [
|
||||
`REVOKE ALL ON "ql3"."approved_action_executions" FROM ql3_worker_credential_manager, ql3_worker_credential_executor`,
|
||||
`GRANT SELECT, INSERT, UPDATE ON "ql3"."approved_action_executions" TO ql3_worker_credential_executor`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 48,
|
||||
migration_id = 'pg-0049-worker-credential-execution-receipts',
|
||||
capabilities = '${CAPABILITIES_V48}'::jsonb,
|
||||
updated_at_ms = floor(
|
||||
extract(epoch FROM transaction_timestamp()) * 1000
|
||||
)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 47
|
||||
AND migration_id = 'pg-0048-worker-credential-preapproved-activation'
|
||||
AND capabilities = '${CAPABILITIES_V47}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 47'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
const CAPABILITIES_V48 =
|
||||
'{"api_credential":1,"api_credential_admin":1,"api_credential_pepper_binding":1,"approved_action":1,"approved_action_execution":1,"cluster_execution_revision":1,"cluster_recovery":1,"cluster_recovery_claim":1,"cluster_scheduler_admission":1,"database_role_grants":1,"identity_admin":1,"plugin_package_admission":1,"plugin_package_authority_split":1,"plugin_package_automation_publication":1,"plugin_package_automation_start_guard":1,"plugin_package_identity_keyset_ledger":1,"plugin_package_install":1,"plugin_package_lifecycle":1,"plugin_package_lifecycle_plan":1,"plugin_package_management_quota":1,"plugin_package_materialized_revision":1,"plugin_package_proposal":1,"plugin_package_publisher_provenance":1,"plugin_package_publisher_trust_authority":1,"plugin_package_publisher_trust_transition":1,"plugin_package_quarantine":1,"plugin_package_task_reconciliation":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_task_attempt_admission":1,"project_policy":1,"project_tool_definition_snapshot":1,"run_core":1,"run_dispatch_lease":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1,"step_run":1,"task_definition":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_evidence":1,"tool_execution_failure_completion":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"trigger_definition":1,"worker_attestation":1,"worker_credential":1,"worker_credential_delivery":1,"worker_credential_execution_receipt":1,"worker_credential_management_plan":1,"worker_credential_preapproved_activation":1,"worker_credential_stage_discard":1,"worker_session":1}';
|
||||
export const CAPABILITIES_V49 = CAPABILITIES_V48.replace(
|
||||
'"worker_credential_management_plan":1,',
|
||||
'"worker_credential_management_identity_keyset_ledger":1,"worker_credential_management_plan":1,"worker_credential_management_quota":1,',
|
||||
);
|
||||
|
||||
export const pg0050WorkerCredentialManagementBoundaryMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0050-worker-credential-management-boundary',
|
||||
statements: [
|
||||
`
|
||||
CREATE TABLE "ql3"."worker_credential_management_quota_buckets" (
|
||||
project_id varchar(128) NOT NULL,
|
||||
subject_type varchar(32) NOT NULL,
|
||||
subject_id varchar(255) NOT NULL,
|
||||
operation varchar(64) NOT NULL,
|
||||
window_started_at_ms bigint NOT NULL,
|
||||
consumed_count integer NOT NULL,
|
||||
receipt_ids jsonb NOT NULL,
|
||||
updated_at_ms bigint NOT NULL,
|
||||
CONSTRAINT worker_credential_management_quota_buckets_pkey
|
||||
PRIMARY KEY (project_id, subject_type, subject_id, operation),
|
||||
CONSTRAINT ql3_worker_credential_management_quota_project_fk
|
||||
FOREIGN KEY (project_id) REFERENCES "ql3"."projects"(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_worker_credential_management_quota_identity_check
|
||||
CHECK (subject_type = 'user' AND char_length(subject_id) BETWEEN 1 AND 255
|
||||
AND subject_id !~ '[[:cntrl:]]'),
|
||||
CONSTRAINT ql3_worker_credential_management_quota_operation_check
|
||||
CHECK (operation IN ('worker-credential.plan', 'worker-credential.propose',
|
||||
'worker-credential.decide', 'worker-credential.inspect')),
|
||||
CONSTRAINT ql3_worker_credential_management_quota_window_check
|
||||
CHECK (window_started_at_ms >= 0 AND consumed_count BETWEEN 1 AND 1000
|
||||
AND updated_at_ms >= window_started_at_ms),
|
||||
CONSTRAINT ql3_worker_credential_management_quota_receipts_check
|
||||
CHECK (jsonb_typeof(receipt_ids) = 'array'
|
||||
AND jsonb_array_length(receipt_ids) = consumed_count
|
||||
AND octet_length(receipt_ids::text) BETWEEN 3 AND 262144)
|
||||
)
|
||||
`.trim(),
|
||||
`REVOKE ALL ON "ql3"."worker_credential_management_quota_buckets" FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress, ql3_worker_credential_executor`,
|
||||
`GRANT SELECT, INSERT, UPDATE ON "ql3"."worker_credential_management_quota_buckets" TO ql3_worker_credential_manager`,
|
||||
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" DROP CONSTRAINT ql3_plugin_package_identity_keyset_authority_check`,
|
||||
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" ADD CONSTRAINT ql3_plugin_package_identity_keyset_authority_check CHECK (authority IN ('plugin-package-management', 'worker-credential-management'))`,
|
||||
`REVOKE ALL ON "ql3"."plugin_package_identity_keyset_ledger" FROM ql3_worker_credential_manager, ql3_worker_credential_executor`,
|
||||
`GRANT SELECT, INSERT, UPDATE ON "ql3"."plugin_package_identity_keyset_ledger" TO ql3_worker_credential_manager`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 49,
|
||||
migration_id = 'pg-0050-worker-credential-management-boundary',
|
||||
capabilities = '${CAPABILITIES_V49}'::jsonb,
|
||||
updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 48
|
||||
AND migration_id = 'pg-0049-worker-credential-execution-receipts'
|
||||
AND capabilities = '${CAPABILITIES_V48}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 48'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { CAPABILITIES_V49 } from './pg-0050-worker-credential-management-boundary';
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V50 = CAPABILITIES_V49.replace(
|
||||
'"approved_action":1,',
|
||||
'"approved_action":1,"automation_management_boundary":1,',
|
||||
);
|
||||
|
||||
export const pg0051AutomationManagementBoundaryMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0051-automation-management-boundary',
|
||||
statements: [
|
||||
`
|
||||
DO $ql3$
|
||||
DECLARE
|
||||
role_invalid boolean;
|
||||
BEGIN
|
||||
SELECT roles.rolname IS NULL
|
||||
OR roles.rolcanlogin IS NOT TRUE
|
||||
OR roles.rolsuper IS NOT FALSE
|
||||
OR roles.rolcreatedb IS NOT FALSE
|
||||
OR roles.rolcreaterole IS NOT FALSE
|
||||
OR roles.rolreplication IS NOT FALSE
|
||||
OR roles.rolbypassrls IS NOT FALSE
|
||||
INTO role_invalid
|
||||
FROM (SELECT 1) AS expected
|
||||
LEFT JOIN pg_catalog.pg_roles AS roles
|
||||
ON roles.rolname = 'ql3_automation_manager';
|
||||
|
||||
IF role_invalid IS NOT FALSE THEN
|
||||
RAISE EXCEPTION
|
||||
'required QingLong automation manager role is missing or privileged'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
EXECUTE format(
|
||||
'GRANT CONNECT ON DATABASE %I TO ql3_automation_manager',
|
||||
current_database()
|
||||
);
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`GRANT USAGE ON SCHEMA "ql3" TO ql3_automation_manager`,
|
||||
`REVOKE ALL ON ALL TABLES IN SCHEMA "ql3" FROM ql3_automation_manager`,
|
||||
`REVOKE ALL ON ALL FUNCTIONS IN SCHEMA "ql3" FROM ql3_automation_manager`,
|
||||
`GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities", "ql3"."projects", "ql3"."project_role_bindings", "ql3"."plugin_package_task_ownerships" TO ql3_automation_manager`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."security_audit_events" TO ql3_automation_manager`,
|
||||
`GRANT SELECT, INSERT, UPDATE ON "ql3"."task_definitions", "ql3"."triggers", "ql3"."trigger_schedules" TO ql3_automation_manager`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."task_definition_revisions", "ql3"."task_execution_revisions", "ql3"."trigger_revisions" TO ql3_automation_manager`,
|
||||
`REVOKE SELECT, INSERT, UPDATE ON "ql3"."task_definitions", "ql3"."triggers", "ql3"."trigger_schedules" FROM ql3_admin`,
|
||||
`REVOKE SELECT, INSERT ON "ql3"."task_definition_revisions", "ql3"."task_execution_revisions", "ql3"."trigger_revisions" FROM ql3_admin`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 50,
|
||||
migration_id = 'pg-0051-automation-management-boundary',
|
||||
capabilities = '${CAPABILITIES_V50}'::jsonb,
|
||||
updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 49
|
||||
AND migration_id = 'pg-0050-worker-credential-management-boundary'
|
||||
AND capabilities = '${CAPABILITIES_V49}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 49'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { CAPABILITIES_V50 } from './pg-0051-automation-management-boundary';
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V51 = CAPABILITIES_V50.replace(
|
||||
'"automation_management_boundary":1,',
|
||||
'"automation_management_boundary":1,"automation_management_identity_keyset_ledger":1,',
|
||||
);
|
||||
|
||||
export const pg0052AutomationManagementIdentityKeysetLedgerMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0052-automation-management-identity-keyset-ledger',
|
||||
statements: [
|
||||
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" DROP CONSTRAINT ql3_plugin_package_identity_keyset_authority_check`,
|
||||
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" ADD CONSTRAINT ql3_plugin_package_identity_keyset_authority_check CHECK (authority IN ('plugin-package-management', 'worker-credential-management', 'automation-management'))`,
|
||||
`REVOKE ALL ON "ql3"."plugin_package_identity_keyset_ledger" FROM ql3_automation_manager`,
|
||||
`GRANT SELECT, INSERT, UPDATE ON "ql3"."plugin_package_identity_keyset_ledger" TO ql3_automation_manager`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 51,
|
||||
migration_id = 'pg-0052-automation-management-identity-keyset-ledger',
|
||||
capabilities = '${CAPABILITIES_V51}'::jsonb,
|
||||
updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 50
|
||||
AND migration_id = 'pg-0051-automation-management-boundary'
|
||||
AND capabilities = '${CAPABILITIES_V50}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 50'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { CAPABILITIES_V51 } from './pg-0052-automation-management-identity-keyset-ledger';
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V52 = CAPABILITIES_V51.replace(
|
||||
'"plugin_package_workflow_admission":1,',
|
||||
'"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,',
|
||||
);
|
||||
|
||||
export const pg0053PluginPackageWorkflowRunListIndexMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0053-plugin-package-workflow-run-list-index',
|
||||
statements: [
|
||||
`CREATE INDEX ql3_plugin_package_workflow_admission_workflow_history_idx ON "ql3"."plugin_package_workflow_admissions" (project_id, package_name, workflow_id, admitted_at_ms, run_id)`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 52,
|
||||
migration_id = 'pg-0053-plugin-package-workflow-run-list-index',
|
||||
capabilities = '${CAPABILITIES_V52}'::jsonb,
|
||||
updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 51
|
||||
AND migration_id = 'pg-0052-automation-management-identity-keyset-ledger'
|
||||
AND capabilities = '${CAPABILITIES_V51}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 51'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { CAPABILITIES_V52 } from './pg-0053-plugin-package-workflow-run-list-index';
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V53 = CAPABILITIES_V52.replace(
|
||||
'"approved_action":1,',
|
||||
'"approval_management_boundary":1,"approved_action":1,',
|
||||
);
|
||||
|
||||
export const pg0054ApprovalManagementBoundaryMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0054-approval-management-boundary',
|
||||
statements: [
|
||||
`
|
||||
DO $ql3$
|
||||
DECLARE
|
||||
role_invalid boolean;
|
||||
BEGIN
|
||||
SELECT roles.rolname IS NULL
|
||||
OR roles.rolcanlogin IS NOT TRUE
|
||||
OR roles.rolsuper IS NOT FALSE
|
||||
OR roles.rolcreatedb IS NOT FALSE
|
||||
OR roles.rolcreaterole IS NOT FALSE
|
||||
OR roles.rolreplication IS NOT FALSE
|
||||
OR roles.rolbypassrls IS NOT FALSE
|
||||
INTO role_invalid
|
||||
FROM (SELECT 1) AS expected
|
||||
LEFT JOIN pg_catalog.pg_roles AS roles
|
||||
ON roles.rolname = 'ql3_approval_manager';
|
||||
|
||||
IF role_invalid IS NOT FALSE THEN
|
||||
RAISE EXCEPTION
|
||||
'required QingLong approval manager role is missing or privileged'
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
EXECUTE format(
|
||||
'GRANT CONNECT ON DATABASE %I TO ql3_approval_manager',
|
||||
current_database()
|
||||
);
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`GRANT USAGE ON SCHEMA "ql3" TO ql3_approval_manager`,
|
||||
`REVOKE ALL ON ALL TABLES IN SCHEMA "ql3" FROM ql3_approval_manager`,
|
||||
`REVOKE ALL ON ALL FUNCTIONS IN SCHEMA "ql3" FROM ql3_approval_manager`,
|
||||
`GRANT SELECT ON "ql3"."schema_migrations", "ql3"."schema_capabilities", "ql3"."projects", "ql3"."project_role_bindings", "ql3"."tool_invocation_preview_artifacts" TO ql3_approval_manager`,
|
||||
`GRANT SELECT, INSERT ON "ql3"."security_audit_events" TO ql3_approval_manager`,
|
||||
`GRANT SELECT, UPDATE ON "ql3"."approval_requests" TO ql3_approval_manager`,
|
||||
`GRANT SELECT, INSERT, UPDATE ON "ql3"."plugin_package_identity_keyset_ledger" TO ql3_approval_manager`,
|
||||
`GRANT EXECUTE ON FUNCTION "ql3"."lock_approval_policy_fence"(varchar, varchar, varchar, integer, integer) TO ql3_approval_manager`,
|
||||
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" DROP CONSTRAINT ql3_plugin_package_identity_keyset_authority_check`,
|
||||
`ALTER TABLE "ql3"."plugin_package_identity_keyset_ledger" ADD CONSTRAINT ql3_plugin_package_identity_keyset_authority_check CHECK (authority IN ('plugin-package-management', 'worker-credential-management', 'automation-management', 'approval-management'))`,
|
||||
`
|
||||
DO $ql3$
|
||||
BEGIN
|
||||
UPDATE "ql3"."schema_capabilities"
|
||||
SET contract_version = 53,
|
||||
migration_id = 'pg-0054-approval-management-boundary',
|
||||
capabilities = '${CAPABILITIES_V53}'::jsonb,
|
||||
updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint
|
||||
WHERE contract_name = 'control-core'
|
||||
AND contract_version = 52
|
||||
AND migration_id = 'pg-0053-plugin-package-workflow-run-list-index'
|
||||
AND capabilities = '${CAPABILITIES_V52}'::jsonb;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'control-core capability is not at version 52'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
import {
|
||||
MigrationStreamHistoryCorruptionError,
|
||||
type MigrationStreamRecord,
|
||||
type MigrationStreamStore,
|
||||
type MigrationStreamTransaction,
|
||||
} from '@qinglong/runtime-core';
|
||||
import type {
|
||||
PostgresClient as PostgresMigrationClient,
|
||||
PostgresPool as PostgresMigrationPool,
|
||||
PostgresQueryable as PostgresMigrationQueryable,
|
||||
PostgresQueryResult as PostgresMigrationQueryResult,
|
||||
} from '@qinglong/runtime-core';
|
||||
|
||||
export type {
|
||||
PostgresClient as PostgresMigrationClient,
|
||||
PostgresPool as PostgresMigrationPool,
|
||||
PostgresQueryable as PostgresMigrationQueryable,
|
||||
PostgresQueryResult as PostgresMigrationQueryResult,
|
||||
} from '@qinglong/runtime-core';
|
||||
|
||||
export const POSTGRESQL_MAIN_MIGRATION_STREAM_ID = 'postgresql-main';
|
||||
export const POSTGRESQL_MIGRATION_SCHEMA = 'ql3';
|
||||
export const POSTGRESQL_MIGRATION_HISTORY_TABLE = 'schema_migrations';
|
||||
|
||||
const POSTGRESQL_MIGRATION_LOCK_KEY = [0x514c, 0x0300] as const;
|
||||
const POSTGRESQL_MIGRATION_STATEMENT_TIMEOUT_MS = 15_000;
|
||||
const POSTGRESQL_MIGRATION_LOCK_TIMEOUT_MS = 5_000;
|
||||
const POSTGRESQL_MIGRATION_IDLE_TRANSACTION_TIMEOUT_MS = 15_000;
|
||||
|
||||
export type PostgresMigrationContext = PostgresMigrationQueryable;
|
||||
|
||||
export class PostgresMigrationLeaderUnavailableError extends Error {
|
||||
constructor() {
|
||||
super('PostgreSQL migration leader lock is unavailable');
|
||||
this.name = 'PostgresMigrationLeaderUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
interface HistoryRow extends Record<string, unknown> {
|
||||
streamId: unknown;
|
||||
dialect: unknown;
|
||||
migrationId: unknown;
|
||||
checksum: unknown;
|
||||
appliedAtMs: unknown;
|
||||
}
|
||||
|
||||
interface AdvisoryLockRow extends Record<string, unknown> {
|
||||
acquired: unknown;
|
||||
}
|
||||
|
||||
const CREATE_HISTORY_SQL = `
|
||||
CREATE TABLE IF NOT EXISTS "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}" (
|
||||
migration_id varchar(128) PRIMARY KEY,
|
||||
stream_id varchar(64) NOT NULL,
|
||||
dialect varchar(16) NOT NULL
|
||||
CONSTRAINT ql3_schema_migrations_dialect_check
|
||||
CHECK (dialect = 'postgresql'),
|
||||
checksum char(64) NOT NULL
|
||||
CONSTRAINT ql3_schema_migrations_checksum_check
|
||||
CHECK (checksum ~ '^[0-9a-f]{64}$'),
|
||||
applied_at_ms bigint NOT NULL
|
||||
CONSTRAINT ql3_schema_migrations_applied_at_check
|
||||
CHECK (applied_at_ms >= 0)
|
||||
)`.trim();
|
||||
|
||||
const SELECT_HISTORY_SQL = `
|
||||
SELECT
|
||||
stream_id AS "streamId",
|
||||
dialect,
|
||||
migration_id AS "migrationId",
|
||||
checksum,
|
||||
applied_at_ms AS "appliedAtMs"
|
||||
FROM "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}"
|
||||
WHERE migration_id = $1
|
||||
`.trim();
|
||||
|
||||
const SELECT_ALL_HISTORY_SQL = `
|
||||
SELECT
|
||||
stream_id AS "streamId",
|
||||
dialect,
|
||||
migration_id AS "migrationId",
|
||||
checksum,
|
||||
applied_at_ms AS "appliedAtMs"
|
||||
FROM "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}"
|
||||
ORDER BY applied_at_ms, migration_id
|
||||
`.trim();
|
||||
|
||||
const INSERT_HISTORY_SQL = `
|
||||
INSERT INTO "${POSTGRESQL_MIGRATION_SCHEMA}"."${POSTGRESQL_MIGRATION_HISTORY_TABLE}" (
|
||||
migration_id,
|
||||
stream_id,
|
||||
dialect,
|
||||
checksum,
|
||||
applied_at_ms
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`.trim();
|
||||
|
||||
function parseAppliedAtMs(value: unknown, migrationId: string): number {
|
||||
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 new MigrationStreamHistoryCorruptionError(migrationId);
|
||||
}
|
||||
|
||||
async function findHistoryRecord(
|
||||
queryable: PostgresMigrationQueryable,
|
||||
migrationId: string,
|
||||
): Promise<MigrationStreamRecord | null> {
|
||||
const result = await queryable.query<HistoryRow>(SELECT_HISTORY_SQL, [
|
||||
migrationId,
|
||||
]);
|
||||
if (result.rows.length === 0) return null;
|
||||
if (result.rows.length !== 1) {
|
||||
throw new MigrationStreamHistoryCorruptionError(migrationId);
|
||||
}
|
||||
const row = result.rows[0];
|
||||
if (!row) {
|
||||
throw new MigrationStreamHistoryCorruptionError(migrationId);
|
||||
}
|
||||
return {
|
||||
streamId: row.streamId as string,
|
||||
dialect: row.dialect as 'postgresql',
|
||||
migrationId: row.migrationId as string,
|
||||
checksum: row.checksum as string,
|
||||
appliedAtMs: parseAppliedAtMs(row.appliedAtMs, migrationId),
|
||||
};
|
||||
}
|
||||
|
||||
export async function readPostgresMigrationHistory(
|
||||
queryable: PostgresMigrationQueryable,
|
||||
): Promise<readonly MigrationStreamRecord[]> {
|
||||
const result = await queryable.query<HistoryRow>(SELECT_ALL_HISTORY_SQL);
|
||||
return result.rows.map((row) => ({
|
||||
streamId: row.streamId as string,
|
||||
dialect: row.dialect as 'postgresql',
|
||||
migrationId: row.migrationId as string,
|
||||
checksum: row.checksum as string,
|
||||
appliedAtMs: parseAppliedAtMs(
|
||||
row.appliedAtMs,
|
||||
typeof row.migrationId === 'string' ? row.migrationId : 'unknown',
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
async function configureTransaction(
|
||||
client: PostgresMigrationClient,
|
||||
): Promise<void> {
|
||||
await client.query(`SELECT set_config('statement_timeout', $1, true)`, [
|
||||
`${POSTGRESQL_MIGRATION_STATEMENT_TIMEOUT_MS}ms`,
|
||||
]);
|
||||
await client.query(`SELECT set_config('lock_timeout', $1, true)`, [
|
||||
`${POSTGRESQL_MIGRATION_LOCK_TIMEOUT_MS}ms`,
|
||||
]);
|
||||
await client.query(
|
||||
`SELECT set_config('idle_in_transaction_session_timeout', $1, true)`,
|
||||
[`${POSTGRESQL_MIGRATION_IDLE_TRANSACTION_TIMEOUT_MS}ms`],
|
||||
);
|
||||
}
|
||||
|
||||
async function acquireLeaderLock(
|
||||
client: PostgresMigrationClient,
|
||||
): Promise<void> {
|
||||
const result = await client.query<AdvisoryLockRow>(
|
||||
'SELECT pg_try_advisory_xact_lock($1, $2) AS acquired',
|
||||
POSTGRESQL_MIGRATION_LOCK_KEY,
|
||||
);
|
||||
if (result.rows.length !== 1 || result.rows[0]?.acquired !== true) {
|
||||
throw new PostgresMigrationLeaderUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PostgreSQL migration history adapter without a runtime dependency on `pg`.
|
||||
* The cluster-only package owns the concrete Pool binding; edge and standalone
|
||||
* builds can compile this contract without installing or importing the driver.
|
||||
*/
|
||||
export class PostgresMigrationStreamStore
|
||||
implements MigrationStreamStore<PostgresMigrationContext>
|
||||
{
|
||||
constructor(private readonly pool: PostgresMigrationPool) {}
|
||||
|
||||
async ensureHistory(): Promise<void> {
|
||||
await this.withLeaderTransaction(async (client) => {
|
||||
await client.query(
|
||||
`CREATE SCHEMA IF NOT EXISTS "${POSTGRESQL_MIGRATION_SCHEMA}"`,
|
||||
);
|
||||
await client.query(CREATE_HISTORY_SQL);
|
||||
});
|
||||
}
|
||||
|
||||
async findById(migrationId: string): Promise<MigrationStreamRecord | null> {
|
||||
return findHistoryRecord(this.pool, migrationId);
|
||||
}
|
||||
|
||||
async listAll(): Promise<readonly MigrationStreamRecord[]> {
|
||||
return readPostgresMigrationHistory(this.pool);
|
||||
}
|
||||
|
||||
async transaction<T>(
|
||||
work: (
|
||||
transaction: MigrationStreamTransaction<PostgresMigrationContext>,
|
||||
) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return this.withLeaderTransaction(async (client) =>
|
||||
work({
|
||||
context: client,
|
||||
findById: (migrationId) => findHistoryRecord(client, migrationId),
|
||||
insert: async (record) => {
|
||||
if (
|
||||
record.streamId !== POSTGRESQL_MAIN_MIGRATION_STREAM_ID ||
|
||||
record.dialect !== 'postgresql'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'PostgreSQL migration history record has the wrong stream or dialect',
|
||||
);
|
||||
}
|
||||
await client.query(INSERT_HISTORY_SQL, [
|
||||
record.migrationId,
|
||||
record.streamId,
|
||||
record.dialect,
|
||||
record.checksum,
|
||||
record.appliedAtMs,
|
||||
]);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async withLeaderTransaction<T>(
|
||||
work: (client: PostgresMigrationClient) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const client = await this.pool.connect();
|
||||
let began = false;
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
began = true;
|
||||
await configureTransaction(client);
|
||||
await acquireLeaderLock(client);
|
||||
const result = await work(client);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (began) {
|
||||
try {
|
||||
await client.query('ROLLBACK');
|
||||
} catch {
|
||||
// Preserve the migration error. A broken connection is discarded by
|
||||
// the concrete driver when release() runs.
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createHash } from 'crypto';
|
||||
import type { MigrationStreamStep } from '@qinglong/runtime-core';
|
||||
import type { PostgresMigrationContext } from './postgresMigrationStreamStore';
|
||||
|
||||
export interface PostgresSqlMigrationDefinition {
|
||||
readonly id: string;
|
||||
readonly statements: readonly string[];
|
||||
}
|
||||
|
||||
export function checksumPostgresStatements(
|
||||
statements: readonly string[],
|
||||
): string {
|
||||
return createHash('sha256')
|
||||
.update(
|
||||
JSON.stringify({
|
||||
format: 1,
|
||||
statements,
|
||||
}),
|
||||
)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function definePostgresSqlMigration(
|
||||
definition: PostgresSqlMigrationDefinition,
|
||||
): MigrationStreamStep<PostgresMigrationContext> {
|
||||
const statements = Object.freeze([...definition.statements]);
|
||||
return Object.freeze({
|
||||
id: definition.id,
|
||||
checksum: checksumPostgresStatements(statements),
|
||||
async up(context: PostgresMigrationContext) {
|
||||
for (const statement of statements) await context.query(statement);
|
||||
},
|
||||
});
|
||||
}
|
||||
+1392
File diff suppressed because it is too large
Load Diff
+222
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+341
@@ -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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
+159
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1321
File diff suppressed because it is too large
Load Diff
+787
@@ -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();
|
||||
}
|
||||
}
|
||||
+650
@@ -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();
|
||||
}
|
||||
}
|
||||
+598
@@ -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();
|
||||
}
|
||||
}
|
||||
+1275
File diff suppressed because it is too large
Load Diff
+484
@@ -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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
+288
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+594
@@ -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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
+583
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+840
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user