mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): close public local release pair
This commit is contained in:
@@ -12,9 +12,15 @@ function writeSyntheticLocalReleaseSelection(options) {
|
||||
const releaseSetDigest = sha256(`release-set:${options.image}`);
|
||||
const manifestDigest = sha256(`catalog-manifest:${options.image}`);
|
||||
const consumptionReportDigest = sha256(`catalog-report:${options.image}`);
|
||||
const operatorImage =
|
||||
options.operatorImage ||
|
||||
options.image.replace(
|
||||
'/qinglong3-local-application@',
|
||||
'/qinglong3-local-operator@',
|
||||
);
|
||||
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: options.sourceRevision ?? '3'.repeat(40),
|
||||
@@ -39,6 +45,11 @@ function writeSyntheticLocalReleaseSelection(options) {
|
||||
image: options.image,
|
||||
allowRootService: options.allowRootService,
|
||||
},
|
||||
operator: {
|
||||
kind: 'short-lived',
|
||||
image: operatorImage,
|
||||
network: 'none-by-default',
|
||||
},
|
||||
verification: {
|
||||
releaseSet: 'standalone_structure_identity_and_self_digest',
|
||||
sourceRecordsReplayed: false,
|
||||
|
||||
@@ -168,6 +168,11 @@ function auditClusterImageCiWorkflow(
|
||||
dockerfile: 'deploy/containers/ql3-local-application/Dockerfile',
|
||||
target: 'runtime',
|
||||
},
|
||||
{
|
||||
image: 'local-operator',
|
||||
dockerfile: 'deploy/containers/ql3-local-operator/Dockerfile',
|
||||
target: 'runtime',
|
||||
},
|
||||
{
|
||||
image: 'worker',
|
||||
dockerfile: 'deploy/containers/ql3-worker/Dockerfile',
|
||||
@@ -185,7 +190,7 @@ function auditClusterImageCiWorkflow(
|
||||
JSON.stringify(expectedOciMatrix)
|
||||
) {
|
||||
throw new Error(
|
||||
'image CI matrices must contain only exact control/control-ai/admin/local/worker amd64/arm64 evidence targets',
|
||||
'image CI matrices must contain only exact control/control-ai/admin/local/local-operator/worker amd64/arm64 evidence targets',
|
||||
);
|
||||
}
|
||||
const expectedTrivyInputs = {
|
||||
@@ -205,7 +210,13 @@ function auditClusterImageCiWorkflow(
|
||||
localImageJob,
|
||||
'qinglong3-local-application:ci-${{ matrix.image_arch }}',
|
||||
'${{ runner.temp }}/ql3-local-${{ matrix.image_arch }}.trivyignore.yaml',
|
||||
'local',
|
||||
'Local application',
|
||||
],
|
||||
[
|
||||
localImageJob,
|
||||
'qinglong3-local-operator:ci-${{ matrix.image_arch }}',
|
||||
'${{ runner.temp }}/ql3-local-operator-${{ matrix.image_arch }}.trivyignore.yaml',
|
||||
'Local operator',
|
||||
],
|
||||
[
|
||||
clusterImageJob,
|
||||
@@ -217,7 +228,8 @@ function auditClusterImageCiWorkflow(
|
||||
const step = job?.steps?.find(
|
||||
(entry) =>
|
||||
entry.uses ===
|
||||
'aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25',
|
||||
'aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25' &&
|
||||
entry.with?.['image-ref'] === imageRef,
|
||||
);
|
||||
if (
|
||||
!step ||
|
||||
@@ -246,14 +258,19 @@ function auditClusterImageCiWorkflow(
|
||||
requireOccurrences(
|
||||
source,
|
||||
/node scripts\/ql3-image-os-vulnerability-policy\.cjs/g,
|
||||
2,
|
||||
3,
|
||||
'native image CI must materialize reviewed image-scoped Trivy exceptions',
|
||||
);
|
||||
requirePattern(
|
||||
source,
|
||||
/--image=local\s+--output=\$\{\{ runner\.temp \}\}\/ql3-local-\$\{\{ matrix\.image_arch \}\}\.trivyignore\.yaml/,
|
||||
/--image=local \\\s+--output=\$\{\{ runner\.temp \}\}\/ql3-local-\$\{\{ matrix\.image_arch \}\}\.trivyignore\.yaml/,
|
||||
'local native image CI must materialize the local exception view',
|
||||
);
|
||||
requirePattern(
|
||||
source,
|
||||
/--image=local-operator \\\s+--output=\$\{\{ runner\.temp \}\}\/ql3-local-operator-\$\{\{ matrix\.image_arch \}\}\.trivyignore\.yaml/,
|
||||
'Local operator native image CI must materialize its own exception view',
|
||||
);
|
||||
requirePattern(
|
||||
source,
|
||||
/--image=\$\{\{ matrix\.image \}\}\s+--output=\$\{\{ runner\.temp \}\}\/ql3-\$\{\{ matrix\.image \}\}-\$\{\{ matrix\.image_arch \}\}\.trivyignore\.yaml/,
|
||||
@@ -489,8 +506,8 @@ function auditClusterImageCiWorkflow(
|
||||
);
|
||||
requirePattern(
|
||||
source,
|
||||
/- image: control\s+dockerfile: deploy\/containers\/ql3-cluster-control\/Dockerfile\s+target: runtime\s+- image: control-ai\s+dockerfile: deploy\/containers\/ql3-cluster-control\/Dockerfile\s+target: runtime-ai\s+- image: admin\s+dockerfile: deploy\/containers\/ql3-cluster-admin\/Dockerfile\s+target: runtime\s+- image: local\s+dockerfile: deploy\/containers\/ql3-local-application\/Dockerfile\s+target: runtime\s+- image: worker\s+dockerfile: deploy\/containers\/ql3-worker\/Dockerfile\s+target: runtime/,
|
||||
'OCI evidence CI must build independent control, control-ai, admin, local and worker images',
|
||||
/- image: control\s+dockerfile: deploy\/containers\/ql3-cluster-control\/Dockerfile\s+target: runtime\s+- image: control-ai\s+dockerfile: deploy\/containers\/ql3-cluster-control\/Dockerfile\s+target: runtime-ai\s+- image: admin\s+dockerfile: deploy\/containers\/ql3-cluster-admin\/Dockerfile\s+target: runtime\s+- image: local\s+dockerfile: deploy\/containers\/ql3-local-application\/Dockerfile\s+target: runtime\s+- image: local-operator\s+dockerfile: deploy\/containers\/ql3-local-operator\/Dockerfile\s+target: runtime\s+- image: worker\s+dockerfile: deploy\/containers\/ql3-worker\/Dockerfile\s+target: runtime/,
|
||||
'OCI evidence CI must build independent control, control-ai, admin, local, local-operator and worker images',
|
||||
);
|
||||
requirePattern(
|
||||
source,
|
||||
@@ -519,7 +536,14 @@ function auditClusterImageCiWorkflow(
|
||||
'local image CI must generate and inventory-check the exact local SBOM profile',
|
||||
);
|
||||
return {
|
||||
images: ['control', 'control-ai', 'admin', 'local', 'worker'],
|
||||
images: [
|
||||
'control',
|
||||
'control-ai',
|
||||
'admin',
|
||||
'local',
|
||||
'local-operator',
|
||||
'worker',
|
||||
],
|
||||
nativeArchitectures: ['amd64', 'arm64'],
|
||||
runtimeInventory: true,
|
||||
clusterAdminProductFacade: true,
|
||||
@@ -844,7 +868,7 @@ function auditReleaseWorkflow(source) {
|
||||
);
|
||||
}
|
||||
if (
|
||||
!/ql3-release-set-contract\.cjs[\s\S]*--mode=record-image[\s\S]*--version="\$\{RELEASE_VERSION\}"[\s\S]*--source-revision="\$\{GITHUB_SHA\}"[\s\S]*--source-ref="\$\{GITHUB_REF\}"[\s\S]*--release-scope="\$\{RELEASE_SCOPE\}"[\s\S]*--repository-owner="\$\{owner\}"[\s\S]*--candidate="\$\{RUNNER_TEMP\}\/\$\{\{ matrix\.repository \}\}-release-candidate-contract\.json"[\s\S]*--image="\$\{\{ matrix\.image \}\}"[\s\S]*--digest="\$\{DIGEST\}"/.test(
|
||||
!/ql3-release-set-contract\.cjs[\s\S]*--mode=record-image[\s\S]*--version="\$\{RELEASE_VERSION\}"[\s\S]*--source-revision="\$\{GITHUB_SHA\}"[\s\S]*--source-ref="\$\{GITHUB_REF\}"[\s\S]*--release-scope="\$\{RELEASE_SCOPE\}"[\s\S]*--repository-owner="\$\{owner\}"[\s\S]*--candidate="\$\{RUNNER_TEMP\}\/\$\{\{ matrix\.repository \}\}-release-candidate-contract\.json"[\s\S]*--image="\$\{\{ matrix\.image \}\}"[\s\S]*--local-role-verification="\$\{\{ matrix\.local_role_verification \}\}"[\s\S]*--digest="\$\{DIGEST\}"/.test(
|
||||
publishSteps[recordIndex]?.run ?? '',
|
||||
) ||
|
||||
JSON.stringify(publishSteps[recordUploadIndex]?.with) !==
|
||||
@@ -991,10 +1015,10 @@ function auditReleaseWorkflow(source) {
|
||||
localCatalogDeploymentSteps[5]?.run ?? '',
|
||||
) ||
|
||||
localCatalogDeploymentSteps[6]?.id !== 'local-selection' ||
|
||||
!/ql3-deployment-lock-contract\.cjs[\s\S]*--mode=local-create[\s\S]*--consumption-bundle="\$\{CONSUMPTION_BUNDLE\}"[\s\S]*--allow-root-service=false[\s\S]*--output="\$\{selection\}"[\s\S]*ql3-deployment-lock-contract\.cjs[\s\S]*--mode=local-audit[\s\S]*--selection="\$\{selection\}"[\s\S]*selection-digest=\$\{selection\.selectionDigest\}[\s\S]*image=\$\{selection\.service\.image\}/u.test(
|
||||
!/ql3-deployment-lock-contract\.cjs[\s\S]*--mode=local-create[\s\S]*--consumption-bundle="\$\{CONSUMPTION_BUNDLE\}"[\s\S]*--allow-root-service=false[\s\S]*--output="\$\{selection\}"[\s\S]*ql3-deployment-lock-contract\.cjs[\s\S]*--mode=local-audit[\s\S]*--selection="\$\{selection\}"[\s\S]*selection-digest=\$\{selection\.selectionDigest\}[\s\S]*image=\$\{selection\.service\.image\}[\s\S]*operator-image=\$\{selection\.operator\.image\}/u.test(
|
||||
localCatalogDeploymentSteps[6]?.run ?? '',
|
||||
) ||
|
||||
!/docker pull "\$\{IMAGE\}"[\s\S]*for profile in edge standalone[\s\S]*ql3-local-compose-rollout-live-contract\.cjs[\s\S]*--image="\$\{IMAGE\}"[\s\S]*--profile="\$\{profile\}"[\s\S]*--release-selection="\$\{RELEASE_SELECTION\}"[\s\S]*--expected-selection-digest="\$\{SELECTION_DIGEST\}"[\s\S]*verified_release_catalog[\s\S]*catalogConsumptionDigest/u.test(
|
||||
!/docker pull "\$\{IMAGE\}"[\s\S]*docker pull "\$\{OPERATOR_IMAGE\}"[\s\S]*"\$\{OPERATOR_IMAGE\}" --version[\s\S]*"\$\{OPERATOR_IMAGE\}" setup --help[\s\S]*for profile in edge standalone[\s\S]*ql3-local-compose-rollout-live-contract\.cjs[\s\S]*--image="\$\{IMAGE\}"[\s\S]*--profile="\$\{profile\}"[\s\S]*--release-selection="\$\{RELEASE_SELECTION\}"[\s\S]*--expected-selection-digest="\$\{SELECTION_DIGEST\}"[\s\S]*verified_release_catalog[\s\S]*catalogConsumptionDigest/u.test(
|
||||
localCatalogDeploymentSteps[7]?.run ?? '',
|
||||
) ||
|
||||
localCatalogDeploymentSteps[8]?.if !== 'always()' ||
|
||||
@@ -1540,7 +1564,14 @@ function auditReleaseWorkflow(source) {
|
||||
immutableArtifactRetentionDays: 1,
|
||||
attestedToPublishedDigest: true,
|
||||
},
|
||||
images: ['control', 'control-ai', 'admin', 'worker', 'local'],
|
||||
images: [
|
||||
'control',
|
||||
'control-ai',
|
||||
'admin',
|
||||
'worker',
|
||||
'local',
|
||||
'local-operator',
|
||||
],
|
||||
platforms: ['linux/amd64', 'linux/arm64'],
|
||||
keylessSignature: true,
|
||||
buildkitAttestations: ['sbom', 'provenance'],
|
||||
@@ -1578,7 +1609,7 @@ function auditReleaseWorkflow(source) {
|
||||
},
|
||||
durableCatalog: {
|
||||
repository: 'qinglong3-release-catalog',
|
||||
artifactType: 'application/vnd.qinglong.release-set.v3+json',
|
||||
artifactType: 'application/vnd.qinglong.release-set.v4+json',
|
||||
planSchema: 'qinglong/release-catalog-plan@v2',
|
||||
receiptSchema: 'qinglong/release-catalog-receipt@v2',
|
||||
tagInventoryDecisionSchema:
|
||||
|
||||
@@ -241,6 +241,39 @@ function expectedImageConfig(architecture, revision, image) {
|
||||
},
|
||||
};
|
||||
}
|
||||
if (image === 'local-operator') {
|
||||
return {
|
||||
architecture,
|
||||
os: 'linux',
|
||||
config: {
|
||||
User: '65532:65532',
|
||||
Env: [
|
||||
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||
'NODE_VERSION=24.18.0',
|
||||
'YARN_VERSION=1.22.22',
|
||||
'NODE_ENV=production',
|
||||
],
|
||||
Entrypoint: [
|
||||
'node',
|
||||
'/opt/qinglong/node_modules/@qinglong/local-owner-cli/dist/product-cli/cli.js',
|
||||
],
|
||||
WorkingDir: '/opt/qinglong',
|
||||
Labels: {
|
||||
'io.qinglong.authority': 'local-owner-management',
|
||||
'io.qinglong.lifecycle': 'short-lived',
|
||||
'io.qinglong.network': 'none-by-default',
|
||||
'org.opencontainers.image.description':
|
||||
'QingLong 3.0 short-lived Local management authority',
|
||||
'org.opencontainers.image.licenses': 'Apache-2.0',
|
||||
'org.opencontainers.image.revision': revision,
|
||||
'org.opencontainers.image.source':
|
||||
'https://github.com/whyour/qinglong',
|
||||
'org.opencontainers.image.title': 'QingLong 3.0 Local Operator',
|
||||
'org.opencontainers.image.version': QL3_VERSION,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const isControl = image === 'control' || image === 'control-ai';
|
||||
const isControlAi = image === 'control-ai';
|
||||
return {
|
||||
@@ -529,7 +562,9 @@ function auditClusterOciLayout(options = {}) {
|
||||
const image = resolveImageProfile(options.image).id;
|
||||
const expectedPlatforms = options.expectedPlatforms || EXPECTED_PLATFORMS;
|
||||
const maximumPlatformBytes =
|
||||
image === 'local' ? 128 * 1024 * 1024 : 512 * 1024 * 1024;
|
||||
image === 'local' || image === 'local-operator'
|
||||
? 128 * 1024 * 1024
|
||||
: 512 * 1024 * 1024;
|
||||
if (
|
||||
!path.isAbsolute(layoutRoot) ||
|
||||
typeof expectedRevision !== 'string' ||
|
||||
|
||||
@@ -14,7 +14,7 @@ const {
|
||||
const { inspectReleaseSet } = require('./ql3-release-set-contract.cjs');
|
||||
|
||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||
const LOCAL_SELECTION_SCHEMA = 'qinglong/local-compose-release-image@v2';
|
||||
const LOCAL_SELECTION_SCHEMA = 'qinglong/local-compose-release-image@v3';
|
||||
const KUBERNETES_LOCK_SCHEMA = 'qinglong/kubernetes-deployment-lock@v2';
|
||||
const MAX_RELEASE_SET_BYTES = 1024 * 1024;
|
||||
const MAX_MANIFEST_BYTES = 8 * 1024 * 1024;
|
||||
@@ -193,6 +193,7 @@ function createLocalSelection(releaseSet, options) {
|
||||
fail('allow-root-service must be an explicit boolean');
|
||||
}
|
||||
const local = imageByName(releaseSet, 'local');
|
||||
const operator = imageByName(releaseSet, 'local-operator');
|
||||
const unsigned = {
|
||||
schemaVersion: 1,
|
||||
schema: LOCAL_SELECTION_SCHEMA,
|
||||
@@ -205,6 +206,11 @@ function createLocalSelection(releaseSet, options) {
|
||||
image: local.reference,
|
||||
allowRootService: options.allowRootService,
|
||||
},
|
||||
operator: {
|
||||
kind: 'short-lived',
|
||||
image: operator.reference,
|
||||
network: 'none-by-default',
|
||||
},
|
||||
verification: {
|
||||
releaseSet: inspection.verification,
|
||||
sourceRecordsReplayed: inspection.sourceRecordsReplayed,
|
||||
@@ -233,6 +239,7 @@ function auditLocalSelection(actual, releaseSet, options) {
|
||||
catalogManifestDigest: actual.catalog.manifestDigest,
|
||||
immutableReference: actual.catalog.immutableReference,
|
||||
image: actual.service.image,
|
||||
operatorImage: actual.operator.image,
|
||||
networkAccess: false,
|
||||
deploymentMutation: false,
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ const IMAGES = Object.freeze([
|
||||
'control',
|
||||
'control-ai',
|
||||
'local',
|
||||
'local-operator',
|
||||
'worker',
|
||||
]);
|
||||
const MAX_POLICY_BYTES = 256 * 1024;
|
||||
@@ -108,6 +109,7 @@ function auditImageOsVulnerabilityPolicy(
|
||||
control: 0,
|
||||
'control-ai': 0,
|
||||
local: 0,
|
||||
'local-operator': 0,
|
||||
worker: 0,
|
||||
}),
|
||||
});
|
||||
@@ -118,6 +120,7 @@ function auditImageOsVulnerabilityPolicy(
|
||||
control: 0,
|
||||
'control-ai': 0,
|
||||
local: 0,
|
||||
'local-operator': 0,
|
||||
worker: 0,
|
||||
};
|
||||
const seen = new Set();
|
||||
|
||||
@@ -120,6 +120,14 @@ function resolveReleaseSelection(input, temporaryRoot, uid) {
|
||||
const unsigned = { ...selection };
|
||||
delete unsigned.selectionDigest;
|
||||
const computedDigest = sha256(JSON.stringify(unsigned));
|
||||
const applicationImageMatch =
|
||||
/^ghcr\.io\/([a-z0-9](?:[a-z0-9-]{0,38}))\/qinglong3-local-application@sha256:[a-f0-9]{64}$/u.exec(
|
||||
selection.service?.image ?? '',
|
||||
);
|
||||
const operatorImageMatch =
|
||||
/^ghcr\.io\/([a-z0-9](?:[a-z0-9-]{0,38}))\/qinglong3-local-operator@sha256:[a-f0-9]{64}$/u.exec(
|
||||
selection.operator?.image ?? '',
|
||||
);
|
||||
if (
|
||||
!stat.isFile() ||
|
||||
stat.isSymbolicLink() ||
|
||||
@@ -135,7 +143,7 @@ function resolveReleaseSelection(input, temporaryRoot, uid) {
|
||||
computedDigest !== input.releaseSelection.expectedSelectionDigest ||
|
||||
selection.selectionDigest !== computedDigest ||
|
||||
selection.schemaVersion !== 1 ||
|
||||
selection.schema !== 'qinglong/local-compose-release-image@v2' ||
|
||||
selection.schema !== 'qinglong/local-compose-release-image@v3' ||
|
||||
!/^3\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/u.test(
|
||||
selection.release?.version ?? '',
|
||||
) ||
|
||||
@@ -148,6 +156,11 @@ function resolveReleaseSelection(input, temporaryRoot, uid) {
|
||||
selection.service?.kind !== 'compose' ||
|
||||
selection.service?.image !== input.image ||
|
||||
selection.service?.allowRootService !== (uid === 0) ||
|
||||
!applicationImageMatch ||
|
||||
!operatorImageMatch ||
|
||||
applicationImageMatch[1] !== operatorImageMatch[1] ||
|
||||
selection.operator?.kind !== 'short-lived' ||
|
||||
selection.operator?.network !== 'none-by-default' ||
|
||||
selection.catalog?.schema !==
|
||||
'qinglong/release-catalog-consumption-ceremony@v1' ||
|
||||
!DIGEST_PATTERN.test(selection.catalog?.manifestDigest ?? '') ||
|
||||
|
||||
@@ -38,6 +38,13 @@ const LOCAL_IMAGES = Object.freeze([
|
||||
runtime_root:
|
||||
'deploy/containers/ql3-local-application/runtime-dependencies',
|
||||
}),
|
||||
Object.freeze({
|
||||
image: 'local-operator',
|
||||
repository: 'qinglong3-local-operator',
|
||||
dockerfile: 'deploy/containers/ql3-local-operator/Dockerfile',
|
||||
target: 'runtime',
|
||||
runtime_root: 'deploy/containers/ql3-local-operator/runtime-dependencies',
|
||||
}),
|
||||
]);
|
||||
const CLUSTER_IMAGES = Object.freeze([
|
||||
Object.freeze({
|
||||
@@ -208,7 +215,15 @@ function createReleaseCandidateContract(options) {
|
||||
version: manifest.version,
|
||||
});
|
||||
});
|
||||
const publishMatrix = images.map(({ dockerfile, target, ...image }) => image);
|
||||
const publishMatrix = images.map(({ dockerfile, target, ...image }) => ({
|
||||
...image,
|
||||
local_role_verification:
|
||||
image.image === 'local'
|
||||
? 'application_rollout_verified'
|
||||
: image.image === 'local-operator'
|
||||
? 'operator_entrypoint_verified'
|
||||
: 'not_applicable',
|
||||
}));
|
||||
const osMatrix = images.flatMap((image) =>
|
||||
nativeArchitectures.map((architecture) => ({
|
||||
image: image.image,
|
||||
@@ -285,7 +300,7 @@ function createReleaseCandidateContract(options) {
|
||||
'durable-oci-release-catalog',
|
||||
'offline-deployment-lock-materialization',
|
||||
...(options.releaseScope !== 'cluster'
|
||||
? ['edge-and-standalone-rollout']
|
||||
? ['edge-and-standalone-rollout', 'local-operator-entrypoint']
|
||||
: []),
|
||||
...(options.releaseScope !== 'local'
|
||||
? [
|
||||
|
||||
@@ -16,7 +16,7 @@ const CATALOG_PUBLICATION_DECISION_SCHEMA =
|
||||
const CATALOG_RECEIPT_SCHEMA = 'qinglong/release-catalog-receipt@v2';
|
||||
const CATALOG_TAG_INVENTORY_DECISION_SCHEMA =
|
||||
'qinglong/release-catalog-tag-inventory-decision@v1';
|
||||
const ARTIFACT_TYPE = 'application/vnd.qinglong.release-set.v3+json';
|
||||
const ARTIFACT_TYPE = 'application/vnd.qinglong.release-set.v4+json';
|
||||
const FILE_MEDIA_TYPE = ARTIFACT_TYPE;
|
||||
const OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.v1+json';
|
||||
const OCI_EMPTY_CONFIG_MEDIA_TYPE = 'application/vnd.oci.empty.v1+json';
|
||||
|
||||
@@ -311,10 +311,10 @@ function validatePublicationPlan(plan) {
|
||||
!Array.isArray(plan.images) ||
|
||||
plan.images.length !==
|
||||
(plan.release.scope === 'local'
|
||||
? 1
|
||||
? 2
|
||||
: plan.release.scope === 'cluster'
|
||||
? 4
|
||||
: 5)
|
||||
: 6)
|
||||
) {
|
||||
fail('publication plan shape is invalid');
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ const {
|
||||
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';
|
||||
const RELEASE_SET_SCHEMA = 'qinglong/release-set@v3';
|
||||
const IMAGE_RECORD_SCHEMA = 'qinglong/release-set-image-record@v2';
|
||||
const RELEASE_SET_SCHEMA = 'qinglong/release-set@v4';
|
||||
const MAX_JSON_BYTES = 1024 * 1024;
|
||||
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
||||
const OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/u;
|
||||
@@ -135,6 +135,14 @@ function selectedImage(candidate, imageName) {
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
function expectedLocalRoleVerification(imageName) {
|
||||
return imageName === 'local'
|
||||
? 'application_rollout_verified'
|
||||
: imageName === 'local-operator'
|
||||
? 'operator_entrypoint_verified'
|
||||
: 'not_applicable';
|
||||
}
|
||||
|
||||
function deriveVerifiedImageRecord(
|
||||
candidate,
|
||||
repositoryOwner,
|
||||
@@ -166,7 +174,7 @@ function deriveVerifiedImageRecord(
|
||||
remoteDigestVerified: true,
|
||||
keylessSignatureVerified: true,
|
||||
githubAttestations: [...REQUIRED_IMAGE_ATTESTATIONS],
|
||||
localProfileRolloutVerified: selected.image === 'local',
|
||||
localRoleVerification: expectedLocalRoleVerification(selected.image),
|
||||
tagPromotion: 'deferred_to_complete_release_set',
|
||||
},
|
||||
};
|
||||
@@ -179,6 +187,12 @@ function deriveVerifiedImageRecord(
|
||||
function createVerifiedImageRecord(options) {
|
||||
const candidate = verifyCandidate(options.candidate, options);
|
||||
const owner = normalizeRepositoryOwner(options.repositoryOwner);
|
||||
if (
|
||||
options.localRoleVerification !==
|
||||
expectedLocalRoleVerification(options.image)
|
||||
) {
|
||||
fail('local role verification differs from the selected image role');
|
||||
}
|
||||
return deriveVerifiedImageRecord(
|
||||
candidate,
|
||||
owner,
|
||||
@@ -214,7 +228,7 @@ function validateImageRecord(record, candidate, repositoryOwner) {
|
||||
'remoteDigestVerified',
|
||||
'keylessSignatureVerified',
|
||||
'githubAttestations',
|
||||
'localProfileRolloutVerified',
|
||||
'localRoleVerification',
|
||||
'tagPromotion',
|
||||
])
|
||||
) {
|
||||
@@ -306,10 +320,14 @@ function createReleaseSet(options) {
|
||||
return record;
|
||||
});
|
||||
const localImages = orderedRecords
|
||||
.filter((record) => record.image.name === 'local')
|
||||
.filter((record) =>
|
||||
LOCAL_IMAGES.some((entry) => entry.image === record.image.name),
|
||||
)
|
||||
.map((record) => record.image.name);
|
||||
const clusterImages = orderedRecords
|
||||
.filter((record) => record.image.name !== 'local')
|
||||
.filter((record) =>
|
||||
CLUSTER_IMAGES.some((entry) => entry.image === record.image.name),
|
||||
)
|
||||
.map((record) => record.image.name);
|
||||
const images = orderedRecords.map((record) => ({
|
||||
...record.image,
|
||||
@@ -497,12 +515,16 @@ function inspectReleaseSet(actual, options) {
|
||||
local: {
|
||||
selected: ['local', 'all'].includes(options.releaseScope),
|
||||
profiles: ['edge', 'standalone'],
|
||||
images: expectedNames.filter((name) => name === 'local'),
|
||||
images: expectedNames.filter((name) =>
|
||||
LOCAL_IMAGES.some((entry) => entry.image === name),
|
||||
),
|
||||
},
|
||||
cluster: {
|
||||
selected: ['cluster', 'all'].includes(options.releaseScope),
|
||||
profiles: ['cluster', 'worker-edge', 'worker-node'],
|
||||
images: expectedNames.filter((name) => name !== 'local'),
|
||||
images: expectedNames.filter((name) =>
|
||||
CLUSTER_IMAGES.some((entry) => entry.image === name),
|
||||
),
|
||||
},
|
||||
};
|
||||
if (
|
||||
@@ -619,7 +641,7 @@ function parseArguments(argv) {
|
||||
const common = ['candidate', ...identity];
|
||||
const expected =
|
||||
values.mode === 'record-image'
|
||||
? [...common, 'digest', 'image', 'output']
|
||||
? [...common, 'digest', 'image', 'local-role-verification', 'output']
|
||||
: values.mode === 'aggregate'
|
||||
? [...common, 'evidence-receipts', 'output', 'records']
|
||||
: values.mode === 'audit'
|
||||
@@ -643,6 +665,9 @@ function parseArguments(argv) {
|
||||
releaseScope: values['release-scope'],
|
||||
repositoryOwner: values['repository-owner'],
|
||||
...(values.image ? { image: values.image } : {}),
|
||||
...(values['local-role-verification']
|
||||
? { localRoleVerification: values['local-role-verification'] }
|
||||
: {}),
|
||||
...(values.digest ? { digest: values.digest } : {}),
|
||||
...(values.records ? { records: values.records } : {}),
|
||||
...(values['evidence-receipts']
|
||||
|
||||
Reference in New Issue
Block a user