mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 18:08:20 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user