mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
206 lines
6.2 KiB
JavaScript
206 lines
6.2 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { createHash } = require('node:crypto');
|
|
const fs = require('node:fs/promises');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
pluginPackageContentTreeDigest,
|
|
} = require('@qinglong/runtime-core/plugin-package-bundle');
|
|
const {
|
|
createPluginPackageResourceGenerationFromReferences,
|
|
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
|
|
const {
|
|
InvalidLocalPluginPackageResourceSourceError,
|
|
LocalPluginPackageResourceByteSource,
|
|
} = require('../dist/plugin-package/pluginPackageResourceMaterialization');
|
|
|
|
const LOCK_DIGEST = 'a'.repeat(64);
|
|
|
|
function digest(value) {
|
|
return createHash('sha256').update(value).digest('hex');
|
|
}
|
|
|
|
function blobName(index, entryPath) {
|
|
return `${String(index).padStart(4, '0')}-${digest(entryPath)}.blob`;
|
|
}
|
|
|
|
async function stageFixture() {
|
|
const root = await fs.realpath(
|
|
await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-resource-source-')),
|
|
);
|
|
await fs.chmod(root, 0o700);
|
|
const stage = path.join(root, LOCK_DIGEST);
|
|
const blobs = path.join(stage, 'blobs');
|
|
await fs.mkdir(stage, { mode: 0o700 });
|
|
await fs.mkdir(blobs, { mode: 0o700 });
|
|
|
|
const materials = new Map([
|
|
['package.json', Buffer.from('{"apiVersion":"qinglong.io/v1alpha1"}')],
|
|
[
|
|
'tasks/collect.json',
|
|
Buffer.from(
|
|
JSON.stringify({
|
|
schema: 'qinglong/plugin-package-task-resource@v1',
|
|
id: 'collect',
|
|
}),
|
|
),
|
|
],
|
|
]);
|
|
const entries = [...materials.entries()].map(
|
|
([entryPath, material], index) => ({
|
|
path: entryPath,
|
|
bytes: material.byteLength,
|
|
digest: digest(material),
|
|
blob: blobName(index, entryPath),
|
|
}),
|
|
);
|
|
const contentDigest = pluginPackageContentTreeDigest(
|
|
entries.slice(1).map(({ path: entryPath, bytes, digest }) => ({
|
|
path: entryPath,
|
|
bytes,
|
|
digest,
|
|
})),
|
|
);
|
|
const generation = createPluginPackageResourceGenerationFromReferences({
|
|
installationId: 'install-001',
|
|
projectId: 'project-001',
|
|
packageName: 'example-monitor',
|
|
lockDigest: LOCK_DIGEST,
|
|
generation: 1,
|
|
previousActiveLockDigest: null,
|
|
contentDigest,
|
|
resources: [{ kind: 'task', path: 'tasks/collect.json' }],
|
|
});
|
|
const inspectionEntries = entries.map(
|
|
({ path: entryPath, bytes, digest }) => ({
|
|
path: entryPath,
|
|
bytes,
|
|
digest,
|
|
}),
|
|
);
|
|
const receipt = {
|
|
schema: 'qinglong/plugin-package-stage-receipt@v1',
|
|
lockDigest: LOCK_DIGEST,
|
|
inspection: {
|
|
mediaType: 'application/vnd.qinglong.package.v1+tar',
|
|
lockDigest: LOCK_DIGEST,
|
|
packageName: generation.packageName,
|
|
packageVersion: '1.0.0',
|
|
artifactBytes: 4096,
|
|
artifactDigest: 'b'.repeat(64),
|
|
manifestDigest: entries[0].digest,
|
|
contentBytes: entries[1].bytes,
|
|
contentDigest,
|
|
entries: inspectionEntries,
|
|
signature: {
|
|
publisher: 'example.test',
|
|
keyId: 'key-001',
|
|
signatureDigest: 'c'.repeat(64),
|
|
keyNotBeforeMs: 0,
|
|
keyNotAfterMs: 10_000,
|
|
verifiedAtMs: 100,
|
|
},
|
|
},
|
|
entries,
|
|
};
|
|
for (const [index, entry] of entries.entries()) {
|
|
const file = path.join(blobs, entry.blob);
|
|
await fs.writeFile(file, materials.get(entry.path), { mode: 0o600 });
|
|
await fs.chmod(file, 0o600);
|
|
assert.equal(index < 10_000, true);
|
|
}
|
|
const receiptPath = path.join(stage, 'receipt.json');
|
|
await fs.writeFile(receiptPath, JSON.stringify(receipt), { mode: 0o600 });
|
|
await fs.chmod(receiptPath, 0o600);
|
|
return { root, stage, blobs, materials, entries, generation };
|
|
}
|
|
|
|
async function cleanup(root) {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
|
|
test('opens one private stage session and verifies each exact blob on demand', async () => {
|
|
const fixture = await stageFixture();
|
|
try {
|
|
const source = new LocalPluginPackageResourceByteSource({
|
|
stagingRoot: fixture.root,
|
|
});
|
|
const reader = await source.open(fixture.generation);
|
|
assert.deepEqual(
|
|
await reader.read('package.json', 64 * 1024),
|
|
fixture.materials.get('package.json'),
|
|
);
|
|
assert.deepEqual(
|
|
await reader.read('tasks/collect.json', 1024 * 1024),
|
|
fixture.materials.get('tasks/collect.json'),
|
|
);
|
|
await assert.rejects(
|
|
reader.read('tasks/collect.json', 1024 * 1024),
|
|
/duplicated/,
|
|
);
|
|
await reader.close();
|
|
await assert.rejects(reader.read('package.json', 64 * 1024), /closed/);
|
|
} finally {
|
|
await cleanup(fixture.root);
|
|
}
|
|
});
|
|
|
|
test('fails closed on blob tampering, extras and an over-tight caller bound', async () => {
|
|
const tampered = await stageFixture();
|
|
try {
|
|
const source = new LocalPluginPackageResourceByteSource({
|
|
stagingRoot: tampered.root,
|
|
});
|
|
const reader = await source.open(tampered.generation);
|
|
await fs.writeFile(
|
|
path.join(tampered.blobs, tampered.entries[1].blob),
|
|
Buffer.alloc(tampered.entries[1].bytes, 0x78),
|
|
);
|
|
await assert.rejects(
|
|
reader.read('tasks/collect.json', 1024 * 1024),
|
|
InvalidLocalPluginPackageResourceSourceError,
|
|
);
|
|
} finally {
|
|
await cleanup(tampered.root);
|
|
}
|
|
|
|
const extra = await stageFixture();
|
|
try {
|
|
await fs.writeFile(path.join(extra.blobs, 'extra'), 'x', { mode: 0o600 });
|
|
const source = new LocalPluginPackageResourceByteSource({
|
|
stagingRoot: extra.root,
|
|
});
|
|
await assert.rejects(
|
|
source.open(extra.generation),
|
|
/incomplete or contains extras/,
|
|
);
|
|
} finally {
|
|
await cleanup(extra.root);
|
|
}
|
|
|
|
const bounded = await stageFixture();
|
|
try {
|
|
const source = new LocalPluginPackageResourceByteSource({
|
|
stagingRoot: bounded.root,
|
|
});
|
|
const reader = await source.open(bounded.generation);
|
|
await assert.rejects(
|
|
reader.read('tasks/collect.json', 1),
|
|
/exceeds its requested bound/,
|
|
);
|
|
} finally {
|
|
await cleanup(bounded.root);
|
|
}
|
|
});
|
|
|
|
test('publishes the local adapter only through its explicit subpath', () => {
|
|
assert.equal(require('..').LocalPluginPackageResourceByteSource, undefined);
|
|
assert.equal(
|
|
require('@qinglong/local-admin/package-resource-materialization')
|
|
.LocalPluginPackageResourceByteSource,
|
|
LocalPluginPackageResourceByteSource,
|
|
);
|
|
});
|