mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:25:29 +08:00
261 lines
8.0 KiB
JavaScript
261 lines
8.0 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const { generateKeyPairSync } = require('node:crypto');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
ClusterPluginPackageRecoveryProcessConfigError,
|
|
loadClusterPluginPackageRegistryCredentialFile,
|
|
loadClusterPluginPackagePublisherTrustFile,
|
|
loadClusterPluginPackageRecoveryProcessConfig,
|
|
runClusterPluginPackageRecoveryProcess,
|
|
} = require('@qinglong/cluster-admin/plugin-package-recovery-process');
|
|
|
|
function environment(overrides = {}) {
|
|
return {
|
|
QL3_CLUSTER_IDENTITY: 'cluster-production-001',
|
|
QL3_KUBERNETES_NAMESPACE: 'qinglong3-system',
|
|
QL3_PLUGIN_PACKAGE_OCI_REGISTRIES: 'ghcr.io,registry.example.com:5443',
|
|
QL3_PLUGIN_PACKAGE_PUBLISHER_TRUST_FILE: '/trust/publishers.json',
|
|
QL3_PLUGIN_PACKAGE_OCI_TIMEOUT_MS: '12000',
|
|
QL3_PLUGIN_PACKAGE_RECOVERY_PAGE_SIZE: '8',
|
|
QL3_PLUGIN_PACKAGE_RECOVERY_MAX_PAGES: '4',
|
|
QL3_POSTGRES_PACKAGE_EXECUTOR_URL:
|
|
'postgresql://ql3_package_executor:secret@postgres/qinglong',
|
|
QL3_POSTGRES_TLS_MODE: 'disable',
|
|
QL3_POSTGRES_ALLOW_INSECURE: 'true',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('loads one explicit Package-executor-only recovery process configuration', () => {
|
|
const config = loadClusterPluginPackageRecoveryProcessConfig(environment());
|
|
assert.equal(config.clusterIdentity, 'cluster-production-001');
|
|
assert.equal(config.namespace, 'qinglong3-system');
|
|
assert.equal(config.publisherTrustAuthorityId, 'cluster');
|
|
assert.deepEqual(config.allowedRegistries, [
|
|
'ghcr.io',
|
|
'registry.example.com:5443',
|
|
]);
|
|
assert.equal(config.requestTimeoutMs, 12_000);
|
|
assert.equal(config.pageSize, 8);
|
|
assert.equal(config.maxPages, 4);
|
|
assert.equal(config.database.pool.maxConnections, 1);
|
|
assert.equal(config.database.connection.tls.mode, 'disable');
|
|
assert.equal('QL3_POSTGRES_RUNTIME_URL' in config.database.connection, false);
|
|
});
|
|
|
|
test('rejects runtime credentials, duplicate registries and implicit insecure PostgreSQL', async () => {
|
|
for (const invalid of [
|
|
environment({
|
|
QL3_POSTGRES_PACKAGE_EXECUTOR_URL: undefined,
|
|
QL3_POSTGRES_RUNTIME_URL:
|
|
'postgresql://ql3_runtime:secret@postgres/qinglong',
|
|
}),
|
|
environment({
|
|
QL3_PLUGIN_PACKAGE_OCI_REGISTRIES: 'ghcr.io,ghcr.io',
|
|
}),
|
|
environment({
|
|
QL3_POSTGRES_ALLOW_INSECURE: undefined,
|
|
}),
|
|
]) {
|
|
assert.throws(
|
|
() => loadClusterPluginPackageRecoveryProcessConfig(invalid),
|
|
ClusterPluginPackageRecoveryProcessConfigError,
|
|
);
|
|
}
|
|
|
|
let touched = false;
|
|
await assert.rejects(
|
|
runClusterPluginPackageRecoveryProcess({
|
|
environment: environment({
|
|
QL3_PLUGIN_PACKAGE_RECOVERY_MAX_PAGES: '65',
|
|
}),
|
|
openDatabase: async () => {
|
|
touched = true;
|
|
throw new Error('must not open');
|
|
},
|
|
api: {},
|
|
stageAuthority: {},
|
|
}),
|
|
ClusterPluginPackageRecoveryProcessConfigError,
|
|
);
|
|
assert.equal(touched, false);
|
|
});
|
|
|
|
test('loads a bounded read-only publisher trust file', (t) => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-publisher-trust-'));
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const filePath = path.join(root, 'publishers.json');
|
|
const { publicKey } = generateKeyPairSync('ed25519');
|
|
fs.writeFileSync(
|
|
filePath,
|
|
JSON.stringify({
|
|
schema: 'qinglong/plugin-package-publisher-trust@v1',
|
|
keys: [
|
|
{
|
|
publisher: 'packages.example.com',
|
|
keyId: 'release-2026',
|
|
publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }),
|
|
notBeforeMs: 100,
|
|
notAfterMs: 10_000,
|
|
},
|
|
],
|
|
}),
|
|
{ mode: 0o444 },
|
|
);
|
|
fs.chmodSync(filePath, 0o444);
|
|
const trust = loadClusterPluginPackagePublisherTrustFile(filePath);
|
|
assert.equal(trust.size, 1);
|
|
|
|
fs.chmodSync(filePath, 0o666);
|
|
assert.throws(
|
|
() => loadClusterPluginPackagePublisherTrustFile(filePath),
|
|
/read-only regular file/,
|
|
);
|
|
});
|
|
|
|
test('keeps process authority off the cluster-admin root', () => {
|
|
const root = require('@qinglong/cluster-admin');
|
|
const manifest = require('../package.json');
|
|
assert.equal(root.runClusterPluginPackageRecoveryProcess, undefined);
|
|
assert.equal(
|
|
manifest.bin['ql3-plugin-package-recover'],
|
|
'dist/plugin-package/recovery/pluginPackageRecoveryCli.js',
|
|
);
|
|
});
|
|
|
|
test('binds an optional registry credential file without enabling ambient credentials', () => {
|
|
const publicConfig = loadClusterPluginPackageRecoveryProcessConfig(
|
|
environment(),
|
|
);
|
|
assert.equal(publicConfig.registryCredentialFile, undefined);
|
|
const privateConfig = loadClusterPluginPackageRecoveryProcessConfig(
|
|
environment({
|
|
QL3_PLUGIN_PACKAGE_REGISTRY_CREDENTIAL_FILE:
|
|
'/var/run/secrets/qinglong3/registry/credentials.json',
|
|
}),
|
|
);
|
|
assert.equal(
|
|
privateConfig.registryCredentialFile,
|
|
'/var/run/secrets/qinglong3/registry/credentials.json',
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
loadClusterPluginPackageRecoveryProcessConfig(
|
|
environment({
|
|
QL3_PLUGIN_PACKAGE_REGISTRY_CREDENTIAL_FILE: 'credentials.json',
|
|
}),
|
|
),
|
|
ClusterPluginPackageRecoveryProcessConfigError,
|
|
);
|
|
});
|
|
|
|
test('loads exact basic and bearer registry credentials and disposes retained bytes', (t) => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'ql3-registry-credentials-'),
|
|
);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const filePath = path.join(root, 'credentials.json');
|
|
fs.writeFileSync(
|
|
filePath,
|
|
JSON.stringify({
|
|
schema: 'qinglong/plugin-package-registry-credentials@v1',
|
|
credentials: [
|
|
{
|
|
registry: 'ghcr.io',
|
|
scheme: 'bearer',
|
|
token: 'token.value-1',
|
|
},
|
|
{
|
|
registry: 'registry.example.com:5443',
|
|
scheme: 'basic',
|
|
username: 'ql3-admin',
|
|
password: 'private-password',
|
|
},
|
|
],
|
|
}),
|
|
{ mode: 0o440 },
|
|
);
|
|
fs.chmodSync(filePath, 0o440);
|
|
const credentials = loadClusterPluginPackageRegistryCredentialFile(filePath, [
|
|
'ghcr.io',
|
|
'registry.example.com:5443',
|
|
]);
|
|
assert.equal(credentials.authorizationFor('ghcr.io'), 'Bearer token.value-1');
|
|
assert.equal(
|
|
credentials.authorizationFor('registry.example.com:5443'),
|
|
`Basic ${Buffer.from('ql3-admin:private-password').toString('base64')}`,
|
|
);
|
|
assert.equal(credentials.authorizationFor('registry.example.com'), undefined);
|
|
credentials.dispose();
|
|
assert.equal(credentials.authorizationFor('ghcr.io'), undefined);
|
|
credentials.dispose();
|
|
});
|
|
|
|
test('rejects overbroad, duplicate and publicly readable registry credentials', (t) => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'ql3-invalid-registry-credentials-'),
|
|
);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const filePath = path.join(root, 'credentials.json');
|
|
const write = (credentials, mode = 0o440) => {
|
|
if (fs.existsSync(filePath)) fs.chmodSync(filePath, 0o600);
|
|
fs.writeFileSync(
|
|
filePath,
|
|
JSON.stringify({
|
|
schema: 'qinglong/plugin-package-registry-credentials@v1',
|
|
credentials,
|
|
}),
|
|
{ mode },
|
|
);
|
|
fs.chmodSync(filePath, mode);
|
|
};
|
|
write([
|
|
{
|
|
registry: 'not-allowed.example.com',
|
|
scheme: 'bearer',
|
|
token: 'private-token',
|
|
},
|
|
]);
|
|
assert.throws(
|
|
() => loadClusterPluginPackageRegistryCredentialFile(filePath, ['ghcr.io']),
|
|
/binding is invalid/,
|
|
);
|
|
|
|
write([
|
|
{
|
|
registry: 'ghcr.io',
|
|
scheme: 'bearer',
|
|
token: 'private-token',
|
|
},
|
|
{
|
|
registry: 'ghcr.io',
|
|
scheme: 'basic',
|
|
username: 'owner',
|
|
password: 'private-password',
|
|
},
|
|
]);
|
|
assert.throws(
|
|
() => loadClusterPluginPackageRegistryCredentialFile(filePath, ['ghcr.io']),
|
|
/binding is invalid/,
|
|
);
|
|
|
|
write(
|
|
[
|
|
{
|
|
registry: 'ghcr.io',
|
|
scheme: 'bearer',
|
|
token: 'private-token',
|
|
},
|
|
],
|
|
0o444,
|
|
);
|
|
assert.throws(
|
|
() => loadClusterPluginPackageRegistryCredentialFile(filePath, ['ghcr.io']),
|
|
/private regular file/,
|
|
);
|
|
});
|