mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 03:18:09 +08:00
feat(ql3): close public local release pair
This commit is contained in:
@@ -22,11 +22,13 @@ import {
|
||||
} from '../foundation/files';
|
||||
import { deploymentPaths } from '../foundation/render';
|
||||
|
||||
const SELECTION_SCHEMA = 'qinglong/local-compose-image-selection@v2';
|
||||
const SELECTION_SCHEMA = 'qinglong/local-compose-image-selection@v3';
|
||||
const CATALOG_SCHEMA = 'qinglong/release-catalog-consumption-ceremony@v1';
|
||||
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
||||
const IMAGE_PATTERN =
|
||||
/^ghcr\.io\/([a-z0-9](?:[a-z0-9-]{0,38}))\/qinglong3-local-application@sha256:[0-9a-f]{64}$/;
|
||||
const OPERATOR_IMAGE_PATTERN =
|
||||
/^ghcr\.io\/([a-z0-9](?:[a-z0-9-]{0,38}))\/qinglong3-local-operator@sha256:[0-9a-f]{64}$/;
|
||||
const VERSION_PATTERN = /^3\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
|
||||
const SOURCE_REVISION_PATTERN = /^[a-f0-9]{40}$/;
|
||||
const SOURCE_REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
||||
@@ -63,6 +65,7 @@ function selectionContents(selection: Readonly<ComposeImageSelection>): string {
|
||||
` catalog_manifest_digest: ${selection.catalogManifestDigest}`,
|
||||
` catalog_consumption_report_digest: ${selection.catalogConsumptionReportDigest}`,
|
||||
` catalog_discovery_tag_authority: ${selection.catalogDiscoveryTagAuthority}`,
|
||||
` operator_image: ${selection.operatorImage}`,
|
||||
` allow_root_service: ${selection.allowRootService}`,
|
||||
'services:',
|
||||
' qinglong3:',
|
||||
@@ -94,7 +97,7 @@ function parseSelection(
|
||||
label: string,
|
||||
): Readonly<ComposeImageSelection> {
|
||||
const match =
|
||||
/^x-qinglong-image-selection:\n schema: qinglong\/local-compose-image-selection@v2\n generation: (0|[1-9][0-9]{0,5})\n previous_generation: (0|[1-9][0-9]{0,5})\n rollback_target_generation: (0|[1-9][0-9]{0,5})\n mutation_id: ([0-9a-f-]+)\n changed_at_ms: ([0-9]+)\n release_selection_digest: (sha256:[a-f0-9]{64})\n release_set_digest: (sha256:[a-f0-9]{64})\n release_version: ([^\n]+)\n release_source_revision: ([a-f0-9]{40})\n release_source_ref: ([^\n]+)\n release_scope: (local|all)\n catalog_schema: qinglong\/release-catalog-consumption-ceremony@v1\n catalog_source_repository: ([^\n]+)\n catalog_workflow_identity: ([^\n]+)\n catalog_immutable_reference: ([^\n]+)\n catalog_manifest_digest: (sha256:[a-f0-9]{64})\n catalog_consumption_report_digest: (sha256:[a-f0-9]{64})\n catalog_discovery_tag_authority: none\n allow_root_service: (true|false)\nservices:\n qinglong3:\n image: ([^\n]+)\n labels:\n io\.qinglong\.deployment\.generation: "([0-9]+)"\n io\.qinglong\.deployment\.mutation: "([0-9a-f-]+)"\n io\.qinglong\.release\.selection: "(sha256:[a-f0-9]{64})"\n io\.qinglong\.release\.set: "(sha256:[a-f0-9]{64})"\n io\.qinglong\.release\.catalog-manifest: "(sha256:[a-f0-9]{64})"\n io\.qinglong\.release\.catalog-report: "(sha256:[a-f0-9]{64})"\n$/.exec(
|
||||
/^x-qinglong-image-selection:\n schema: qinglong\/local-compose-image-selection@v3\n generation: (0|[1-9][0-9]{0,5})\n previous_generation: (0|[1-9][0-9]{0,5})\n rollback_target_generation: (0|[1-9][0-9]{0,5})\n mutation_id: ([0-9a-f-]+)\n changed_at_ms: ([0-9]+)\n release_selection_digest: (sha256:[a-f0-9]{64})\n release_set_digest: (sha256:[a-f0-9]{64})\n release_version: ([^\n]+)\n release_source_revision: ([a-f0-9]{40})\n release_source_ref: ([^\n]+)\n release_scope: (local|all)\n catalog_schema: qinglong\/release-catalog-consumption-ceremony@v1\n catalog_source_repository: ([^\n]+)\n catalog_workflow_identity: ([^\n]+)\n catalog_immutable_reference: ([^\n]+)\n catalog_manifest_digest: (sha256:[a-f0-9]{64})\n catalog_consumption_report_digest: (sha256:[a-f0-9]{64})\n catalog_discovery_tag_authority: none\n operator_image: ([^\n]+)\n allow_root_service: (true|false)\nservices:\n qinglong3:\n image: ([^\n]+)\n labels:\n io\.qinglong\.deployment\.generation: "([0-9]+)"\n io\.qinglong\.deployment\.mutation: "([0-9a-f-]+)"\n io\.qinglong\.release\.selection: "(sha256:[a-f0-9]{64})"\n io\.qinglong\.release\.set: "(sha256:[a-f0-9]{64})"\n io\.qinglong\.release\.catalog-manifest: "(sha256:[a-f0-9]{64})"\n io\.qinglong\.release\.catalog-report: "(sha256:[a-f0-9]{64})"\n$/.exec(
|
||||
contents,
|
||||
);
|
||||
if (!match) {
|
||||
@@ -122,15 +125,17 @@ function parseSelection(
|
||||
const catalogImmutableReference = match[14]!;
|
||||
const catalogManifestDigest = match[15]!;
|
||||
const catalogConsumptionReportDigest = match[16]!;
|
||||
const allowRootService = match[17] === 'true';
|
||||
const image = match[18]!;
|
||||
const labelGeneration = Number(match[19]);
|
||||
const labelMutationId = match[20];
|
||||
const labelSelectionDigest = match[21];
|
||||
const labelReleaseSetDigest = match[22];
|
||||
const labelCatalogManifestDigest = match[23];
|
||||
const labelCatalogReportDigest = match[24];
|
||||
const operatorImage = match[17]!;
|
||||
const allowRootService = match[18] === 'true';
|
||||
const image = match[19]!;
|
||||
const labelGeneration = Number(match[20]);
|
||||
const labelMutationId = match[21];
|
||||
const labelSelectionDigest = match[22];
|
||||
const labelReleaseSetDigest = match[23];
|
||||
const labelCatalogManifestDigest = match[24];
|
||||
const labelCatalogReportDigest = match[25];
|
||||
const imageMatch = IMAGE_PATTERN.exec(image);
|
||||
const operatorImageMatch = OPERATOR_IMAGE_PATTERN.exec(operatorImage);
|
||||
if (
|
||||
generation < 1 ||
|
||||
previousGeneration !== generation - 1 ||
|
||||
@@ -150,6 +155,8 @@ function parseSelection(
|
||||
!DIGEST_PATTERN.test(catalogManifestDigest) ||
|
||||
!DIGEST_PATTERN.test(catalogConsumptionReportDigest) ||
|
||||
!imageMatch ||
|
||||
!operatorImageMatch ||
|
||||
operatorImageMatch[1] !== imageMatch[1] ||
|
||||
catalogImmutableReference !==
|
||||
`ghcr.io/${imageMatch?.[1]}/qinglong3-release-catalog@${catalogManifestDigest}` ||
|
||||
labelGeneration !== generation ||
|
||||
@@ -168,6 +175,7 @@ function parseSelection(
|
||||
mutationId,
|
||||
changedAtMs,
|
||||
image,
|
||||
operatorImage,
|
||||
allowRootService,
|
||||
selectionDigest,
|
||||
releaseSetDigest,
|
||||
@@ -235,6 +243,7 @@ function releaseAuthorityFromSelection(
|
||||
): Readonly<LocalComposeReleaseAuthority> {
|
||||
return Object.freeze({
|
||||
image: selection.image,
|
||||
operatorImage: selection.operatorImage,
|
||||
allowRootService: selection.allowRootService,
|
||||
selectionDigest: selection.selectionDigest,
|
||||
releaseSetDigest: selection.releaseSetDigest,
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const MAX_SELECTION_BYTES = 64 * 1024;
|
||||
const LOCAL_SELECTION_SCHEMA = 'qinglong/local-compose-release-image@v2';
|
||||
const LOCAL_SELECTION_SCHEMA = 'qinglong/local-compose-release-image@v3';
|
||||
const CATALOG_SCHEMA = 'qinglong/release-catalog-consumption-ceremony@v1';
|
||||
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
||||
const VERSION_PATTERN = /^3\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
|
||||
@@ -11,6 +11,8 @@ const SOURCE_REVISION_PATTERN = /^[a-f0-9]{40}$/;
|
||||
const SOURCE_REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
||||
const IMAGE_PATTERN =
|
||||
/^ghcr\.io\/([a-z0-9](?:[a-z0-9-]{0,38}))\/qinglong3-local-application@(sha256:[a-f0-9]{64})$/;
|
||||
const OPERATOR_IMAGE_PATTERN =
|
||||
/^ghcr\.io\/([a-z0-9](?:[a-z0-9-]{0,38}))\/qinglong3-local-operator@(sha256:[a-f0-9]{64})$/;
|
||||
|
||||
export interface LocalComposeReleaseSelectionInput {
|
||||
readonly path: string;
|
||||
@@ -19,6 +21,7 @@ export interface LocalComposeReleaseSelectionInput {
|
||||
|
||||
export interface LocalComposeReleaseAuthority {
|
||||
readonly image: string;
|
||||
readonly operatorImage: string;
|
||||
readonly allowRootService: boolean;
|
||||
readonly selectionDigest: string;
|
||||
readonly releaseSetDigest: string;
|
||||
@@ -171,6 +174,7 @@ export function resolveLocalComposeReleaseSelection(
|
||||
'schemaVersion',
|
||||
'selectionDigest',
|
||||
'service',
|
||||
'operator',
|
||||
'verification',
|
||||
],
|
||||
'release selection',
|
||||
@@ -195,6 +199,7 @@ export function resolveLocalComposeReleaseSelection(
|
||||
'catalog',
|
||||
);
|
||||
exactKeys(value.service, ['allowRootService', 'image', 'kind'], 'service');
|
||||
exactKeys(value.operator, ['image', 'kind', 'network'], 'operator');
|
||||
exactKeys(
|
||||
value.verification,
|
||||
[
|
||||
@@ -210,9 +215,13 @@ export function resolveLocalComposeReleaseSelection(
|
||||
const release = value.release;
|
||||
const catalog = value.catalog;
|
||||
const service = value.service;
|
||||
const operator = value.operator;
|
||||
const verification = value.verification;
|
||||
const image = typeof service.image === 'string' ? service.image : '';
|
||||
const imageMatch = IMAGE_PATTERN.exec(image);
|
||||
const operatorImage =
|
||||
typeof operator.image === 'string' ? operator.image : '';
|
||||
const operatorImageMatch = OPERATOR_IMAGE_PATTERN.exec(operatorImage);
|
||||
const { selectionDigest, ...unsigned } = value;
|
||||
const calculatedDigest = digest(JSON.stringify(unsigned));
|
||||
if (
|
||||
@@ -239,10 +248,14 @@ export function resolveLocalComposeReleaseSelection(
|
||||
catalog.releaseSetDigest !== value.releaseSetDigest ||
|
||||
catalog.discoveryTagAuthority !== 'none' ||
|
||||
!imageMatch ||
|
||||
!operatorImageMatch ||
|
||||
operatorImageMatch[1] !== imageMatch[1] ||
|
||||
catalog.immutableReference !==
|
||||
`ghcr.io/${imageMatch[1]}/qinglong3-release-catalog@${catalog.manifestDigest}` ||
|
||||
service.kind !== 'compose' ||
|
||||
service.allowRootService !== allowRootService ||
|
||||
operator.kind !== 'short-lived' ||
|
||||
operator.network !== 'none-by-default' ||
|
||||
verification.releaseSet !==
|
||||
'standalone_structure_identity_and_self_digest' ||
|
||||
verification.sourceRecordsReplayed !== false ||
|
||||
@@ -262,6 +275,7 @@ export function resolveLocalComposeReleaseSelection(
|
||||
expectedSelectionDigest: input.expectedSelectionDigest,
|
||||
authority: Object.freeze({
|
||||
image,
|
||||
operatorImage,
|
||||
allowRootService,
|
||||
selectionDigest,
|
||||
releaseSetDigest: value.releaseSetDigest,
|
||||
|
||||
@@ -56,12 +56,16 @@ function releaseSelection(managementRoot, marker = 'a') {
|
||||
const image = `ghcr.io/example/qinglong3-local-application@sha256:${marker.repeat(
|
||||
64,
|
||||
)}`;
|
||||
const operatorImage = image.replace(
|
||||
'qinglong3-local-application',
|
||||
'qinglong3-local-operator',
|
||||
);
|
||||
const releaseSetDigest = prefixedDigest(`release-set:${image}`);
|
||||
const manifestDigest = prefixedDigest(`catalog-manifest:${image}`);
|
||||
const consumptionReportDigest = prefixedDigest(`catalog-report:${image}`);
|
||||
const unsigned = {
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/local-compose-release-image@v2',
|
||||
schema: 'qinglong/local-compose-release-image@v3',
|
||||
release: {
|
||||
version: '3.0.0-alpha.0',
|
||||
sourceRevision: '3'.repeat(40),
|
||||
@@ -86,6 +90,11 @@ function releaseSelection(managementRoot, marker = 'a') {
|
||||
image,
|
||||
allowRootService: rootAcknowledgement(),
|
||||
},
|
||||
operator: {
|
||||
kind: 'short-lived',
|
||||
image: operatorImage,
|
||||
network: 'none-by-default',
|
||||
},
|
||||
verification: {
|
||||
releaseSet: 'standalone_structure_identity_and_self_digest',
|
||||
sourceRecordsReplayed: false,
|
||||
|
||||
@@ -31,12 +31,16 @@ function sha256(value) {
|
||||
}
|
||||
|
||||
function releaseSelectionForImage(managementRoot, image, allowRootService) {
|
||||
const operatorImage = image.replace(
|
||||
'qinglong3-local-application',
|
||||
'qinglong3-local-operator',
|
||||
);
|
||||
const releaseSetDigest = sha256(`release-set:${image}`);
|
||||
const manifestDigest = sha256(`catalog-manifest:${image}`);
|
||||
const consumptionReportDigest = sha256(`catalog-report:${image}`);
|
||||
const unsigned = {
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/local-compose-release-image@v2',
|
||||
schema: 'qinglong/local-compose-release-image@v3',
|
||||
release: {
|
||||
version: '3.0.0-alpha.0',
|
||||
sourceRevision: '3'.repeat(40),
|
||||
@@ -61,6 +65,11 @@ function releaseSelectionForImage(managementRoot, image, allowRootService) {
|
||||
image,
|
||||
allowRootService,
|
||||
},
|
||||
operator: {
|
||||
kind: 'short-lived',
|
||||
image: operatorImage,
|
||||
network: 'none-by-default',
|
||||
},
|
||||
verification: {
|
||||
releaseSet: 'standalone_structure_identity_and_self_digest',
|
||||
sourceRecordsReplayed: false,
|
||||
@@ -393,7 +402,7 @@ function composeSelection({
|
||||
const selected = JSON.parse(fs.readFileSync(releaseSelection.path, 'utf8'));
|
||||
return [
|
||||
'x-qinglong-image-selection:',
|
||||
' schema: qinglong/local-compose-image-selection@v2',
|
||||
' schema: qinglong/local-compose-image-selection@v3',
|
||||
` generation: ${generation}`,
|
||||
` previous_generation: ${previousGeneration}`,
|
||||
` rollback_target_generation: ${rollbackTargetGeneration}`,
|
||||
@@ -412,6 +421,7 @@ function composeSelection({
|
||||
` catalog_manifest_digest: ${selected.catalog.manifestDigest}`,
|
||||
` catalog_consumption_report_digest: ${selected.catalog.consumptionReportDigest}`,
|
||||
` catalog_discovery_tag_authority: ${selected.catalog.discoveryTagAuthority}`,
|
||||
` operator_image: ${selected.operator.image}`,
|
||||
` allow_root_service: ${selected.service.allowRootService}`,
|
||||
'services:',
|
||||
' qinglong3:',
|
||||
@@ -1056,7 +1066,7 @@ test('renders bounded OpenRC and immutable rootless Compose descriptors', async
|
||||
);
|
||||
assert.match(
|
||||
fs.readFileSync(selectionPath, 'utf8'),
|
||||
/^ schema: qinglong\/local-compose-image-selection@v2$/m,
|
||||
/^ schema: qinglong\/local-compose-image-selection@v3$/m,
|
||||
);
|
||||
assert.match(
|
||||
fs.readFileSync(selectionPath, 'utf8'),
|
||||
@@ -2200,6 +2210,16 @@ test('upgrades and rolls back Compose image selections with generation CAS', asy
|
||||
fs.readFileSync(selectionPath, 'utf8'),
|
||||
new RegExp(`^ image: ${upgradedImage}$`, 'm'),
|
||||
);
|
||||
assert.match(
|
||||
fs.readFileSync(selectionPath, 'utf8'),
|
||||
new RegExp(
|
||||
`^ operator_image: ${upgradedImage.replace(
|
||||
'/qinglong3-local-application@',
|
||||
'/qinglong3-local-operator@',
|
||||
)}$`,
|
||||
'm',
|
||||
),
|
||||
);
|
||||
assert.equal(mode(path.join(revisions, '2.yaml')), 0o600);
|
||||
assert.equal(fs.readFileSync(composePath, 'utf8'), stableDescriptor);
|
||||
|
||||
@@ -2224,6 +2244,16 @@ test('upgrades and rolls back Compose image selections with generation CAS', asy
|
||||
assert.match(active, /^ generation: 3$/m);
|
||||
assert.match(active, /^ rollback_target_generation: 1$/m);
|
||||
assert.match(active, new RegExp(`^ image: ${state.composeImage}$`, 'm'));
|
||||
assert.match(
|
||||
active,
|
||||
new RegExp(
|
||||
`^ operator_image: ${state.composeImage.replace(
|
||||
'/qinglong3-local-application@',
|
||||
'/qinglong3-local-operator@',
|
||||
)}$`,
|
||||
'm',
|
||||
),
|
||||
);
|
||||
assert.equal(
|
||||
fs
|
||||
.readFileSync(path.join(revisions, '2.yaml'), 'utf8')
|
||||
|
||||
Reference in New Issue
Block a user