mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
postgresqlControlSchemaContract,
|
||||
postgresqlMainMigrationManifest,
|
||||
} = require('@qinglong/cluster-postgres');
|
||||
const {
|
||||
MAX_PLUGIN_PACKAGE_RECOVERY_PAGES,
|
||||
} = require('@qinglong/runtime-core/plugin-package-recovery');
|
||||
const {
|
||||
recoverClusterPluginPackages,
|
||||
} = require('@qinglong/cluster-admin/plugin-package-recovery');
|
||||
|
||||
function history() {
|
||||
return postgresqlMainMigrationManifest.migrations.map((migration, index) => ({
|
||||
streamId: postgresqlMainMigrationManifest.id,
|
||||
dialect: postgresqlMainMigrationManifest.dialect,
|
||||
migrationId: migration.id,
|
||||
checksum: migration.checksum,
|
||||
appliedAtMs: index + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
function executorPrivileges() {
|
||||
const insertable = new Set([
|
||||
'security_audit_events',
|
||||
'plugin_package_installs',
|
||||
'plugin_package_install_heads',
|
||||
'plugin_package_install_mutations',
|
||||
'approved_action_dispatches',
|
||||
'approved_action_executions',
|
||||
'plugin_package_admission_receipts',
|
||||
'plugin_package_materialized_revisions',
|
||||
'plugin_package_automation_publications',
|
||||
'plugin_package_automation_publication_heads',
|
||||
'project_tool_definition_snapshots',
|
||||
'project_tool_definition_snapshot_sources',
|
||||
'plugin_package_publisher_provenance',
|
||||
'plugin_package_publisher_revocation_receipts',
|
||||
'plugin_package_publisher_revocation_impacts',
|
||||
'plugin_package_publisher_revocation_impact_items',
|
||||
'plugin_package_publisher_trust_snapshots',
|
||||
'plugin_package_publisher_trust_transition_receipts',
|
||||
'plugin_package_lifecycle_plans',
|
||||
]);
|
||||
const readable = new Set([
|
||||
'schema_migrations',
|
||||
'schema_capabilities',
|
||||
'projects',
|
||||
'project_role_bindings',
|
||||
'approval_requests',
|
||||
'plugin_package_install_proposals',
|
||||
'plugin_package_task_ownerships',
|
||||
'plugin_package_task_reconciliations',
|
||||
'plugin_package_task_reconciliation_items',
|
||||
'plugin_package_quarantine_events',
|
||||
'plugin_package_withdrawal_receipts',
|
||||
'plugin_package_withdrawal_tasks',
|
||||
'plugin_package_publisher_provenance',
|
||||
'plugin_package_publisher_revocation_receipts',
|
||||
'plugin_package_publisher_revocation_impacts',
|
||||
'plugin_package_publisher_revocation_impact_items',
|
||||
'plugin_package_publisher_trust_snapshots',
|
||||
'plugin_package_publisher_trust_heads',
|
||||
'plugin_package_publisher_revocation_proposals',
|
||||
'plugin_package_publisher_trust_transition_proposals',
|
||||
'plugin_package_publisher_trust_transition_receipts',
|
||||
'plugin_package_lifecycle_events',
|
||||
'plugin_package_lifecycle_heads',
|
||||
'plugin_package_lifecycle_receipts',
|
||||
'plugin_package_lifecycle_tasks',
|
||||
'task_definitions',
|
||||
'task_definition_revisions',
|
||||
'task_execution_revisions',
|
||||
...insertable,
|
||||
]);
|
||||
return postgresqlControlSchemaContract.tables.map(({ name: tableName }) => ({
|
||||
tableName,
|
||||
selectAllowed: readable.has(tableName),
|
||||
insertAllowed: insertable.has(tableName),
|
||||
updateAllowed: [
|
||||
'plugin_package_installs',
|
||||
'plugin_package_install_heads',
|
||||
'approval_requests',
|
||||
'approved_action_executions',
|
||||
'plugin_package_publisher_trust_heads',
|
||||
'plugin_package_automation_publication_heads',
|
||||
].includes(tableName),
|
||||
deleteAllowed: false,
|
||||
isOwner: false,
|
||||
}));
|
||||
}
|
||||
|
||||
function database(serverVersionNum = '160014') {
|
||||
const contract = postgresqlControlSchemaContract;
|
||||
let closes = 0;
|
||||
const resource = {
|
||||
pool: {
|
||||
async query(text) {
|
||||
if (text.includes("current_setting('server_version_num')")) {
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
serverVersionNum,
|
||||
currentUser: 'ql3_package_executor',
|
||||
inRecovery: false,
|
||||
transactionReadOnly: 'off',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (text.includes('FROM "ql3"."schema_migrations"')) {
|
||||
return { rows: history() };
|
||||
}
|
||||
if (text.includes('FROM "ql3"."schema_capabilities"')) {
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
contractName: contract.contractName,
|
||||
contractVersion: contract.contractVersion,
|
||||
migrationId: contract.migrationId,
|
||||
capabilities: contract.capabilities,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (text.includes('FROM pg_class tables')) {
|
||||
return {
|
||||
rows: contract.tables.flatMap((table) =>
|
||||
table.columns.map((columnName) => ({
|
||||
tableName: table.name,
|
||||
columnName,
|
||||
})),
|
||||
),
|
||||
};
|
||||
}
|
||||
if (text.includes('FROM pg_indexes')) {
|
||||
return { rows: contract.indexes.map((indexName) => ({ indexName })) };
|
||||
}
|
||||
if (text.includes('FROM pg_constraint')) {
|
||||
return {
|
||||
rows: [
|
||||
...contract.checks.map((constraintName) => ({
|
||||
constraintName,
|
||||
constraintType: 'check',
|
||||
})),
|
||||
...contract.foreignKeys.map((constraintName) => ({
|
||||
constraintName,
|
||||
constraintType: 'foreign_key',
|
||||
})),
|
||||
],
|
||||
};
|
||||
}
|
||||
if (text.includes('FROM pg_proc routines')) {
|
||||
return {
|
||||
rows: contract.functions.map((definition) => ({
|
||||
functionName: definition.name,
|
||||
identityArguments: definition.identityArguments,
|
||||
owner: definition.owner,
|
||||
securityDefiner: definition.securityDefiner,
|
||||
volatility: definition.volatility,
|
||||
configuration: definition.configuration,
|
||||
publicExecute: false,
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (text.includes('FROM pg_catalog.pg_roles')) {
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
canLogin: true,
|
||||
superuser: false,
|
||||
createDatabase: false,
|
||||
createRole: false,
|
||||
replication: false,
|
||||
bypassRowLevelSecurity: false,
|
||||
databaseConnect: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (text.includes('has_schema_privilege')) {
|
||||
return { rows: [{ schemaUsage: true, schemaCreate: false }] };
|
||||
}
|
||||
if (text.includes('has_table_privilege')) {
|
||||
return { rows: executorPrivileges() };
|
||||
}
|
||||
if (text.includes('has_function_privilege')) {
|
||||
return {
|
||||
rows: contract.functions.map(({ name: functionName }) => ({
|
||||
functionName,
|
||||
executeAllowed: ![
|
||||
'enforce_plugin_package_stage_provenance',
|
||||
'plugin_package_automation_start_allowed',
|
||||
'plugin_package_run_start_allowed',
|
||||
'plugin_package_tool_start_allowed',
|
||||
'plugin_package_workflow_admission_snapshot',
|
||||
'plugin_package_workflow_task_attempt_snapshot',
|
||||
].includes(functionName),
|
||||
isOwner: false,
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (text.includes('plugin_package_install_heads')) {
|
||||
return { rows: [] };
|
||||
}
|
||||
throw new Error(`unexpected query: ${text}`);
|
||||
},
|
||||
async connect() {
|
||||
throw new Error('empty recovery must not open a transaction');
|
||||
},
|
||||
},
|
||||
async close() {
|
||||
closes += 1;
|
||||
},
|
||||
};
|
||||
return { resource, closes: () => closes };
|
||||
}
|
||||
|
||||
function options(db, overrides = {}) {
|
||||
const unavailable = async () => {
|
||||
throw new Error('empty recovery must not use Kubernetes');
|
||||
};
|
||||
return {
|
||||
openDatabase: async () => db.resource,
|
||||
api: {
|
||||
readNamespacedConfigMap: unavailable,
|
||||
createNamespacedConfigMap: unavailable,
|
||||
replaceNamespacedConfigMap: unavailable,
|
||||
},
|
||||
stageAuthority: {
|
||||
stage: unavailable,
|
||||
verify: unavailable,
|
||||
publisherEvidence: unavailable,
|
||||
},
|
||||
resourceByteSource: { open: unavailable },
|
||||
clusterIdentity: 'cluster-test',
|
||||
trustAuthorityId: 'cluster',
|
||||
namespace: 'qinglong',
|
||||
now: () => 1_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('rejects invalid bounds before opening PostgreSQL', async () => {
|
||||
const db = database();
|
||||
let opens = 0;
|
||||
await assert.rejects(
|
||||
recoverClusterPluginPackages(
|
||||
options(db, {
|
||||
maxPages: MAX_PLUGIN_PACKAGE_RECOVERY_PAGES + 1,
|
||||
openDatabase: async () => {
|
||||
opens += 1;
|
||||
return db.resource;
|
||||
},
|
||||
}),
|
||||
),
|
||||
/configuration is invalid/,
|
||||
);
|
||||
assert.equal(opens, 0);
|
||||
assert.equal(db.closes(), 0);
|
||||
});
|
||||
|
||||
test('proves an empty executor queue and closes PostgreSQL before returning', async () => {
|
||||
const db = database();
|
||||
const result = await recoverClusterPluginPackages(options(db));
|
||||
|
||||
assert.equal(result.evidence.currentUser, 'ql3_package_executor');
|
||||
assert.deepEqual(result.provenanceRecovery, {
|
||||
pages: 1,
|
||||
scanned: 0,
|
||||
created: 0,
|
||||
existing: 0,
|
||||
remaining: false,
|
||||
safeToAdmit: true,
|
||||
});
|
||||
assert.deepEqual(result.recovery, {
|
||||
pages: 1,
|
||||
scanned: 0,
|
||||
settled: 0,
|
||||
retry: 0,
|
||||
manualRequired: 0,
|
||||
superseded: 0,
|
||||
remaining: false,
|
||||
safeToAdmit: true,
|
||||
});
|
||||
assert.deepEqual(result.taskPublicationRecovery, {
|
||||
pages: 1,
|
||||
scanned: 0,
|
||||
settled: 0,
|
||||
retry: 0,
|
||||
manualRequired: 0,
|
||||
superseded: 0,
|
||||
remaining: false,
|
||||
safeToAdmit: true,
|
||||
});
|
||||
assert.deepEqual(result.automationPublicationRecovery, {
|
||||
pages: 1,
|
||||
scanned: 0,
|
||||
settled: 0,
|
||||
retry: 0,
|
||||
manualRequired: 0,
|
||||
superseded: 0,
|
||||
remaining: false,
|
||||
safeToAdmit: true,
|
||||
});
|
||||
assert.deepEqual(result.toolSnapshotRecovery, {
|
||||
pages: 1,
|
||||
scanned: 0,
|
||||
settled: 0,
|
||||
retry: 0,
|
||||
manualRequired: 0,
|
||||
remaining: false,
|
||||
safeToAdmit: true,
|
||||
});
|
||||
assert.equal(db.closes(), 1);
|
||||
assert.equal(
|
||||
require('@qinglong/cluster-admin').recoverClusterPluginPackages,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolves a database-bound stage authority after readiness', async () => {
|
||||
const db = database();
|
||||
let factories = 0;
|
||||
const configured = options(db);
|
||||
delete configured.stageAuthority;
|
||||
configured.stageAuthorityFactory = async (pool) => {
|
||||
factories += 1;
|
||||
assert.equal(pool, db.resource.pool);
|
||||
return {
|
||||
stage: configured.resourceByteSource.open,
|
||||
verify: configured.resourceByteSource.open,
|
||||
publisherEvidence: configured.resourceByteSource.open,
|
||||
};
|
||||
};
|
||||
await recoverClusterPluginPackages(configured);
|
||||
assert.equal(factories, 1);
|
||||
assert.equal(db.closes(), 1);
|
||||
});
|
||||
|
||||
test('closes PostgreSQL when admin readiness fails', async () => {
|
||||
const db = database('150018');
|
||||
await assert.rejects(
|
||||
recoverClusterPluginPackages(options(db)),
|
||||
(error) => error.code === 'server_version_unsupported',
|
||||
);
|
||||
assert.equal(db.closes(), 1);
|
||||
});
|
||||
Reference in New Issue
Block a user