mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): gate cluster releases through published catalogs
This commit is contained in:
@@ -115,6 +115,15 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
immutableDigestAuthority: 'verified',
|
||||
receiptAttested: true,
|
||||
},
|
||||
catalogDeploymentGate: {
|
||||
scopes: ['cluster', 'all'],
|
||||
catalogAuthority: 'immutable_digest_after_public_consumption',
|
||||
deploymentLockReconstructed: true,
|
||||
k3sNodes: 3,
|
||||
installReceiptAudited: true,
|
||||
fencedRetirementReceiptAudited: true,
|
||||
publicationAuthority: false,
|
||||
},
|
||||
localRolloutPreflight: true,
|
||||
localRolloutApply: true,
|
||||
postPublishVerification: [
|
||||
@@ -126,6 +135,8 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
'release-candidate',
|
||||
'release-set',
|
||||
'durable-catalog',
|
||||
'catalog-consumption',
|
||||
'catalog-bound-k3s-deployment',
|
||||
'release-tags',
|
||||
],
|
||||
},
|
||||
@@ -565,7 +576,7 @@ test('rejects a movable action tag in the privileged release job', () => {
|
||||
)}actions/checkout@v6${releaseSource.slice(offset + pinned.length)}`;
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/privileged publisher|immutable checkout action|release-set job/,
|
||||
/privileged publisher|immutable checkout action|release-set job|cluster release must read/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -718,13 +729,53 @@ test('rejects an image tag created before digest verification completes', () =>
|
||||
});
|
||||
|
||||
test('rejects any publisher step after the deployment lock is published', () => {
|
||||
const mutated = `${releaseSource}\n - name: Post-promotion mutation\n run: echo unsafe\n`;
|
||||
const marker = '\n release-catalog-deployment-live:';
|
||||
assert.equal(releaseSource.includes(marker), true);
|
||||
const mutated = releaseSource.replace(
|
||||
marker,
|
||||
'\n - name: Post-promotion mutation\n run: echo unsafe\n' +
|
||||
marker,
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/release-set job must download only same-run records/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects removal of the downstream catalog deployment release gate', function rejectsMissingCatalogDeploymentGate() {
|
||||
const marker = '\n release-catalog-deployment-live:';
|
||||
const offset = releaseSource.indexOf(marker);
|
||||
assert.notEqual(offset, -1);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(releaseSource.slice(0, offset)),
|
||||
/keep evidence read-only|cluster release must read the newly published immutable catalog/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects running the cluster deployment gate for Local-only releases', function rejectsLocalCatalogDeploymentGate() {
|
||||
const mutated = releaseSource.replace(
|
||||
"inputs.release_scope != 'local'",
|
||||
"inputs.release_scope == 'local'",
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/cluster release must read the newly published immutable catalog/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects publication authority in the post-publish catalog consumer', function rejectsCatalogConsumerPublicationAuthority() {
|
||||
const marker = '\n release-catalog-deployment-live:';
|
||||
const offset = releaseSource.indexOf(marker);
|
||||
assert.notEqual(offset, -1);
|
||||
const mutated = `${releaseSource.slice(0, offset)}${releaseSource
|
||||
.slice(offset)
|
||||
.replace('packages: read', 'packages: write')}`;
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/keep evidence read-only|without publication authority/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects release-set closure before every image publisher succeeds', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
" needs.publish.result == 'success'",
|
||||
|
||||
@@ -38,6 +38,10 @@ const {
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
const {
|
||||
catalogLockedArtifacts,
|
||||
deploymentArtifacts,
|
||||
} = require('../../scripts/ql3-kubernetes-deployment-live-contract.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const version = readReleaseIdentity(root).version;
|
||||
@@ -314,6 +318,112 @@ function kubernetesOptions(set, extra = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
test('materializes the Kubernetes live lock from one audited catalog consumption bundle', function materializesCatalogBoundLiveLock() {
|
||||
const set = releaseSet('cluster');
|
||||
const consumption = Object.freeze({
|
||||
...consumptionAuthority(set),
|
||||
releaseSet: set,
|
||||
});
|
||||
const consumptionBundle = '/private/ql3-release-catalog-consumption';
|
||||
let auditedOptions;
|
||||
const artifacts = catalogLockedArtifacts(
|
||||
{
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
consumptionBundle,
|
||||
},
|
||||
{
|
||||
auditCeremonyBundle(options) {
|
||||
auditedOptions = options;
|
||||
return consumption;
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.deepEqual(auditedOptions, {
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
outputDirectory: consumptionBundle,
|
||||
});
|
||||
assert.equal(artifacts.report.releaseSetDigest, set.releaseSetDigest);
|
||||
assert.equal(
|
||||
artifacts.report.catalog.manifestDigest,
|
||||
consumption.catalogManifestDigest,
|
||||
);
|
||||
assert.equal(artifacts.releaseAuthority.mode, 'verified_release_catalog');
|
||||
assert.equal(
|
||||
artifacts.releaseAuthority.catalogConsumptionDigest,
|
||||
consumption.contentDigest,
|
||||
);
|
||||
assert.deepEqual(artifacts.imageReferences, references(set));
|
||||
assert.equal(artifacts.report.manifest.resources, 7);
|
||||
assert.equal(artifacts.report.manifest.changedResources, 5);
|
||||
assert.equal(artifacts.report.manifest.admissionAuthorityCount, 1);
|
||||
const resources = [];
|
||||
yaml.loadAll(artifacts.manifest, (resource) => resources.push(resource));
|
||||
assert.equal(resources.length, 7);
|
||||
assert.equal(
|
||||
resources.find(
|
||||
(resource) => resource.metadata?.name === 'ql3-retirement-live-target',
|
||||
)?.data?.purpose,
|
||||
'catalog-bound-retirement-live-contract',
|
||||
);
|
||||
for (const resource of resources.filter(
|
||||
(entry) => entry.kind === 'Deployment',
|
||||
)) {
|
||||
assert.equal(
|
||||
resource.metadata.annotations['qinglong.io/release-set-digest'],
|
||||
set.releaseSetDigest,
|
||||
);
|
||||
assert.match(
|
||||
resource.spec.template.spec.containers[0].image,
|
||||
/^ghcr\.io\/qinglong-release\/qinglong3-[a-z-]+@sha256:[a-f0-9]{64}$/u,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('selects catalog-backed live artifacts only for one complete environment', function selectsCompleteCatalogLiveEnvironment() {
|
||||
assert.equal(
|
||||
deploymentArtifacts({}).releaseAuthority.mode,
|
||||
'synthetic_live_fixture',
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
deploymentArtifacts({
|
||||
QL3_RELEASE_CATALOG_CONSUMPTION_BUNDLE: '/private/bundle',
|
||||
}),
|
||||
/configuration is incomplete/u,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
catalogLockedArtifacts({
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
consumptionBundle: '/private/bundle',
|
||||
}),
|
||||
/requires cluster or all scope/u,
|
||||
);
|
||||
const set = releaseSet('all');
|
||||
const consumption = Object.freeze({
|
||||
...consumptionAuthority(set),
|
||||
releaseSet: set,
|
||||
});
|
||||
const environment = {
|
||||
QL3_RELEASE_CATALOG_CONSUMPTION_BUNDLE: '/private/bundle',
|
||||
QL3_RELEASE_SOURCE_REVISION: identity.sourceRevision,
|
||||
QL3_RELEASE_SOURCE_REF: identity.sourceRef,
|
||||
QL3_RELEASE_SCOPE: 'all',
|
||||
QL3_RELEASE_REPOSITORY_OWNER: identity.repositoryOwner,
|
||||
QL3_RELEASE_SOURCE_REPOSITORY: identity.sourceRepository,
|
||||
};
|
||||
const artifacts = deploymentArtifacts(environment, {
|
||||
auditCeremonyBundle: () => consumption,
|
||||
});
|
||||
assert.equal(artifacts.releaseAuthority.mode, 'verified_release_catalog');
|
||||
assert.equal(artifacts.releaseAuthority.scope, 'all');
|
||||
assert.equal(artifacts.report.release.scope, 'all');
|
||||
assert.equal(artifacts.report.requiredImages.length, 4);
|
||||
});
|
||||
|
||||
test('selects one immutable Local Compose image without adding device work', () => {
|
||||
for (const scope of ['local', 'all']) {
|
||||
const set = releaseSet(scope);
|
||||
|
||||
Reference in New Issue
Block a user