Files
qinglong/test/back/ql3DeploymentLockContract.test.cjs
T

705 lines
19 KiB
JavaScript

'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,
);
}
},
);