mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
feat(ql3): close public local release pair
This commit is contained in:
@@ -23,7 +23,14 @@ const releaseSource = fs.readFileSync(
|
||||
test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
assert.deepEqual(auditClusterImageRelease(root), {
|
||||
ci: {
|
||||
images: ['control', 'control-ai', 'admin', 'local', 'worker'],
|
||||
images: [
|
||||
'control',
|
||||
'control-ai',
|
||||
'admin',
|
||||
'local',
|
||||
'local-operator',
|
||||
'worker',
|
||||
],
|
||||
nativeArchitectures: ['amd64', 'arm64'],
|
||||
runtimeInventory: true,
|
||||
clusterAdminProductFacade: true,
|
||||
@@ -85,7 +92,14 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
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'],
|
||||
@@ -123,7 +137,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
},
|
||||
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:
|
||||
|
||||
@@ -29,6 +29,7 @@ function createFixture(t, options = {}) {
|
||||
const isControl = image === 'control' || image === 'control-ai';
|
||||
const isControlAi = image === 'control-ai';
|
||||
const isLocal = image === 'local';
|
||||
const isLocalOperator = image === 'local-operator';
|
||||
const isWorker = image === 'worker';
|
||||
const layoutRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-oci-layout-'));
|
||||
t.after(() => fs.rmSync(layoutRoot, { recursive: true, force: true }));
|
||||
@@ -103,7 +104,7 @@ function createFixture(t, options = {}) {
|
||||
? '0:0'
|
||||
: isWorker
|
||||
? '65532:65532'
|
||||
: isLocal
|
||||
: isLocal || isLocalOperator
|
||||
? '65532:65532'
|
||||
: '10001:10001',
|
||||
...(isControl ? { ExposedPorts: { '5800/tcp': {} } } : {}),
|
||||
@@ -119,6 +120,8 @@ function createFixture(t, options = {}) {
|
||||
? '/opt/qinglong/node_modules/@qinglong/worker-runtime/dist/process/workerProcessCli.js'
|
||||
: isLocal
|
||||
? '/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js'
|
||||
: isLocalOperator
|
||||
? '/opt/qinglong/node_modules/@qinglong/local-owner-cli/dist/product-cli/cli.js'
|
||||
: isControl
|
||||
? isControlAi
|
||||
? '/opt/qinglong/node_modules/@qinglong/cluster-control/dist/aiCli.js'
|
||||
@@ -144,8 +147,17 @@ function createFixture(t, options = {}) {
|
||||
'io.qinglong.profile': 'edge,standalone',
|
||||
}
|
||||
: {}),
|
||||
...(isLocalOperator
|
||||
? {
|
||||
'io.qinglong.authority': 'local-owner-management',
|
||||
'io.qinglong.lifecycle': 'short-lived',
|
||||
'io.qinglong.network': 'none-by-default',
|
||||
}
|
||||
: {}),
|
||||
'org.opencontainers.image.description': isLocal
|
||||
? 'QingLong 3.0 AI-excluded Edge and Standalone runtime'
|
||||
: isLocalOperator
|
||||
? 'QingLong 3.0 short-lived Local management authority'
|
||||
: isWorker
|
||||
? 'QingLong 3.0 headless Remote Worker runtime'
|
||||
: isControl
|
||||
@@ -159,6 +171,8 @@ function createFixture(t, options = {}) {
|
||||
'https://github.com/whyour/qinglong',
|
||||
'org.opencontainers.image.title': isLocal
|
||||
? 'QingLong 3.0 Local Application'
|
||||
: isLocalOperator
|
||||
? 'QingLong 3.0 Local Operator'
|
||||
: isWorker
|
||||
? 'QingLong 3.0 Worker'
|
||||
: isControl
|
||||
@@ -166,7 +180,11 @@ function createFixture(t, options = {}) {
|
||||
? 'QingLong 3.0 Cluster Control AI'
|
||||
: 'QingLong 3.0 Cluster Control'
|
||||
: 'QingLong 3.0 Cluster Admin',
|
||||
...(isLocal || isWorker || isControl || image === 'admin'
|
||||
...(isLocal ||
|
||||
isLocalOperator ||
|
||||
isWorker ||
|
||||
isControl ||
|
||||
image === 'admin'
|
||||
? {
|
||||
'org.opencontainers.image.version': version,
|
||||
}
|
||||
@@ -379,6 +397,21 @@ test('accepts the AI-excluded local image and attestation closure', (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts the short-lived Local operator image and attestation closure', (t) => {
|
||||
const report = auditClusterOciLayout({
|
||||
root,
|
||||
layoutRoot: createFixture(t, { image: 'local-operator' }),
|
||||
expectedRevision: revision,
|
||||
image: 'local-operator',
|
||||
});
|
||||
assert.equal(report.image, 'local-operator');
|
||||
assert.equal(report.maximumPlatformBytes, 128 * 1024 * 1024);
|
||||
assert.deepEqual(
|
||||
report.platforms.map((entry) => entry.spdxApplicationPackages),
|
||||
[9, 9],
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts the headless Worker image and attestation closure', (t) => {
|
||||
const report = auditClusterOciLayout({
|
||||
root,
|
||||
|
||||
@@ -93,6 +93,12 @@ function releaseSet(scope) {
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
image: entry.image,
|
||||
localRoleVerification:
|
||||
entry.image === 'local'
|
||||
? 'application_rollout_verified'
|
||||
: entry.image === 'local-operator'
|
||||
? 'operator_entrypoint_verified'
|
||||
: 'not_applicable',
|
||||
digest: `sha256:${String(index + 1).repeat(64)}`,
|
||||
}),
|
||||
);
|
||||
@@ -429,7 +435,7 @@ test('selects catalog-backed live artifacts only for one complete environment',
|
||||
assert.equal(artifacts.report.requiredImages.length, 4);
|
||||
});
|
||||
|
||||
test('selects one immutable Local Compose image without adding device work', () => {
|
||||
test('selects immutable Local runtime and short-lived operator images without adding steady-state device work', () => {
|
||||
for (const scope of ['local', 'all']) {
|
||||
const set = releaseSet(scope);
|
||||
const selection = createLocalSelection(
|
||||
@@ -437,7 +443,7 @@ test('selects one immutable Local Compose image without adding device work', ()
|
||||
options(set, { allowRootService: false }),
|
||||
);
|
||||
assert.equal(selection.deploymentFamily, 'local');
|
||||
assert.equal(selection.schema, 'qinglong/local-compose-release-image@v2');
|
||||
assert.equal(selection.schema, 'qinglong/local-compose-release-image@v3');
|
||||
assert.equal(selection.releaseSetDigest, set.releaseSetDigest);
|
||||
assert.equal(
|
||||
selection.catalog.releaseSetDigest,
|
||||
@@ -451,6 +457,9 @@ test('selects one immutable Local Compose image without adding device work', ()
|
||||
assert.equal(selection.service.kind, 'compose');
|
||||
assert.equal(selection.service.image, references(set).local);
|
||||
assert.equal(selection.service.allowRootService, false);
|
||||
assert.equal(selection.operator.kind, 'short-lived');
|
||||
assert.equal(selection.operator.image, references(set)['local-operator']);
|
||||
assert.equal(selection.operator.network, 'none-by-default');
|
||||
assert.equal(selection.verification.networkAccess, false);
|
||||
assert.equal(selection.verification.deploymentMutation, false);
|
||||
assert.equal(
|
||||
@@ -500,6 +509,18 @@ test('Local selection rejects cluster scope, implicit root policy and drift', ()
|
||||
),
|
||||
/differs from the verified release set/,
|
||||
);
|
||||
const operatorDrifted = JSON.parse(JSON.stringify(selection));
|
||||
operatorDrifted.operator.image =
|
||||
'ghcr.io/example/qinglong3-local-operator:latest';
|
||||
assert.throws(
|
||||
() =>
|
||||
auditLocalSelection(
|
||||
operatorDrifted,
|
||||
localSet,
|
||||
options(localSet, { allowRootService: true }),
|
||||
),
|
||||
/differs from the verified release set/,
|
||||
);
|
||||
});
|
||||
|
||||
test('deployment materialization rejects missing or mismatched catalog authority', () => {
|
||||
@@ -947,7 +968,7 @@ test('deployment-lock CLI cannot regress to a loose release-set input', () => {
|
||||
assert.match(source, /'consumption-bundle'/u);
|
||||
assert.match(source, /'source-repository'/u);
|
||||
assert.doesNotMatch(source, /['"]release-set['"]/u);
|
||||
assert.match(source, /qinglong\/local-compose-release-image@v2/u);
|
||||
assert.match(source, /qinglong\/local-compose-release-image@v3/u);
|
||||
assert.match(source, /qinglong\/kubernetes-deployment-lock@v2/u);
|
||||
});
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ test('accepts the empty fail-closed production exception policy', () => {
|
||||
control: 0,
|
||||
'control-ai': 0,
|
||||
local: 0,
|
||||
'local-operator': 0,
|
||||
worker: 0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ test('freezes an independent low-resource local release family', () => {
|
||||
});
|
||||
assert.deepEqual(
|
||||
contract.images.map((entry) => entry.image),
|
||||
['local'],
|
||||
['local', 'local-operator'],
|
||||
);
|
||||
assert.equal(contract.releasePlan.clusterEvidenceRequired, false);
|
||||
assert.deepEqual(contract.deploymentFamilies.local.profiles, [
|
||||
@@ -78,7 +78,7 @@ test('freezes an independent low-resource local release family', () => {
|
||||
contractDigest: contract.contractDigest,
|
||||
releaseScope: 'local',
|
||||
workspacePackageCount: 18,
|
||||
images: ['local'],
|
||||
images: ['local', 'local-operator'],
|
||||
clusterEvidenceRequired: false,
|
||||
},
|
||||
);
|
||||
@@ -141,12 +141,16 @@ test('combines local and cluster families without weakening either gate', () =>
|
||||
});
|
||||
assert.deepEqual(
|
||||
contract.images.map((entry) => entry.image),
|
||||
['control', 'control-ai', 'admin', 'worker', 'local'],
|
||||
['control', 'control-ai', 'admin', 'worker', 'local', 'local-operator'],
|
||||
);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('edge-and-standalone-rollout'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('local-operator-entrypoint'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('cross-image-release-set'),
|
||||
true,
|
||||
|
||||
@@ -46,7 +46,10 @@ function selectedImages(scope) {
|
||||
['admin', 'qinglong3-cluster-admin'],
|
||||
['worker', 'qinglong3-worker'],
|
||||
];
|
||||
const local = [['local', 'qinglong3-local-application']];
|
||||
const local = [
|
||||
['local', 'qinglong3-local-application'],
|
||||
['local-operator', 'qinglong3-local-operator'],
|
||||
];
|
||||
return scope === 'local'
|
||||
? local
|
||||
: scope === 'cluster'
|
||||
@@ -79,7 +82,7 @@ function releaseSet(scope) {
|
||||
const names = images.map((entry) => entry.name);
|
||||
const unsigned = {
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/release-set@v3',
|
||||
schema: 'qinglong/release-set@v4',
|
||||
release,
|
||||
candidate: {
|
||||
schema: 'qinglong/release-candidate-contract@v1',
|
||||
@@ -91,12 +94,16 @@ function releaseSet(scope) {
|
||||
local: {
|
||||
selected: ['local', 'all'].includes(scope),
|
||||
profiles: ['edge', 'standalone'],
|
||||
images: names.filter((name) => name === 'local'),
|
||||
images: names.filter((name) =>
|
||||
['local', 'local-operator'].includes(name),
|
||||
),
|
||||
},
|
||||
cluster: {
|
||||
selected: ['cluster', 'all'].includes(scope),
|
||||
profiles: ['cluster', 'worker-edge', 'worker-node'],
|
||||
images: names.filter((name) => name !== 'local'),
|
||||
images: names.filter((name) =>
|
||||
['control', 'control-ai', 'admin', 'worker'].includes(name),
|
||||
),
|
||||
},
|
||||
},
|
||||
evidenceReceipts: privateReleaseEvidenceReceipts(release),
|
||||
@@ -392,8 +399,9 @@ test('supports the complete all-scope image family without runtime coupling', (t
|
||||
'admin',
|
||||
'worker',
|
||||
'local',
|
||||
'local-operator',
|
||||
]);
|
||||
assert.equal(report.releaseSet.imageCount, 5);
|
||||
assert.equal(report.releaseSet.imageCount, 6);
|
||||
assert.equal(report.claims.deploymentMutation, false);
|
||||
});
|
||||
|
||||
|
||||
@@ -67,6 +67,12 @@ function releaseSet(scope) {
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
image: entry.image,
|
||||
localRoleVerification:
|
||||
entry.image === 'local'
|
||||
? 'application_rollout_verified'
|
||||
: entry.image === 'local-operator'
|
||||
? 'operator_entrypoint_verified'
|
||||
: 'not_applicable',
|
||||
digest: `sha256:${String(index + 1).repeat(64)}`,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -71,6 +71,12 @@ function releaseSet(scope) {
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
image: entry.image,
|
||||
localRoleVerification:
|
||||
entry.image === 'local'
|
||||
? 'application_rollout_verified'
|
||||
: entry.image === 'local-operator'
|
||||
? 'operator_entrypoint_verified'
|
||||
: 'not_applicable',
|
||||
digest: `sha256:${String(index + 1).repeat(64)}`,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -51,6 +51,12 @@ function recordsFor(releaseCandidate) {
|
||||
...identity,
|
||||
releaseScope: releaseCandidate.release.scope,
|
||||
image: entry.image,
|
||||
localRoleVerification:
|
||||
entry.image === 'local'
|
||||
? 'application_rollout_verified'
|
||||
: entry.image === 'local-operator'
|
||||
? 'operator_entrypoint_verified'
|
||||
: 'not_applicable',
|
||||
digest: `sha256:${String(index + 1).repeat(64)}`,
|
||||
}),
|
||||
);
|
||||
@@ -72,7 +78,7 @@ function writeCanonical(filePath, value) {
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
||||
}
|
||||
|
||||
test('aggregates the independent Local image into one immutable release set', () => {
|
||||
test('aggregates both independent Local images into one immutable release set', () => {
|
||||
const releaseCandidate = candidate('local');
|
||||
const records = recordsFor(releaseCandidate);
|
||||
const { validationClockMs: _unused, ...localIdentity } = identity;
|
||||
@@ -86,7 +92,7 @@ test('aggregates the independent Local image into one immutable release set', ()
|
||||
});
|
||||
assert.deepEqual(
|
||||
releaseSet.images.map((entry) => entry.name),
|
||||
['local'],
|
||||
['local', 'local-operator'],
|
||||
);
|
||||
assert.equal(releaseSet.deploymentFamilies.local.selected, true);
|
||||
assert.equal(releaseSet.deploymentFamilies.cluster.selected, false);
|
||||
@@ -140,8 +146,11 @@ test('closes cluster and all scopes over the exact candidate image order', () =>
|
||||
...identity,
|
||||
releaseScope: 'all',
|
||||
});
|
||||
assert.equal(allSet.images.length, 5);
|
||||
assert.deepEqual(allSet.deploymentFamilies.local.images, ['local']);
|
||||
assert.equal(allSet.images.length, 6);
|
||||
assert.deepEqual(allSet.deploymentFamilies.local.images, [
|
||||
'local',
|
||||
'local-operator',
|
||||
]);
|
||||
});
|
||||
|
||||
test('requires exact private evidence receipts only for Cluster-capable scopes', () => {
|
||||
@@ -303,6 +312,7 @@ test('rejects mutable identity, malformed owner and post-aggregate drift', () =>
|
||||
repositoryOwner: 'UPPERCASE',
|
||||
releaseScope: 'local',
|
||||
image: 'local',
|
||||
localRoleVerification: 'application_rollout_verified',
|
||||
digest: `sha256:${'1'.repeat(64)}`,
|
||||
}),
|
||||
/lowercase GitHub owner/,
|
||||
@@ -315,6 +325,7 @@ test('rejects mutable identity, malformed owner and post-aggregate drift', () =>
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
image: 'local',
|
||||
localRoleVerification: 'application_rollout_verified',
|
||||
digest: 'latest',
|
||||
}),
|
||||
/exact SHA-256 digest/,
|
||||
@@ -436,6 +447,7 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
|
||||
'--mode=record-image',
|
||||
...common,
|
||||
'--image=local',
|
||||
'--local-role-verification=application_rollout_verified',
|
||||
`--digest=sha256:${'1'.repeat(64)}`,
|
||||
`--output=${recordPath}`,
|
||||
],
|
||||
@@ -443,6 +455,20 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
|
||||
output,
|
||||
);
|
||||
assert.equal(fs.statSync(recordPath).mode & 0o777, 0o600);
|
||||
const operatorRecordPath = path.join(recordsDirectory, 'local-operator.json');
|
||||
runCli(
|
||||
[
|
||||
'--mode=record-image',
|
||||
...common,
|
||||
'--image=local-operator',
|
||||
'--local-role-verification=operator_entrypoint_verified',
|
||||
`--digest=sha256:${'2'.repeat(64)}`,
|
||||
`--output=${operatorRecordPath}`,
|
||||
],
|
||||
root,
|
||||
output,
|
||||
);
|
||||
assert.equal(fs.statSync(operatorRecordPath).mode & 0o777, 0o600);
|
||||
const setPath = path.join(directory, 'release-set.json');
|
||||
runCli(
|
||||
[
|
||||
|
||||
@@ -32,7 +32,10 @@ function publicationPlan() {
|
||||
const version = '3.0.0-alpha.0';
|
||||
const sourceRevision = 'a'.repeat(40);
|
||||
const repository = 'ghcr.io/qinglong-release/qinglong3-local-application';
|
||||
const operatorRepository =
|
||||
'ghcr.io/qinglong-release/qinglong3-local-operator';
|
||||
const imageDigest = `sha256:${'1'.repeat(64)}`;
|
||||
const operatorDigest = `sha256:${'9'.repeat(64)}`;
|
||||
const manifestDigest = `sha256:${'2'.repeat(64)}`;
|
||||
const unsigned = {
|
||||
schemaVersion: 1,
|
||||
@@ -90,6 +93,22 @@ function publicationPlan() {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'local-operator',
|
||||
registryRepository: operatorRepository,
|
||||
immutableReference: `${operatorRepository}@${operatorDigest}`,
|
||||
digest: operatorDigest,
|
||||
tags: [
|
||||
{
|
||||
kind: 'version',
|
||||
reference: `${operatorRepository}:${version}`,
|
||||
},
|
||||
{
|
||||
kind: 'source',
|
||||
reference: `${operatorRepository}:sha-${sourceRevision}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
return Object.freeze({
|
||||
@@ -174,10 +193,14 @@ test('finalizes every exact tag and audits the live terminal state', () => {
|
||||
const plan = publicationPlan();
|
||||
const registry = new FakeRegistry(plan);
|
||||
const observation = finalizeReleaseTags(plan, registry);
|
||||
assert.equal(registry.copyCalls().length, 2);
|
||||
assert.equal(observation.tags.length, 2);
|
||||
assert.equal(registry.copyCalls().length, 4);
|
||||
assert.equal(observation.tags.length, 4);
|
||||
assert.equal(
|
||||
observation.tags.every((tag) => tag.digest === plan.images[0].digest),
|
||||
observation.tags.every(
|
||||
(tag) =>
|
||||
tag.digest ===
|
||||
plan.images.find((image) => image.name === tag.image)?.digest,
|
||||
),
|
||||
true,
|
||||
);
|
||||
const copiesBeforeAudit = registry.copyCalls().length;
|
||||
@@ -185,7 +208,7 @@ test('finalizes every exact tag and audits the live terminal state', () => {
|
||||
schemaVersion: 1,
|
||||
planDigest: plan.planDigest,
|
||||
observationDigest: observation.observationDigest,
|
||||
tagCount: 2,
|
||||
tagCount: 4,
|
||||
allTagsExactDigest: true,
|
||||
registryMutation: false,
|
||||
compatible: true,
|
||||
@@ -228,8 +251,8 @@ test('recovers copy response loss by reusing exact tags and only filling absence
|
||||
);
|
||||
assert.equal(registry.copyCalls().length, 1);
|
||||
const observation = finalizeReleaseTags(plan, registry);
|
||||
assert.equal(registry.copyCalls().length, 2);
|
||||
assert.equal(observation.tags.length, 2);
|
||||
assert.equal(registry.copyCalls().length, 4);
|
||||
assert.equal(observation.tags.length, 4);
|
||||
assert.equal(
|
||||
registry.resolveDigest(plan.images[0].tags[0].reference),
|
||||
plan.images[0].digest,
|
||||
|
||||
Reference in New Issue
Block a user