Files
qinglong/packages/ql3-cluster-admin/test/pluginPackageRecoveryProcess.test.cjs

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/,
);
});