feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,565 @@
// Cluster Plugin Package recovery boundary; keep Kubernetes activation authority explicit.
import { createHash } from 'node:crypto';
import {
PluginPackageActivationConflictError,
PluginPackageActivationUnavailableError,
normalizePluginPackageActivationIntent,
type PluginPackageActivationIntent,
type PluginPackageActivationObservation,
type PluginPackageActivationPublisher,
} from '@qinglong/runtime-core/plugin-package-activation';
import type {
PluginPackageResourceGeneration,
PluginPackageResourceGenerationSource,
} from '@qinglong/runtime-core/plugin-package-resource-generation';
import {
createPluginPackageActivationReceipt,
normalizePluginPackageActivationReceipt,
type PluginPackageActivationReceipt,
} from '@qinglong/runtime-core/plugin-package-install';
const ACTIVE_POINTER_SCHEMA =
'qinglong/plugin-package-kubernetes-active-pointer@v2';
const ACTIVE_POINTER_KEY = 'active.json';
const MANAGED_BY_LABEL = 'app.kubernetes.io/managed-by';
const MANAGED_BY_VALUE = 'qinglong3';
const ACTIVE_LABEL = 'qinglong.io/plugin-package-active';
const TARGET_LABEL = 'qinglong.io/plugin-package-target';
const INTENT_ANNOTATION = 'qinglong.io/plugin-package-intent';
const FIELD_MANAGER = 'qinglong-plugin-package-activation';
const MAX_ACTIVE_POINTER_BYTES = 512 * 1024;
const DNS_LABEL = /^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
const SAFE_IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
const RESOURCE_ID = /^[A-Za-z0-9][A-Za-z0-9._:/+=-]{0,511}$/;
const DIGEST = /^[0-9a-f]{64}$/;
const TARGET_DIGEST_DOMAIN = Buffer.from(
'qinglong/plugin-package-kubernetes-target@v1\0',
'utf8',
);
export interface ClusterPluginPackageStageEvidence {
readonly lockDigest: string;
readonly stageRef: string;
readonly stageReceiptDigest: string;
readonly stageEvidenceDigest: string;
readonly contentDigest: string;
}
export interface ClusterPluginPackageStageEvidenceVerifier {
verify(
intent: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<ClusterPluginPackageStageEvidence>>;
}
export interface PluginPackageKubernetesActivationPublisherOptions {
/** Stable operator-reviewed identity for one Kubernetes API cluster. */
readonly clusterIdentity: string;
readonly namespace: string;
/** Explicit authoritative clock called only for a new publication attempt. */
readonly now: () => number | Promise<number>;
}
export interface PluginPackageKubernetesConfigMap {
readonly apiVersion?: string;
readonly kind?: string;
readonly immutable?: boolean;
readonly data?: Readonly<Record<string, string>>;
readonly binaryData?: Readonly<Record<string, string>>;
readonly metadata?: Readonly<{
name?: string;
namespace?: string;
uid?: string;
resourceVersion?: string;
deletionTimestamp?: Date;
finalizers?: readonly string[];
ownerReferences?: readonly Readonly<Record<string, unknown>>[];
labels?: Readonly<Record<string, string>>;
annotations?: Readonly<Record<string, string>>;
}>;
}
interface ConfigMapWrite extends PluginPackageKubernetesConfigMap {
readonly metadata: NonNullable<PluginPackageKubernetesConfigMap['metadata']>;
readonly data: Readonly<Record<string, string>>;
}
export interface PluginPackageKubernetesConfigMapApi {
readNamespacedConfigMap(
request: Readonly<{
name: string;
namespace: string;
}>,
): Promise<PluginPackageKubernetesConfigMap>;
createNamespacedConfigMap(
request: Readonly<{
namespace: string;
body: ConfigMapWrite;
fieldManager: string;
fieldValidation: 'Strict';
}>,
): Promise<PluginPackageKubernetesConfigMap>;
replaceNamespacedConfigMap(
request: Readonly<{
name: string;
namespace: string;
body: ConfigMapWrite;
fieldManager: string;
fieldValidation: 'Strict';
}>,
): Promise<PluginPackageKubernetesConfigMap>;
}
interface ActivePointer {
readonly schema: typeof ACTIVE_POINTER_SCHEMA;
readonly clusterIdentityDigest: string;
readonly intent: Readonly<PluginPackageActivationIntent>;
readonly receipt: Readonly<PluginPackageActivationReceipt>;
}
interface StoredPointer extends ActivePointer {
readonly resourceVersion: string;
}
function apiStatus(error: unknown): number | null {
if (!error || typeof error !== 'object') return null;
if ('code' in error && typeof error.code === 'number') return error.code;
if (
'response' in error &&
error.response &&
typeof error.response === 'object' &&
'statusCode' in error.response &&
typeof error.response.statusCode === 'number'
) {
return error.response.statusCode;
}
return null;
}
function preserveDomainError(error: unknown): never {
if (
error instanceof PluginPackageActivationConflictError ||
error instanceof PluginPackageActivationUnavailableError
) {
throw error;
}
throw new PluginPackageActivationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
function dataRecord(value: unknown): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
throw new PluginPackageActivationConflictError();
}
const descriptors = Object.getOwnPropertyDescriptors(value);
if (
Object.values(descriptors).some(
(descriptor) =>
descriptor.get !== undefined ||
descriptor.set !== undefined ||
descriptor.enumerable !== true,
)
) {
throw new PluginPackageActivationConflictError();
}
return value as Record<string, unknown>;
}
function exactKeys(value: object, expected: readonly string[]): void {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
actual.length !== canonical.length ||
actual.some((key, index) => key !== canonical[index])
) {
throw new PluginPackageActivationConflictError();
}
}
function boundedResourceId(value: unknown): string {
if (typeof value !== 'string' || !RESOURCE_ID.test(value)) {
throw new PluginPackageActivationUnavailableError();
}
return value;
}
function normalizeIntent(
value: Readonly<PluginPackageActivationIntent>,
): Readonly<PluginPackageActivationIntent> {
try {
return normalizePluginPackageActivationIntent(value);
} catch {
throw new PluginPackageActivationConflictError();
}
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
/**
* Short-lived Kubernetes ConfigMap publisher. It owns no timer, watcher,
* database connection or cache; every replacement is resourceVersion fenced.
*/
export class PluginPackageKubernetesActivationPublisher
implements
PluginPackageActivationPublisher,
PluginPackageResourceGenerationSource
{
readonly #clusterIdentityDigest: string;
constructor(
private readonly api: PluginPackageKubernetesConfigMapApi,
private readonly stageEvidence: ClusterPluginPackageStageEvidenceVerifier,
private readonly options: PluginPackageKubernetesActivationPublisherOptions,
) {
if (
!api ||
typeof api.readNamespacedConfigMap !== 'function' ||
typeof api.createNamespacedConfigMap !== 'function' ||
typeof api.replaceNamespacedConfigMap !== 'function'
) {
throw new TypeError('Plugin Package Kubernetes ConfigMap API is invalid');
}
if (!stageEvidence || typeof stageEvidence.verify !== 'function') {
throw new TypeError(
'Plugin Package cluster stage evidence verifier is invalid',
);
}
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).sort().join(',') !==
'clusterIdentity,namespace,now' ||
!SAFE_IDENTITY.test(options.clusterIdentity) ||
!DNS_LABEL.test(options.namespace) ||
typeof options.now !== 'function'
) {
throw new TypeError(
'Plugin Package Kubernetes activation options are invalid',
);
}
this.#clusterIdentityDigest = createHash('sha256')
.update('qinglong/plugin-package-kubernetes-cluster@v1\0', 'utf8')
.update(options.clusterIdentity, 'utf8')
.digest('hex');
}
#targetDigest(
identity: Readonly<
Pick<PluginPackageActivationIntent, 'projectId' | 'packageName'>
>,
): string {
return createHash('sha256')
.update(TARGET_DIGEST_DOMAIN)
.update(this.#clusterIdentityDigest, 'utf8')
.update('\0', 'utf8')
.update(this.options.namespace, 'utf8')
.update('\0', 'utf8')
.update(identity.projectId, 'utf8')
.update('\0', 'utf8')
.update(identity.packageName, 'utf8')
.digest('hex');
}
#name(
identity: Readonly<
Pick<PluginPackageActivationIntent, 'projectId' | 'packageName'>
>,
): string {
return `ql3p-${this.#targetDigest(identity).slice(0, 52)}`;
}
async #verifyStage(
intent: Readonly<PluginPackageActivationIntent>,
): Promise<void> {
let value: unknown;
try {
value = await this.stageEvidence.verify(intent);
} catch (error) {
return preserveDomainError(error);
}
const evidence = dataRecord(value);
exactKeys(evidence, [
'lockDigest',
'stageRef',
'stageReceiptDigest',
'stageEvidenceDigest',
'contentDigest',
]);
if (
evidence.lockDigest !== intent.lockDigest ||
evidence.stageRef !== intent.stageRef ||
evidence.stageReceiptDigest !== intent.stageReceiptDigest ||
evidence.stageEvidenceDigest !== intent.stageEvidenceDigest ||
evidence.contentDigest !== intent.contentDigest
) {
throw new PluginPackageActivationConflictError();
}
}
#parsePointer(
configMap: PluginPackageKubernetesConfigMap,
expectedName: string,
): Readonly<StoredPointer> {
try {
const metadata = configMap?.metadata;
if (
configMap.apiVersion !== 'v1' ||
configMap.kind !== 'ConfigMap' ||
configMap.immutable === true ||
configMap.binaryData !== undefined ||
!metadata ||
metadata.name !== expectedName ||
metadata.namespace !== this.options.namespace ||
metadata.deletionTimestamp !== undefined ||
(metadata.finalizers?.length ?? 0) !== 0 ||
(metadata.ownerReferences?.length ?? 0) !== 0 ||
!configMap.data
) {
throw new PluginPackageActivationConflictError();
}
const labels = dataRecord(metadata.labels);
exactKeys(labels, [MANAGED_BY_LABEL, ACTIVE_LABEL, TARGET_LABEL]);
const annotations = dataRecord(metadata.annotations);
exactKeys(annotations, [INTENT_ANNOTATION]);
const data = dataRecord(configMap.data);
exactKeys(data, [ACTIVE_POINTER_KEY]);
const serialized = data[ACTIVE_POINTER_KEY];
if (
labels[MANAGED_BY_LABEL] !== MANAGED_BY_VALUE ||
labels[ACTIVE_LABEL] !== 'v2' ||
typeof serialized !== 'string' ||
Buffer.byteLength(serialized, 'utf8') > MAX_ACTIVE_POINTER_BYTES
) {
throw new PluginPackageActivationConflictError();
}
const pointer = dataRecord(JSON.parse(serialized));
exactKeys(pointer, [
'schema',
'clusterIdentityDigest',
'intent',
'receipt',
]);
const intent = normalizeIntent(
pointer.intent as PluginPackageActivationIntent,
);
const receipt = normalizePluginPackageActivationReceipt(pointer.receipt);
const normalized: ActivePointer = Object.freeze({
schema: ACTIVE_POINTER_SCHEMA,
clusterIdentityDigest: this.#clusterIdentityDigest,
intent,
receipt,
});
if (
pointer.schema !== ACTIVE_POINTER_SCHEMA ||
pointer.clusterIdentityDigest !== this.#clusterIdentityDigest ||
this.#name(intent) !== expectedName ||
labels[TARGET_LABEL] !==
Buffer.from(this.#targetDigest(intent), 'hex').toString(
'base64url',
) ||
annotations[INTENT_ANNOTATION] !== intent.intentDigest ||
receipt.intentDigest !== intent.intentDigest ||
receipt.generation !== intent.targetGeneration ||
receipt.contentDigest !== intent.contentDigest ||
`${JSON.stringify(normalized)}\n` !== serialized
) {
throw new PluginPackageActivationConflictError();
}
boundedResourceId(metadata.uid);
return Object.freeze({
...normalized,
resourceVersion: boundedResourceId(metadata.resourceVersion),
});
} catch (error) {
return preserveDomainError(error);
}
}
async #optionalPointer(
identity: Readonly<
Pick<PluginPackageActivationIntent, 'projectId' | 'packageName'>
>,
): Promise<Readonly<StoredPointer> | null> {
const name = this.#name(identity);
try {
return this.#parsePointer(
await this.api.readNamespacedConfigMap({
name,
namespace: this.options.namespace,
}),
name,
);
} catch (error) {
if (apiStatus(error) === 404) return null;
return preserveDomainError(error);
}
}
async #observe(
intent: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<PluginPackageActivationObservation>> {
await this.#verifyStage(intent);
const pointer = await this.#optionalPointer(intent);
if (!pointer) {
if (intent.previousActiveLockDigest !== null) {
throw new PluginPackageActivationConflictError();
}
return Object.freeze({ status: 'not_published' });
}
if (same(pointer.intent, intent)) {
return Object.freeze({ status: 'published', receipt: pointer.receipt });
}
if (
pointer.intent.projectId === intent.projectId &&
pointer.intent.packageName === intent.packageName &&
pointer.intent.lockDigest === intent.previousActiveLockDigest
) {
return Object.freeze({ status: 'not_published' });
}
throw new PluginPackageActivationConflictError();
}
#body(
intent: Readonly<PluginPackageActivationIntent>,
receipt: Readonly<PluginPackageActivationReceipt>,
current: Readonly<StoredPointer> | null,
): ConfigMapWrite {
const targetDigest = this.#targetDigest(intent);
const pointer: Readonly<ActivePointer> = Object.freeze({
schema: ACTIVE_POINTER_SCHEMA,
clusterIdentityDigest: this.#clusterIdentityDigest,
intent,
receipt,
});
const serialized = `${JSON.stringify(pointer)}\n`;
if (Buffer.byteLength(serialized, 'utf8') > MAX_ACTIVE_POINTER_BYTES) {
throw new PluginPackageActivationUnavailableError();
}
return Object.freeze({
apiVersion: 'v1',
kind: 'ConfigMap',
immutable: false,
metadata: Object.freeze({
name: this.#name(intent),
namespace: this.options.namespace,
...(current ? { resourceVersion: current.resourceVersion } : {}),
labels: Object.freeze({
[MANAGED_BY_LABEL]: MANAGED_BY_VALUE,
[ACTIVE_LABEL]: 'v2',
[TARGET_LABEL]: Buffer.from(targetDigest, 'hex').toString(
'base64url',
),
}),
annotations: Object.freeze({
[INTENT_ANNOTATION]: intent.intentDigest,
}),
}),
data: Object.freeze({ [ACTIVE_POINTER_KEY]: serialized }),
});
}
async inspect(
value: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<PluginPackageActivationObservation>> {
try {
return await this.#observe(normalizeIntent(value));
} catch (error) {
return preserveDomainError(error);
}
}
async findActiveResourceGeneration(
projectId: string,
packageName: string,
): Promise<Readonly<PluginPackageResourceGeneration> | null> {
if (
typeof projectId !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(projectId) ||
typeof packageName !== 'string' ||
!DNS_LABEL.test(packageName)
) {
throw new TypeError('Plugin Package active resource identity is invalid');
}
try {
return (
(await this.#optionalPointer(Object.freeze({ projectId, packageName })))
?.intent.resourceGeneration ?? null
);
} catch (error) {
return preserveDomainError(error);
}
}
async publish(
value: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<PluginPackageActivationReceipt>> {
const intent = normalizeIntent(value);
try {
const first = await this.#observe(intent);
if (first.status === 'published') return first.receipt;
const current = await this.#optionalPointer(intent);
if (current && same(current.intent, intent)) return current.receipt;
if (
(!current && intent.previousActiveLockDigest !== null) ||
(current &&
(current.intent.projectId !== intent.projectId ||
current.intent.packageName !== intent.packageName ||
current.intent.lockDigest !== intent.previousActiveLockDigest))
) {
throw new PluginPackageActivationConflictError();
}
const activatedAtMs = await this.options.now();
if (!Number.isSafeInteger(activatedAtMs) || activatedAtMs < 0) {
throw new PluginPackageActivationUnavailableError();
}
const receipt = createPluginPackageActivationReceipt({
activationRef: `k8s-configmap:${this.#targetDigest(intent)}`,
intentDigest: intent.intentDigest,
generation: intent.targetGeneration,
contentDigest: intent.contentDigest,
activatedAtMs,
});
const body = this.#body(intent, receipt, current);
try {
if (current) {
await this.api.replaceNamespacedConfigMap({
name: this.#name(intent),
namespace: this.options.namespace,
body,
fieldManager: FIELD_MANAGER,
fieldValidation: 'Strict',
});
} else {
await this.api.createNamespacedConfigMap({
namespace: this.options.namespace,
body,
fieldManager: FIELD_MANAGER,
fieldValidation: 'Strict',
});
}
} catch (error) {
if (apiStatus(error) !== 409) return preserveDomainError(error);
const winner = await this.#observe(intent);
if (winner.status === 'published') return winner.receipt;
throw new PluginPackageActivationConflictError();
}
const final = await this.#observe(intent);
if (final.status !== 'published') {
throw new PluginPackageActivationUnavailableError();
}
return final.receipt;
} catch (error) {
return preserveDomainError(error);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,450 @@
// Cluster Plugin Package recovery boundary; keep recovery coordination authority explicit.
import type {
OpenPostgresDatabase,
PostgresDatabaseResource,
PostgresPool,
} from '@qinglong/runtime-core';
import {
MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE,
assertPluginPackageInstallMatchesLock,
} from '@qinglong/runtime-core/plugin-package-install';
import {
MAX_PLUGIN_PACKAGE_RECOVERY_PAGES,
PluginPackageRecoveryCoordinator,
type PluginPackageRecoveryCycleResult,
} from '@qinglong/runtime-core/plugin-package-recovery';
import {
PluginPackageAutomationPublicationCoordinator,
PluginPackageAutomationPublicationRecoveryCoordinator,
type PluginPackageAutomationPublicationRecoveryCycleResult,
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import type { PluginPackageResourceByteSource } from '@qinglong/runtime-core/plugin-package-resource-materialization';
import {
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGES,
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGE_SIZE,
PluginPackageTaskPublicationCoordinator,
PluginPackageTaskPublicationRecoveryCoordinator,
type PluginPackageTaskPublicationRecoveryCycleResult,
} from '@qinglong/runtime-core/plugin-package-task-publication';
import {
MAX_PROJECT_TOOL_SNAPSHOT_RECOVERY_PAGES,
MAX_PROJECT_TOOL_SNAPSHOT_RECOVERY_PAGE_SIZE,
MAX_PROJECT_TOOL_SNAPSHOT_SOURCE_PAGE_SIZE,
ProjectToolDefinitionSnapshotPublicationCoordinator,
ProjectToolDefinitionSnapshotRecoveryCoordinator,
type ProjectToolDefinitionSnapshotRecoveryCycleResult,
} from '@qinglong/runtime-core/project-tool-definition-snapshot';
import { createBuiltInTaskSpecSemanticRegistry } from '@qinglong/runtime-core/task-spec-semantic';
import {
assertPostgresPackageExecutorSchemaReady,
PostgresPluginPackageAutomationPublicationRepository,
PostgresPluginPackageMaterializedRevisionRepository,
PostgresPluginPackagePublisherProvenanceRepository,
PostgresPluginPackageTaskReconciliationRepository,
PostgresProjectToolDefinitionSnapshotRepository,
type PostgresSchemaReadinessReport,
} from '@qinglong/cluster-postgres/package-executor';
import { PostgresPluginPackageInstallRepository } from '@qinglong/cluster-postgres/plugin-package-install';
import {
PluginPackageKubernetesActivationPublisher,
type PluginPackageKubernetesConfigMapApi,
} from './pluginPackageKubernetesActivation';
import {
ClusterPluginPackageOciResourceByteSource,
ClusterPluginPackageOciStageAuthority,
clusterPluginPackageActivationEvidence,
pluginPackageStageVerificationFailure,
type ClusterPluginPackageStageAuthority,
} from './pluginPackageOciStage';
import {
ClusterPluginPackageProvenanceInstallRepository,
recoverClusterPluginPackagePublisherProvenance,
type ClusterPluginPackagePublisherProvenanceRecoveryResult,
} from '../publisher/pluginPackagePublisherProvenanceRecovery';
export interface ClusterPluginPackageRecoveryOptions {
readonly openDatabase: OpenPostgresDatabase;
readonly api: PluginPackageKubernetesConfigMapApi;
readonly stageAuthority?: ClusterPluginPackageStageAuthority;
readonly stageAuthorityFactory?: (
pool: PostgresPool,
) =>
| ClusterPluginPackageStageAuthority
| Promise<ClusterPluginPackageStageAuthority>;
readonly resourceByteSource?: PluginPackageResourceByteSource;
readonly trustAuthorityId: string;
readonly clusterIdentity: string;
readonly namespace: string;
readonly now: () => number | Promise<number>;
readonly pageSize?: number;
readonly maxPages?: number;
}
export interface ClusterPluginPackageRecoveryResult {
readonly evidence: PostgresSchemaReadinessReport;
readonly provenanceRecovery: Readonly<ClusterPluginPackagePublisherProvenanceRecoveryResult>;
readonly recovery: Readonly<PluginPackageRecoveryCycleResult>;
readonly taskPublicationRecovery: Readonly<PluginPackageTaskPublicationRecoveryCycleResult>;
readonly automationPublicationRecovery: Readonly<PluginPackageAutomationPublicationRecoveryCycleResult>;
readonly toolSnapshotRecovery: Readonly<ProjectToolDefinitionSnapshotRecoveryCycleResult>;
}
export class ClusterPluginPackageRecoveryRequiredError extends Error {
constructor(readonly recovery: Readonly<PluginPackageRecoveryCycleResult>) {
super('Cluster has unresolved Plugin Package recovery work');
this.name = 'ClusterPluginPackageRecoveryRequiredError';
}
}
export class ClusterPluginPackagePublisherProvenanceRecoveryRequiredError extends Error {
constructor(
readonly recovery: Readonly<ClusterPluginPackagePublisherProvenanceRecoveryResult>,
) {
super('Cluster has unresolved Plugin Package publisher provenance work');
this.name =
'ClusterPluginPackagePublisherProvenanceRecoveryRequiredError';
}
}
export class ClusterPluginPackageTaskPublicationRequiredError extends Error {
constructor(
readonly recovery: Readonly<PluginPackageTaskPublicationRecoveryCycleResult>,
) {
super('Cluster has unresolved Plugin Package Task publication work');
this.name = 'ClusterPluginPackageTaskPublicationRequiredError';
}
}
export class ClusterPluginPackageAutomationPublicationRequiredError extends Error {
constructor(
readonly recovery: Readonly<PluginPackageAutomationPublicationRecoveryCycleResult>,
) {
super(
'Cluster has unresolved Plugin Package Workflow/Prompt publication work',
);
this.name = 'ClusterPluginPackageAutomationPublicationRequiredError';
}
}
export class ClusterPluginPackageToolSnapshotRequiredError extends Error {
constructor(
readonly recovery: Readonly<ProjectToolDefinitionSnapshotRecoveryCycleResult>,
) {
super('Cluster has unresolved Plugin Package Tool snapshot work');
this.name = 'ClusterPluginPackageToolSnapshotRequiredError';
}
}
function assertOptions(options: ClusterPluginPackageRecoveryOptions): void {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) =>
![
'openDatabase',
'api',
'stageAuthority',
'stageAuthorityFactory',
'resourceByteSource',
'trustAuthorityId',
'clusterIdentity',
'namespace',
'now',
'pageSize',
'maxPages',
].includes(key),
) ||
typeof options.openDatabase !== 'function' ||
(options.stageAuthority === undefined) ===
(options.stageAuthorityFactory === undefined) ||
(options.stageAuthorityFactory !== undefined &&
typeof options.stageAuthorityFactory !== 'function') ||
(options.resourceByteSource !== undefined &&
(!options.resourceByteSource ||
typeof options.resourceByteSource.open !== 'function')) ||
typeof options.trustAuthorityId !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(
options.trustAuthorityId,
) ||
typeof options.now !== 'function' ||
(options.pageSize !== undefined &&
(!Number.isSafeInteger(options.pageSize) ||
options.pageSize < 1 ||
options.pageSize >
Math.min(
MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE,
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGE_SIZE,
MAX_PROJECT_TOOL_SNAPSHOT_RECOVERY_PAGE_SIZE,
))) ||
(options.maxPages !== undefined &&
(!Number.isSafeInteger(options.maxPages) ||
options.maxPages < 1 ||
options.maxPages >
Math.min(
MAX_PLUGIN_PACKAGE_RECOVERY_PAGES,
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGES,
MAX_PROJECT_TOOL_SNAPSHOT_RECOVERY_PAGES,
)))
) {
throw new TypeError(
'Cluster Plugin Package recovery configuration is invalid',
);
}
}
function assertStageAuthority(
stageAuthority: ClusterPluginPackageStageAuthority,
hasResourceByteSource: boolean,
): void {
if (
!stageAuthority ||
typeof stageAuthority.stage !== 'function' ||
typeof stageAuthority.publisherEvidence !== 'function' ||
typeof stageAuthority.verify !== 'function' ||
(!hasResourceByteSource &&
!(stageAuthority instanceof ClusterPluginPackageOciStageAuthority))
) {
throw new TypeError(
'Cluster Plugin Package recovery stage authority is invalid',
);
}
}
/**
* One-shot admin Job composition. The database is always closed before this
* function settles, and no repository or Kubernetes authority escapes.
*/
export async function recoverClusterPluginPackages(
options: ClusterPluginPackageRecoveryOptions,
): Promise<Readonly<ClusterPluginPackageRecoveryResult>> {
assertOptions(options);
let database: PostgresDatabaseResource | undefined;
let result: Readonly<ClusterPluginPackageRecoveryResult> | undefined;
let failure: unknown;
try {
database = await options.openDatabase();
const evidence = await assertPostgresPackageExecutorSchemaReady(
database.pool,
);
const stageAuthority =
options.stageAuthority ??
(await options.stageAuthorityFactory!(database.pool));
assertStageAuthority(
stageAuthority,
options.resourceByteSource !== undefined,
);
const installRepository = new PostgresPluginPackageInstallRepository(
database.pool,
);
const provenanceRepository =
new PostgresPluginPackagePublisherProvenanceRepository(database.pool);
const provenanceRecovery =
await recoverClusterPluginPackagePublisherProvenance(
installRepository,
provenanceRepository,
stageAuthority,
{
trustAuthorityId: options.trustAuthorityId,
...(options.pageSize === undefined
? {}
: { pageSize: options.pageSize }),
...(options.maxPages === undefined
? {}
: { maxPages: options.maxPages }),
},
);
if (!provenanceRecovery.safeToAdmit) {
throw new ClusterPluginPackagePublisherProvenanceRecoveryRequiredError(
provenanceRecovery,
);
}
const repository = new ClusterPluginPackageProvenanceInstallRepository(
installRepository,
provenanceRepository,
stageAuthority,
options.trustAuthorityId,
);
const publisher = new PluginPackageKubernetesActivationPublisher(
options.api,
{
async verify(intent) {
try {
const [record, lock] = await Promise.all([
repository.find(intent.projectId, intent.packageName),
repository.findLock(intent.lockDigest),
]);
if (
!record ||
!lock ||
record.installationId !== intent.installationId ||
record.lockDigest !== intent.lockDigest ||
record.stageReceipt === null ||
record.stageReceipt.stageRef !== intent.stageRef ||
record.stageReceipt.receiptDigest !== intent.stageReceiptDigest ||
record.stageReceipt.evidenceDigest !==
intent.stageEvidenceDigest ||
record.stageReceipt.contentDigest !== intent.contentDigest
) {
return pluginPackageStageVerificationFailure(
new Error('durable stage identity conflict'),
);
}
assertPluginPackageInstallMatchesLock(lock, record);
await stageAuthority.verify(lock, record.stageReceipt);
await provenanceRepository.assertInstallationNotRevoked(
record.installationId,
);
return clusterPluginPackageActivationEvidence(intent);
} catch (error) {
return pluginPackageStageVerificationFailure(error);
}
},
},
{
clusterIdentity: options.clusterIdentity,
namespace: options.namespace,
now: options.now,
},
);
const resourceByteSource =
options.resourceByteSource ??
new ClusterPluginPackageOciResourceByteSource({
authority:
stageAuthority as ClusterPluginPackageOciStageAuthority,
lockSource: repository,
});
const recovery = await new PluginPackageRecoveryCoordinator({
repository,
stageProvider: stageAuthority,
publisher,
now: options.now,
}).recover({
...(options.pageSize === undefined ? {} : { pageSize: options.pageSize }),
...(options.maxPages === undefined ? {} : { maxPages: options.maxPages }),
});
if (!recovery.safeToAdmit) {
throw new ClusterPluginPackageRecoveryRequiredError(recovery);
}
const taskSpecSemanticRegistry = createBuiltInTaskSpecSemanticRegistry();
const taskReconciliationRepository =
new PostgresPluginPackageTaskReconciliationRepository(
database.pool,
taskSpecSemanticRegistry,
);
const materializedRepository =
new PostgresPluginPackageMaterializedRevisionRepository(
database.pool,
taskSpecSemanticRegistry,
);
const taskPublicationRecovery =
await new PluginPackageTaskPublicationRecoveryCoordinator({
source: taskReconciliationRepository,
publisher: new PluginPackageTaskPublicationCoordinator({
generationSource: publisher,
lockSource: repository,
byteSource: resourceByteSource,
materializedRepository,
reconciliationRepository: taskReconciliationRepository,
taskSpecSemanticRegistry,
}),
}).recover({
...(options.pageSize === undefined
? {}
: { pageSize: options.pageSize }),
...(options.maxPages === undefined
? {}
: { maxPages: options.maxPages }),
});
if (!taskPublicationRecovery.safeToAdmit) {
throw new ClusterPluginPackageTaskPublicationRequiredError(
taskPublicationRecovery,
);
}
const automationPublicationRepository =
new PostgresPluginPackageAutomationPublicationRepository(database.pool);
const automationPublicationRecovery =
await new PluginPackageAutomationPublicationRecoveryCoordinator({
source: automationPublicationRepository,
publisher: new PluginPackageAutomationPublicationCoordinator({
generationSource: publisher,
materializedRepository,
repository: automationPublicationRepository,
taskSpecSemanticRegistry,
now: options.now,
}),
}).recover({
...(options.pageSize === undefined
? {}
: { pageSize: options.pageSize }),
...(options.maxPages === undefined
? {}
: { maxPages: options.maxPages }),
});
if (!automationPublicationRecovery.safeToAdmit) {
throw new ClusterPluginPackageAutomationPublicationRequiredError(
automationPublicationRecovery,
);
}
const toolSnapshotRepository =
new PostgresProjectToolDefinitionSnapshotRepository(database.pool);
const toolSnapshotRecovery =
await new ProjectToolDefinitionSnapshotRecoveryCoordinator({
source: toolSnapshotRepository,
publisher: new ProjectToolDefinitionSnapshotPublicationCoordinator({
source: toolSnapshotRepository,
materializedRepository,
repository: toolSnapshotRepository,
taskSpecSemanticRegistry,
pageSize: Math.min(
options.pageSize ?? 16,
MAX_PROJECT_TOOL_SNAPSHOT_SOURCE_PAGE_SIZE,
),
}),
}).recover({
...(options.pageSize === undefined
? {}
: { pageSize: options.pageSize }),
...(options.maxPages === undefined
? {}
: { maxPages: options.maxPages }),
});
if (!toolSnapshotRecovery.safeToAdmit) {
throw new ClusterPluginPackageToolSnapshotRequiredError(
toolSnapshotRecovery,
);
}
result = Object.freeze({
evidence,
provenanceRecovery,
recovery,
taskPublicationRecovery,
automationPublicationRecovery,
toolSnapshotRecovery,
});
} catch (error) {
failure = error;
}
if (database) {
try {
await database.close();
} catch (closeError) {
if (failure !== undefined) {
throw new AggregateError(
[failure, closeError],
'Cluster Plugin Package recovery failed and PostgreSQL did not close',
);
}
throw closeError;
}
}
if (failure !== undefined) throw failure;
if (!result) {
throw new Error('Cluster Plugin Package recovery produced no result');
}
return result;
}
@@ -0,0 +1,55 @@
#!/usr/bin/env node
// Cluster Plugin Package recovery boundary; keep the operational CLI explicit.
import { runClusterPluginPackageRecoveryProcess } from './pluginPackageRecoveryProcess';
const USAGE = 'Usage: ql3-plugin-package-recover';
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-plugin-package-recovery',
event: 'recovery_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_PLUGIN_PACKAGE_RECOVERY_CLI_USAGE_INVALID',
message: USAGE,
})}\n`,
);
process.exitCode = 64;
return;
}
try {
await runClusterPluginPackageRecoveryProcess({
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,885 @@
// Cluster Plugin Package recovery boundary; keep process composition explicit.
import { closeSync, constants, fstatSync, openSync, readSync } from 'node:fs';
import { isAbsolute } from 'node:path';
import type {
OpenPostgresDatabase,
PostgresPool,
} from '@qinglong/runtime-core';
import { MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE } from '@qinglong/runtime-core/plugin-package-install';
import {
PluginPackagePublisherTrustRegistry,
type PluginPackagePublisherKeyDefinition,
} from '@qinglong/runtime-core/plugin-package-bundle';
import {
createPluginPackagePublisherTrustSnapshot,
createPluginPackagePublisherEffectiveTrustRegistry,
type PluginPackagePublisherTrustSnapshot,
} from '@qinglong/runtime-core/plugin-package-publisher-trust';
import {
MAX_PLUGIN_PACKAGE_RECOVERY_PAGES,
type PluginPackageRecoveryCycleResult,
} from '@qinglong/runtime-core/plugin-package-recovery';
import type { PluginPackageAutomationPublicationRecoveryCycleResult } from '@qinglong/runtime-core/plugin-package-automation-publication';
import type { PluginPackageResourceByteSource } from '@qinglong/runtime-core/plugin-package-resource-materialization';
import type { PluginPackageTaskPublicationRecoveryCycleResult } from '@qinglong/runtime-core/plugin-package-task-publication';
import type { ProjectToolDefinitionSnapshotRecoveryCycleResult } from '@qinglong/runtime-core/project-tool-definition-snapshot';
import {
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
loadPostgresCertificateAuthorityFile,
loadPostgresConnectionEnvironment,
PostgresPluginPackagePublisherTrustAuthorityRepository,
type PostgresConnectionOptions,
type PostgresPoolOptions,
} from '@qinglong/cluster-postgres/package-executor';
import {
recoverClusterPluginPackages,
type ClusterPluginPackageRecoveryResult,
} from './pluginPackageRecovery';
import type { ClusterPluginPackagePublisherProvenanceRecoveryResult } from '../publisher/pluginPackagePublisherProvenanceRecovery';
import {
ClusterPluginPackageOciStageAuthority,
type ClusterPluginPackageOciFetch,
type ClusterPluginPackageRegistryCredentialProvider,
type ClusterPluginPackageStageAuthority,
} from './pluginPackageOciStage';
import type { PluginPackageKubernetesConfigMapApi } from './pluginPackageKubernetesActivation';
export type ClusterPluginPackageRecoveryProcessEnvironment = Readonly<
Record<string, string | undefined>
>;
export interface ClusterPluginPackageRecoveryProcessConfig {
readonly clusterIdentity: string;
readonly namespace: string;
readonly allowedRegistries: readonly string[];
readonly publisherTrustFile: string;
readonly publisherTrustAuthorityId: string;
readonly registryCredentialFile?: string;
readonly requestTimeoutMs: number;
readonly pageSize: number;
readonly maxPages: number;
readonly database: Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
}>;
}
export interface ClusterPluginPackageRecoveryProcessEvent {
readonly schemaVersion: 1;
readonly component: 'qinglong3-plugin-package-recovery';
readonly event: 'recovery_started' | 'recovery_completed';
readonly clusterIdentity: string;
readonly provenanceRecovery?: Readonly<ClusterPluginPackagePublisherProvenanceRecoveryResult>;
readonly recovery?: Readonly<PluginPackageRecoveryCycleResult>;
readonly taskPublicationRecovery?: Readonly<PluginPackageTaskPublicationRecoveryCycleResult>;
readonly automationPublicationRecovery?: Readonly<PluginPackageAutomationPublicationRecoveryCycleResult>;
readonly toolSnapshotRecovery?: Readonly<ProjectToolDefinitionSnapshotRecoveryCycleResult>;
}
export interface RunClusterPluginPackageRecoveryProcessOptions {
readonly environment: ClusterPluginPackageRecoveryProcessEnvironment;
readonly emit?: (
event: ClusterPluginPackageRecoveryProcessEvent,
) => void | Promise<void>;
readonly openDatabase?: OpenPostgresDatabase;
readonly api?: PluginPackageKubernetesConfigMapApi;
readonly stageAuthority?: ClusterPluginPackageStageAuthority;
readonly resourceByteSource?: PluginPackageResourceByteSource;
readonly trust?: PluginPackagePublisherTrustRegistry;
readonly fetch?: ClusterPluginPackageOciFetch;
}
export interface ClusterPluginPackageRegistryCredentialFile
extends ClusterPluginPackageRegistryCredentialProvider {
dispose(): void;
}
export interface ClusterPluginPackagePublisherTrustFileEvidence {
readonly registry: PluginPackagePublisherTrustRegistry;
readonly snapshot: Readonly<PluginPackagePublisherTrustSnapshot>;
readonly definitions: readonly Readonly<PluginPackagePublisherKeyDefinition>[];
}
export class ClusterPluginPackageRecoveryProcessConfigError extends TypeError {
readonly code = 'QL3_PLUGIN_PACKAGE_RECOVERY_PROCESS_CONFIG_INVALID';
constructor(message: string) {
super(
`Plugin Package recovery process configuration is invalid: ${message}`,
);
this.name = 'ClusterPluginPackageRecoveryProcessConfigError';
}
}
const TRUST_SCHEMA = 'qinglong/plugin-package-publisher-trust@v1';
const REGISTRY_CREDENTIAL_SCHEMA =
'qinglong/plugin-package-registry-credentials@v1';
const MAX_TRUST_FILE_BYTES = 256 * 1024;
const MAX_REGISTRY_CREDENTIAL_FILE_BYTES = 256 * 1024;
const MAX_REGISTRY_CREDENTIALS = 32;
const SAFE_IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
const DNS_LABEL = /^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
const REGISTRY =
/^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*)(?::([1-9][0-9]{0,4}))?$/;
const BEARER_TOKEN = /^[A-Za-z0-9._~+/-]+={0,2}$/;
class LoadedClusterPluginPackageRegistryCredentialFile
implements ClusterPluginPackageRegistryCredentialFile
{
readonly #authorizations: Map<string, Buffer>;
constructor(authorizations: Map<string, Buffer>) {
this.#authorizations = authorizations;
}
authorizationFor(registry: string): string | undefined {
if (typeof registry !== 'string' || !REGISTRY.test(registry)) {
return undefined;
}
return this.#authorizations.get(registry)?.toString('ascii');
}
dispose(): void {
for (const authorization of this.#authorizations.values()) {
authorization.fill(0);
}
this.#authorizations.clear();
}
}
function boundedValue(
environment: ClusterPluginPackageRecoveryProcessEnvironment,
name: string,
maximumLength: number,
required = false,
): string | undefined {
const value = environment[name];
if (value === undefined || value === '') {
if (required) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${name} is required`,
);
}
return undefined;
}
if (value.length > maximumLength || /[\0\r\n]/.test(value)) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${name} is invalid`,
);
}
return value;
}
function booleanValue(
environment: ClusterPluginPackageRecoveryProcessEnvironment,
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 ClusterPluginPackageRecoveryProcessConfigError(
`${name} must be true or false`,
);
}
function integerValue(
environment: ClusterPluginPackageRecoveryProcessEnvironment,
name: string,
defaultValue: number,
minimum: number,
maximum: number,
): number {
const value = environment[name];
if (value === undefined || value === '') return defaultValue;
if (!/^\d+$/.test(value)) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${name} must be an integer`,
);
}
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${name} must be between ${minimum} and ${maximum}`,
);
}
return parsed;
}
function loadConnection(
environment: ClusterPluginPackageRecoveryProcessEnvironment,
): Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
}> {
let connection: PostgresConnectionOptions;
try {
connection = loadPostgresConnectionEnvironment(environment, {
connectionString: 'QL3_POSTGRES_PACKAGE_EXECUTOR_URL',
host: 'QL3_POSTGRES_PACKAGE_EXECUTOR_HOST',
port: 'QL3_POSTGRES_PACKAGE_EXECUTOR_PORT',
database: 'QL3_POSTGRES_PACKAGE_EXECUTOR_DATABASE',
user: 'QL3_POSTGRES_PACKAGE_EXECUTOR_USER',
password: 'QL3_POSTGRES_PACKAGE_EXECUTOR_PASSWORD',
});
} catch (error) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
error instanceof Error
? error.message
: 'PostgreSQL Package executor connection is invalid',
);
}
const mode = environment.QL3_POSTGRES_TLS_MODE ?? 'verify-full';
if (mode !== 'verify-full' && mode !== 'disable') {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'QL3_POSTGRES_TLS_MODE must be verify-full or disable',
);
}
if (
mode === 'disable' &&
!booleanValue(environment, 'QL3_POSTGRES_ALLOW_INSECURE')
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'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 ClusterPluginPackageRecoveryProcessConfigError(
'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 ClusterPluginPackageRecoveryProcessConfigError(
'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 ClusterPluginPackageRecoveryProcessConfigError(
'QL3_POSTGRES_TLS_CA_FILE must contain a bounded trusted CA bundle',
);
}
}
const applicationName =
boundedValue(environment, 'QL3_POSTGRES_APPLICATION_NAME', 63) ??
'qinglong3-plugin-package-recovery';
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,62}$/.test(applicationName)) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'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,
}),
});
}
export function loadClusterPluginPackageRecoveryProcessConfig(
environment: ClusterPluginPackageRecoveryProcessEnvironment,
): Readonly<ClusterPluginPackageRecoveryProcessConfig> {
if (
!environment ||
typeof environment !== 'object' ||
Array.isArray(environment)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'environment must be an object',
);
}
const clusterIdentity = boundedValue(
environment,
'QL3_CLUSTER_IDENTITY',
256,
true,
)!;
const namespace = boundedValue(
environment,
'QL3_KUBERNETES_NAMESPACE',
63,
true,
)!;
const registryValue = boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_OCI_REGISTRIES',
4096,
true,
)!;
const allowedRegistries = registryValue.split(',');
const publisherTrustFile = boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_PUBLISHER_TRUST_FILE',
4096,
true,
)!;
const registryCredentialFile = boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_REGISTRY_CREDENTIAL_FILE',
4096,
);
if (
!SAFE_IDENTITY.test(clusterIdentity) ||
!DNS_LABEL.test(namespace) ||
allowedRegistries.length < 1 ||
allowedRegistries.length > 32 ||
allowedRegistries.some((registry) => !REGISTRY.test(registry)) ||
new Set(allowedRegistries).size !== allowedRegistries.length ||
!isAbsolute(publisherTrustFile) ||
(registryCredentialFile !== undefined &&
!isAbsolute(registryCredentialFile))
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'cluster, namespace, registry or publisher trust binding is invalid',
);
}
return Object.freeze({
clusterIdentity,
namespace,
allowedRegistries: Object.freeze(allowedRegistries),
publisherTrustFile,
publisherTrustAuthorityId:
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_TRUST_AUTHORITY_ID',
128,
) ?? 'cluster',
...(registryCredentialFile === undefined ? {} : { registryCredentialFile }),
requestTimeoutMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_OCI_TIMEOUT_MS',
15_000,
1_000,
60_000,
),
pageSize: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_RECOVERY_PAGE_SIZE',
16,
1,
MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE,
),
maxPages: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_RECOVERY_MAX_PAGES',
16,
1,
MAX_PLUGIN_PACKAGE_RECOVERY_PAGES,
),
database: loadConnection(environment),
});
}
function readClusterPluginPackageRegistryCredentialFile(
filePath: string,
): Buffer {
if (
typeof filePath !== 'string' ||
filePath.length < 1 ||
filePath.length > 4096 ||
/[\0\r\n]/.test(filePath) ||
!isAbsolute(filePath)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file path is invalid',
);
}
let descriptor: number;
try {
descriptor = openSync(filePath, constants.O_RDONLY);
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file is unavailable',
);
}
try {
const stat = fstatSync(descriptor);
if (
!stat.isFile() ||
(stat.mode & 0o027) !== 0 ||
!Number.isSafeInteger(stat.size) ||
stat.size < 1 ||
stat.size > MAX_REGISTRY_CREDENTIAL_FILE_BYTES
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file is not a bounded private regular file',
);
}
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) {
bytes.fill(0);
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file changed while reading',
);
}
return bytes.subarray(0, offset);
} finally {
closeSync(descriptor);
}
}
function credentialRecord(
value: unknown,
label: string,
): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.getPrototypeOf(value) !== Object.prototype
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${label} must be an object`,
);
}
return value as Record<string, unknown>;
}
function scrubCredentialSource(value: unknown): void {
if (!value || typeof value !== 'object') return;
const credentials = (value as { credentials?: unknown }).credentials;
if (!Array.isArray(credentials)) return;
for (const candidate of credentials) {
if (!candidate || typeof candidate !== 'object') continue;
const record = candidate as Record<string, unknown>;
if (typeof record.password === 'string') record.password = '';
if (typeof record.token === 'string') record.token = '';
}
}
function zeroAuthorizations(authorizations: Map<string, Buffer>): void {
for (const authorization of authorizations.values()) {
authorization.fill(0);
}
authorizations.clear();
}
export function loadClusterPluginPackageRegistryCredentialFile(
filePath: string,
allowedRegistries: readonly string[],
): ClusterPluginPackageRegistryCredentialFile {
if (
!Array.isArray(allowedRegistries) ||
allowedRegistries.length < 1 ||
allowedRegistries.length > MAX_REGISTRY_CREDENTIALS ||
allowedRegistries.some(
(registry) => typeof registry !== 'string' || !REGISTRY.test(registry),
) ||
new Set(allowedRegistries).size !== allowedRegistries.length
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential allowlist is invalid',
);
}
const bytes = readClusterPluginPackageRegistryCredentialFile(filePath);
let value: unknown;
try {
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file is not valid JSON',
);
} finally {
bytes.fill(0);
}
const authorizations = new Map<string, Buffer>();
try {
const root = credentialRecord(value, 'registry credential file');
if (
Object.keys(root).sort().join(',') !== 'credentials,schema' ||
root.schema !== REGISTRY_CREDENTIAL_SCHEMA ||
!Array.isArray(root.credentials) ||
root.credentials.length < 1 ||
root.credentials.length > MAX_REGISTRY_CREDENTIALS
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file shape is invalid',
);
}
const allowed = new Set(allowedRegistries);
for (const [index, candidate] of root.credentials.entries()) {
const entry = credentialRecord(candidate, `registry credential ${index}`);
const registry = entry.registry;
const scheme = entry.scheme;
if (
typeof registry !== 'string' ||
!REGISTRY.test(registry) ||
!allowed.has(registry) ||
authorizations.has(registry)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`registry credential ${index} binding is invalid`,
);
}
let authorization: Buffer;
if (scheme === 'basic') {
if (
Object.keys(entry).sort().join(',') !==
'password,registry,scheme,username' ||
typeof entry.username !== 'string' ||
Buffer.byteLength(entry.username, 'utf8') < 1 ||
Buffer.byteLength(entry.username, 'utf8') > 256 ||
/[\0-\x1f\x7f:]/.test(entry.username) ||
typeof entry.password !== 'string' ||
Buffer.byteLength(entry.password, 'utf8') < 1 ||
Buffer.byteLength(entry.password, 'utf8') > 4096 ||
/[\0-\x1f\x7f]/.test(entry.password)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`registry credential ${index} basic value is invalid`,
);
}
const userPassword = Buffer.from(
`${entry.username}:${entry.password}`,
'utf8',
);
try {
authorization = Buffer.from(
`Basic ${userPassword.toString('base64')}`,
'ascii',
);
} finally {
userPassword.fill(0);
}
} else if (scheme === 'bearer') {
if (
Object.keys(entry).sort().join(',') !== 'registry,scheme,token' ||
typeof entry.token !== 'string' ||
entry.token.length < 1 ||
entry.token.length > 8192 ||
!BEARER_TOKEN.test(entry.token)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`registry credential ${index} bearer value is invalid`,
);
}
authorization = Buffer.from(`Bearer ${entry.token}`, 'ascii');
} else {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`registry credential ${index} scheme is invalid`,
);
}
authorizations.set(registry, authorization);
}
return new LoadedClusterPluginPackageRegistryCredentialFile(authorizations);
} catch (error) {
zeroAuthorizations(authorizations);
throw error;
} finally {
scrubCredentialSource(value);
}
}
export function loadClusterPluginPackagePublisherTrustFileEvidence(
filePath: string,
): Readonly<ClusterPluginPackagePublisherTrustFileEvidence> {
if (
typeof filePath !== 'string' ||
filePath.length < 1 ||
filePath.length > 4096 ||
/[\0\r\n]/.test(filePath) ||
!isAbsolute(filePath)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file path is invalid',
);
}
let descriptor: number;
try {
descriptor = openSync(filePath, constants.O_RDONLY);
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file is unavailable',
);
}
let bytes: Buffer;
try {
const stat = fstatSync(descriptor);
if (
!stat.isFile() ||
(stat.mode & 0o022) !== 0 ||
!Number.isSafeInteger(stat.size) ||
stat.size < 1 ||
stat.size > MAX_TRUST_FILE_BYTES
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file is not a bounded read-only regular file',
);
}
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) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file changed while reading',
);
}
bytes = bytes.subarray(0, offset);
} finally {
closeSync(descriptor);
}
let value: unknown;
try {
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file is not valid JSON',
);
} finally {
bytes.fill(0);
}
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.keys(value).sort().join(',') !== 'keys,schema' ||
(value as { schema?: unknown }).schema !== TRUST_SCHEMA ||
!Array.isArray((value as { keys?: unknown }).keys)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file shape is invalid',
);
}
try {
const definitions = (
value as { keys: PluginPackagePublisherKeyDefinition[] }
).keys;
const frozenDefinitions = Object.freeze(
definitions.map((definition) => Object.freeze({ ...definition })),
);
return Object.freeze({
registry: new PluginPackagePublisherTrustRegistry(frozenDefinitions),
snapshot:
createPluginPackagePublisherTrustSnapshot(frozenDefinitions),
definitions: frozenDefinitions,
});
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust keys are invalid',
);
}
}
export function loadClusterPluginPackagePublisherTrustFile(
filePath: string,
): PluginPackagePublisherTrustRegistry {
return loadClusterPluginPackagePublisherTrustFileEvidence(filePath).registry;
}
async function productionKubernetesApi(): Promise<PluginPackageKubernetesConfigMapApi> {
const kubernetes = await import('@kubernetes/client-node');
const config = new kubernetes.KubeConfig();
config.loadFromCluster();
return config.makeApiClient(
kubernetes.CoreV1Api,
) as unknown as PluginPackageKubernetesConfigMapApi;
}
function processEvent(
config: Readonly<ClusterPluginPackageRecoveryProcessConfig>,
event: ClusterPluginPackageRecoveryProcessEvent['event'],
provenanceRecovery?: Readonly<ClusterPluginPackagePublisherProvenanceRecoveryResult>,
recovery?: Readonly<PluginPackageRecoveryCycleResult>,
taskPublicationRecovery?: Readonly<PluginPackageTaskPublicationRecoveryCycleResult>,
automationPublicationRecovery?: Readonly<PluginPackageAutomationPublicationRecoveryCycleResult>,
toolSnapshotRecovery?: Readonly<ProjectToolDefinitionSnapshotRecoveryCycleResult>,
): Readonly<ClusterPluginPackageRecoveryProcessEvent> {
return Object.freeze({
schemaVersion: 1,
component: 'qinglong3-plugin-package-recovery',
event,
clusterIdentity: config.clusterIdentity,
...(provenanceRecovery === undefined ? {} : { provenanceRecovery }),
...(recovery === undefined ? {} : { recovery }),
...(taskPublicationRecovery === undefined
? {}
: { taskPublicationRecovery }),
...(automationPublicationRecovery === undefined
? {}
: { automationPublicationRecovery }),
...(toolSnapshotRecovery === undefined ? {} : { toolSnapshotRecovery }),
});
}
async function emit(
sink: RunClusterPluginPackageRecoveryProcessOptions['emit'],
value: Readonly<ClusterPluginPackageRecoveryProcessEvent>,
): Promise<void> {
if (!sink) return;
try {
await sink(value);
} catch {
// Diagnostics cannot replace recovery or database close outcomes.
}
}
/** Runs exactly one admin recovery cycle and owns no resident authority. */
export async function runClusterPluginPackageRecoveryProcess(
options: RunClusterPluginPackageRecoveryProcessOptions,
): Promise<Readonly<ClusterPluginPackageRecoveryResult>> {
if (
!options ||
typeof options !== 'object' ||
(options.emit !== undefined && typeof options.emit !== 'function') ||
(options.openDatabase !== undefined &&
typeof options.openDatabase !== 'function') ||
(options.resourceByteSource !== undefined &&
(!options.resourceByteSource ||
typeof options.resourceByteSource.open !== 'function')) ||
(options.fetch !== undefined && typeof options.fetch !== 'function')
) {
throw new TypeError('Plugin Package recovery process options are invalid');
}
const config = loadClusterPluginPackageRecoveryProcessConfig(
options.environment,
);
const trustEvidence =
options.trust === undefined && options.stageAuthority === undefined
? loadClusterPluginPackagePublisherTrustFileEvidence(
config.publisherTrustFile,
)
: undefined;
const registryCredentials =
options.stageAuthority !== undefined ||
config.registryCredentialFile === undefined
? undefined
: loadClusterPluginPackageRegistryCredentialFile(
config.registryCredentialFile,
config.allowedRegistries,
);
try {
const api = options.api ?? (await productionKubernetesApi());
const openDatabase =
options.openDatabase ??
createPostgresDatabaseOpener({
role: 'package-executor',
connection: config.database.connection,
pool: config.database.pool,
onPoolError() {
// Awaited recovery queries and final close remain authoritative.
},
});
await emit(options.emit, processEvent(config, 'recovery_started'));
const result = await recoverClusterPluginPackages({
openDatabase,
api,
...(options.stageAuthority === undefined
? {
stageAuthorityFactory: async (pool: PostgresPool) => {
let effectiveTrust = options.trust;
if (effectiveTrust === undefined && trustEvidence !== undefined) {
const authority =
await new PostgresPluginPackagePublisherTrustAuthorityRepository(
pool,
).findAuthority(config.publisherTrustAuthorityId);
if (!authority) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'durable publisher trust authority is unavailable',
);
}
effectiveTrust =
createPluginPackagePublisherEffectiveTrustRegistry(
trustEvidence.definitions,
authority.effectiveSnapshot,
);
}
if (effectiveTrust === undefined) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust evidence is unavailable',
);
}
return new ClusterPluginPackageOciStageAuthority({
allowedRegistries: config.allowedRegistries,
trust: effectiveTrust,
...(registryCredentials === undefined
? {}
: { credentialProvider: registryCredentials }),
...(options.fetch === undefined
? {}
: { fetch: options.fetch }),
requestTimeoutMs: config.requestTimeoutMs,
});
},
}
: { stageAuthority: options.stageAuthority }),
...(options.resourceByteSource === undefined
? {}
: { resourceByteSource: options.resourceByteSource }),
trustAuthorityId: config.publisherTrustAuthorityId,
clusterIdentity: config.clusterIdentity,
namespace: config.namespace,
now: Date.now,
pageSize: config.pageSize,
maxPages: config.maxPages,
});
await emit(
options.emit,
processEvent(
config,
'recovery_completed',
result.provenanceRecovery,
result.recovery,
result.taskPublicationRecovery,
result.automationPublicationRecovery,
result.toolSnapshotRecovery,
),
);
return result;
} finally {
registryCredentials?.dispose();
}
}