mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:58:17 +08:00
511 lines
16 KiB
JavaScript
511 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
const {
|
|
CLOSURE_RECEIPT_SCHEMA,
|
|
PUBLICATION_PLAN_SCHEMA,
|
|
TAG_OBSERVATION_SCHEMA,
|
|
auditClosureReceipt,
|
|
createClosureReceipt,
|
|
createPublicationPlan,
|
|
createPublicationTagObservation,
|
|
parseArguments,
|
|
runCli,
|
|
} = require('../../scripts/ql3-release-publication-closure-contract.cjs');
|
|
const {
|
|
ARTIFACT_TYPE,
|
|
OCI_EMPTY_CONFIG_DIGEST,
|
|
OCI_EMPTY_CONFIG_MEDIA_TYPE,
|
|
OCI_MANIFEST_MEDIA_TYPE,
|
|
createCatalogPlan,
|
|
createCatalogReceipt,
|
|
} = require('../../scripts/ql3-release-catalog-contract.cjs');
|
|
const {
|
|
createReleaseSet,
|
|
createVerifiedImageRecord,
|
|
} = require('../../scripts/ql3-release-set-contract.cjs');
|
|
const {
|
|
createReleaseCandidateContract,
|
|
} = require('../../scripts/ql3-release-candidate-contract.cjs');
|
|
const {
|
|
readReleaseIdentity,
|
|
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
|
const {
|
|
createDeploymentReadinessReceipt,
|
|
} = require('../../scripts/ql3-release-deployment-readiness-contract.cjs');
|
|
const {
|
|
privateReleaseEvidenceReceipts,
|
|
} = require('./ql3ReleaseEvidenceFixture.cjs');
|
|
|
|
const root = path.resolve(__dirname, '../..');
|
|
const version = readReleaseIdentity(root).version;
|
|
const identity = Object.freeze({
|
|
version,
|
|
sourceRevision: 'e'.repeat(40),
|
|
sourceRef: `refs/tags/v${version}`,
|
|
repositoryOwner: 'qinglong-release',
|
|
sourceRepository: 'qinglong-release/qinglong',
|
|
});
|
|
|
|
function sha256(value) {
|
|
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
|
}
|
|
|
|
function releaseSet(scope) {
|
|
const candidate = createReleaseCandidateContract({
|
|
root,
|
|
version,
|
|
sourceRevision: identity.sourceRevision,
|
|
sourceRef: identity.sourceRef,
|
|
releaseScope: scope,
|
|
});
|
|
const records = candidate.images.map((entry, index) =>
|
|
createVerifiedImageRecord({
|
|
root,
|
|
candidate,
|
|
...identity,
|
|
releaseScope: scope,
|
|
image: entry.image,
|
|
digest: `sha256:${String(index + 1).repeat(64)}`,
|
|
}),
|
|
);
|
|
return createReleaseSet({
|
|
root,
|
|
candidate,
|
|
records,
|
|
evidenceReceipts: privateReleaseEvidenceReceipts(candidate.release),
|
|
...identity,
|
|
validationClockMs: Date.parse('2026-08-18T00:05:00.000Z'),
|
|
releaseScope: scope,
|
|
});
|
|
}
|
|
|
|
function manifestFor(plan) {
|
|
return JSON.stringify({
|
|
schemaVersion: 2,
|
|
mediaType: OCI_MANIFEST_MEDIA_TYPE,
|
|
artifactType: ARTIFACT_TYPE,
|
|
config: {
|
|
mediaType: OCI_EMPTY_CONFIG_MEDIA_TYPE,
|
|
digest: OCI_EMPTY_CONFIG_DIGEST,
|
|
size: 2,
|
|
},
|
|
layers: [
|
|
{
|
|
mediaType: ARTIFACT_TYPE,
|
|
digest: plan.releaseSet.contentDigest,
|
|
size: plan.releaseSet.bytes,
|
|
annotations: {
|
|
'org.opencontainers.image.title': plan.releaseSet.fileName,
|
|
},
|
|
},
|
|
],
|
|
annotations: { ...plan.catalog.annotations },
|
|
});
|
|
}
|
|
|
|
function catalogConsumption(set, manifestDigest, marker) {
|
|
return {
|
|
compatible: true,
|
|
releaseScope: set.release.scope,
|
|
sourceRepository: identity.sourceRepository,
|
|
workflowIdentity: `https://github.com/${identity.sourceRepository}/.github/workflows/ql3-image-release.yml@${identity.sourceRef}`,
|
|
releaseSetDigest: set.releaseSetDigest,
|
|
catalogManifestDigest: manifestDigest,
|
|
immutableReference: `ghcr.io/${identity.repositoryOwner}/qinglong3-release-catalog@${manifestDigest}`,
|
|
imageCount: set.images.length,
|
|
discoveryTagAuthority: 'none',
|
|
externalToolResultsReplayed: false,
|
|
deploymentMutation: false,
|
|
contentDigest: sha256(marker),
|
|
releaseSet: set,
|
|
};
|
|
}
|
|
|
|
function localReport(profile, consumption, marker) {
|
|
return {
|
|
value: {
|
|
schemaVersion: 1,
|
|
profile,
|
|
generation: 2,
|
|
exactRepoDigest: true,
|
|
composeMerge: true,
|
|
rolloutActive: true,
|
|
durableReceipt: true,
|
|
sqliteWriteContract: 50,
|
|
sqliteBackup: true,
|
|
sqliteWriteObservation: { rows: 1 },
|
|
sqliteRestorePrepared: true,
|
|
sqliteRestoreCommitted: true,
|
|
sqliteRestoreRolloutRecovered: true,
|
|
sqliteRestoreReplayUnchanged: true,
|
|
sqliteEvidenceCollected: true,
|
|
sqliteCollectedRolloutReplayUnchanged: true,
|
|
gracefulCleanup: true,
|
|
releaseAuthority: {
|
|
mode: 'verified_release_catalog',
|
|
sourceRevision: identity.sourceRevision,
|
|
sourceRef: identity.sourceRef,
|
|
scope: consumption.releaseScope,
|
|
releaseSetDigest: consumption.releaseSetDigest,
|
|
catalogManifestDigest: consumption.catalogManifestDigest,
|
|
catalogConsumptionDigest: consumption.contentDigest,
|
|
selectionDigest: sha256('local-selection'),
|
|
},
|
|
compatible: true,
|
|
},
|
|
digest: sha256(marker),
|
|
};
|
|
}
|
|
|
|
function clusterReport(consumption) {
|
|
return {
|
|
value: {
|
|
schemaVersion: 1,
|
|
schema: 'qinglong/kubernetes-deployment-live-evidence@v1',
|
|
kubernetes: {
|
|
serverVersion: 'v1.34.3+k3s1',
|
|
architecture: 'linux/amd64',
|
|
nodeCount: 3,
|
|
clusterUid: 'cluster-fixture',
|
|
},
|
|
deployment: {
|
|
replicas: 0,
|
|
immutableImages: true,
|
|
headPhase: 'committed',
|
|
headGeneration: 2,
|
|
deploymentDigest: sha256('deployment'),
|
|
},
|
|
releaseAuthority: {
|
|
mode: 'verified_release_catalog',
|
|
version,
|
|
sourceRevision: identity.sourceRevision,
|
|
sourceRef: identity.sourceRef,
|
|
scope: consumption.releaseScope,
|
|
releaseSetDigest: consumption.releaseSetDigest,
|
|
catalogManifestDigest: consumption.catalogManifestDigest,
|
|
catalogConsumptionDigest: consumption.contentDigest,
|
|
immutableReference: consumption.immutableReference,
|
|
},
|
|
preflightDigest: sha256('preflight'),
|
|
receiptDigest: sha256('deployment-receipt'),
|
|
receiptAuditCompatible: true,
|
|
retirement: {
|
|
receiptAuditCompatible: true,
|
|
targetAbsent: true,
|
|
uidResourceVersionDeletePreconditions: true,
|
|
deploymentHeadCas: true,
|
|
unixSocketProxy: true,
|
|
},
|
|
serverSideDryRun: true,
|
|
serverSideApply: true,
|
|
convergenceRead: true,
|
|
deploymentHeadCas: true,
|
|
resourceInventoryClosed: true,
|
|
crossResourceAtomicity: false,
|
|
cleanupComplete: true,
|
|
},
|
|
digest: sha256('cluster-report'),
|
|
};
|
|
}
|
|
|
|
function deploymentReadiness(set, manifestDigest) {
|
|
const scope = set.release.scope;
|
|
const finalizerConsumption = catalogConsumption(
|
|
set,
|
|
manifestDigest,
|
|
'finalizer-consumption',
|
|
);
|
|
const input = {
|
|
identity: { ...identity, releaseScope: scope },
|
|
finalizerConsumption,
|
|
};
|
|
if (scope !== 'cluster') {
|
|
const consumption = catalogConsumption(
|
|
set,
|
|
manifestDigest,
|
|
'local-consumption',
|
|
);
|
|
input.local = {
|
|
consumption,
|
|
edge: localReport('edge', consumption, 'edge-report'),
|
|
standalone: localReport('standalone', consumption, 'standalone-report'),
|
|
};
|
|
}
|
|
if (scope !== 'local') {
|
|
const consumption = catalogConsumption(
|
|
set,
|
|
manifestDigest,
|
|
'cluster-consumption',
|
|
);
|
|
input.cluster = { consumption, report: clusterReport(consumption) };
|
|
}
|
|
return createDeploymentReadinessReceipt(input);
|
|
}
|
|
|
|
function fixture(scope = 'all') {
|
|
const set = releaseSet(scope);
|
|
const options = { ...identity, releaseScope: scope };
|
|
const catalogPlan = createCatalogPlan(set, options);
|
|
const manifest = manifestFor(catalogPlan);
|
|
const manifestDigest = sha256(manifest);
|
|
const catalogReceipt = createCatalogReceipt(
|
|
catalogPlan,
|
|
manifest,
|
|
manifestDigest,
|
|
);
|
|
const readiness = deploymentReadiness(set, manifestDigest);
|
|
const publicationPlan = createPublicationPlan(
|
|
set,
|
|
catalogPlan,
|
|
catalogReceipt,
|
|
readiness,
|
|
manifest,
|
|
manifestDigest,
|
|
options,
|
|
);
|
|
const tags = publicationPlan.images.flatMap((image) =>
|
|
image.tags.map((tag) => ({
|
|
image: image.name,
|
|
kind: tag.kind,
|
|
reference: tag.reference,
|
|
digest: image.digest,
|
|
})),
|
|
);
|
|
const observation = createPublicationTagObservation(publicationPlan, tags);
|
|
return {
|
|
set,
|
|
options,
|
|
catalogPlan,
|
|
manifest,
|
|
manifestDigest,
|
|
catalogReceipt,
|
|
readiness,
|
|
publicationPlan,
|
|
observation,
|
|
};
|
|
}
|
|
|
|
function temporaryDirectory(t) {
|
|
const directory = fs.realpathSync(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-release-closure-')),
|
|
);
|
|
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
|
return directory;
|
|
}
|
|
|
|
function writeCanonical(filePath, value) {
|
|
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
}
|
|
|
|
test('plans tag publication only from a verified immutable catalog', () => {
|
|
for (const scope of ['local', 'cluster', 'all']) {
|
|
const { publicationPlan, set } = fixture(scope);
|
|
assert.equal(publicationPlan.schema, PUBLICATION_PLAN_SCHEMA);
|
|
assert.equal(
|
|
publicationPlan.promotionPolicy.authority,
|
|
'verified_catalog_bound_deployments',
|
|
);
|
|
assert.equal(
|
|
publicationPlan.requiredPrerequisites.deploymentReadiness,
|
|
'scope_exact_receipt_attested_before_tag_promotion',
|
|
);
|
|
assert.equal(publicationPlan.promotionPolicy.registryTagCas, false);
|
|
assert.equal(publicationPlan.images.length, set.images.length);
|
|
assert.equal(
|
|
publicationPlan.images.every((image) => image.tags.length === 2),
|
|
true,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('creates a deterministic final closure receipt for every exact tag', () => {
|
|
const { publicationPlan, observation } = fixture();
|
|
const first = createClosureReceipt(publicationPlan, observation);
|
|
const replay = createClosureReceipt(publicationPlan, observation);
|
|
assert.equal(first.schema, CLOSURE_RECEIPT_SCHEMA);
|
|
assert.equal(observation.schema, TAG_OBSERVATION_SCHEMA);
|
|
assert.deepEqual(first, replay);
|
|
assert.equal(first.publishedTags.length, publicationPlan.images.length * 2);
|
|
assert.equal(first.verification.catalogReadyBeforeTagMutation, true);
|
|
assert.equal(first.verification.deploymentReadyBeforeTagMutation, true);
|
|
assert.equal(first.verification.allTagsExactDigest, true);
|
|
assert.equal(first.verification.registryTagCas, false);
|
|
assert.equal(
|
|
auditClosureReceipt(first, publicationPlan, observation).compatible,
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('rejects missing, reordered, extra or digest-drifted tag observations', () => {
|
|
const { publicationPlan, observation } = fixture('cluster');
|
|
const variants = [
|
|
observation.tags.slice(1),
|
|
[...observation.tags].reverse(),
|
|
[...observation.tags, observation.tags[0]],
|
|
observation.tags.map((entry, index) =>
|
|
index === 0 ? { ...entry, digest: `sha256:${'f'.repeat(64)}` } : entry,
|
|
),
|
|
];
|
|
for (const tags of variants) {
|
|
assert.throws(
|
|
() => createPublicationTagObservation(publicationPlan, tags),
|
|
/observations differ from the exact plan/,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('rejects a publication plan detached from release-set or catalog evidence', () => {
|
|
const current = fixture('local');
|
|
const other = fixture('cluster');
|
|
assert.throws(
|
|
() =>
|
|
createPublicationPlan(
|
|
current.set,
|
|
other.catalogPlan,
|
|
other.catalogReceipt,
|
|
current.readiness,
|
|
other.manifest,
|
|
other.manifestDigest,
|
|
current.options,
|
|
),
|
|
/catalog plan differs from the standalone release set/,
|
|
);
|
|
const weakenedReceipt = structuredClone(current.catalogReceipt);
|
|
weakenedReceipt.verification.keylessSignature = 'not_verified';
|
|
assert.throws(
|
|
() =>
|
|
createPublicationPlan(
|
|
current.set,
|
|
current.catalogPlan,
|
|
weakenedReceipt,
|
|
current.readiness,
|
|
current.manifest,
|
|
current.manifestDigest,
|
|
current.options,
|
|
),
|
|
/catalog receipt differs from the verified OCI manifest/,
|
|
);
|
|
});
|
|
|
|
test('rejects tampered plan, observation and closure self-digests', () => {
|
|
const { publicationPlan, observation } = fixture('local');
|
|
const badPlan = structuredClone(publicationPlan);
|
|
badPlan.promotionPolicy.registryTagCas = true;
|
|
assert.throws(
|
|
() => createPublicationTagObservation(badPlan, observation.tags),
|
|
/publication plan (?:shape|digest) is invalid/,
|
|
);
|
|
const recomputedWeakenedPlan = structuredClone(publicationPlan);
|
|
recomputedWeakenedPlan.promotionPolicy.registryTagCas = true;
|
|
delete recomputedWeakenedPlan.planDigest;
|
|
recomputedWeakenedPlan.planDigest = sha256(
|
|
JSON.stringify(recomputedWeakenedPlan),
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createPublicationTagObservation(recomputedWeakenedPlan, observation.tags),
|
|
/publication plan shape is invalid/,
|
|
);
|
|
const badObservation = structuredClone(observation);
|
|
badObservation.observationDigest = `sha256:${'a'.repeat(64)}`;
|
|
assert.throws(
|
|
() => createClosureReceipt(publicationPlan, badObservation),
|
|
/observation digest is invalid/,
|
|
);
|
|
const receipt = createClosureReceipt(publicationPlan, observation);
|
|
const badReceipt = structuredClone(receipt);
|
|
badReceipt.verification.registryTagCas = true;
|
|
assert.throws(
|
|
() => auditClosureReceipt(badReceipt, publicationPlan, observation),
|
|
/closure receipt differs/,
|
|
);
|
|
});
|
|
|
|
test('runs plan, close and audit as canonical no-replace CLI stages', (t) => {
|
|
const value = fixture('cluster');
|
|
const directory = temporaryDirectory(t);
|
|
const files = {
|
|
releaseSet: path.join(directory, 'release-set.json'),
|
|
catalogPlan: path.join(directory, 'catalog-plan.json'),
|
|
catalogReceipt: path.join(directory, 'catalog-receipt.json'),
|
|
readiness: path.join(directory, 'deployment-readiness.json'),
|
|
manifest: path.join(directory, 'manifest.json'),
|
|
publicationPlan: path.join(directory, 'publication-plan.json'),
|
|
observation: path.join(directory, 'observation.json'),
|
|
receipt: path.join(directory, 'receipt.json'),
|
|
};
|
|
writeCanonical(files.releaseSet, value.set);
|
|
writeCanonical(files.catalogPlan, value.catalogPlan);
|
|
writeCanonical(files.catalogReceipt, value.catalogReceipt);
|
|
writeCanonical(files.readiness, value.readiness);
|
|
fs.writeFileSync(files.manifest, value.manifest, { mode: 0o600 });
|
|
writeCanonical(files.observation, value.observation);
|
|
const planArgs = [
|
|
'--mode=plan',
|
|
`--version=${version}`,
|
|
`--source-revision=${identity.sourceRevision}`,
|
|
`--source-ref=${identity.sourceRef}`,
|
|
'--release-scope=cluster',
|
|
`--repository-owner=${identity.repositoryOwner}`,
|
|
`--source-repository=${identity.sourceRepository}`,
|
|
`--release-set=${files.releaseSet}`,
|
|
`--catalog-plan=${files.catalogPlan}`,
|
|
`--catalog-receipt=${files.catalogReceipt}`,
|
|
`--deployment-readiness=${files.readiness}`,
|
|
`--catalog-manifest=${files.manifest}`,
|
|
`--catalog-manifest-digest=${value.manifestDigest}`,
|
|
`--output=${files.publicationPlan}`,
|
|
];
|
|
const output = { write() {} };
|
|
assert.equal(runCli(planArgs, output).schema, PUBLICATION_PLAN_SCHEMA);
|
|
assert.throws(() => runCli(planArgs, output), /EEXIST/);
|
|
assert.equal(
|
|
runCli(
|
|
[
|
|
'--mode=close',
|
|
`--plan=${files.publicationPlan}`,
|
|
`--observations=${files.observation}`,
|
|
`--output=${files.receipt}`,
|
|
],
|
|
output,
|
|
).schema,
|
|
CLOSURE_RECEIPT_SCHEMA,
|
|
);
|
|
assert.equal(
|
|
runCli(
|
|
[
|
|
'--mode=audit',
|
|
`--plan=${files.publicationPlan}`,
|
|
`--observations=${files.observation}`,
|
|
`--receipt=${files.receipt}`,
|
|
],
|
|
output,
|
|
).compatible,
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('rejects ambiguous CLI modes and extra arguments', () => {
|
|
assert.throws(
|
|
() => parseArguments(['--mode=close']),
|
|
/arguments are invalid/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseArguments([
|
|
'--mode=audit',
|
|
'--plan=/tmp/plan',
|
|
'--observations=/tmp/observations',
|
|
'--receipt=/tmp/receipt',
|
|
'--extra=true',
|
|
]),
|
|
/arguments are invalid/,
|
|
);
|
|
});
|