mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): publish durable release catalog
This commit is contained in:
@@ -6,8 +6,13 @@ const crypto = require('node:crypto');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const {
|
||||
CLUSTER_IMAGES,
|
||||
LOCAL_IMAGES,
|
||||
RELEASE_SCOPES,
|
||||
SCHEMA: RELEASE_CANDIDATE_SCHEMA,
|
||||
auditReleaseCandidateContract,
|
||||
} = require('./ql3-release-candidate-contract.cjs');
|
||||
const { VERSION_PATTERN } = require('./lib/ql3-release-identity.cjs');
|
||||
|
||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||
const IMAGE_RECORD_SCHEMA = 'qinglong/release-set-image-record@v1';
|
||||
@@ -312,6 +317,156 @@ function auditReleaseSet(actual, options) {
|
||||
});
|
||||
}
|
||||
|
||||
function inspectReleaseSet(actual, options) {
|
||||
const repositoryOwner = normalizeRepositoryOwner(options.repositoryOwner);
|
||||
if (
|
||||
!RELEASE_SCOPES.includes(options.releaseScope) ||
|
||||
typeof options.version !== 'string' ||
|
||||
!VERSION_PATTERN.test(options.version) ||
|
||||
!/^[a-f0-9]{40}$/u.test(options.sourceRevision || '') ||
|
||||
options.sourceRef !== `refs/tags/v${options.version}`
|
||||
) {
|
||||
fail('expected release identity is invalid');
|
||||
}
|
||||
if (
|
||||
!exactKeys(actual, [
|
||||
'schemaVersion',
|
||||
'schema',
|
||||
'release',
|
||||
'candidate',
|
||||
'repositoryOwner',
|
||||
'platforms',
|
||||
'deploymentFamilies',
|
||||
'images',
|
||||
'promotion',
|
||||
'requiredVerification',
|
||||
'releaseSetDigest',
|
||||
]) ||
|
||||
actual.schemaVersion !== 1 ||
|
||||
actual.schema !== RELEASE_SET_SCHEMA ||
|
||||
!exactKeys(actual.release, [
|
||||
'version',
|
||||
'sourceRevision',
|
||||
'sourceRef',
|
||||
'scope',
|
||||
]) ||
|
||||
actual.release.version !== options.version ||
|
||||
actual.release.sourceRevision !== options.sourceRevision ||
|
||||
actual.release.sourceRef !== options.sourceRef ||
|
||||
actual.release.scope !== options.releaseScope ||
|
||||
!exactKeys(actual.candidate, ['schema', 'contractDigest']) ||
|
||||
actual.candidate.schema !== RELEASE_CANDIDATE_SCHEMA ||
|
||||
!DIGEST_PATTERN.test(actual.candidate.contractDigest || '') ||
|
||||
actual.repositoryOwner !== repositoryOwner ||
|
||||
JSON.stringify(actual.platforms) !==
|
||||
JSON.stringify(['linux/amd64', 'linux/arm64']) ||
|
||||
!exactKeys(actual.deploymentFamilies, ['local', 'cluster']) ||
|
||||
!Array.isArray(actual.images) ||
|
||||
!exactKeys(actual.promotion, [
|
||||
'authority',
|
||||
'versionTags',
|
||||
'sourceTags',
|
||||
'crossRepositoryAtomicity',
|
||||
'recovery',
|
||||
]) ||
|
||||
JSON.stringify(actual.promotion) !==
|
||||
JSON.stringify({
|
||||
authority: 'complete_verified_release_set',
|
||||
versionTags: 'promote_after_complete_set_audit',
|
||||
sourceTags: 'promote_after_complete_set_audit',
|
||||
crossRepositoryAtomicity: false,
|
||||
recovery: 'verify_exact_digest_then_continue',
|
||||
}) ||
|
||||
!exactKeys(actual.requiredVerification, [
|
||||
'imageKeylessSignature',
|
||||
'imageAttestations',
|
||||
'releaseSetBuildProvenance',
|
||||
]) ||
|
||||
JSON.stringify(actual.requiredVerification) !==
|
||||
JSON.stringify({
|
||||
imageKeylessSignature: true,
|
||||
imageAttestations: [...REQUIRED_IMAGE_ATTESTATIONS],
|
||||
releaseSetBuildProvenance: true,
|
||||
})
|
||||
) {
|
||||
fail('standalone release set shape or identity is invalid');
|
||||
}
|
||||
|
||||
const selected =
|
||||
options.releaseScope === 'local'
|
||||
? [...LOCAL_IMAGES]
|
||||
: options.releaseScope === 'cluster'
|
||||
? [...CLUSTER_IMAGES]
|
||||
: [...CLUSTER_IMAGES, ...LOCAL_IMAGES];
|
||||
const expectedNames = selected.map((entry) => entry.image);
|
||||
if (actual.images.length !== selected.length) {
|
||||
fail('standalone release set image count is invalid');
|
||||
}
|
||||
for (let index = 0; index < selected.length; index += 1) {
|
||||
const image = actual.images[index];
|
||||
const expected = selected[index];
|
||||
const repository = `ghcr.io/${repositoryOwner}/${expected.repository}`;
|
||||
if (
|
||||
!exactKeys(image, [
|
||||
'name',
|
||||
'repository',
|
||||
'digest',
|
||||
'reference',
|
||||
'versionTag',
|
||||
'sourceTag',
|
||||
'platforms',
|
||||
'imageRecordDigest',
|
||||
]) ||
|
||||
image.name !== expected.image ||
|
||||
image.repository !== expected.repository ||
|
||||
!DIGEST_PATTERN.test(image.digest || '') ||
|
||||
image.reference !== `${repository}@${image.digest}` ||
|
||||
image.versionTag !== `${repository}:${options.version}` ||
|
||||
image.sourceTag !== `${repository}:sha-${options.sourceRevision}` ||
|
||||
JSON.stringify(image.platforms) !== JSON.stringify(actual.platforms) ||
|
||||
!DIGEST_PATTERN.test(image.imageRecordDigest || '')
|
||||
) {
|
||||
fail(`standalone release set image is invalid: ${expected.image}`);
|
||||
}
|
||||
}
|
||||
|
||||
const expectedFamilies = {
|
||||
local: {
|
||||
selected: ['local', 'all'].includes(options.releaseScope),
|
||||
profiles: ['edge', 'standalone'],
|
||||
images: expectedNames.filter((name) => name === 'local'),
|
||||
},
|
||||
cluster: {
|
||||
selected: ['cluster', 'all'].includes(options.releaseScope),
|
||||
profiles: ['cluster', 'worker-edge', 'worker-node'],
|
||||
images: expectedNames.filter((name) => name !== 'local'),
|
||||
},
|
||||
};
|
||||
if (
|
||||
JSON.stringify(actual.deploymentFamilies) !==
|
||||
JSON.stringify(expectedFamilies)
|
||||
) {
|
||||
fail('standalone release set deployment families are invalid');
|
||||
}
|
||||
const { releaseSetDigest, ...unsigned } = actual;
|
||||
if (
|
||||
!DIGEST_PATTERN.test(releaseSetDigest || '') ||
|
||||
releaseSetDigest !== sha256(JSON.stringify(unsigned))
|
||||
) {
|
||||
fail('standalone release set digest is invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
compatible: true,
|
||||
releaseSetDigest,
|
||||
releaseScope: actual.release.scope,
|
||||
imageCount: actual.images.length,
|
||||
images: Object.freeze([...expectedNames]),
|
||||
references: Object.freeze(actual.images.map((entry) => entry.reference)),
|
||||
verification: 'standalone_structure_identity_and_self_digest',
|
||||
sourceRecordsReplayed: false,
|
||||
});
|
||||
}
|
||||
|
||||
function readRecordDirectory(directoryPath, candidate) {
|
||||
const resolved = resolveCanonicalAbsolute(directoryPath, 'records');
|
||||
const stat = fs.lstatSync(resolved);
|
||||
@@ -345,8 +500,7 @@ function parseArguments(argv) {
|
||||
fail('arguments are invalid');
|
||||
values[match[1]] = match[2];
|
||||
}
|
||||
const common = [
|
||||
'candidate',
|
||||
const identity = [
|
||||
'mode',
|
||||
'release-scope',
|
||||
'repository-owner',
|
||||
@@ -354,6 +508,7 @@ function parseArguments(argv) {
|
||||
'source-revision',
|
||||
'version',
|
||||
];
|
||||
const common = ['candidate', ...identity];
|
||||
const expected =
|
||||
values.mode === 'record-image'
|
||||
? [...common, 'digest', 'image', 'output']
|
||||
@@ -361,6 +516,8 @@ function parseArguments(argv) {
|
||||
? [...common, 'output', 'records']
|
||||
: values.mode === 'audit'
|
||||
? [...common, 'records', 'report']
|
||||
: values.mode === 'inspect'
|
||||
? [...identity, 'report']
|
||||
: [];
|
||||
if (
|
||||
expected.length === 0 ||
|
||||
@@ -371,7 +528,7 @@ function parseArguments(argv) {
|
||||
}
|
||||
return Object.freeze({
|
||||
mode: values.mode,
|
||||
candidate: values.candidate,
|
||||
...(values.candidate ? { candidate: values.candidate } : {}),
|
||||
version: values.version,
|
||||
sourceRevision: values['source-revision'],
|
||||
sourceRef: values['source-ref'],
|
||||
@@ -387,6 +544,12 @@ function parseArguments(argv) {
|
||||
|
||||
function runCli(argv, root = DEFAULT_ROOT, output = process.stdout) {
|
||||
const options = parseArguments(argv);
|
||||
if (options.mode === 'inspect') {
|
||||
const report = readCanonicalJson(options.report, 'release set');
|
||||
const inspection = inspectReleaseSet(report, options);
|
||||
output.write(canonicalJson(inspection));
|
||||
return inspection;
|
||||
}
|
||||
const candidate = readCanonicalJson(options.candidate, 'release candidate');
|
||||
if (options.mode === 'record-image') {
|
||||
const record = createVerifiedImageRecord({ ...options, candidate, root });
|
||||
@@ -435,6 +598,7 @@ module.exports = Object.freeze({
|
||||
auditReleaseSet,
|
||||
createReleaseSet,
|
||||
createVerifiedImageRecord,
|
||||
inspectReleaseSet,
|
||||
parseArguments,
|
||||
runCli,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user