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,461 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { readFile, writeFile, chmod, mkdtemp, rm } = require('node:fs/promises');
|
||||
const { tmpdir } = require('node:os');
|
||||
const { join, resolve } = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
ClusterPluginPackageManagementProcessConfigError,
|
||||
loadClusterPluginPackageManagementProcessConfig,
|
||||
startClusterPluginPackageManagementProcess,
|
||||
} = require('@qinglong/cluster-admin/plugin-package-management-process');
|
||||
|
||||
const SERVER_KEY = resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls/server-key.pem',
|
||||
);
|
||||
const SERVER_CERT = resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls/server-cert.pem',
|
||||
);
|
||||
|
||||
function enabledEnvironment(paths, overrides = {}) {
|
||||
return {
|
||||
QL3_PLUGIN_PACKAGE_MANAGEMENT_ENABLED: 'true',
|
||||
QL3_PROFILE: 'cluster-admin',
|
||||
QL3_PLUGIN_PACKAGE_MANAGEMENT_HOST: '127.0.0.1',
|
||||
QL3_PLUGIN_PACKAGE_MANAGEMENT_PORT: '8443',
|
||||
QL3_PLUGIN_PACKAGE_MANAGEMENT_TLS_CERT_FILE: paths.certificateFile,
|
||||
QL3_PLUGIN_PACKAGE_MANAGEMENT_TLS_KEY_FILE: paths.privateKeyFile,
|
||||
QL3_PLUGIN_PACKAGE_MANAGEMENT_IDENTITY_KEYSET_FILE:
|
||||
paths.identityKeysetFile,
|
||||
QL3_PLUGIN_PACKAGE_PUBLISHER_TRUST_FILE: paths.publisherTrustFile,
|
||||
QL3_PLUGIN_PACKAGE_TRUST_AUTHORITY_PROJECT_ID:
|
||||
'cluster-trust-authority',
|
||||
QL3_POSTGRES_PACKAGE_MANAGER_URL:
|
||||
'postgresql://ql3_package_manager:secret@postgres.example.test/ql3',
|
||||
QL3_POSTGRES_PACKAGE_MANAGER_TLS_MODE: 'disable',
|
||||
QL3_POSTGRES_PACKAGE_MANAGER_ALLOW_INSECURE: 'true',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function publisherAuthority() {
|
||||
const snapshot = {
|
||||
schema: 'qinglong/plugin-package-publisher-trust-snapshot@v1',
|
||||
keys: [
|
||||
{
|
||||
publisher: 'publisher-a.example',
|
||||
keyId: 'key-a',
|
||||
publicKeyDigest: 'a'.repeat(64),
|
||||
notBeforeMs: 1,
|
||||
notAfterMs: 10_000,
|
||||
},
|
||||
],
|
||||
snapshotDigest: 'b'.repeat(64),
|
||||
};
|
||||
const head = {
|
||||
schema: 'qinglong/plugin-package-publisher-trust-head@v1',
|
||||
authorityId: 'cluster',
|
||||
generation: 1,
|
||||
baseSnapshotDigest: snapshot.snapshotDigest,
|
||||
effectiveTrustDigest: snapshot.snapshotDigest,
|
||||
updatedAtMs: 1_000,
|
||||
headDigest: 'c'.repeat(64),
|
||||
};
|
||||
return {
|
||||
publisherTrustEvidence: { registry: {}, snapshot },
|
||||
async observePublisherTrust(_pool, input) {
|
||||
assert.equal(input.authorityId, 'cluster');
|
||||
assert.deepEqual(input.snapshot, snapshot);
|
||||
return {
|
||||
status: 'created',
|
||||
head,
|
||||
effectiveSnapshot: snapshot,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function readiness() {
|
||||
return {
|
||||
ready: true,
|
||||
writablePrimary: true,
|
||||
serverVersionNum: 180004,
|
||||
serverMajor: 18,
|
||||
currentUser: 'ql3_package_manager',
|
||||
contractName: 'control-core',
|
||||
contractVersion: 24,
|
||||
migrationIds: ['pg-0025-plugin-package-materialized-revisions'],
|
||||
};
|
||||
}
|
||||
|
||||
function identities(overrides = {}) {
|
||||
let reloads = 0;
|
||||
return {
|
||||
get reloads() {
|
||||
return reloads;
|
||||
},
|
||||
provider: {
|
||||
async reload() {
|
||||
reloads += 1;
|
||||
if (overrides.reload) return overrides.reload();
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
generation: 4,
|
||||
digest: 'keyset-digest',
|
||||
issuer: 'https://identity.example.test/',
|
||||
audience: 'qinglong3-package-management',
|
||||
activeKeyIds: ['identity-key-2'],
|
||||
revokedKeyIds: ['identity-key-1'],
|
||||
};
|
||||
},
|
||||
bind() {
|
||||
throw new Error('HTTP stub must not authenticate');
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function tlsFixture(run) {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'ql3-management-process-'));
|
||||
const paths = {
|
||||
certificateFile: join(directory, 'tls.crt'),
|
||||
privateKeyFile: join(directory, 'tls.key'),
|
||||
identityKeysetFile: join(directory, 'keyset.json'),
|
||||
publisherTrustFile: join(directory, 'publisher-trust.json'),
|
||||
};
|
||||
try {
|
||||
await writeFile(paths.certificateFile, await readFile(SERVER_CERT), {
|
||||
mode: 0o644,
|
||||
});
|
||||
await writeFile(paths.privateKeyFile, await readFile(SERVER_KEY), {
|
||||
mode: 0o640,
|
||||
});
|
||||
await writeFile(paths.identityKeysetFile, '{}\n', { mode: 0o644 });
|
||||
await writeFile(paths.publisherTrustFile, '{}\n', { mode: 0o644 });
|
||||
return await run(paths);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('disabled gate reads no profile, trust, TLS or PostgreSQL authority', async () => {
|
||||
const reads = [];
|
||||
const environment = new Proxy(
|
||||
{ QL3_PLUGIN_PACKAGE_MANAGEMENT_ENABLED: 'false' },
|
||||
{
|
||||
get(target, property) {
|
||||
reads.push(property);
|
||||
if (property === 'QL3_PLUGIN_PACKAGE_MANAGEMENT_ENABLED') {
|
||||
return target[property];
|
||||
}
|
||||
throw new Error(`disabled config read ${String(property)}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
let opened = 0;
|
||||
const runtime = await startClusterPluginPackageManagementProcess({
|
||||
environment,
|
||||
async openDatabase() {
|
||||
opened += 1;
|
||||
throw new Error('must not open');
|
||||
},
|
||||
});
|
||||
assert.equal(runtime.status, 'disabled');
|
||||
assert.equal(opened, 0);
|
||||
assert.deepEqual(reads, ['QL3_PLUGIN_PACKAGE_MANAGEMENT_ENABLED']);
|
||||
await runtime.close();
|
||||
});
|
||||
|
||||
test('loads one explicit manager-only HTTPS and database configuration', () => {
|
||||
const paths = {
|
||||
certificateFile: '/run/ql3-management/tls.crt',
|
||||
privateKeyFile: '/run/ql3-management/tls.key',
|
||||
identityKeysetFile: '/run/ql3-management/keyset.json',
|
||||
publisherTrustFile: '/run/ql3-management/publisher-trust.json',
|
||||
};
|
||||
const config = loadClusterPluginPackageManagementProcessConfig(
|
||||
enabledEnvironment(paths),
|
||||
);
|
||||
assert.equal(config.enabled, true);
|
||||
assert.equal(config.profile, 'cluster-admin');
|
||||
assert.equal(config.port, 8443);
|
||||
assert.equal(config.database.connection.tls.mode, 'disable');
|
||||
assert.equal(config.database.pool.maxConnections, 2);
|
||||
assert.equal(
|
||||
config.database.pool.applicationName,
|
||||
'qinglong3-plugin-package-manager',
|
||||
);
|
||||
assert.equal(config.http.maxConnections, 64);
|
||||
assert.equal(config.http.maxConcurrentRequests, 32);
|
||||
assert.equal(config.http.maxRateLimitPeers, 1024);
|
||||
assert.deepEqual(config.quota, {
|
||||
windowMs: 60_000,
|
||||
proposeLimit: 30,
|
||||
decideLimit: 60,
|
||||
inspectLimit: 600,
|
||||
});
|
||||
assert.deepEqual(config.publisherTrust, {
|
||||
file: '/run/ql3-management/publisher-trust.json',
|
||||
authorityProjectId: 'cluster-trust-authority',
|
||||
authorityId: 'cluster',
|
||||
observerId: 'cluster-package-manager',
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects profile drift, implicit insecure PostgreSQL and unsafe files', () => {
|
||||
const paths = {
|
||||
certificateFile: '/run/ql3-management/tls.crt',
|
||||
privateKeyFile: '/run/ql3-management/tls.key',
|
||||
identityKeysetFile: '/run/ql3-management/keyset.json',
|
||||
publisherTrustFile: '/run/ql3-management/publisher-trust.json',
|
||||
};
|
||||
for (const environment of [
|
||||
enabledEnvironment(paths, { QL3_PROFILE: 'cluster-control' }),
|
||||
enabledEnvironment(paths, {
|
||||
QL3_POSTGRES_PACKAGE_MANAGER_ALLOW_INSECURE: 'false',
|
||||
}),
|
||||
enabledEnvironment(paths, {
|
||||
QL3_PLUGIN_PACKAGE_MANAGEMENT_TLS_KEY_FILE: 'relative.key',
|
||||
}),
|
||||
]) {
|
||||
assert.throws(
|
||||
() => loadClusterPluginPackageManagementProcessConfig(environment),
|
||||
ClusterPluginPackageManagementProcessConfigError,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects configured request and connection ceilings above hard bounds', () => {
|
||||
for (const overrides of [
|
||||
{
|
||||
QL3_PLUGIN_PACKAGE_MANAGEMENT_MAX_BODY_BYTES: String(256 * 1024 + 1),
|
||||
},
|
||||
{ QL3_PLUGIN_PACKAGE_MANAGEMENT_MAX_CONNECTIONS: '513' },
|
||||
{ QL3_PLUGIN_PACKAGE_MANAGEMENT_INSPECT_QUOTA: '1001' },
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
loadClusterPluginPackageManagementProcessConfig(
|
||||
enabledEnvironment(
|
||||
{
|
||||
certificateFile: '/run/ql3-management/tls.crt',
|
||||
privateKeyFile: '/run/ql3-management/tls.key',
|
||||
identityKeysetFile: '/run/ql3-management/keyset.json',
|
||||
publisherTrustFile:
|
||||
'/run/ql3-management/publisher-trust.json',
|
||||
},
|
||||
overrides,
|
||||
),
|
||||
),
|
||||
ClusterPluginPackageManagementProcessConfigError,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('starts only after Package manager readiness and keyset validation then closes in order', async () => {
|
||||
await tlsFixture(async (paths) => {
|
||||
const order = [];
|
||||
const identity = identities();
|
||||
const pool = {
|
||||
async query() {
|
||||
throw new Error('construction must not query PostgreSQL');
|
||||
},
|
||||
async connect() {
|
||||
throw new Error('construction must not acquire PostgreSQL');
|
||||
},
|
||||
};
|
||||
let privateKey;
|
||||
let httpOptions;
|
||||
const runtime = await startClusterPluginPackageManagementProcess({
|
||||
environment: enabledEnvironment(paths),
|
||||
identities: identity.provider,
|
||||
...publisherAuthority(),
|
||||
async openDatabase() {
|
||||
order.push('database.open');
|
||||
return {
|
||||
pool,
|
||||
async close() {
|
||||
order.push('database.close');
|
||||
},
|
||||
};
|
||||
},
|
||||
async assertReady(observedPool) {
|
||||
order.push('database.ready');
|
||||
assert.equal(observedPool, pool);
|
||||
return readiness();
|
||||
},
|
||||
async startHttp(options) {
|
||||
order.push('http.start');
|
||||
httpOptions = options;
|
||||
privateKey = options.tls.privateKey;
|
||||
assert.equal(
|
||||
privateKey.some((value) => value !== 0),
|
||||
true,
|
||||
);
|
||||
return {
|
||||
status: 'active',
|
||||
address: { host: '127.0.0.1', port: 9443 },
|
||||
availabilityStatus: () => 'ready',
|
||||
withdraw() {},
|
||||
async close() {
|
||||
order.push('http.close');
|
||||
},
|
||||
};
|
||||
},
|
||||
now: () => 1_000,
|
||||
});
|
||||
|
||||
assert.equal(runtime.status, 'active');
|
||||
assert.deepEqual(order, ['database.open', 'database.ready', 'http.start']);
|
||||
assert.equal(identity.reloads, 1);
|
||||
assert.equal(
|
||||
privateKey.every((value) => value === 0),
|
||||
true,
|
||||
);
|
||||
assert.equal(typeof httpOptions.transport.execute, 'function');
|
||||
assert.equal(httpOptions.identities, identity.provider);
|
||||
assert.deepEqual(runtime.identity.activeKeyIds, ['identity-key-2']);
|
||||
assert.equal(runtime.database.contractVersion, 24);
|
||||
|
||||
await Promise.all([runtime.close(), runtime.close()]);
|
||||
assert.deepEqual(order, [
|
||||
'database.open',
|
||||
'database.ready',
|
||||
'http.start',
|
||||
'http.close',
|
||||
'database.close',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('closes the Package manager database when readiness or HTTP startup fails', async () => {
|
||||
await tlsFixture(async (paths) => {
|
||||
for (const failureAt of ['readiness', 'http']) {
|
||||
let closes = 0;
|
||||
const identity = identities();
|
||||
await assert.rejects(
|
||||
startClusterPluginPackageManagementProcess({
|
||||
environment: enabledEnvironment(paths),
|
||||
identities: identity.provider,
|
||||
...publisherAuthority(),
|
||||
async openDatabase() {
|
||||
return {
|
||||
pool: {
|
||||
async query() {
|
||||
throw new Error('must not query');
|
||||
},
|
||||
async connect() {
|
||||
throw new Error('must not connect');
|
||||
},
|
||||
},
|
||||
async close() {
|
||||
closes += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
async assertReady() {
|
||||
if (failureAt === 'readiness') {
|
||||
throw new Error('readiness failed');
|
||||
}
|
||||
return readiness();
|
||||
},
|
||||
async startHttp() {
|
||||
throw new Error('HTTP failed');
|
||||
},
|
||||
}),
|
||||
new RegExp(`${failureAt} failed`, 'i'),
|
||||
);
|
||||
assert.equal(closes, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects publicly readable private TLS authority before opening a listener', async () => {
|
||||
await tlsFixture(async (paths) => {
|
||||
await chmod(paths.privateKeyFile, 0o644);
|
||||
let starts = 0;
|
||||
await assert.rejects(
|
||||
startClusterPluginPackageManagementProcess({
|
||||
environment: enabledEnvironment(paths),
|
||||
identities: identities().provider,
|
||||
...publisherAuthority(),
|
||||
async openDatabase() {
|
||||
return {
|
||||
pool: {
|
||||
async query() {
|
||||
throw new Error('must not query');
|
||||
},
|
||||
async connect() {
|
||||
throw new Error('must not connect');
|
||||
},
|
||||
},
|
||||
async close() {},
|
||||
};
|
||||
},
|
||||
async assertReady() {
|
||||
return readiness();
|
||||
},
|
||||
async startHttp() {
|
||||
starts += 1;
|
||||
throw new Error('must not start');
|
||||
},
|
||||
}),
|
||||
ClusterPluginPackageManagementProcessConfigError,
|
||||
);
|
||||
assert.equal(starts, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('clears loaded private key bytes when certificate loading fails', async () => {
|
||||
await tlsFixture(async (paths) => {
|
||||
const privateKeyBytes = await readFile(paths.privateKeyFile);
|
||||
await chmod(paths.certificateFile, 0o666);
|
||||
const originalAllocate = Buffer.alloc;
|
||||
let privateKeyStorage;
|
||||
Buffer.alloc = function allocate(size, ...rest) {
|
||||
const bytes = originalAllocate.call(Buffer, size, ...rest);
|
||||
if (size === privateKeyBytes.length + 1 && !privateKeyStorage) {
|
||||
privateKeyStorage = bytes;
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
try {
|
||||
await assert.rejects(
|
||||
startClusterPluginPackageManagementProcess({
|
||||
environment: enabledEnvironment(paths),
|
||||
identities: identities().provider,
|
||||
...publisherAuthority(),
|
||||
async openDatabase() {
|
||||
return {
|
||||
pool: {
|
||||
async query() {
|
||||
throw new Error('must not query');
|
||||
},
|
||||
async connect() {
|
||||
throw new Error('must not connect');
|
||||
},
|
||||
},
|
||||
async close() {},
|
||||
};
|
||||
},
|
||||
async assertReady() {
|
||||
return readiness();
|
||||
},
|
||||
async startHttp() {
|
||||
throw new Error('must not start');
|
||||
},
|
||||
}),
|
||||
ClusterPluginPackageManagementProcessConfigError,
|
||||
);
|
||||
} finally {
|
||||
Buffer.alloc = originalAllocate;
|
||||
}
|
||||
assert.ok(privateKeyStorage);
|
||||
assert.equal(
|
||||
privateKeyStorage.every((value) => value === 0),
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user