mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 09:58:46 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { createHash } = require('node:crypto');
|
||||
|
||||
const {
|
||||
RESOURCE_TIERS,
|
||||
createWorkloadPlans,
|
||||
} = require('./ql3-linux-resource-gate.cjs');
|
||||
|
||||
const ARCHITECTURES = Object.freeze(['x64', 'arm64']);
|
||||
const TIER_NAMES = Object.freeze([
|
||||
'router-stress-ci',
|
||||
'edge-release-ci',
|
||||
'cluster-control-ci',
|
||||
]);
|
||||
const MAX_EVIDENCE_BYTES = 8 * 1024 * 1024;
|
||||
const MAX_CANONICAL_DEPTH = 32;
|
||||
const MAX_CANONICAL_NODES = 100_000;
|
||||
const REVISION_PATTERN = /^[0-9a-f]{40}$/;
|
||||
const REPOSITORY_PATTERN =
|
||||
/^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})\/[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})$/;
|
||||
const RUN_ID_PATTERN = /^[1-9][0-9]{0,31}$/;
|
||||
const NODE_VERSION = 'v24.18.0';
|
||||
|
||||
class QingLong3LinuxResourceReleaseEvidenceError extends Error {
|
||||
constructor(message) {
|
||||
super(`QingLong 3.0 Linux resource release evidence failed: ${message}`);
|
||||
this.name = 'QingLong3LinuxResourceReleaseEvidenceError';
|
||||
}
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
throw new QingLong3LinuxResourceReleaseEvidenceError(message);
|
||||
}
|
||||
|
||||
function isRecord(value) {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
Object.getPrototypeOf(value) === Object.prototype
|
||||
);
|
||||
}
|
||||
|
||||
function assertRecord(value, label) {
|
||||
if (!isRecord(value)) fail(`${label} must be a plain object`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function assertExactKeys(value, expected, label) {
|
||||
const actual = Object.keys(assertRecord(value, label)).sort();
|
||||
const wanted = [...expected].sort();
|
||||
if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
|
||||
fail(`${label} fields are invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertSafeInteger(value, label, minimum = 0) {
|
||||
if (!Number.isSafeInteger(value) || value < minimum) {
|
||||
fail(`${label} must be an integer >= ${minimum}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function canonicalize(value, depth = 0, budget = { nodes: 0 }) {
|
||||
budget.nodes += 1;
|
||||
if (budget.nodes > MAX_CANONICAL_NODES) fail('evidence node budget exceeded');
|
||||
if (depth > MAX_CANONICAL_DEPTH) fail('evidence depth budget exceeded');
|
||||
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isFinite(value)) fail('evidence contains a non-finite number');
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => canonicalize(entry, depth + 1, budget));
|
||||
}
|
||||
if (!isRecord(value)) fail('evidence contains an unsupported value');
|
||||
const result = {};
|
||||
for (const key of Object.keys(value).sort()) {
|
||||
if (!key || key.length > 128) fail('evidence contains an invalid key');
|
||||
result[key] = canonicalize(value[key], depth + 1, budget);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function evidenceDigest(value) {
|
||||
return createHash('sha256')
|
||||
.update('qinglong/linux-resource-release-evidence\0')
|
||||
.update(JSON.stringify(canonicalize(value)))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
function normalizeSource(source) {
|
||||
assertExactKeys(
|
||||
source,
|
||||
['repository', 'revision', 'workflow', 'runId', 'runAttempt'],
|
||||
'source',
|
||||
);
|
||||
if (
|
||||
typeof source.repository !== 'string' ||
|
||||
!REPOSITORY_PATTERN.test(source.repository)
|
||||
) {
|
||||
fail('source repository is invalid');
|
||||
}
|
||||
if (
|
||||
typeof source.revision !== 'string' ||
|
||||
!REVISION_PATTERN.test(source.revision)
|
||||
) {
|
||||
fail('source revision must be a lowercase 40-character commit SHA');
|
||||
}
|
||||
if (
|
||||
typeof source.workflow !== 'string' ||
|
||||
source.workflow.length < 1 ||
|
||||
source.workflow.length > 128 ||
|
||||
/[\u0000-\u001f\u007f]/.test(source.workflow)
|
||||
) {
|
||||
fail('source workflow is invalid');
|
||||
}
|
||||
if (
|
||||
typeof source.runId !== 'string' ||
|
||||
!RUN_ID_PATTERN.test(source.runId)
|
||||
) {
|
||||
fail('source runId is invalid');
|
||||
}
|
||||
assertSafeInteger(source.runAttempt, 'source runAttempt', 1);
|
||||
return Object.freeze({
|
||||
repository: source.repository,
|
||||
revision: source.revision,
|
||||
workflow: source.workflow,
|
||||
runId: source.runId,
|
||||
runAttempt: source.runAttempt,
|
||||
});
|
||||
}
|
||||
|
||||
function validateMemoryEvents(before, after, tierName) {
|
||||
assertRecord(before, `${tierName} memoryEventsBefore`);
|
||||
assertRecord(after, `${tierName} memoryEventsAfter`);
|
||||
for (const event of ['max', 'oom', 'oom_kill', 'oom_group_kill']) {
|
||||
const previous = assertSafeInteger(
|
||||
before[event],
|
||||
`${tierName} memoryEventsBefore.${event}`,
|
||||
);
|
||||
const current = assertSafeInteger(
|
||||
after[event],
|
||||
`${tierName} memoryEventsAfter.${event}`,
|
||||
);
|
||||
if (current !== previous) fail(`${tierName} memory event ${event} changed`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateTierReport(report, expectedTier, expectedArchitecture) {
|
||||
assertExactKeys(
|
||||
report,
|
||||
[
|
||||
'schemaVersion',
|
||||
'tier',
|
||||
'evidenceClass',
|
||||
'supportedMinimum',
|
||||
'identity',
|
||||
'envelope',
|
||||
'workloads',
|
||||
'gates',
|
||||
],
|
||||
`${expectedTier} report`,
|
||||
);
|
||||
const tier = RESOURCE_TIERS[expectedTier];
|
||||
if (
|
||||
report.schemaVersion !== 1 ||
|
||||
report.tier !== expectedTier ||
|
||||
report.evidenceClass !== tier.evidenceClass ||
|
||||
report.supportedMinimum !== tier.supportedMinimum
|
||||
) {
|
||||
fail(`${expectedTier} identity is invalid`);
|
||||
}
|
||||
|
||||
assertExactKeys(
|
||||
report.identity,
|
||||
['platform', 'architecture', 'node', 'uid', 'gid'],
|
||||
`${expectedTier} process identity`,
|
||||
);
|
||||
if (
|
||||
report.identity.platform !== 'linux' ||
|
||||
report.identity.architecture !== expectedArchitecture ||
|
||||
report.identity.node !== NODE_VERSION ||
|
||||
report.identity.uid !== 65532 ||
|
||||
report.identity.gid !== 65532
|
||||
) {
|
||||
fail(`${expectedTier} did not run under the reviewed native identity`);
|
||||
}
|
||||
|
||||
assertExactKeys(
|
||||
report.envelope,
|
||||
[
|
||||
'memoryMaxBytes',
|
||||
'memoryPeakBytes',
|
||||
'swapMaxBytes',
|
||||
'cpuQuotaCores',
|
||||
'pidsMax',
|
||||
'noNewPrivileges',
|
||||
'seccompMode',
|
||||
'rootReadOnly',
|
||||
'workspaceReadOnly',
|
||||
'tmpWritable',
|
||||
'memoryEventsBefore',
|
||||
'memoryEventsAfter',
|
||||
],
|
||||
`${expectedTier} envelope`,
|
||||
);
|
||||
if (
|
||||
report.envelope.memoryMaxBytes !== tier.memoryMaxBytes ||
|
||||
report.envelope.swapMaxBytes !== tier.swapMaxBytes ||
|
||||
report.envelope.cpuQuotaCores !== tier.cpuQuotaCores ||
|
||||
report.envelope.pidsMax !== tier.pidsMax ||
|
||||
report.envelope.noNewPrivileges !== 1 ||
|
||||
report.envelope.seccompMode !== 2 ||
|
||||
report.envelope.rootReadOnly !== true ||
|
||||
report.envelope.workspaceReadOnly !== true ||
|
||||
report.envelope.tmpWritable !== true
|
||||
) {
|
||||
fail(`${expectedTier} envelope drifted`);
|
||||
}
|
||||
const peak = assertSafeInteger(
|
||||
report.envelope.memoryPeakBytes,
|
||||
`${expectedTier} memoryPeakBytes`,
|
||||
1,
|
||||
);
|
||||
if (peak > tier.memoryMaxBytes) fail(`${expectedTier} memory peak exceeded`);
|
||||
validateMemoryEvents(
|
||||
report.envelope.memoryEventsBefore,
|
||||
report.envelope.memoryEventsAfter,
|
||||
expectedTier,
|
||||
);
|
||||
|
||||
if (!Array.isArray(report.workloads)) fail(`${expectedTier} workloads are invalid`);
|
||||
const expectedNames = createWorkloadPlans('/workspace', expectedTier).map(
|
||||
({ name }) => name,
|
||||
);
|
||||
const actualNames = report.workloads.map((entry, index) => {
|
||||
assertExactKeys(entry, ['name', 'report'], `${expectedTier} workload ${index}`);
|
||||
if (!isRecord(entry.report)) fail(`${expectedTier} workload report is invalid`);
|
||||
return entry.name;
|
||||
});
|
||||
if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) {
|
||||
fail(`${expectedTier} workload set drifted`);
|
||||
}
|
||||
|
||||
assertExactKeys(report.gates, ['passed', 'violations'], `${expectedTier} gates`);
|
||||
if (
|
||||
report.gates.passed !== true ||
|
||||
!Array.isArray(report.gates.violations) ||
|
||||
report.gates.violations.length !== 0
|
||||
) {
|
||||
fail(`${expectedTier} gate did not pass`);
|
||||
}
|
||||
canonicalize(report);
|
||||
return report;
|
||||
}
|
||||
|
||||
function architecturePayload(source, architecture, reports) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
fixture: 'qinglong/linux-resource-architecture-evidence@v1',
|
||||
source,
|
||||
architecture,
|
||||
reports,
|
||||
gates: {
|
||||
nativeArchitecture: true,
|
||||
exactTierSet: true,
|
||||
exactResourceEnvelopes: true,
|
||||
memoryEventsStable: true,
|
||||
allTierGatesPassed: true,
|
||||
sourceBound: true,
|
||||
passed: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function bundleArchitectureEvidence({ source, architecture, reports }) {
|
||||
const normalizedSource = normalizeSource(source);
|
||||
if (!ARCHITECTURES.includes(architecture)) fail('architecture is invalid');
|
||||
assertExactKeys(reports, TIER_NAMES, 'reports');
|
||||
const normalizedReports = Object.freeze(
|
||||
Object.fromEntries(
|
||||
TIER_NAMES.map((tierName) => [
|
||||
tierName,
|
||||
validateTierReport(reports[tierName], tierName, architecture),
|
||||
]),
|
||||
),
|
||||
);
|
||||
const payload = architecturePayload(
|
||||
normalizedSource,
|
||||
architecture,
|
||||
normalizedReports,
|
||||
);
|
||||
return Object.freeze({
|
||||
...payload,
|
||||
bundleDigest: evidenceDigest(payload),
|
||||
});
|
||||
}
|
||||
|
||||
function validateArchitectureEvidence(value, expectedSource, architecture) {
|
||||
assertExactKeys(
|
||||
value,
|
||||
[
|
||||
'schemaVersion',
|
||||
'fixture',
|
||||
'source',
|
||||
'architecture',
|
||||
'reports',
|
||||
'gates',
|
||||
'bundleDigest',
|
||||
],
|
||||
`${architecture} architecture evidence`,
|
||||
);
|
||||
const rebuilt = bundleArchitectureEvidence({
|
||||
source: value.source,
|
||||
architecture: value.architecture,
|
||||
reports: value.reports,
|
||||
});
|
||||
if (
|
||||
value.schemaVersion !== rebuilt.schemaVersion ||
|
||||
value.fixture !== rebuilt.fixture ||
|
||||
value.architecture !== architecture ||
|
||||
JSON.stringify(value.gates) !== JSON.stringify(rebuilt.gates) ||
|
||||
value.bundleDigest !== rebuilt.bundleDigest
|
||||
) {
|
||||
fail(`${architecture} architecture evidence digest or gates drifted`);
|
||||
}
|
||||
if (JSON.stringify(rebuilt.source) !== JSON.stringify(normalizeSource(expectedSource))) {
|
||||
fail(`${architecture} architecture evidence belongs to another source`);
|
||||
}
|
||||
return rebuilt;
|
||||
}
|
||||
|
||||
function tierSummary(report) {
|
||||
return Object.freeze({
|
||||
tier: report.tier,
|
||||
evidenceClass: report.evidenceClass,
|
||||
memoryMaxBytes: report.envelope.memoryMaxBytes,
|
||||
memoryPeakBytes: report.envelope.memoryPeakBytes,
|
||||
cpuQuotaCores: report.envelope.cpuQuotaCores,
|
||||
pidsMax: report.envelope.pidsMax,
|
||||
workloadCount: report.workloads.length,
|
||||
});
|
||||
}
|
||||
|
||||
function releasePayload(source, x64, arm64) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
fixture: 'qinglong/linux-resource-cross-architecture-evidence@v1',
|
||||
source,
|
||||
architectures: [x64, arm64].map((entry) => ({
|
||||
architecture: entry.architecture,
|
||||
node: entry.reports['router-stress-ci'].identity.node,
|
||||
bundleDigest: entry.bundleDigest,
|
||||
tiers: TIER_NAMES.map((tierName) => tierSummary(entry.reports[tierName])),
|
||||
})),
|
||||
gates: {
|
||||
nativeX64Passed: true,
|
||||
nativeArm64Passed: true,
|
||||
sameSourceRevision: true,
|
||||
sameWorkflowRun: true,
|
||||
exactTierParity: true,
|
||||
releaseEvidenceComplete: true,
|
||||
passed: true,
|
||||
},
|
||||
limitations: [
|
||||
'CI cgroup evidence is not a supported minimum hardware claim',
|
||||
'CI evidence does not replace fixed-device power-loss, flash, thermal, or soak evidence',
|
||||
'GitHub workflow identity binding is not a cryptographic hardware attestation',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function mergeCrossArchitectureEvidence({ source, x64, arm64 }) {
|
||||
const normalizedSource = normalizeSource(source);
|
||||
const validatedX64 = validateArchitectureEvidence(x64, normalizedSource, 'x64');
|
||||
const validatedArm64 = validateArchitectureEvidence(
|
||||
arm64,
|
||||
normalizedSource,
|
||||
'arm64',
|
||||
);
|
||||
if (validatedX64.bundleDigest === validatedArm64.bundleDigest) {
|
||||
fail('architecture bundles must be independently measured');
|
||||
}
|
||||
const payload = releasePayload(
|
||||
normalizedSource,
|
||||
validatedX64,
|
||||
validatedArm64,
|
||||
);
|
||||
return Object.freeze({
|
||||
...payload,
|
||||
releaseDigest: evidenceDigest(payload),
|
||||
});
|
||||
}
|
||||
|
||||
function readJsonFile(filePath, label) {
|
||||
const absolute = path.resolve(filePath);
|
||||
if (!Number.isInteger(fs.constants.O_NOFOLLOW)) {
|
||||
fail('platform does not support no-follow evidence reads');
|
||||
}
|
||||
let handle;
|
||||
try {
|
||||
handle = fs.openSync(
|
||||
absolute,
|
||||
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
|
||||
);
|
||||
} catch (error) {
|
||||
fail(
|
||||
`${label} must be a readable non-symlink file (${error?.code ?? 'unknown'})`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
const stat = fs.fstatSync(handle);
|
||||
if (!stat.isFile() || stat.size < 2 || stat.size > MAX_EVIDENCE_BYTES) {
|
||||
fail(`${label} size is invalid`);
|
||||
}
|
||||
const content = fs.readFileSync(handle);
|
||||
if (content.length < 2 || content.length > MAX_EVIDENCE_BYTES) {
|
||||
fail(`${label} size is invalid`);
|
||||
}
|
||||
let value;
|
||||
try {
|
||||
value = JSON.parse(content.toString('utf8'));
|
||||
} catch {
|
||||
fail(`${label} must contain valid JSON`);
|
||||
}
|
||||
canonicalize(value);
|
||||
return value;
|
||||
} finally {
|
||||
fs.closeSync(handle);
|
||||
}
|
||||
}
|
||||
|
||||
function writeJsonFile(filePath, value) {
|
||||
const absolute = path.resolve(filePath);
|
||||
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
||||
const handle = fs.openSync(absolute, 'wx', 0o600);
|
||||
try {
|
||||
fs.writeFileSync(handle, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
||||
} finally {
|
||||
fs.closeSync(handle);
|
||||
}
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = {};
|
||||
for (const argument of argv) {
|
||||
const separator = argument.indexOf('=');
|
||||
if (separator < 3 || !argument.startsWith('--')) {
|
||||
fail(`unsupported argument ${argument}`);
|
||||
}
|
||||
const name = argument.slice(2, separator);
|
||||
if (Object.hasOwn(options, name)) fail(`duplicate argument --${name}`);
|
||||
options[name] = argument.slice(separator + 1);
|
||||
}
|
||||
const mode = options.mode;
|
||||
const common = [
|
||||
'mode',
|
||||
'repository',
|
||||
'revision',
|
||||
'workflow',
|
||||
'run-id',
|
||||
'run-attempt',
|
||||
'output',
|
||||
];
|
||||
const modeKeys =
|
||||
mode === 'bundle'
|
||||
? [...common, 'architecture', ...TIER_NAMES]
|
||||
: mode === 'merge'
|
||||
? [...common, 'x64', 'arm64']
|
||||
: fail('--mode must be bundle or merge');
|
||||
if (JSON.stringify(Object.keys(options).sort()) !== JSON.stringify(modeKeys.sort())) {
|
||||
fail(`${mode} arguments are incomplete or widened`);
|
||||
}
|
||||
const runAttempt = Number(options['run-attempt']);
|
||||
const source = normalizeSource({
|
||||
repository: options.repository,
|
||||
revision: options.revision,
|
||||
workflow: options.workflow,
|
||||
runId: options['run-id'],
|
||||
runAttempt,
|
||||
});
|
||||
return Object.freeze({ ...options, mode, source });
|
||||
}
|
||||
|
||||
function main() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
if (options.mode === 'bundle') {
|
||||
const reports = Object.fromEntries(
|
||||
TIER_NAMES.map((tierName) => [
|
||||
tierName,
|
||||
readJsonFile(options[tierName], tierName),
|
||||
]),
|
||||
);
|
||||
const result = bundleArchitectureEvidence({
|
||||
source: options.source,
|
||||
architecture: options.architecture,
|
||||
reports,
|
||||
});
|
||||
writeJsonFile(options.output, result);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ schemaVersion: 1, mode: 'bundle', architecture: result.architecture, bundleDigest: result.bundleDigest, passed: true })}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = mergeCrossArchitectureEvidence({
|
||||
source: options.source,
|
||||
x64: readJsonFile(options.x64, 'x64 evidence'),
|
||||
arm64: readJsonFile(options.arm64, 'arm64 evidence'),
|
||||
});
|
||||
writeJsonFile(options.output, result);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ schemaVersion: 1, mode: 'merge', releaseDigest: result.releaseDigest, passed: true })}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ARCHITECTURES,
|
||||
MAX_EVIDENCE_BYTES,
|
||||
NODE_VERSION,
|
||||
QingLong3LinuxResourceReleaseEvidenceError,
|
||||
TIER_NAMES,
|
||||
bundleArchitectureEvidence,
|
||||
evidenceDigest,
|
||||
mergeCrossArchitectureEvidence,
|
||||
normalizeSource,
|
||||
parseArguments,
|
||||
readJsonFile,
|
||||
validateArchitectureEvidence,
|
||||
validateTierReport,
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
process.stderr.write(
|
||||
`${error instanceof Error ? error.message : String(error)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user