mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
const { createHash } = require('node:crypto');
|
||||
|
||||
const {
|
||||
auditWorkerCredentialManagementReleaseEvidence,
|
||||
clearSourceDocuments,
|
||||
readDocument,
|
||||
readSourceDocuments,
|
||||
} = require('./ql3-worker-credential-management-release-evidence.cjs');
|
||||
|
||||
const FIXTURE = 'qinglong/worker-credential-management-release-gate@v1';
|
||||
const MAX_EVIDENCE_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
const MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
||||
const COMMIT_PATTERN = /^[a-f0-9]{40,64}$/;
|
||||
const VERSION_PATTERN =
|
||||
/^3\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/;
|
||||
|
||||
class WorkerCredentialManagementReleaseGateError extends Error {
|
||||
constructor(message) {
|
||||
super(`Worker management release gate failed: ${message}`);
|
||||
this.name = 'WorkerCredentialManagementReleaseGateError';
|
||||
}
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
throw new WorkerCredentialManagementReleaseGateError(message);
|
||||
}
|
||||
|
||||
function exactKeys(value, keys) {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
JSON.stringify(Object.keys(value).sort()) ===
|
||||
JSON.stringify([...keys].sort())
|
||||
);
|
||||
}
|
||||
|
||||
function reportDigest(bytes) {
|
||||
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
||||
}
|
||||
|
||||
function optionKeys() {
|
||||
return [
|
||||
'reportFile',
|
||||
'ceremonyReportFile',
|
||||
'durableAuditReportFile',
|
||||
'pkiRotationReportFile',
|
||||
'caRolloverReportFile',
|
||||
'sourceCommit',
|
||||
'releaseVersion',
|
||||
];
|
||||
}
|
||||
|
||||
function auditWorkerCredentialManagementReleaseGate(
|
||||
options,
|
||||
dependencies = { now: Date.now },
|
||||
) {
|
||||
if (!exactKeys(options, optionKeys())) fail('options are invalid');
|
||||
if (!exactKeys(dependencies, ['now']) || typeof dependencies.now !== 'function') {
|
||||
fail('clock is invalid');
|
||||
}
|
||||
if (!COMMIT_PATTERN.test(options.sourceCommit)) {
|
||||
fail('source commit must be one immutable Git object ID');
|
||||
}
|
||||
if (!VERSION_PATTERN.test(options.releaseVersion)) {
|
||||
fail('release version must be one QingLong 3 SemVer image tag');
|
||||
}
|
||||
const nowMs = dependencies.now();
|
||||
if (!Number.isSafeInteger(nowMs) || nowMs < 0) fail('clock is invalid');
|
||||
|
||||
const report = readDocument(options.reportFile, 'release evidence report');
|
||||
let documents;
|
||||
try {
|
||||
documents = readSourceDocuments(options);
|
||||
const audit = auditWorkerCredentialManagementReleaseEvidence(
|
||||
report.value,
|
||||
documents,
|
||||
);
|
||||
if (!audit.compatible) fail('source-aware evidence audit is incompatible');
|
||||
|
||||
const observedAtMs = Date.parse(report.value.observedAt);
|
||||
if (!Number.isFinite(observedAtMs)) fail('evidence timestamp is invalid');
|
||||
if (observedAtMs > nowMs + MAX_FUTURE_SKEW_MS) {
|
||||
fail('evidence timestamp is too far in the future');
|
||||
}
|
||||
if (nowMs - observedAtMs > MAX_EVIDENCE_AGE_MS) {
|
||||
fail('evidence is older than the 24 hour release window');
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
fixture: FIXTURE,
|
||||
compatible: true,
|
||||
sourceCommit: options.sourceCommit,
|
||||
releaseVersion: options.releaseVersion,
|
||||
evidenceReportSha256: reportDigest(report.bytes),
|
||||
maximumEvidenceAgeSeconds: MAX_EVIDENCE_AGE_MS / 1000,
|
||||
});
|
||||
} finally {
|
||||
report.bytes.fill(0);
|
||||
if (documents) clearSourceDocuments(documents);
|
||||
}
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const values = {};
|
||||
for (const argument of argv) {
|
||||
if (argument === '--') continue;
|
||||
const match = /^--([a-z-]+)=(.+)$/.exec(argument);
|
||||
if (!match || Object.hasOwn(values, match[1])) fail('arguments are invalid');
|
||||
values[match[1]] = match[2];
|
||||
}
|
||||
const expected = [
|
||||
'report',
|
||||
'ceremony-report',
|
||||
'durable-audit-report',
|
||||
'pki-rotation-report',
|
||||
'ca-rollover-report',
|
||||
'source-commit',
|
||||
'release-version',
|
||||
];
|
||||
if (
|
||||
JSON.stringify(Object.keys(values).sort()) !==
|
||||
JSON.stringify(expected.sort())
|
||||
) {
|
||||
fail('arguments are invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
reportFile: values.report,
|
||||
ceremonyReportFile: values['ceremony-report'],
|
||||
durableAuditReportFile: values['durable-audit-report'],
|
||||
pkiRotationReportFile: values['pki-rotation-report'],
|
||||
caRolloverReportFile: values['ca-rollover-report'],
|
||||
sourceCommit: values['source-commit'],
|
||||
releaseVersion: values['release-version'],
|
||||
});
|
||||
}
|
||||
|
||||
function runCli(argv) {
|
||||
const result = auditWorkerCredentialManagementReleaseGate(
|
||||
parseArguments(argv),
|
||||
{ now: Date.now },
|
||||
);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
runCli(process.argv.slice(2));
|
||||
} catch (error) {
|
||||
process.stderr.write(
|
||||
`${error instanceof Error ? error.message : 'release gate failed'}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FIXTURE,
|
||||
MAX_EVIDENCE_AGE_MS,
|
||||
MAX_FUTURE_SKEW_MS,
|
||||
WorkerCredentialManagementReleaseGateError,
|
||||
auditWorkerCredentialManagementReleaseGate,
|
||||
parseArguments,
|
||||
runCli,
|
||||
};
|
||||
Reference in New Issue
Block a user