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,409 @@
/** Plugin Package management service boundary. */
import { PostgresApprovalRequestRepository } from '@qinglong/cluster-postgres/approved-action';
import { PostgresPluginPackageInstallInventoryReader } from '@qinglong/cluster-postgres/package-manager';
import { PostgresPluginPackageInstallProposalRepository } from '@qinglong/cluster-postgres/plugin-package-proposal';
import { PostgresProjectPolicyRepository } from '@qinglong/cluster-postgres/project-policy';
import type { PostgresPool } from '@qinglong/runtime-core';
import {
PluginPackageManagementAuthorizationError,
PluginPackageManagementConflictError,
PluginPackageManagementQuotaExceededError,
PluginPackageManagementRequestError,
PluginPackageManagementUnavailableError,
createPluginPackageManagementService,
type InspectPluginPackageInstallResult,
type PluginPackageManagementQuotaPort,
type PluginPackageManagementService as RuntimePluginPackageManagementService,
} from '@qinglong/runtime-core/plugin-package-management';
import {
MAX_PLUGIN_PACKAGE_INSTALL_INVENTORY_PAGE_SIZE,
normalizePluginPackageInstallInventoryCursor,
type PluginPackageInstallInventoryItem,
type PluginPackageInstallInventoryPage,
} from '@qinglong/runtime-core/plugin-package-install';
import { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy';
import {
normalizeSecurityPrincipal,
type SecurityPolicyDecision,
type SecurityPrincipal,
} from '@qinglong/runtime-core/security';
export const CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_DECISION_MODE =
'separation_of_duty' as const;
type ClusterPluginPackageManagementMutationService = Pick<
RuntimePluginPackageManagementService,
'propose' | 'decide' | 'inspect'
>;
export interface InspectAuthorizedClusterPluginPackageRequest {
readonly actionRef: string;
readonly approvalRequestId: string;
readonly inspectionId: string;
readonly principal: SecurityPrincipal;
}
export interface InspectAuthorizedClusterPluginPackageInstallationRequest {
readonly projectId: string;
readonly packageName: string;
readonly inspectionId: string;
readonly principal: SecurityPrincipal;
}
export interface ListAuthorizedClusterPluginPackageInstallationsRequest {
readonly projectId: string;
readonly limit: number;
readonly after?: Readonly<{ packageName: string }>;
readonly inspectionId: string;
readonly principal: SecurityPrincipal;
}
export type ClusterPluginPackageManagementService =
ClusterPluginPackageManagementMutationService &
Readonly<{
inspectAuthorized(
request: InspectAuthorizedClusterPluginPackageRequest,
): Promise<Readonly<InspectPluginPackageInstallResult>>;
inspectInstallationAuthorized(
request: InspectAuthorizedClusterPluginPackageInstallationRequest,
): Promise<Readonly<PluginPackageInstallInventoryItem> | null>;
listInstallationsAuthorized(
request: ListAuthorizedClusterPluginPackageInstallationsRequest,
): Promise<Readonly<PluginPackageInstallInventoryPage>>;
}>;
export interface ClusterPluginPackageManagementOptions {
readonly pool: PostgresPool;
readonly approvalLifetimeMs?: number;
readonly now?: () => number;
readonly quota?: PluginPackageManagementQuotaPort;
}
const INSPECTION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
const PROJECT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const PACKAGE_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
export function createClusterPluginPackageManagementService(
options: ClusterPluginPackageManagementOptions,
): Readonly<ClusterPluginPackageManagementService> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) =>
key !== 'pool' &&
key !== 'approvalLifetimeMs' &&
key !== 'now' &&
key !== 'quota',
)
) {
throw new TypeError(
'cluster Plugin Package management options are invalid',
);
}
const now = options.now ?? Date.now;
const policy = new ProjectPolicyEngine(
new PostgresProjectPolicyRepository(options.pool),
);
const installations = new PostgresPluginPackageInstallInventoryReader(
options.pool,
);
const service = createPluginPackageManagementService(
policy,
new PostgresPluginPackageInstallProposalRepository(options.pool),
new PostgresApprovalRequestRepository(options.pool),
Object.freeze({
async dispatchBatch(): Promise<never> {
throw new Error(
'cluster Plugin Package management cannot execute approved actions',
);
},
}),
{
decisionMode: CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_DECISION_MODE,
consumer: Object.freeze({
subject: Object.freeze({
type: 'system' as const,
id: 'cluster_package_management_unreachable_consumer',
}),
authenticationId: 'cluster-package-management-unreachable-consumer',
}),
...(options.approvalLifetimeMs === undefined
? {}
: { approvalLifetimeMs: options.approvalLifetimeMs }),
now,
...(options.quota === undefined ? {} : { quota: options.quota }),
},
);
const allowed = (
decision: Readonly<SecurityPolicyDecision>,
allowApproval: boolean,
): boolean =>
decision.fence !== null &&
(decision.effect === 'allow' ||
(allowApproval && decision.effect === 'require_approval'));
const authorizeInstallationInventory = async (
projectId: string,
inspectionId: string,
principalValue: SecurityPrincipal,
): Promise<Readonly<SecurityPrincipal>> => {
const observedAtMs = now();
if (!Number.isSafeInteger(observedAtMs) || observedAtMs < 0) {
throw new PluginPackageManagementUnavailableError();
}
let principal: Readonly<SecurityPrincipal>;
try {
principal = normalizeSecurityPrincipal(principalValue, observedAtMs);
} catch {
throw new PluginPackageManagementAuthorizationError();
}
let decision: Readonly<SecurityPolicyDecision>;
try {
decision = await policy.authorize(principal, projectId, 'package.manage');
} catch (error) {
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
if (!allowed(decision, true)) {
throw new PluginPackageManagementAuthorizationError();
}
if (options.quota) {
try {
await options.quota.consume({
projectId,
subject: principal.subject,
operation: 'plugin-package.inspect',
idempotencyKey: inspectionId,
});
} catch (error) {
if (error instanceof PluginPackageManagementQuotaExceededError) {
throw error;
}
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
}
return principal;
};
return Object.freeze({
propose: service.propose,
decide: service.decide,
inspect: service.inspect,
async inspectInstallationAuthorized(
request: InspectAuthorizedClusterPluginPackageInstallationRequest,
): Promise<Readonly<PluginPackageInstallInventoryItem> | null> {
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
Object.keys(request).sort().join('\0') !==
['inspectionId', 'packageName', 'principal', 'projectId']
.sort()
.join('\0') ||
typeof request.projectId !== 'string' ||
!PROJECT_ID_PATTERN.test(request.projectId) ||
typeof request.packageName !== 'string' ||
!PACKAGE_NAME_PATTERN.test(request.packageName) ||
typeof request.inspectionId !== 'string' ||
!INSPECTION_ID_PATTERN.test(request.inspectionId)
) {
throw new PluginPackageManagementRequestError(
'installation inspection request is invalid',
);
}
await authorizeInstallationInventory(
request.projectId,
request.inspectionId,
request.principal,
);
try {
return await installations.findCurrent(
request.projectId,
request.packageName,
);
} catch (error) {
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
},
async listInstallationsAuthorized(
request: ListAuthorizedClusterPluginPackageInstallationsRequest,
): Promise<Readonly<PluginPackageInstallInventoryPage>> {
const keys =
request && typeof request === 'object' && !Array.isArray(request)
? Object.keys(request)
: [];
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
!keys.includes('projectId') ||
!keys.includes('limit') ||
!keys.includes('inspectionId') ||
!keys.includes('principal') ||
keys.some(
(key) =>
![
'after',
'inspectionId',
'limit',
'principal',
'projectId',
].includes(key),
) ||
typeof request.projectId !== 'string' ||
!PROJECT_ID_PATTERN.test(request.projectId) ||
!Number.isSafeInteger(request.limit) ||
request.limit < 1 ||
request.limit > MAX_PLUGIN_PACKAGE_INSTALL_INVENTORY_PAGE_SIZE ||
typeof request.inspectionId !== 'string' ||
!INSPECTION_ID_PATTERN.test(request.inspectionId)
) {
throw new PluginPackageManagementRequestError(
'installation list request is invalid',
);
}
let after: Readonly<{ packageName: string }> | undefined;
try {
after =
request.after === undefined
? undefined
: normalizePluginPackageInstallInventoryCursor(request.after);
} catch {
throw new PluginPackageManagementRequestError(
'installation list cursor is invalid',
);
}
await authorizeInstallationInventory(
request.projectId,
request.inspectionId,
request.principal,
);
try {
return await installations.listCurrentPage({
projectId: request.projectId,
limit: request.limit,
...(after === undefined ? {} : { after }),
});
} catch (error) {
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
},
async inspectAuthorized(
request: InspectAuthorizedClusterPluginPackageRequest,
): Promise<Readonly<InspectPluginPackageInstallResult>> {
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
Object.keys(request).length !== 4 ||
Object.keys(request).some(
(key) =>
![
'actionRef',
'approvalRequestId',
'inspectionId',
'principal',
].includes(key),
) ||
typeof request.inspectionId !== 'string' ||
!INSPECTION_ID_PATTERN.test(request.inspectionId)
) {
throw new PluginPackageManagementRequestError(
'inspection request is invalid',
);
}
const observedAtMs = now();
if (!Number.isSafeInteger(observedAtMs) || observedAtMs < 0) {
throw new PluginPackageManagementUnavailableError();
}
let principal: Readonly<SecurityPrincipal>;
try {
principal = normalizeSecurityPrincipal(request.principal, observedAtMs);
} catch {
throw new PluginPackageManagementAuthorizationError();
}
const current = await service.inspect(
request.actionRef,
request.approvalRequestId,
);
const projectId =
current.proposal?.projectId ?? current.approvalRequest?.projectId;
if (!projectId) {
throw new PluginPackageManagementConflictError(
'Plugin Package management state does not exist',
);
}
if (
(current.proposal &&
current.approvalRequest &&
(current.proposal.projectId !== current.approvalRequest.projectId ||
current.approvalRequest.action.actionRef !==
current.proposal.actionRef ||
current.approvalRequest.action.actionDigest !==
current.proposal.actionDigest ||
current.approvalRequest.action.previewDigest !==
current.proposal.previewDigest)) ||
(current.proposal &&
current.proposal.actionRef !== request.actionRef) ||
(current.approvalRequest &&
current.approvalRequest.id !== request.approvalRequestId)
) {
throw new PluginPackageManagementUnavailableError();
}
let packageDecision: Readonly<SecurityPolicyDecision>;
let approvalDecision: Readonly<SecurityPolicyDecision> | undefined;
try {
packageDecision = await policy.authorize(
principal,
projectId,
'package.manage',
);
if (!allowed(packageDecision, true)) {
approvalDecision = await policy.authorize(
principal,
projectId,
'approval.decide',
);
}
} catch (error) {
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
if (
!allowed(packageDecision, true) &&
(!approvalDecision || !allowed(approvalDecision, false))
) {
throw new PluginPackageManagementAuthorizationError();
}
if (options.quota) {
try {
await options.quota.consume({
projectId,
subject: principal.subject,
operation: 'plugin-package.inspect',
idempotencyKey: request.inspectionId,
});
} catch (error) {
if (error instanceof PluginPackageManagementQuotaExceededError) {
throw error;
}
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
}
return current;
},
});
}
@@ -0,0 +1,111 @@
#!/usr/bin/env node
/** One-shot Plugin Package management process CLI boundary. */
import {
startClusterPluginPackageManagementProcess,
type ClusterPluginPackageManagementProcessRuntime,
} from './pluginPackageManagementProcess';
const USAGE = 'Usage: ql3-plugin-package-manage';
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-management',
event: 'management_failed',
name:
typeof candidate?.name === 'string' && candidate.name.length <= 128
? candidate.name
: 'Error',
...(typeof candidate?.code === 'string' && candidate.code.length <= 128
? { code: candidate.code }
: {}),
});
}
function emit(value: Readonly<Record<string, unknown>>): void {
process.stdout.write(`${JSON.stringify(value)}\n`);
}
async function run(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_MANAGEMENT_CLI_USAGE_INVALID',
message: USAGE,
})}\n`,
);
process.exitCode = 64;
return;
}
let runtime: Readonly<ClusterPluginPackageManagementProcessRuntime>;
try {
runtime = await startClusterPluginPackageManagementProcess({
environment: process.env,
onError() {
emit({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management',
event: 'management_unavailable',
});
},
});
} catch (error) {
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
process.exitCode = 1;
return;
}
if (runtime.status === 'disabled') {
emit({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management',
event: 'management_disabled',
});
return;
}
emit({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management',
event: 'management_started',
address: runtime.address,
identityGeneration: runtime.identity.generation,
databaseContractVersion: runtime.database.contractVersion,
databaseMigrationCount: runtime.database.migrationIds.length,
});
let stopping: Promise<void> | undefined;
const stop = (): Promise<void> => {
stopping ??= runtime.close().then(() => {
emit({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management',
event: 'management_stopped',
});
});
return stopping;
};
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.once(signal, () => {
void stop().then(
() => {
process.exitCode = 0;
},
(error) => {
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
process.exitCode = 1;
},
);
});
}
}
void run(process.argv.slice(2));
@@ -0,0 +1,102 @@
#!/usr/bin/env node
/** One-shot Plugin Package management client CLI boundary. */
import {
ClusterPluginPackageManagementClientRemoteError,
executeClusterPluginPackageManagementClient,
} from '../../management-support/pluginPackageManagementClient';
const USAGE =
'Usage: ql3-plugin-package-client --config=/absolute/client.json --command=/absolute/command.json --assertion=/absolute/assertion.jwt';
function parseArguments(
argv: readonly string[],
): Readonly<{
configFile: string;
commandFile: string;
assertionFile: string;
}> | null {
if (argv.length !== 3) return null;
const values = new Map<string, string>();
for (const argument of argv) {
const match = /^--(config|command|assertion)=(\/.+)$/.exec(argument);
if (!match || values.has(match[1]!)) return null;
values.set(match[1]!, match[2]!);
}
if (
!values.has('config') ||
!values.has('command') ||
!values.has('assertion')
) {
return null;
}
return Object.freeze({
configFile: values.get('config')!,
commandFile: values.get('command')!,
assertionFile: values.get('assertion')!,
});
}
function failureFact(error: unknown): Readonly<Record<string, unknown>> {
const candidate = error as {
readonly code?: unknown;
};
return Object.freeze({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management-client',
event: 'command_failed',
code:
typeof candidate?.code === 'string' &&
candidate.code.length <= 128
? candidate.code
: 'QL3_PLUGIN_PACKAGE_MANAGEMENT_CLIENT_FAILED',
...(error instanceof ClusterPluginPackageManagementClientRemoteError
? {
statusCode: error.statusCode,
responseCode: error.responseCode,
requestId: error.requestId,
...(error.retryAfterSeconds === null
? {}
: { retryAfterSeconds: error.retryAfterSeconds }),
}
: {}),
});
}
async function run(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
process.stdout.write(`${USAGE}\n`);
return;
}
const paths = parseArguments(argv);
if (!paths) {
process.stderr.write(
`${JSON.stringify({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management-client',
event: 'usage_invalid',
code: 'QL3_PLUGIN_PACKAGE_MANAGEMENT_CLIENT_USAGE_INVALID',
})}\n`,
);
process.exitCode = 64;
return;
}
try {
const result =
await executeClusterPluginPackageManagementClient(paths);
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management-client',
event: 'command_completed',
requestId: result.requestId,
result: result.result,
})}\n`,
);
} catch (error) {
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
process.exitCode = 1;
}
}
void run(process.argv.slice(2));
@@ -0,0 +1,796 @@
/** Explicit Kubernetes PortForward client boundary for Plugin Package management. */
import {
createPrivateKey,
X509Certificate,
} from 'node:crypto';
import { Duplex, PassThrough, Writable } from 'node:stream';
import { TextDecoder } from 'node:util';
import {
ClusterPluginPackageManagementClientConfigurationError,
ClusterPluginPackageManagementClientRemoteError,
ClusterPluginPackageManagementClientRequestError,
executeClusterPluginPackageManagementClient,
readCanonicalFile,
type ClusterPluginPackageManagementClientPaths,
type ClusterPluginPackageManagementClientRawConnection,
type ClusterPluginPackageManagementClientResult,
} from '../../management-support/pluginPackageManagementClient';
const MAX_KUBERNETES_CONFIG_BYTES = 16 * 1024;
const MAX_KUBECONFIG_BYTES = 256 * 1024;
const MAX_KUBERNETES_CA_BYTES = 256 * 1024;
const MAX_KUBERNETES_CLIENT_MATERIAL_BYTES = 256 * 1024;
const MAX_KUBERNETES_TOKEN_BYTES = 16 * 1024;
const MANAGEMENT_NAME = 'ql3-plugin-package-management';
const MANAGEMENT_PORT = 8443;
const MANAGEMENT_LABEL_SELECTOR =
'app.kubernetes.io/name=ql3-plugin-package-management,' +
'app.kubernetes.io/component=plugin-package-management';
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
const DNS_LABEL_PATTERN =
/^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
const CONTEXT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,255}$/;
const POD_NAME_PATTERN =
/^ql3-plugin-package-management-[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?(?:-[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?)?$/;
const TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~+/-]{0,16383}$/;
type JsonObject = Record<string, unknown>;
type KubernetesModule = typeof import('@kubernetes/client-node', {
with: { 'resolution-mode': 'import' }
});
type KubernetesConfig = InstanceType<KubernetesModule['KubeConfig']>;
interface ReviewedKubernetesClientConfig {
readonly schemaVersion: 1;
readonly kubeconfigFile: string;
readonly context: string;
readonly namespace: string;
readonly apiTimeoutMs: number;
}
interface KubernetesPod {
readonly metadata?: {
readonly name?: string;
readonly namespace?: string;
readonly uid?: string;
readonly deletionTimestamp?: unknown;
readonly labels?: Readonly<Record<string, string>>;
};
readonly spec?: {
readonly serviceAccountName?: string;
readonly automountServiceAccountToken?: boolean;
readonly containers?: readonly Readonly<{ readonly name?: string }>[];
};
readonly status?: {
readonly phase?: string;
readonly conditions?: readonly Readonly<{
readonly type?: string;
readonly status?: string;
}>[];
readonly containerStatuses?: readonly Readonly<{
readonly name?: string;
readonly ready?: boolean;
}>[];
};
}
interface KubernetesPodList {
readonly metadata?: {
readonly continue?: string;
};
readonly items?: readonly KubernetesPod[];
}
export interface ClusterPluginPackageManagementKubernetesPodApi {
listNamespacedPod(
request: Readonly<{
namespace: string;
labelSelector: string;
limit: number;
timeoutSeconds: number;
watch: false;
}>,
): Promise<KubernetesPodList>;
}
export interface ClusterPluginPackageManagementKubernetesRuntime {
readonly pods: ClusterPluginPackageManagementKubernetesPodApi;
openPortForward(
request: Readonly<{
namespace: string;
podName: string;
port: 8443;
}>,
): Promise<ClusterPluginPackageManagementClientRawConnection>;
}
export interface ClusterPluginPackageManagementPortForwardWebSocket {
addEventListener(
type: 'close' | 'error',
listener: () => void,
): void;
close(): void;
}
export interface ClusterPluginPackageManagementPortForwardApi {
portForward(
namespace: string,
podName: string,
targetPorts: number[],
output: Writable,
error: Writable,
input: PassThrough,
retryCount: 0,
): Promise<
| ClusterPluginPackageManagementPortForwardWebSocket
| (() => ClusterPluginPackageManagementPortForwardWebSocket | null)
>;
}
export interface ClusterPluginPackageManagementKubernetesClientPaths
extends ClusterPluginPackageManagementClientPaths {
readonly kubernetesFile: string;
}
export interface ClusterPluginPackageManagementKubernetesClientOptions {
readonly createRuntime?: (
kubeConfig: KubernetesConfig,
kubernetes: KubernetesModule,
) => ClusterPluginPackageManagementKubernetesRuntime;
}
export class ClusterPluginPackageManagementKubernetesClientConfigurationError extends TypeError {
readonly code =
'QL3_PLUGIN_PACKAGE_MANAGEMENT_KUBERNETES_CLIENT_CONFIG_INVALID';
constructor() {
super('Kubernetes Plugin Package management client configuration is invalid');
this.name =
'ClusterPluginPackageManagementKubernetesClientConfigurationError';
}
}
export class ClusterPluginPackageManagementKubernetesClientTunnelError extends Error {
readonly code =
'QL3_PLUGIN_PACKAGE_MANAGEMENT_KUBERNETES_CLIENT_TUNNEL_FAILED';
constructor(readonly cause?: unknown) {
super('Kubernetes Plugin Package management tunnel failed');
this.name = 'ClusterPluginPackageManagementKubernetesClientTunnelError';
}
}
function configurationFailure(): ClusterPluginPackageManagementKubernetesClientConfigurationError {
return new ClusterPluginPackageManagementKubernetesClientConfigurationError();
}
function exactObject(
value: unknown,
expectedKeys: readonly string[],
): asserts value is JsonObject {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw configurationFailure();
}
const actual = Object.keys(value).sort();
const expected = [...expectedKeys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw configurationFailure();
}
}
function decodeUtf8(bytes: Buffer): string {
try {
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch {
throw configurationFailure();
}
}
function parseJson(bytes: Buffer): unknown {
try {
return JSON.parse(decodeUtf8(bytes));
} catch (error) {
if (
error instanceof
ClusterPluginPackageManagementKubernetesClientConfigurationError
) {
throw error;
}
throw configurationFailure();
}
}
function readPrivateFile(filePath: string, maximumBytes: number): Buffer {
try {
return readCanonicalFile(filePath, maximumBytes, 'private');
} catch {
throw configurationFailure();
}
}
function normalizeConfig(
value: unknown,
): Readonly<ReviewedKubernetesClientConfig> {
exactObject(value, [
'schemaVersion',
'kubeconfigFile',
'context',
'namespace',
'apiTimeoutMs',
]);
if (
value.schemaVersion !== 1 ||
typeof value.kubeconfigFile !== 'string' ||
typeof value.context !== 'string' ||
!CONTEXT_PATTERN.test(value.context) ||
typeof value.namespace !== 'string' ||
!DNS_LABEL_PATTERN.test(value.namespace) ||
!Number.isSafeInteger(value.apiTimeoutMs) ||
(value.apiTimeoutMs as number) < 1_000 ||
(value.apiTimeoutMs as number) > 30_000
) {
throw configurationFailure();
}
return Object.freeze({
schemaVersion: 1,
kubeconfigFile: value.kubeconfigFile,
context: value.context,
namespace: value.namespace,
apiTimeoutMs: value.apiTimeoutMs as number,
});
}
function decodeCanonicalBase64(
value: unknown,
maximumBytes: number,
): Buffer {
if (
typeof value !== 'string' ||
value.length < 4 ||
value.length > maximumBytes * 2 ||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
value,
)
) {
throw configurationFailure();
}
const bytes = Buffer.from(value, 'base64');
if (
bytes.length < 1 ||
bytes.length > maximumBytes ||
bytes.toString('base64') !== value
) {
bytes.fill(0);
throw configurationFailure();
}
return bytes;
}
function validateRawKubeconfig(
value: unknown,
config: Readonly<ReviewedKubernetesClientConfig>,
): void {
exactObject(value, [
'apiVersion',
'kind',
'clusters',
'users',
'contexts',
'current-context',
]);
if (
value.apiVersion !== 'v1' ||
value.kind !== 'Config' ||
value['current-context'] !== config.context ||
!Array.isArray(value.clusters) ||
value.clusters.length !== 1 ||
!Array.isArray(value.users) ||
value.users.length !== 1 ||
!Array.isArray(value.contexts) ||
value.contexts.length !== 1
) {
throw configurationFailure();
}
const clusterEntry = value.clusters[0];
const userEntry = value.users[0];
const contextEntry = value.contexts[0];
exactObject(clusterEntry, ['name', 'cluster']);
const rawCluster = clusterEntry.cluster;
exactObject(rawCluster, [
'server',
'certificate-authority-data',
]);
exactObject(userEntry, ['name', 'user']);
const rawUser = userEntry.user;
if (!rawUser || typeof rawUser !== 'object' || Array.isArray(rawUser)) {
throw configurationFailure();
}
exactObject(contextEntry, ['name', 'context']);
const rawContext = contextEntry.context;
exactObject(rawContext, [
'cluster',
'user',
'namespace',
]);
if (
typeof clusterEntry.name !== 'string' ||
!CONTEXT_PATTERN.test(clusterEntry.name) ||
typeof userEntry.name !== 'string' ||
!CONTEXT_PATTERN.test(userEntry.name) ||
contextEntry.name !== config.context ||
rawContext.cluster !== clusterEntry.name ||
rawContext.user !== userEntry.name ||
rawContext.namespace !== config.namespace
) {
throw configurationFailure();
}
const userKeys = Object.keys(rawUser).sort();
if (
JSON.stringify(userKeys) !== JSON.stringify(['token']) &&
JSON.stringify(userKeys) !==
JSON.stringify(
['client-certificate-data', 'client-key-data'].sort(),
)
) {
throw configurationFailure();
}
}
function validateKubeConfig(
kubeConfig: KubernetesConfig,
config: Readonly<ReviewedKubernetesClientConfig>,
): void {
kubeConfig.setCurrentContext(config.context);
if (kubeConfig.getCurrentContext() !== config.context) {
throw configurationFailure();
}
const context = kubeConfig.getContextObject(config.context);
const cluster = kubeConfig.getCurrentCluster();
const user = kubeConfig.getCurrentUser();
if (
!context ||
context.namespace !== config.namespace ||
!cluster ||
!user
) {
throw configurationFailure();
}
let server: URL;
try {
server = new URL(cluster.server);
} catch {
throw configurationFailure();
}
if (
server.protocol !== 'https:' ||
server.username !== '' ||
server.password !== '' ||
(server.pathname !== '' && server.pathname !== '/') ||
server.search !== '' ||
server.hash !== '' ||
server.hostname.length < 1 ||
cluster.skipTLSVerify !== false ||
cluster.proxyUrl != null ||
cluster.caFile != null ||
typeof cluster.caData !== 'string' ||
(cluster.tlsServerName != null &&
cluster.tlsServerName !== server.hostname)
) {
throw configurationFailure();
}
const ca = decodeCanonicalBase64(
cluster.caData,
MAX_KUBERNETES_CA_BYTES,
);
try {
new X509Certificate(ca);
} catch {
throw configurationFailure();
} finally {
ca.fill(0);
}
if (
user.exec != null ||
user.authProvider != null ||
user.certFile != null ||
user.keyFile != null ||
user.username != null ||
user.password != null ||
user.impersonateUser != null
) {
throw configurationFailure();
}
const hasToken = user.token != null;
const hasCertificate =
user.certData != null || user.keyData != null;
if (
hasToken === hasCertificate ||
(hasToken &&
(typeof user.token !== 'string' ||
Buffer.byteLength(user.token, 'utf8') >
MAX_KUBERNETES_TOKEN_BYTES ||
CONTROL_PATTERN.test(user.token) ||
!TOKEN_PATTERN.test(user.token)))
) {
throw configurationFailure();
}
if (hasCertificate) {
const certificate = decodeCanonicalBase64(
user.certData,
MAX_KUBERNETES_CLIENT_MATERIAL_BYTES,
);
const privateKey = decodeCanonicalBase64(
user.keyData,
MAX_KUBERNETES_CLIENT_MATERIAL_BYTES,
);
try {
const parsedCertificate = new X509Certificate(certificate);
const parsedPrivateKey = createPrivateKey(privateKey);
if (!parsedCertificate.checkPrivateKey(parsedPrivateKey)) {
throw configurationFailure();
}
} catch (error) {
if (
error instanceof
ClusterPluginPackageManagementKubernetesClientConfigurationError
) {
throw error;
}
throw configurationFailure();
} finally {
certificate.fill(0);
privateKey.fill(0);
}
}
}
function isReviewedPod(
value: KubernetesPod,
namespace: string,
): value is KubernetesPod & {
readonly metadata: {
readonly name: string;
readonly namespace: string;
readonly uid: string;
};
} {
const labels = value.metadata?.labels;
return (
typeof value.metadata?.name === 'string' &&
POD_NAME_PATTERN.test(value.metadata.name) &&
value.metadata.namespace === namespace &&
typeof value.metadata.uid === 'string' &&
value.metadata.uid.length >= 8 &&
value.metadata.uid.length <= 128 &&
!CONTROL_PATTERN.test(value.metadata.uid) &&
value.metadata.deletionTimestamp === undefined &&
labels?.['app.kubernetes.io/name'] === MANAGEMENT_NAME &&
labels?.['app.kubernetes.io/component'] ===
'plugin-package-management' &&
value.spec?.serviceAccountName === MANAGEMENT_NAME &&
value.spec?.automountServiceAccountToken === false &&
value.spec?.containers?.some(({ name }) => name === 'management') ===
true &&
value.status?.phase === 'Running' &&
value.status.conditions?.some(
({ type, status }) => type === 'Ready' && status === 'True',
) === true &&
value.status.containerStatuses?.some(
({ name, ready }) => name === 'management' && ready === true,
) === true
);
}
function selectManagementPod(
value: KubernetesPodList,
namespace: string,
): string {
if (
!value ||
typeof value !== 'object' ||
!Array.isArray(value.items) ||
value.items.length < 1 ||
value.items.length > 3 ||
(value.metadata?.continue !== undefined &&
value.metadata.continue !== '')
) {
throw new ClusterPluginPackageManagementKubernetesClientTunnelError();
}
const current = value.items.filter(
({ metadata }) => metadata?.deletionTimestamp === undefined,
);
if (
current.length < 1 ||
current.length > 2 ||
!current.every((pod) => isReviewedPod(pod, namespace))
) {
throw new ClusterPluginPackageManagementKubernetesClientTunnelError();
}
return current
.map(({ metadata }) => metadata!.name!)
.sort()[0]!;
}
function deadline<T>(
operation: Promise<T>,
timeoutMs: number,
disposeLate?: (value: T) => void | Promise<void>,
): Promise<T> {
return new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
reject(
new ClusterPluginPackageManagementKubernetesClientTunnelError(),
);
}, timeoutMs);
operation.then(
(value) => {
if (settled) {
void Promise.resolve(disposeLate?.(value)).catch(() => {});
return;
}
settled = true;
clearTimeout(timer);
resolve(value);
},
(error) => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(
error instanceof
ClusterPluginPackageManagementKubernetesClientTunnelError
? error
: new ClusterPluginPackageManagementKubernetesClientTunnelError(
error,
),
);
},
);
});
}
export async function openClusterPluginPackageManagementPortForward(
forward: ClusterPluginPackageManagementPortForwardApi,
request: Readonly<{
namespace: string;
podName: string;
port: 8443;
}>,
): Promise<ClusterPluginPackageManagementClientRawConnection> {
const incoming = new PassThrough();
const outgoing = new PassThrough();
let connection: Duplex | undefined;
let pendingError = false;
const errors = new Writable({
write(chunk: Buffer | string, _encoding, callback) {
const bytes = Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk);
const failed = bytes.length > 0;
bytes.fill(0);
if (failed) {
pendingError = true;
connection?.destroy(
new ClusterPluginPackageManagementKubernetesClientTunnelError(),
);
}
callback();
},
});
const handle = await forward.portForward(
request.namespace,
request.podName,
[request.port],
incoming,
errors,
outgoing,
0,
);
const webSocket =
typeof handle === 'function' ? handle() : handle;
if (!webSocket) {
throw new ClusterPluginPackageManagementKubernetesClientTunnelError();
}
const nodeStreamPair = {
readable: incoming,
writable: outgoing,
};
// Node supports a { readable, writable } pair of Node streams here, while
// @types/node@24.13.3 currently models only the equivalent Web Streams pair.
connection = Duplex.from(
nodeStreamPair as unknown as Parameters<typeof Duplex.from>[0],
);
if (pendingError) {
connection.destroy(
new ClusterPluginPackageManagementKubernetesClientTunnelError(),
);
}
let closed = false;
const tunnelFailure = () => {
if (!closed) {
connection?.destroy(
new ClusterPluginPackageManagementKubernetesClientTunnelError(),
);
}
};
webSocket.addEventListener('close', tunnelFailure);
webSocket.addEventListener('error', tunnelFailure);
return Object.freeze({
stream: connection,
close() {
if (closed) return;
closed = true;
connection?.end();
incoming.end();
outgoing.end();
errors.end();
webSocket.close();
},
});
}
function productionRuntime(
kubeConfig: KubernetesConfig,
kubernetes: KubernetesModule,
): ClusterPluginPackageManagementKubernetesRuntime {
const pods = kubeConfig.makeApiClient(
kubernetes.CoreV1Api,
) as unknown as ClusterPluginPackageManagementKubernetesPodApi;
const forward = new kubernetes.PortForward(
kubeConfig,
true,
) as unknown as ClusterPluginPackageManagementPortForwardApi;
const runtime: ClusterPluginPackageManagementKubernetesRuntime = {
pods,
openPortForward: (request) =>
openClusterPluginPackageManagementPortForward(
forward,
request,
),
};
return Object.freeze(runtime);
}
export async function executeClusterPluginPackageManagementKubernetesClient(
paths: ClusterPluginPackageManagementKubernetesClientPaths,
options: ClusterPluginPackageManagementKubernetesClientOptions = {},
): Promise<Readonly<ClusterPluginPackageManagementClientResult>> {
exactObject(paths, [
'configFile',
'commandFile',
'assertionFile',
'kubernetesFile',
]);
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some((key) => key !== 'createRuntime') ||
(options.createRuntime !== undefined &&
typeof options.createRuntime !== 'function')
) {
throw configurationFailure();
}
let kubernetesConfigBytes: Buffer | undefined;
let kubeconfigBytes: Buffer | undefined;
try {
kubernetesConfigBytes = readPrivateFile(
paths.kubernetesFile,
MAX_KUBERNETES_CONFIG_BYTES,
);
const config = normalizeConfig(parseJson(kubernetesConfigBytes));
kubeconfigBytes = readPrivateFile(
config.kubeconfigFile,
MAX_KUBECONFIG_BYTES,
);
const kubernetes = await import('@kubernetes/client-node');
const kubeConfig = new kubernetes.KubeConfig();
try {
const kubeconfigText = decodeUtf8(kubeconfigBytes);
validateRawKubeconfig(parseJson(kubeconfigBytes), config);
kubeConfig.loadFromString(kubeconfigText);
validateKubeConfig(kubeConfig, config);
} catch (error) {
if (
error instanceof
ClusterPluginPackageManagementKubernetesClientConfigurationError
) {
throw error;
}
throw configurationFailure();
}
const runtime = (options.createRuntime ?? productionRuntime)(
kubeConfig,
kubernetes,
);
if (
!runtime ||
typeof runtime !== 'object' ||
typeof runtime.pods?.listNamespacedPod !== 'function' ||
typeof runtime.openPortForward !== 'function'
) {
throw configurationFailure();
}
const expectedHostname =
`${MANAGEMENT_NAME}.${config.namespace}.svc`;
return await executeClusterPluginPackageManagementClient(
{
configFile: paths.configFile,
commandFile: paths.commandFile,
assertionFile: paths.assertionFile,
},
{
async connect(target) {
if (
target.hostname !== expectedHostname ||
target.port !== MANAGEMENT_PORT
) {
throw configurationFailure();
}
const list = await deadline(
runtime.pods.listNamespacedPod({
namespace: config.namespace,
labelSelector: MANAGEMENT_LABEL_SELECTOR,
limit: 3,
timeoutSeconds: Math.ceil(config.apiTimeoutMs / 1_000),
watch: false,
}),
config.apiTimeoutMs,
);
const podName = selectManagementPod(
list,
config.namespace,
);
return await deadline(
runtime.openPortForward({
namespace: config.namespace,
podName,
port: MANAGEMENT_PORT,
}),
config.apiTimeoutMs,
async (connection) => {
await connection.close();
},
);
},
},
);
} catch (error) {
if (
error instanceof ClusterPluginPackageManagementClientRequestError &&
error.cause instanceof
ClusterPluginPackageManagementKubernetesClientTunnelError
) {
throw error.cause;
}
if (
error instanceof
ClusterPluginPackageManagementKubernetesClientConfigurationError ||
error instanceof
ClusterPluginPackageManagementKubernetesClientTunnelError ||
error instanceof
ClusterPluginPackageManagementClientConfigurationError ||
error instanceof ClusterPluginPackageManagementClientRequestError ||
error instanceof ClusterPluginPackageManagementClientRemoteError
) {
throw error;
}
throw new ClusterPluginPackageManagementKubernetesClientTunnelError(
error,
);
} finally {
kubernetesConfigBytes?.fill(0);
kubeconfigBytes?.fill(0);
}
}
@@ -0,0 +1,113 @@
#!/usr/bin/env node
/** One-shot Kubernetes-tunneled Plugin Package management client CLI boundary. */
import {
ClusterPluginPackageManagementClientRemoteError,
} from '../../management-support/pluginPackageManagementClient';
import {
executeClusterPluginPackageManagementKubernetesClient,
} from './pluginPackageManagementKubernetesClient';
const USAGE =
'Usage: ql3-plugin-package-client-kubernetes ' +
'--config=/absolute/client.json --command=/absolute/command.json ' +
'--assertion=/absolute/assertion.jwt ' +
'--kubernetes=/absolute/kubernetes.json';
function parseArguments(
argv: readonly string[],
): Readonly<{
configFile: string;
commandFile: string;
assertionFile: string;
kubernetesFile: string;
}> | null {
if (argv.length !== 4) return null;
const values = new Map<string, string>();
for (const argument of argv) {
const match =
/^--(config|command|assertion|kubernetes)=(\/.+)$/.exec(argument);
if (!match || values.has(match[1]!)) return null;
values.set(match[1]!, match[2]!);
}
if (
!values.has('config') ||
!values.has('command') ||
!values.has('assertion') ||
!values.has('kubernetes')
) {
return null;
}
return Object.freeze({
configFile: values.get('config')!,
commandFile: values.get('command')!,
assertionFile: values.get('assertion')!,
kubernetesFile: values.get('kubernetes')!,
});
}
function failureFact(error: unknown): Readonly<Record<string, unknown>> {
const candidate = error as { readonly code?: unknown };
return Object.freeze({
schemaVersion: 1,
component:
'qinglong3-plugin-package-management-kubernetes-client',
event: 'command_failed',
code:
typeof candidate?.code === 'string' &&
candidate.code.length <= 128
? candidate.code
: 'QL3_PLUGIN_PACKAGE_MANAGEMENT_KUBERNETES_CLIENT_FAILED',
...(error instanceof ClusterPluginPackageManagementClientRemoteError
? {
statusCode: error.statusCode,
responseCode: error.responseCode,
requestId: error.requestId,
...(error.retryAfterSeconds === null
? {}
: { retryAfterSeconds: error.retryAfterSeconds }),
}
: {}),
});
}
async function run(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
process.stdout.write(`${USAGE}\n`);
return;
}
const paths = parseArguments(argv);
if (!paths) {
process.stderr.write(
`${JSON.stringify({
schemaVersion: 1,
component:
'qinglong3-plugin-package-management-kubernetes-client',
event: 'usage_invalid',
code:
'QL3_PLUGIN_PACKAGE_MANAGEMENT_KUBERNETES_CLIENT_USAGE_INVALID',
})}\n`,
);
process.exitCode = 64;
return;
}
try {
const result =
await executeClusterPluginPackageManagementKubernetesClient(paths);
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
component:
'qinglong3-plugin-package-management-kubernetes-client',
event: 'command_completed',
requestId: result.requestId,
result: result.result,
})}\n`,
);
} catch (error) {
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
process.exitCode = 1;
}
}
void run(process.argv.slice(2));
@@ -0,0 +1,710 @@
/** Optional bounded Plugin Package management process composition boundary. */
import type {
OpenPostgresDatabase,
PostgresDatabaseResource,
} from '@qinglong/runtime-core';
import type {
ObservePluginPackagePublisherTrustSnapshotInput,
ObservePluginPackagePublisherTrustSnapshotResult,
} from '@qinglong/runtime-core/plugin-package-publisher-trust';
import {
assertPostgresPackageManagerSchemaReady,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
loadPostgresCertificateAuthorityFile,
loadPostgresConnectionEnvironment,
PostgresPluginPackageIdentityKeysetLedgerRepository,
PostgresPluginPackageManagementQuotaRepository,
PostgresPluginPackagePublisherTrustAuthorityRepository,
type PostgresConnectionOptions,
type PostgresPoolOptions,
type PostgresSchemaReadinessReport,
} from '@qinglong/cluster-postgres/package-manager';
import {
createClusterPluginPackageIdentityKeysetFile,
type ClusterPluginPackageIdentityKeysetFile,
type ClusterPluginPackageIdentityKeysetSnapshot,
} from '../../management-support/pluginPackageIdentityKeyset';
import { createClusterPluginPackageManagementService } from './pluginPackageManagement';
import { createClusterPluginPackageLifecycleManagementService } from '../lifecycle/pluginPackageLifecycleManagement';
import {
loadClusterPluginPackagePublisherTrustFileEvidence,
type ClusterPluginPackagePublisherTrustFileEvidence,
} from '../recovery/pluginPackageRecoveryProcess';
import { createClusterPluginPackagePublisherTrustManagementService } from '../publisher/pluginPackagePublisherTrustManagement';
import {
startClusterPluginPackageManagementHttp,
type ClusterPluginPackageManagementHttpApplication,
type StartClusterPluginPackageManagementHttpOptions,
} from '../../management-support/pluginPackageManagementHttp';
import { createClusterPluginPackageManagementTransport } from './pluginPackageManagementTransport';
import {
absoluteManagementEnvironmentFile,
booleanManagementEnvironmentValue,
boundedManagementEnvironmentValue,
integerManagementEnvironmentValue,
readManagementTlsFile,
} from '../../management-support/managementProcessSupport';
const SAFE_HOST = /^[A-Za-z0-9][A-Za-z0-9.:-]{0,254}$/;
const SAFE_APPLICATION_NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,62}$/;
export type ClusterPluginPackageManagementProcessEnvironment = Readonly<
Record<string, string | undefined>
>;
export type ClusterPluginPackageManagementProcessConfig =
| Readonly<{
enabled: false;
}>
| Readonly<{
enabled: true;
profile: 'cluster-admin';
host: string;
port: number;
certificateFile: string;
privateKeyFile: string;
identityKeysetFile: string;
publisherTrust: Readonly<{
file: string;
authorityProjectId: string;
authorityId: string;
observerId: string;
}>;
approvalLifetimeMs: number;
quota: Readonly<{
windowMs: number;
proposeLimit: number;
decideLimit: number;
inspectLimit: number;
}>;
http: Readonly<{
maxBodyBytes: number;
maxConnections: number;
maxConcurrentRequests: number;
requestTimeoutMs: number;
drainTimeoutMs: number;
rateWindowMs: number;
peerRequestLimit: number;
globalRequestLimit: number;
maxRateLimitPeers: number;
}>;
database: Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
}>;
}>;
export type ClusterPluginPackageManagementProcessRuntime =
| Readonly<{
status: 'disabled';
close(): Promise<void>;
}>
| Readonly<{
status: 'active';
address: Readonly<{ host: string; port: number }>;
database: PostgresSchemaReadinessReport;
identity: ClusterPluginPackageIdentityKeysetSnapshot;
publisherTrust: Readonly<{
generation: number;
baseSnapshotDigest: string;
effectiveTrustDigest: string;
}>;
availabilityStatus(): 'ready' | 'unavailable' | 'stopped';
close(): Promise<void>;
}>;
export interface StartClusterPluginPackageManagementProcessOptions {
readonly environment: ClusterPluginPackageManagementProcessEnvironment;
readonly openDatabase?: OpenPostgresDatabase;
readonly identities?: ClusterPluginPackageIdentityKeysetFile;
readonly publisherTrustEvidence?: ClusterPluginPackagePublisherTrustFileEvidence;
readonly observePublisherTrust?: (
pool: PostgresDatabaseResource['pool'],
input: ObservePluginPackagePublisherTrustSnapshotInput,
) => Promise<Readonly<ObservePluginPackagePublisherTrustSnapshotResult>>;
readonly assertReady?: (
pool: PostgresDatabaseResource['pool'],
) => Promise<PostgresSchemaReadinessReport>;
readonly startHttp?: (
options: StartClusterPluginPackageManagementHttpOptions,
) => Promise<Readonly<ClusterPluginPackageManagementHttpApplication>>;
readonly now?: () => number;
readonly onError?: (error: unknown) => void;
}
export class ClusterPluginPackageManagementProcessConfigError extends TypeError {
readonly code = 'QL3_PLUGIN_PACKAGE_MANAGEMENT_PROCESS_CONFIG_INVALID';
constructor(message: string) {
super(
`Plugin Package management process configuration is invalid: ${message}`,
);
this.name = 'ClusterPluginPackageManagementProcessConfigError';
}
}
function configFailure(
message: string,
): ClusterPluginPackageManagementProcessConfigError {
return new ClusterPluginPackageManagementProcessConfigError(message);
}
function boundedValue(
environment: ClusterPluginPackageManagementProcessEnvironment,
name: string,
maximumLength: number,
required = false,
): string | undefined {
return boundedManagementEnvironmentValue(
environment,
name,
maximumLength,
configFailure,
required,
);
}
function booleanValue(
environment: ClusterPluginPackageManagementProcessEnvironment,
name: string,
): boolean {
return booleanManagementEnvironmentValue(environment, name, configFailure);
}
function integerValue(
environment: ClusterPluginPackageManagementProcessEnvironment,
name: string,
fallback: number,
minimum: number,
maximum: number,
): number {
return integerManagementEnvironmentValue(
environment,
name,
fallback,
minimum,
maximum,
configFailure,
);
}
function absoluteFile(
environment: ClusterPluginPackageManagementProcessEnvironment,
name: string,
): string {
return absoluteManagementEnvironmentFile(environment, name, configFailure);
}
function loadConnection(
environment: ClusterPluginPackageManagementProcessEnvironment,
): Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
}> {
let connection: PostgresConnectionOptions;
try {
connection = loadPostgresConnectionEnvironment(environment, {
connectionString: 'QL3_POSTGRES_PACKAGE_MANAGER_URL',
host: 'QL3_POSTGRES_PACKAGE_MANAGER_HOST',
port: 'QL3_POSTGRES_PACKAGE_MANAGER_PORT',
database: 'QL3_POSTGRES_PACKAGE_MANAGER_DATABASE',
user: 'QL3_POSTGRES_PACKAGE_MANAGER_USER',
password: 'QL3_POSTGRES_PACKAGE_MANAGER_PASSWORD',
});
} catch (error) {
throw configFailure(
error instanceof Error
? error.message
: 'PostgreSQL Package manager connection is invalid',
);
}
const mode =
environment.QL3_POSTGRES_PACKAGE_MANAGER_TLS_MODE ?? 'verify-full';
if (mode !== 'verify-full' && mode !== 'disable') {
throw configFailure(
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_MODE must be verify-full or disable',
);
}
if (
mode === 'disable' &&
!booleanValue(environment, 'QL3_POSTGRES_PACKAGE_MANAGER_ALLOW_INSECURE')
) {
throw configFailure(
'disabling Package manager PostgreSQL TLS requires QL3_POSTGRES_PACKAGE_MANAGER_ALLOW_INSECURE=true',
);
}
const servername = boundedValue(
environment,
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_SERVERNAME',
253,
);
if (mode === 'verify-full' && !isPostgresTlsDnsServername(servername)) {
throw configFailure(
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_SERVERNAME must be an explicit DNS name',
);
}
const caFile = boundedValue(
environment,
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_CA_FILE',
4_096,
);
if (mode === 'disable' && caFile !== undefined) {
throw configFailure(
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_CA_FILE cannot be used when TLS is disabled',
);
}
let ca: string | undefined;
if (caFile !== undefined) {
try {
ca = loadPostgresCertificateAuthorityFile(caFile);
} catch {
throw configFailure(
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_CA_FILE is invalid',
);
}
}
const applicationName =
boundedValue(
environment,
'QL3_POSTGRES_PACKAGE_MANAGER_APPLICATION_NAME',
63,
) ?? 'qinglong3-plugin-package-manager';
if (!SAFE_APPLICATION_NAME.test(applicationName)) {
throw configFailure(
'QL3_POSTGRES_PACKAGE_MANAGER_APPLICATION_NAME is invalid',
);
}
return Object.freeze({
connection: Object.freeze({
...connection,
tls:
mode === 'disable'
? { mode: 'disable' as const }
: {
mode: 'verify-full' as const,
servername: servername!,
...(ca === undefined ? {} : { ca }),
},
}),
pool: Object.freeze({
applicationName,
maxConnections: integerValue(
environment,
'QL3_POSTGRES_PACKAGE_MANAGER_MAX_CONNECTIONS',
2,
1,
4,
),
connectionTimeoutMs: integerValue(
environment,
'QL3_POSTGRES_PACKAGE_MANAGER_CONNECTION_TIMEOUT_MS',
5_000,
100,
60_000,
),
}),
});
}
export function loadClusterPluginPackageManagementProcessConfig(
environment: ClusterPluginPackageManagementProcessEnvironment,
): Readonly<ClusterPluginPackageManagementProcessConfig> {
if (!environment || typeof environment !== 'object') {
throw configFailure('environment is invalid');
}
if (!booleanValue(environment, 'QL3_PLUGIN_PACKAGE_MANAGEMENT_ENABLED')) {
return Object.freeze({ enabled: false as const });
}
if (environment.QL3_PROFILE !== 'cluster-admin') {
throw configFailure(
'QL3_PROFILE must be cluster-admin when management is enabled',
);
}
const host =
boundedValue(environment, 'QL3_PLUGIN_PACKAGE_MANAGEMENT_HOST', 255) ??
'0.0.0.0';
if (!SAFE_HOST.test(host)) {
throw configFailure('QL3_PLUGIN_PACKAGE_MANAGEMENT_HOST is invalid');
}
const http = Object.freeze({
maxBodyBytes: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_MAX_BODY_BYTES',
64 * 1024,
1_024,
256 * 1024,
),
maxConnections: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_MAX_CONNECTIONS',
64,
1,
512,
),
maxConcurrentRequests: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_MAX_CONCURRENT_REQUESTS',
32,
1,
256,
),
requestTimeoutMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_REQUEST_TIMEOUT_MS',
10_000,
1_000,
60_000,
),
drainTimeoutMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_DRAIN_TIMEOUT_MS',
5_000,
100,
60_000,
),
rateWindowMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_RATE_WINDOW_MS',
60_000,
1_000,
5 * 60_000,
),
peerRequestLimit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_PEER_REQUEST_LIMIT',
60,
1,
10_000,
),
globalRequestLimit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_GLOBAL_REQUEST_LIMIT',
600,
1,
100_000,
),
maxRateLimitPeers: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_MAX_RATE_LIMIT_PEERS',
1_024,
1,
16_384,
),
});
if (http.globalRequestLimit < http.peerRequestLimit) {
throw configFailure(
'global request limit cannot be below the peer request limit',
);
}
return Object.freeze({
enabled: true as const,
profile: 'cluster-admin' as const,
host,
port: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_PORT',
8_443,
1,
65_535,
),
certificateFile: absoluteFile(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_TLS_CERT_FILE',
),
privateKeyFile: absoluteFile(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_TLS_KEY_FILE',
),
identityKeysetFile: absoluteFile(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_IDENTITY_KEYSET_FILE',
),
publisherTrust: Object.freeze({
file: absoluteFile(
environment,
'QL3_PLUGIN_PACKAGE_PUBLISHER_TRUST_FILE',
),
authorityProjectId: boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_TRUST_AUTHORITY_PROJECT_ID',
128,
true,
)!,
authorityId:
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_TRUST_AUTHORITY_ID',
128,
) ?? 'cluster',
observerId:
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_TRUST_OBSERVER_ID',
128,
) ?? 'cluster-package-manager',
}),
approvalLifetimeMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_APPROVAL_LIFETIME_MS',
15 * 60_000,
1_000,
24 * 60 * 60_000,
),
quota: Object.freeze({
windowMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_QUOTA_WINDOW_MS',
60_000,
1_000,
5 * 60_000,
),
proposeLimit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_PROPOSE_QUOTA',
30,
1,
1_000,
),
decideLimit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_DECIDE_QUOTA',
60,
1,
1_000,
),
inspectLimit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_INSPECT_QUOTA',
600,
1,
1_000,
),
}),
http,
database: loadConnection(environment),
});
}
function readTlsFile(filePath: string, privateMaterial: boolean): Buffer {
return readManagementTlsFile(filePath, privateMaterial, configFailure);
}
export async function startClusterPluginPackageManagementProcess(
options: StartClusterPluginPackageManagementProcessOptions,
): Promise<Readonly<ClusterPluginPackageManagementProcessRuntime>> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) =>
![
'environment',
'openDatabase',
'identities',
'publisherTrustEvidence',
'observePublisherTrust',
'assertReady',
'startHttp',
'now',
'onError',
].includes(key),
) ||
!options.environment ||
typeof options.environment !== 'object' ||
(options.openDatabase !== undefined &&
typeof options.openDatabase !== 'function') ||
(options.identities !== undefined &&
(typeof options.identities.reload !== 'function' ||
typeof options.identities.bind !== 'function')) ||
(options.publisherTrustEvidence !== undefined &&
(!options.publisherTrustEvidence ||
typeof options.publisherTrustEvidence !== 'object' ||
!options.publisherTrustEvidence.registry ||
!options.publisherTrustEvidence.snapshot)) ||
(options.observePublisherTrust !== undefined &&
typeof options.observePublisherTrust !== 'function') ||
(options.assertReady !== undefined &&
typeof options.assertReady !== 'function') ||
(options.startHttp !== undefined &&
typeof options.startHttp !== 'function') ||
(options.now !== undefined && typeof options.now !== 'function') ||
(options.onError !== undefined && typeof options.onError !== 'function')
) {
throw configFailure('options are invalid');
}
const config = loadClusterPluginPackageManagementProcessConfig(
options.environment,
);
if (!config.enabled) {
return Object.freeze({
status: 'disabled' as const,
close: () => Promise.resolve(),
});
}
const now = options.now ?? Date.now;
let http: Readonly<ClusterPluginPackageManagementHttpApplication> | undefined;
let database: PostgresDatabaseResource | undefined;
let unavailableError: unknown;
let closePromise: Promise<void> | undefined;
const report = (error: unknown): void => {
try {
options.onError?.(error);
} catch {
// Diagnostics do not own availability or cleanup.
}
};
const openDatabase =
options.openDatabase ??
createPostgresDatabaseOpener({
role: 'package-manager',
connection: config.database.connection,
pool: config.database.pool,
onPoolError(error) {
const firstAvailabilityError = unavailableError === undefined;
unavailableError ??= error;
http?.withdraw(error);
if (firstAvailabilityError) report(error);
},
});
try {
database = await openDatabase();
const evidence = await (
options.assertReady ?? assertPostgresPackageManagerSchemaReady
)(database.pool);
if (unavailableError !== undefined) throw unavailableError;
const identities =
options.identities ??
createClusterPluginPackageIdentityKeysetFile({
filePath: config.identityKeysetFile,
now,
ledger: new PostgresPluginPackageIdentityKeysetLedgerRepository(
database.pool,
),
});
const identity = await identities.reload();
const publisherTrustEvidence =
options.publisherTrustEvidence ??
loadClusterPluginPackagePublisherTrustFileEvidence(
config.publisherTrust.file,
);
const publisherTrustObservation = await (
options.observePublisherTrust ??
(async (pool, input) =>
new PostgresPluginPackagePublisherTrustAuthorityRepository(
pool,
).observeSnapshot(input))
)(database.pool, {
authorityId: config.publisherTrust.authorityId,
snapshot: publisherTrustEvidence.snapshot,
observedBy: config.publisherTrust.observerId,
observedAtMs: now(),
});
const quota = new PostgresPluginPackageManagementQuotaRepository(
database.pool,
{
windowMs: config.quota.windowMs,
limits: {
'plugin-package.propose': config.quota.proposeLimit,
'plugin-package.decide': config.quota.decideLimit,
'plugin-package.inspect': config.quota.inspectLimit,
},
},
);
const service = createClusterPluginPackageManagementService({
pool: database.pool,
approvalLifetimeMs: config.approvalLifetimeMs,
now,
quota,
});
const lifecycle =
createClusterPluginPackageLifecycleManagementService({
pool: database.pool,
approvalLifetimeMs: config.approvalLifetimeMs,
now,
});
const publisherTrust =
createClusterPluginPackagePublisherTrustManagementService({
pool: database.pool,
authorityProjectId: config.publisherTrust.authorityProjectId,
trustAuthorityId: config.publisherTrust.authorityId,
materialSnapshot: publisherTrustEvidence.snapshot,
approvalLifetimeMs: config.approvalLifetimeMs,
now,
quota,
});
const transport = createClusterPluginPackageManagementTransport({
service,
lifecycle,
publisherTrust,
now,
});
const privateKey = readTlsFile(config.privateKeyFile, true);
try {
const certificate = readTlsFile(config.certificateFile, false);
http = await (
options.startHttp ?? startClusterPluginPackageManagementHttp
)({
host: config.host,
port: config.port,
tls: { privateKey, certificate },
transport,
identities,
limits: config.http,
now,
onError: report,
});
} finally {
privateKey.fill(0);
}
if (unavailableError !== undefined) {
http.withdraw(unavailableError);
}
return Object.freeze({
status: 'active' as const,
address: http.address,
database: evidence,
identity,
publisherTrust: Object.freeze({
generation: publisherTrustObservation.head.generation,
baseSnapshotDigest:
publisherTrustObservation.head.baseSnapshotDigest,
effectiveTrustDigest:
publisherTrustObservation.head.effectiveTrustDigest,
}),
availabilityStatus: () => http?.availabilityStatus() ?? 'stopped',
close(): Promise<void> {
if (closePromise) return closePromise;
closePromise = (async () => {
let primaryError: unknown;
try {
await http?.close();
} catch (error) {
primaryError = error;
}
try {
await database?.close();
} catch (error) {
primaryError ??= error;
}
if (primaryError) throw primaryError;
})();
return closePromise;
},
});
} catch (error) {
try {
await http?.close();
} catch {
// Preserve startup failure.
}
try {
await database?.close();
} catch {
// Preserve startup failure.
}
throw error;
}
}