mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
352 lines
11 KiB
JavaScript
352 lines
11 KiB
JavaScript
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',
|
|
'lock_run_management_policy_fence',
|
|
].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);
|
|
});
|