mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): materialize offline deployment locks
This commit is contained in:
@@ -31,6 +31,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
clusterAdminContextPreflight: true,
|
||||
clusterAdminContextReadiness: true,
|
||||
releaseVersionAudit: true,
|
||||
deploymentLockMaterialization: true,
|
||||
ociAttestations: true,
|
||||
osVulnerabilityScan: {
|
||||
scanner: 'trivy@0.70.0',
|
||||
@@ -149,7 +150,29 @@ test('rejects removal of the durable release-catalog contract tests', () => {
|
||||
);
|
||||
assert.throws(
|
||||
() => auditClusterImageCiWorkflow(mutated),
|
||||
/durable catalog and workflow negative tests/,
|
||||
/durable catalog, deployment-lock and workflow negative tests/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects removal of the deployment-lock materialization contract tests', () => {
|
||||
const mutated = ciSource.replace(
|
||||
'test/back/ql3DeploymentLockContract.test.cjs',
|
||||
'test/back/deployment-lock-tests-removed.test.cjs',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditClusterImageCiWorkflow(mutated),
|
||||
/deployment-lock and workflow negative tests/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects removal of the deployment image surface audit', () => {
|
||||
const mutated = ciSource.replace(
|
||||
'pnpm audit:deployment-lock-surfaces:ql3',
|
||||
'echo deployment-image-surfaces-removed',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditClusterImageCiWorkflow(mutated),
|
||||
/reviewed deployment image surfaces/,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,704 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const test = require('node:test');
|
||||
const yaml = require('js-yaml');
|
||||
const {
|
||||
auditDeploymentImageSurfaces,
|
||||
auditKubernetesLock,
|
||||
auditLocalSelection,
|
||||
createKubernetesLock,
|
||||
createLocalSelection,
|
||||
parseArguments,
|
||||
runCli,
|
||||
} = require('../../scripts/ql3-deployment-lock-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 root = path.resolve(__dirname, '../..');
|
||||
const version = readReleaseIdentity(root).version;
|
||||
const identity = Object.freeze({
|
||||
version,
|
||||
sourceRevision: 'd'.repeat(40),
|
||||
sourceRef: `refs/tags/v${version}`,
|
||||
repositoryOwner: 'qinglong-release',
|
||||
});
|
||||
const roleOrder = Object.freeze(['control', 'control-ai', 'admin', 'worker']);
|
||||
|
||||
function executableOnPath(name) {
|
||||
const candidates = [
|
||||
process.env.QL3_KUBECTL_BIN,
|
||||
'/Applications/Docker.app/Contents/Resources/bin/kubectl',
|
||||
...(process.env.PATH ?? '')
|
||||
.split(path.delimiter)
|
||||
.filter(Boolean)
|
||||
.map((directory) => path.join(directory, name)),
|
||||
].filter(Boolean);
|
||||
return candidates.find((candidate) => {
|
||||
try {
|
||||
fs.accessSync(candidate, fs.constants.X_OK);
|
||||
return fs.lstatSync(candidate).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const kubectlPath = executableOnPath('kubectl');
|
||||
|
||||
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,
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
});
|
||||
}
|
||||
|
||||
function options(scope, extra = {}) {
|
||||
return Object.freeze({
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
function references(set) {
|
||||
return Object.fromEntries(
|
||||
set.images.map((image) => [image.name, image.reference]),
|
||||
);
|
||||
}
|
||||
|
||||
function fixtureManifest() {
|
||||
return `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ql3-control
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ql3-control
|
||||
spec:
|
||||
initContainers:
|
||||
- name: migrate
|
||||
image: ghcr.io/example/qinglong3-cluster-control:source
|
||||
containers:
|
||||
- name: control
|
||||
image: qinglong3-cluster-control@sha256:${'0'.repeat(64)}
|
||||
- name: sidecar
|
||||
image: busybox:1.36
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ql3-control-ai
|
||||
spec:
|
||||
template:
|
||||
metadata: {}
|
||||
spec:
|
||||
containers:
|
||||
- name: control-ai
|
||||
image: registry.example:5000/team/qinglong3-cluster-control-ai:source
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: ql3-admin
|
||||
spec:
|
||||
template:
|
||||
metadata: {}
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: admin
|
||||
image: qinglong3-cluster-admin@sha256:${'0'.repeat(64)}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ql3-worker
|
||||
spec:
|
||||
template:
|
||||
metadata: {}
|
||||
spec:
|
||||
initContainers:
|
||||
- name: worker-init
|
||||
image: qinglong3-worker:source
|
||||
containers:
|
||||
- name: worker
|
||||
image: ghcr.io/example/qinglong3-worker@sha256:${'0'.repeat(64)}
|
||||
ephemeralContainers:
|
||||
- name: worker-debug
|
||||
image: qinglong3-worker:debug
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ql3-plugin-package-secret-action-admission
|
||||
data:
|
||||
image: qinglong3-cluster-admin@sha256:${'0'.repeat(64)}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ql3-control
|
||||
spec:
|
||||
selector:
|
||||
app: ql3-control
|
||||
ports:
|
||||
- port: 80
|
||||
`;
|
||||
}
|
||||
|
||||
function temporaryDirectory(t) {
|
||||
const directory = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-deployment-lock-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
return directory;
|
||||
}
|
||||
|
||||
function writeCanonical(filePath, value) {
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
||||
}
|
||||
|
||||
function kubernetesOptions(extra = {}) {
|
||||
return options('cluster', {
|
||||
requiredImages: roleOrder.join(','),
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
test('selects one immutable Local Compose image without adding device work', () => {
|
||||
for (const scope of ['local', 'all']) {
|
||||
const set = releaseSet(scope);
|
||||
const selection = createLocalSelection(
|
||||
set,
|
||||
options(scope, { allowRootService: false }),
|
||||
);
|
||||
assert.equal(selection.deploymentFamily, 'local');
|
||||
assert.equal(selection.releaseSetDigest, set.releaseSetDigest);
|
||||
assert.equal(selection.service.kind, 'compose');
|
||||
assert.equal(selection.service.image, references(set).local);
|
||||
assert.equal(selection.service.allowRootService, false);
|
||||
assert.equal(selection.verification.networkAccess, false);
|
||||
assert.equal(selection.verification.deploymentMutation, false);
|
||||
assert.match(selection.selectionDigest, /^sha256:[a-f0-9]{64}$/u);
|
||||
assert.equal(
|
||||
auditLocalSelection(
|
||||
selection,
|
||||
set,
|
||||
options(scope, { allowRootService: false }),
|
||||
).compatible,
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Local selection rejects cluster scope, implicit root policy and drift', () => {
|
||||
const clusterSet = releaseSet('cluster');
|
||||
assert.throws(
|
||||
() =>
|
||||
createLocalSelection(
|
||||
clusterSet,
|
||||
options('cluster', { allowRootService: false }),
|
||||
),
|
||||
/local or all release set/,
|
||||
);
|
||||
const localSet = releaseSet('local');
|
||||
assert.throws(
|
||||
() => createLocalSelection(localSet, options('local')),
|
||||
/explicit boolean/,
|
||||
);
|
||||
const selection = createLocalSelection(
|
||||
localSet,
|
||||
options('local', { allowRootService: true }),
|
||||
);
|
||||
const drifted = JSON.parse(JSON.stringify(selection));
|
||||
drifted.service.image = 'ghcr.io/example/qinglong3-local:latest';
|
||||
assert.throws(
|
||||
() =>
|
||||
auditLocalSelection(
|
||||
drifted,
|
||||
localSet,
|
||||
options('local', { allowRootService: true }),
|
||||
),
|
||||
/differs from the verified release set/,
|
||||
);
|
||||
});
|
||||
|
||||
test('materializes every supported Kubernetes image authority from one release set', () => {
|
||||
const set = releaseSet('cluster');
|
||||
const expectedReferences = references(set);
|
||||
const created = createKubernetesLock(
|
||||
set,
|
||||
fixtureManifest(),
|
||||
kubernetesOptions(),
|
||||
);
|
||||
const resources = [];
|
||||
yaml.loadAll(created.outputManifest, (resource) => resources.push(resource));
|
||||
const renderedImages = resources.flatMap((resource) => {
|
||||
const podSpec =
|
||||
resource.kind === 'Job'
|
||||
? resource.spec?.template?.spec
|
||||
: resource.spec?.template?.spec;
|
||||
return ['initContainers', 'containers', 'ephemeralContainers'].flatMap(
|
||||
(key) => podSpec?.[key]?.map((container) => container.image) ?? [],
|
||||
);
|
||||
});
|
||||
assert.deepEqual(created.report.requiredImages, roleOrder);
|
||||
assert.deepEqual(
|
||||
created.report.imageOccurrences.map(({ name, count }) => [name, count]),
|
||||
[
|
||||
['control', 2],
|
||||
['control-ai', 1],
|
||||
['admin', 2],
|
||||
['worker', 3],
|
||||
],
|
||||
);
|
||||
assert.equal(created.report.manifest.resources, 6);
|
||||
assert.equal(created.report.manifest.changedResources, 5);
|
||||
assert.equal(created.report.manifest.admissionAuthorityCount, 1);
|
||||
assert.equal(created.report.verification.unknownImageAuthorities, 0);
|
||||
assert.equal(created.report.verification.mutableQingLongImages, 0);
|
||||
assert.equal(created.report.verification.networkAccess, false);
|
||||
assert.equal(created.report.verification.kubernetesMutation, false);
|
||||
assert.equal(renderedImages.includes('busybox:1.36'), true);
|
||||
for (const role of roleOrder) {
|
||||
assert.equal(
|
||||
renderedImages.includes(expectedReferences[role]) || role === 'admin',
|
||||
true,
|
||||
);
|
||||
}
|
||||
const admission = resources.find(
|
||||
(resource) =>
|
||||
resource.kind === 'ConfigMap' &&
|
||||
resource.metadata.name === 'ql3-plugin-package-secret-action-admission',
|
||||
);
|
||||
assert.equal(admission.data.image, expectedReferences.admin);
|
||||
for (const resource of resources.filter(
|
||||
(entry) => entry.kind !== 'Service',
|
||||
)) {
|
||||
assert.equal(
|
||||
resource.metadata.annotations['qinglong.io/release-set-digest'],
|
||||
set.releaseSetDigest,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
created.outputManifest.includes('qinglong3-cluster-control:source'),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
auditKubernetesLock(
|
||||
created.outputManifest,
|
||||
created.report,
|
||||
set,
|
||||
fixtureManifest(),
|
||||
kubernetesOptions(),
|
||||
).compatible,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('Kubernetes materialization rejects unsupported scope and incomplete role closure', () => {
|
||||
const localSet = releaseSet('local');
|
||||
assert.throws(
|
||||
() =>
|
||||
createKubernetesLock(localSet, fixtureManifest(), {
|
||||
...options('local'),
|
||||
requiredImages: 'control',
|
||||
}),
|
||||
/cluster or all release set/,
|
||||
);
|
||||
const clusterSet = releaseSet('cluster');
|
||||
const withoutWorker = fixtureManifest().replace(
|
||||
/---\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n name: ql3-worker[\s\S]*?(?=---\napiVersion: v1\nkind: ConfigMap)/u,
|
||||
'',
|
||||
);
|
||||
assert.throws(
|
||||
() => createKubernetesLock(clusterSet, withoutWorker, kubernetesOptions()),
|
||||
/required image was not rendered: worker/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects mutable, malformed and hidden QingLong image authorities', () => {
|
||||
const set = releaseSet('cluster');
|
||||
const hidden = `${fixtureManifest()}---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: hidden-authority\ndata:\n image: ${
|
||||
references(set).control
|
||||
}\n`;
|
||||
assert.throws(
|
||||
() => createKubernetesLock(set, hidden, kubernetesOptions()),
|
||||
/unhandled QingLong image authority: data.image/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createKubernetesLock(
|
||||
set,
|
||||
fixtureManifest().replace(
|
||||
'qinglong3-cluster-control@sha256:',
|
||||
'qinglong3-cluster-control-not-an-image@sha256:',
|
||||
),
|
||||
kubernetesOptions(),
|
||||
),
|
||||
/unhandled QingLong image authority|container image is malformed/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createKubernetesLock(
|
||||
set,
|
||||
fixtureManifest().replace(
|
||||
'qinglong3-worker:source',
|
||||
'qinglong3-worker',
|
||||
),
|
||||
kubernetesOptions(),
|
||||
),
|
||||
/container image is malformed/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects unsafe YAML structure and invalid admission authority', () => {
|
||||
const set = releaseSet('cluster');
|
||||
assert.throws(
|
||||
() =>
|
||||
createKubernetesLock(
|
||||
set,
|
||||
'apiVersion: v1\nkind: Pod\nkind: Job\n',
|
||||
kubernetesOptions({ requiredImages: 'control' }),
|
||||
),
|
||||
/duplicate-free YAML/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createKubernetesLock(
|
||||
set,
|
||||
'- not\n- a\n- resource\n',
|
||||
kubernetesOptions({ requiredImages: 'control' }),
|
||||
),
|
||||
/resource must be one mapping/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createKubernetesLock(
|
||||
set,
|
||||
fixtureManifest().replace(
|
||||
'data:\n image: qinglong3-cluster-admin',
|
||||
'data:\n image: busybox',
|
||||
),
|
||||
kubernetesOptions(),
|
||||
),
|
||||
/admission image authority is invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test('audit detects source, locked manifest and report drift', () => {
|
||||
const set = releaseSet('cluster');
|
||||
const created = createKubernetesLock(
|
||||
set,
|
||||
fixtureManifest(),
|
||||
kubernetesOptions(),
|
||||
);
|
||||
const report = JSON.parse(JSON.stringify(created.report));
|
||||
report.manifest.resources += 1;
|
||||
assert.throws(
|
||||
() =>
|
||||
auditKubernetesLock(
|
||||
created.outputManifest,
|
||||
report,
|
||||
set,
|
||||
fixtureManifest(),
|
||||
kubernetesOptions(),
|
||||
),
|
||||
/differs from the verified release set/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
auditKubernetesLock(
|
||||
`${created.outputManifest}\n`,
|
||||
created.report,
|
||||
set,
|
||||
fixtureManifest(),
|
||||
kubernetesOptions(),
|
||||
),
|
||||
/differs from the verified release set/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
auditKubernetesLock(
|
||||
created.outputManifest,
|
||||
created.report,
|
||||
set,
|
||||
`${fixtureManifest()}\n`,
|
||||
kubernetesOptions(),
|
||||
),
|
||||
/differs from the verified release set/,
|
||||
);
|
||||
});
|
||||
|
||||
test('CLI creates and audits no-replace Local and Kubernetes outputs', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const output = { write() {} };
|
||||
const localSet = releaseSet('local');
|
||||
const localSetPath = path.join(directory, 'local-set.json');
|
||||
const selectionPath = path.join(directory, 'selection.json');
|
||||
writeCanonical(localSetPath, localSet);
|
||||
const localIdentity = [
|
||||
`--version=${version}`,
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
`--source-ref=${identity.sourceRef}`,
|
||||
'--release-scope=local',
|
||||
`--repository-owner=${identity.repositoryOwner}`,
|
||||
`--release-set=${localSetPath}`,
|
||||
'--allow-root-service=false',
|
||||
];
|
||||
runCli(
|
||||
['--mode=local-create', ...localIdentity, `--output=${selectionPath}`],
|
||||
root,
|
||||
output,
|
||||
);
|
||||
assert.equal(fs.statSync(selectionPath).mode & 0o777, 0o600);
|
||||
assert.equal(
|
||||
runCli(
|
||||
['--mode=local-audit', ...localIdentity, `--selection=${selectionPath}`],
|
||||
root,
|
||||
output,
|
||||
).compatible,
|
||||
true,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
runCli(
|
||||
['--mode=local-create', ...localIdentity, `--output=${selectionPath}`],
|
||||
root,
|
||||
output,
|
||||
),
|
||||
/output must be unused/,
|
||||
);
|
||||
|
||||
const clusterSet = releaseSet('cluster');
|
||||
const clusterSetPath = path.join(directory, 'cluster-set.json');
|
||||
const sourcePath = path.join(directory, 'rendered.yaml');
|
||||
const lockedPath = path.join(directory, 'locked.yaml');
|
||||
const reportPath = path.join(directory, 'lock.json');
|
||||
writeCanonical(clusterSetPath, clusterSet);
|
||||
fs.writeFileSync(sourcePath, fixtureManifest(), { mode: 0o600 });
|
||||
const clusterIdentity = [
|
||||
`--version=${version}`,
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
`--source-ref=${identity.sourceRef}`,
|
||||
'--release-scope=cluster',
|
||||
`--repository-owner=${identity.repositoryOwner}`,
|
||||
`--release-set=${clusterSetPath}`,
|
||||
`--manifest=${sourcePath}`,
|
||||
`--required-images=${roleOrder.join(',')}`,
|
||||
];
|
||||
runCli(
|
||||
[
|
||||
'--mode=kubernetes-create',
|
||||
...clusterIdentity,
|
||||
`--output-manifest=${lockedPath}`,
|
||||
`--output-report=${reportPath}`,
|
||||
],
|
||||
root,
|
||||
output,
|
||||
);
|
||||
assert.equal(fs.statSync(lockedPath).mode & 0o777, 0o600);
|
||||
assert.equal(fs.statSync(reportPath).mode & 0o777, 0o600);
|
||||
assert.equal(
|
||||
runCli(
|
||||
[
|
||||
'--mode=kubernetes-audit',
|
||||
...clusterIdentity,
|
||||
`--locked-manifest=${lockedPath}`,
|
||||
`--report=${reportPath}`,
|
||||
],
|
||||
root,
|
||||
output,
|
||||
).compatible,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('CLI rejects symlinks, open arguments, output aliasing and policy ambiguity', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const localSetPath = path.join(directory, 'local-set.json');
|
||||
const linkedSetPath = path.join(directory, 'linked-set.json');
|
||||
writeCanonical(localSetPath, releaseSet('local'));
|
||||
fs.symlinkSync(localSetPath, linkedSetPath);
|
||||
const base = [
|
||||
'--mode=local-create',
|
||||
`--version=${version}`,
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
`--source-ref=${identity.sourceRef}`,
|
||||
'--release-scope=local',
|
||||
`--repository-owner=${identity.repositoryOwner}`,
|
||||
`--release-set=${linkedSetPath}`,
|
||||
'--allow-root-service=false',
|
||||
`--output=${path.join(directory, 'selection.json')}`,
|
||||
];
|
||||
assert.throws(
|
||||
() => runCli(base, root, { write() {} }),
|
||||
/canonical regular file/,
|
||||
);
|
||||
assert.throws(
|
||||
() => parseArguments([...base, '--extra=value']),
|
||||
/arguments are invalid/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
parseArguments(
|
||||
base.map((argument) =>
|
||||
argument === '--allow-root-service=false'
|
||||
? '--allow-root-service=maybe'
|
||||
: argument,
|
||||
),
|
||||
),
|
||||
/must be true or false/,
|
||||
);
|
||||
|
||||
const clusterSetPath = path.join(directory, 'cluster-set.json');
|
||||
const sourcePath = path.join(directory, 'rendered.yaml');
|
||||
const aliasedOutput = path.join(directory, 'same-output');
|
||||
writeCanonical(clusterSetPath, releaseSet('cluster'));
|
||||
fs.writeFileSync(sourcePath, fixtureManifest(), { mode: 0o600 });
|
||||
assert.throws(
|
||||
() =>
|
||||
runCli(
|
||||
[
|
||||
'--mode=kubernetes-create',
|
||||
`--version=${version}`,
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
`--source-ref=${identity.sourceRef}`,
|
||||
'--release-scope=cluster',
|
||||
`--repository-owner=${identity.repositoryOwner}`,
|
||||
`--release-set=${clusterSetPath}`,
|
||||
`--manifest=${sourcePath}`,
|
||||
`--required-images=${roleOrder.join(',')}`,
|
||||
`--output-manifest=${aliasedOutput}`,
|
||||
`--output-report=${aliasedOutput}`,
|
||||
],
|
||||
root,
|
||||
{ write() {} },
|
||||
),
|
||||
/paths must differ/,
|
||||
);
|
||||
});
|
||||
|
||||
test('source-surface audit freezes every reviewed cluster and worker authority', () => {
|
||||
assert.deepEqual(auditDeploymentImageSurfaces(root), {
|
||||
schemaVersion: 1,
|
||||
deploymentYamlFiles: 224,
|
||||
imageOccurrences: {
|
||||
control: 2,
|
||||
'control-ai': 1,
|
||||
admin: 26,
|
||||
worker: 2,
|
||||
},
|
||||
admissionAuthorityCount: 2,
|
||||
materialization: 'offline_post_render',
|
||||
networkAccess: false,
|
||||
kubernetesMutation: false,
|
||||
compatible: true,
|
||||
});
|
||||
assert.deepEqual(
|
||||
runCli(['--mode=surfaces-audit'], root, { write() {} }),
|
||||
auditDeploymentImageSurfaces(root),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'real Kustomize renders are locked after nested overlay transforms',
|
||||
{ skip: kubectlPath ? false : 'kubectl is unavailable' },
|
||||
() => {
|
||||
const set = releaseSet('cluster');
|
||||
const expectedReferences = references(set);
|
||||
for (const entry of [
|
||||
{
|
||||
directory: 'deploy/kubernetes/ql3-cluster/overlays/cloudnative-pg',
|
||||
requiredImages: ['control'],
|
||||
},
|
||||
{
|
||||
directory: 'deploy/kubernetes/ql3-cluster/overlays/cluster-ai-example',
|
||||
requiredImages: ['control-ai'],
|
||||
},
|
||||
{
|
||||
directory: 'deploy/kubernetes/ql3-worker/overlays/node',
|
||||
requiredImages: ['worker'],
|
||||
},
|
||||
{
|
||||
directory:
|
||||
'deploy/kubernetes/ql3-cluster/operations/plugin-package-executor/cloudnative-pg',
|
||||
requiredImages: ['admin'],
|
||||
},
|
||||
]) {
|
||||
const rendered = spawnSync(
|
||||
kubectlPath,
|
||||
['kustomize', path.join(root, entry.directory)],
|
||||
{
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
rendered.status,
|
||||
0,
|
||||
`${entry.directory}: ${rendered.stderr}`,
|
||||
);
|
||||
const created = createKubernetesLock(
|
||||
set,
|
||||
rendered.stdout,
|
||||
kubernetesOptions({
|
||||
requiredImages: entry.requiredImages.join(','),
|
||||
}),
|
||||
);
|
||||
for (const role of entry.requiredImages) {
|
||||
assert.equal(
|
||||
created.outputManifest.includes(expectedReferences[role]),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
created.report.imageOccurrences.find((image) => image.name === role)
|
||||
.count > 0,
|
||||
true,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
created.outputManifest.includes(
|
||||
'@sha256:0000000000000000000000000000000000000000000000000000000000000000',
|
||||
),
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -48,6 +48,10 @@ test('freezes an independent low-resource local release family', () => {
|
||||
/^sha256:[a-f0-9]{64}$/u,
|
||||
);
|
||||
assert.match(contract.contractDigest, /^sha256:[a-f0-9]{64}$/u);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('offline-deployment-lock-materialization'),
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(
|
||||
auditReleaseCandidateContract(contract, {
|
||||
root,
|
||||
@@ -109,6 +113,10 @@ test('combines local and cluster families without weakening either gate', () =>
|
||||
contract.requiredGates.includes('durable-oci-release-catalog'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('offline-deployment-lock-materialization'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('worker-management-production-evidence'),
|
||||
true,
|
||||
|
||||
Reference in New Issue
Block a user