Files
ApiCore/deploy/k8s/tests/manifest-policy.test.mjs
T
Nick SzaunigandClaude Opus 5.5 fab7f7e12f feat: T4 wave 10 – bottling quality, phenology and honey chain UI, Telegram link/receive and ops
- Bottling quality: water content above the legal limit (strict >) answers
  409 CONFIRMATION_REQUIRED bound to the measured value; prefill for jar
  size, best-before (shelfLifeMonths, month-end clamping) and water content;
  impossible dates give 400; water-content checks in src/common/quality.
- Frontend phenology: "Entwicklung melden" sheet, IndexedDB v4 queue,
  season header on the apiary page, confirmed revoke for all apiaries.
- Frontend honey chain: "Vorschlag aus …" hints with explicit take-over,
  prefill status note, movement ledger with withdraw/correct dialogs.
- Telegram: link tokens (hashed, single use), private-chat receiver with
  lease and offset CAS, bounded poison-update skip, Retry-After, pending
  deliveries skipped on disconnect; system API for deliveries and retry;
  worker-only secret wiring and manifest policy tests.
- ARCHITECTURE.md §7, §9, §10, §11.11–11.13, Annahmen 51–55.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-14 22:47:17 +02:00

544 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Policy checks for the rendered overlay (spec 2026-10-12 sections 5.2, 5.3, 6, 7).
// The CI renders deploy/k8s/overlays/encry and every rollout stage and hands
// them over as JSON (see .gitea/workflows/manifests.yml). Locally:
// kubectl kustomize deploy/k8s/overlays/encry > /tmp/rendered.yaml
// yq ea -o=json '[.]' /tmp/rendered.yaml > /tmp/rendered.json
// node deploy/k8s/tests/render-stages.mjs /tmp/stages
// RENDERED_MANIFESTS=/tmp/rendered.json RENDERED_STAGES=/tmp/stages/stages.json \
// node --test deploy/k8s/tests/manifest-policy.test.mjs
// RENDERED_MANIFESTS is the target state (all components) and gets the full
// security checks. RENDERED_STAGES holds every intermediate rollout stage;
// each must keep the application reachable.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
NAMESPACE, ip, isEmptySelector, matchesSelector, modelOf, podTemplateOf, policyTypes, syntheticPod,
} from './netpol-model.mjs';
const APP_PODS = ['apicore-backend', 'apicore-worker', 'apicore-job', 'apicore-frontend'];
const DB_CLIENT_APPS = ['apicore-backend', 'apicore-worker', 'apicore-job', 'apicore-db-backup'];
const KNOWN_JOBS = ['daily-evaluation', 'task-templates', 'cleanup', 'app-backup', 'rekey'];
const PRIVATE_V4 = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '100.64.0.0/10', '169.254.0.0/16', '127.0.0.0/8'];
const PRIVATE_V6 = ['fc00::/7', 'fe80::/10', '::1/128'];
const MIGRATE = ['node', 'node_modules/prisma/build/index.js', 'migrate', 'deploy'];
const SEED = ['node', 'dist/seed.js'];
const POSTGRES = 'StatefulSet/apicore-postgres';
const res = (cpuReq, cpuLim, memReq, memLim) => ({
requests: { cpu: cpuReq, memory: memReq },
limits: { cpu: cpuLim, memory: memLim },
});
const RESOURCES = {
api: res('50m', '500m', '192Mi', '512Mi'),
worker: res('20m', '200m', '128Mi', '256Mi'),
frontend: res('30m', '300m', '128Mi', '256Mi'),
job: res('50m', '500m', '192Mi', '512Mi'),
};
const NEVER = '0 0 31 2 *';
const CRON_JOBS = {
cleanup: { args: ['cleanup'], schedule: '0 4 * * *', suspend: false, backoffLimit: 1, deadline: 1800 },
'daily-evaluation': { args: ['daily-evaluation'], schedule: '30 4 * * *', suspend: false, backoffLimit: 1, deadline: 1800 },
'task-templates': { args: ['task-templates'], schedule: '0 5 * * *', suspend: false, backoffLimit: 1, deadline: 1800 },
'app-backup': { args: ['app-backup'], schedule: '0 * * * *', suspend: false, backoffLimit: 0, deadline: 3600 },
rekey: { args: ['rekey'], schedule: NEVER, suspend: true, backoffLimit: 0, deadline: 1800 },
'rekey-dry-run': { args: ['rekey', '--dry-run'], schedule: NEVER, suspend: true, backoffLimit: 0, deadline: 1800 },
};
// Selectors are immutable (StatefulSet) or carry live traffic (Services), so
// they must equal what runs in the cluster (read with kubectl on 2026-10-13).
const LIVE_SELECTORS = {
'Deployment/apicore-backend': { app: 'apicore-backend' },
'Deployment/apicore-frontend': { app: 'apicore-frontend' },
'StatefulSet/apicore-postgres': { 'app.kubernetes.io/name': 'apicore-postgres' },
};
const LIVE_SERVICES = {
'apicore-backend': { selector: { app: 'apicore-backend' }, ports: [{ port: 80, targetPort: 3001 }] },
'apicore-frontend': { selector: { app: 'apicore-frontend' }, ports: [{ port: 80, targetPort: 3000 }] },
'apicore-postgres': {
selector: { 'app.kubernetes.io/name': 'apicore-postgres' },
ports: [{ name: 'postgres', port: 5432, targetPort: 'postgres' }],
},
};
// Live hardening of the postgres container; the repo must never weaken it.
const POSTGRES_SECURITY = {
runAsUser: 70,
runAsGroup: 70,
runAsNonRoot: true,
readOnlyRootFilesystem: true,
allowPrivilegeEscalation: false,
capabilities: { drop: ['ALL'] },
};
// Pods created by controllers outside this repo; policies may target them.
const SOLVER_LABELS = { 'acme.cert-manager.io/http01-solver': 'true' };
const TRAEFIK = syntheticPod('kube-system', { 'app.kubernetes.io/name': 'rke2-traefik' });
const KUBE_DNS = syntheticPod('kube-system', { 'k8s-app': 'kube-dns' });
const SOLVER = syntheticPod(NAMESPACE, SOLVER_LABELS, [{ name: 'http', containerPort: 8089 }]);
function loadJson(variable) {
const file = process.env[variable];
if (!file) throw new Error(`${variable} must point to rendered manifests as JSON (see header)`);
return JSON.parse(readFileSync(file, 'utf8'));
}
const target = modelOf(loadJson('RENDERED_MANIFESTS').filter(Boolean));
const stages = Object.entries(loadJson('RENDERED_STAGES')).map(([name, docs]) => [name, modelOf(docs.filter(Boolean))]);
function onePod(model, app) {
const found = model.byApp(app);
assert.equal(found.length, 1, `expected exactly one pod template with app=${app}`);
return found[0];
}
const mainContainer = (pod) => pod.containers[0];
const envOf = (container) => Object.fromEntries((container.env ?? []).map((e) => [e.name, e.value]));
const postgresOf = (model) => {
const postgres = model.byOwner(POSTGRES);
assert.ok(postgres, 'postgres StatefulSet missing');
return postgres;
};
// --- target state: security --------------------------------------------------
test('default deny for ingress and egress exists', () => {
const denyAll = target.policies.filter(
(p) =>
isEmptySelector(p.spec.podSelector) &&
policyTypes(p).includes('Ingress') &&
policyTypes(p).includes('Egress') &&
(p.spec.ingress ?? []).length === 0 &&
(p.spec.egress ?? []).length === 0,
);
assert.equal(denyAll.length, 1, 'expected one namespace-wide default-deny policy');
for (const pod of target.pods) {
assert.ok(target.policiesFor(pod, 'Ingress').length > 0, `${pod.owner} is not isolated for ingress`);
assert.ok(target.policiesFor(pod, 'Egress').length > 0, `${pod.owner} is not isolated for egress`);
}
});
test('every pod template can resolve DNS (UDP+TCP 53 to kube-dns)', () => {
assert.ok(target.pods.length > 0, 'no pod templates rendered');
const otherKubeSystemPod = syntheticPod('kube-system', { app: 'something-else' });
for (const pod of [...target.pods, SOLVER]) {
for (const protocol of ['UDP', 'TCP']) {
assert.ok(target.canConnect(pod, KUBE_DNS, 53, protocol), `${pod.owner} cannot reach kube-dns on ${protocol}/53`);
assert.ok(!target.canConnect(pod, otherKubeSystemPod, 53, protocol), `${pod.owner} reaches any kube-system pod on 53`);
assert.ok(!target.canConnect(pod, ip('1.1.1.1'), 53, protocol), `${pod.owner} reaches an outside resolver on 53`);
}
}
});
test('postgres accepts 5432 only from db-client pods; only api, worker, jobs and db-backup carry db-client', () => {
const postgres = postgresOf(target);
const clients = target.pods.filter((p) => p.labels['db-client'] === 'true');
assert.deepEqual([...new Set(clients.map((p) => p.labels.app))].sort(), [...DB_CLIENT_APPS].sort());
for (const client of clients) {
assert.ok(target.canConnect(client, postgres, 5432), `${client.owner} cannot reach postgres on 5432`);
assert.ok(!target.canConnect(client, postgres, 22), `${client.owner} reaches postgres on a port other than 5432`);
}
const strangers = [
TRAEFIK,
SOLVER,
syntheticPod('other', { 'db-client': 'true' }),
syntheticPod(NAMESPACE, { app: 'intruder' }),
ip('10.42.0.99'),
];
for (const stranger of strangers) {
assert.ok(!target.allowsIngress(postgres, stranger, 5432), `${stranger.owner} reaches postgres`);
}
for (const pod of target.pods.filter((p) => p.labels['db-client'] !== 'true' && p !== postgres)) {
assert.ok(!target.canConnect(pod, postgres, 5432), `${pod.owner} reaches postgres without db-client`);
}
});
test('frontend has no path to 5432 and no ipBlock egress', () => {
const frontend = onePod(target, 'apicore-frontend');
// Egress alone must block it, whatever ingress policies exist live.
assert.ok(!target.allowsEgress(frontend, postgresOf(target), 5432), 'frontend egress allows postgres');
for (const address of ['10.43.0.5', '10.42.3.7', '1.1.1.1']) {
assert.ok(!target.allowsEgress(frontend, ip(address), 5432), `frontend egress allows ${address}:5432`);
}
for (const policy of target.policiesFor(frontend, 'Egress')) {
for (const rule of policy.spec.egress ?? []) {
assert.ok((rule.to ?? []).length > 0, `${policy.metadata.name} allows egress to everything`);
assert.ok(!rule.to.some((peer) => peer.ipBlock), `${policy.metadata.name} gives the frontend ipBlock egress`);
}
}
});
test('worker and job pods accept no ingress', () => {
const targets = target.pods.filter((p) => ['apicore-worker', 'apicore-job', 'apicore-db-backup'].includes(p.labels.app));
assert.ok(targets.some((p) => p.labels.app === 'apicore-worker'), 'worker missing');
assert.ok(targets.some((p) => p.labels.app === 'apicore-job'), 'job pods missing');
for (const pod of targets) {
const ingressPolicies = target.policiesFor(pod, 'Ingress');
assert.ok(ingressPolicies.length > 0, `${pod.owner} is not isolated for ingress`);
for (const policy of ingressPolicies) {
assert.equal((policy.spec.ingress ?? []).length, 0, `${policy.metadata.name} opens ingress to ${pod.owner}`);
}
}
});
test('every 0.0.0.0/0 and ::/0 egress excludes private ranges and allows only TCP 443', () => {
for (const policy of target.policies) {
for (const rule of policy.spec.egress ?? []) {
assert.ok((rule.to ?? []).length > 0, `${policy.metadata.name} has an egress rule without destinations`);
for (const peer of rule.to.filter((p) => p.ipBlock)) {
const { cidr, except = [] } = peer.ipBlock;
const required = cidr === '0.0.0.0/0' ? PRIVATE_V4 : cidr === '::/0' ? PRIVATE_V6 : null;
assert.ok(required, `${policy.metadata.name}: unexpected ipBlock ${cidr}`);
for (const range of required) assert.ok(except.includes(range), `${policy.metadata.name}: ${cidr} lacks except ${range}`);
assert.deepEqual(
(rule.ports ?? []).map((p) => [p.protocol ?? 'TCP', p.port, p.endPort]),
[['TCP', 443, undefined]],
`${policy.metadata.name}: public egress must be TCP 443 only`,
);
}
}
}
const blocked = ['10.0.0.1', '172.16.0.1', '192.168.1.10', '100.64.0.1', '169.254.169.254', '127.0.0.1', 'fc00::1', 'fe80::1', '::1'];
for (const app of ['apicore-backend', 'apicore-worker']) {
const pod = onePod(target, app);
assert.ok(target.canConnect(pod, ip('1.1.1.1'), 443), `${app} cannot reach a public IPv4 on 443`);
assert.ok(target.canConnect(pod, ip('2606:4700::1111'), 443), `${app} cannot reach a public IPv6 on 443`);
assert.ok(!target.canConnect(pod, ip('1.1.1.1'), 80), `${app} reaches a public IP on 80`);
for (const address of blocked) assert.ok(!target.canConnect(pod, ip(address), 443), `${app} reaches ${address}:443`);
}
for (const pod of [...target.pods.filter((p) => !['apicore-backend', 'apicore-worker'].includes(p.labels.app)), SOLVER]) {
assert.ok(!target.canConnect(pod, ip('1.1.1.1'), 443), `${pod.owner} reaches the internet`);
}
});
test('traefik reaches frontend 3000 and api 3001; the api takes nothing else', () => {
const frontend = onePod(target, 'apicore-frontend');
const api = onePod(target, 'apicore-backend');
assert.ok(target.canConnect(TRAEFIK, frontend, 3000), 'traefik cannot reach the frontend');
assert.ok(target.canConnect(TRAEFIK, api, 3001), 'traefik cannot reach the api');
assert.ok(target.canConnect(frontend, api, 3001), 'frontend cannot reach the api');
assert.ok(!target.allowsIngress(api, syntheticPod('other', { app: 'apicore-frontend' }), 3001), 'api open to other namespaces');
assert.ok(!target.allowsIngress(api, onePod(target, 'apicore-worker'), 3001), 'api open to the worker');
assert.ok(!target.allowsIngress(frontend, api, 3000), 'frontend open to the api');
assert.ok(!target.allowsIngress(api, TRAEFIK, 3002), 'api open on another port');
});
test('cert-manager http01 solver pods: traefik reaches 8089, nothing else does', () => {
assert.ok(target.canConnect(TRAEFIK, SOLVER, 8089), 'traefik cannot reach the http01 solver');
assert.ok(!target.allowsIngress(SOLVER, TRAEFIK, 3000), 'solver open on another port');
const others = [
...target.pods,
syntheticPod('other', { 'app.kubernetes.io/name': 'rke2-traefik' }),
syntheticPod(NAMESPACE, { app: 'intruder' }),
ip('1.1.1.1'),
];
for (const other of others) {
assert.ok(!target.allowsIngress(SOLVER, other, 8089), `${other.owner} reaches the solver`);
}
assert.ok(!target.canConnect(SOLVER, postgresOf(target), 5432), 'solver reaches postgres');
});
test('all app pods: automountServiceAccountToken false, readOnlyRootFilesystem true, /tmp emptyDir', () => {
const appPods = target.pods.filter((p) => APP_PODS.includes(p.labels.app));
assert.deepEqual([...new Set(appPods.map((p) => p.labels.app))].sort(), [...APP_PODS].sort());
for (const pod of appPods) {
assert.equal(pod.spec.automountServiceAccountToken, false, `${pod.owner} mounts the service account token`);
assert.equal(pod.spec.securityContext?.runAsNonRoot, true, `${pod.owner} may run as root`);
const emptyDirs = new Set((pod.spec.volumes ?? []).filter((v) => v.emptyDir).map((v) => v.name));
for (const c of [...pod.initContainers, ...pod.containers]) {
const where = `${pod.owner} container ${c.name}`;
assert.equal(c.securityContext?.readOnlyRootFilesystem, true, `${where}: root filesystem writable`);
assert.equal(c.securityContext?.allowPrivilegeEscalation, false, `${where}: privilege escalation`);
assert.deepEqual(c.securityContext?.capabilities?.drop, ['ALL'], `${where}: capabilities not dropped`);
const tmp = (c.volumeMounts ?? []).find((m) => m.mountPath === '/tmp');
assert.ok(tmp && emptyDirs.has(tmp.name), `${where}: /tmp is not an emptyDir`);
}
}
});
test('cronjobs: Forbid, Europe/Berlin, command node dist/cli.js <known job>', () => {
const cronJobs = target.docs.filter((d) => d.kind === 'CronJob');
for (const cj of cronJobs) assert.equal(cj.spec.concurrencyPolicy, 'Forbid', `${cj.metadata.name}`);
const appJobs = cronJobs.filter((cj) => podTemplateOf(cj).metadata?.labels?.app === 'apicore-job');
assert.deepEqual(
appJobs.map((cj) => cj.metadata.name).sort(),
Object.keys(CRON_JOBS).map((name) => `apicore-job-${name}`).sort(),
);
for (const cj of appJobs) {
const name = cj.metadata.name.replace(/^apicore-job-/, '');
const want = CRON_JOBS[name];
const template = podTemplateOf(cj);
const container = template.spec.containers[0];
assert.equal(template.spec.containers.length, 1, `${name}: one container`);
assert.equal(cj.spec.timeZone, 'Europe/Berlin', `${name}: timeZone`);
assert.equal(cj.spec.schedule, want.schedule, `${name}: schedule`);
assert.equal(cj.spec.suspend ?? false, want.suspend, `${name}: suspend`);
assert.equal(cj.spec.startingDeadlineSeconds, 600, `${name}: startingDeadlineSeconds`);
assert.equal(cj.spec.successfulJobsHistoryLimit, 3, `${name}: successfulJobsHistoryLimit`);
assert.equal(cj.spec.failedJobsHistoryLimit, 3, `${name}: failedJobsHistoryLimit`);
assert.equal(cj.spec.jobTemplate.spec.ttlSecondsAfterFinished, 86400, `${name}: ttlSecondsAfterFinished`);
assert.equal(cj.spec.jobTemplate.spec.backoffLimit, want.backoffLimit, `${name}: backoffLimit`);
assert.equal(cj.spec.jobTemplate.spec.activeDeadlineSeconds, want.deadline, `${name}: activeDeadlineSeconds`);
assert.equal(template.spec.restartPolicy, 'Never', `${name}: restartPolicy`);
assert.equal(template.metadata.labels['apicore.io/job'], name, `${name}: apicore.io/job label`);
assert.deepEqual(container.command, ['node', 'dist/cli.js', ...want.args], `${name}: command`);
assert.ok(KNOWN_JOBS.includes(container.command[2]), `${name}: unknown job ${container.command[2]}`);
assert.equal(envOf(container).APICORE_ROLE, 'job', `${name}: APICORE_ROLE`);
}
const backup = appJobs.find((cj) => cj.metadata.name === 'apicore-job-app-backup');
const backupPod = podTemplateOf(backup).spec;
const dataVolume = backupPod.volumes.find((v) => v.persistentVolumeClaim?.claimName === 'apicore-data');
assert.ok(dataVolume, 'app-backup does not use the apicore-data claim');
assert.ok(
backupPod.containers[0].volumeMounts.some((m) => m.name === dataVolume.name && m.mountPath === '/data'),
'app-backup does not mount apicore-data on /data',
);
});
test('resources match the spec table; HPA api 1–2 at 70 % CPU', () => {
const api = onePod(target, 'apicore-backend');
const worker = onePod(target, 'apicore-worker');
assert.deepEqual(mainContainer(api).resources, RESOURCES.api, 'api resources');
assert.deepEqual(mainContainer(worker).resources, RESOURCES.worker, 'worker resources');
assert.deepEqual(mainContainer(onePod(target, 'apicore-frontend')).resources, RESOURCES.frontend, 'frontend resources');
for (const job of target.byApp('apicore-job')) assert.deepEqual(mainContainer(job).resources, RESOURCES.job, `${job.owner} resources`);
for (const pod of [api, worker]) {
for (const c of pod.initContainers) assert.deepEqual(c.resources, RESOURCES.job, `${pod.owner} init ${c.name} resources`);
}
const hpas = target.docs.filter((d) => d.kind === 'HorizontalPodAutoscaler');
assert.equal(hpas.length, 1, 'expected one HPA');
const [hpa] = hpas;
assert.equal(hpa.apiVersion, 'autoscaling/v2');
assert.deepEqual(hpa.spec.scaleTargetRef, { apiVersion: 'apps/v1', kind: 'Deployment', name: 'apicore-backend' });
assert.equal(hpa.spec.minReplicas, 1);
assert.equal(hpa.spec.maxReplicas, 2);
assert.deepEqual(hpa.spec.metrics, [
{ type: 'Resource', resource: { name: 'cpu', target: { type: 'Utilization', averageUtilization: 70 } } },
]);
assert.equal(api.doc.spec.replicas, undefined, 'the HPA owns the api replica count');
});
// The Telegram bot token belongs to the worker alone (Spec T4c): one optional
// secretKeyRef on its main container, no envFrom, no volume, no other pod.
const TELEGRAM_SECRET = 'apicore-telegram';
const TELEGRAM_TOKEN_KEY = 'TELEGRAM_BOT_TOKEN';
/** Every place a pod template can pull `secretName` in: env, envFrom, volumes. */
function secretReferences(pod, secretName) {
const refs = [];
const containers = [
...pod.containers.map((c) => ({ c, init: false })),
...pod.initContainers.map((c) => ({ c, init: true })),
];
for (const { c, init } of containers) {
for (const e of c.env ?? []) {
const ref = e.valueFrom?.secretKeyRef;
if (ref?.name === secretName) refs.push({ kind: 'env', container: c.name, init, name: e.name, ref });
}
for (const from of c.envFrom ?? []) {
if (from.secretRef?.name === secretName) refs.push({ kind: 'envFrom', container: c.name, init });
}
}
for (const v of pod.spec.volumes ?? []) {
const projected = (v.projected?.sources ?? []).some((src) => src.secret?.name === secretName);
if (v.secret?.secretName === secretName || projected) refs.push({ kind: 'volume', volume: v.name });
}
return refs;
}
for (const [label, model] of [['target', target], ...stages]) {
test(`${label}: only the worker references the apicore-telegram secret, and only optionally`, () => {
const workers = model.byApp('apicore-worker');
if (label === 'target' || workers.length > 0) {
assert.equal(workers.length, 1, `${label}: expected exactly one apicore-worker pod`);
}
for (const pod of model.pods) {
const refs = secretReferences(pod, TELEGRAM_SECRET);
if (pod.labels.app !== 'apicore-worker') {
assert.deepEqual(refs, [], `${pod.owner} must not use secret ${TELEGRAM_SECRET}`);
continue;
}
assert.equal(refs.length, 1, `${pod.owner}: expected exactly one reference to ${TELEGRAM_SECRET}`);
const [only] = refs;
assert.equal(only.kind, 'env', `${pod.owner}: the secret must come in as a single env variable`);
assert.equal(only.init, false, `${pod.owner}: init containers must not see the bot token`);
assert.equal(only.container, mainContainer(pod).name, `${pod.owner}: only the main container takes the token`);
assert.equal(only.name, TELEGRAM_TOKEN_KEY);
assert.deepEqual(only.ref, { name: TELEGRAM_SECRET, key: TELEGRAM_TOKEN_KEY, optional: true });
const tokenEnvs = [...pod.containers, ...pod.initContainers].flatMap((c) =>
(c.env ?? []).filter((e) => e.name === TELEGRAM_TOKEN_KEY),
);
assert.equal(tokenEnvs.length, 1, `${pod.owner}: expected exactly one ${TELEGRAM_TOKEN_KEY} entry`);
assert.equal(tokenEnvs[0].value, undefined, `${pod.owner}: ${TELEGRAM_TOKEN_KEY} must not be a literal`);
}
// No other pod may carry the token by value or under that variable name.
for (const pod of model.pods.filter((p) => p.labels.app !== 'apicore-worker')) {
for (const c of [...pod.containers, ...pod.initContainers]) {
assert.ok(
!(c.env ?? []).some((e) => e.name === TELEGRAM_TOKEN_KEY),
`${pod.owner}/${c.name} defines ${TELEGRAM_TOKEN_KEY}`,
);
}
}
});
test(`${label}: the bot name sits in the shared ConfigMap, the token never does`, () => {
const configMaps = model.docs.filter((d) => d.kind === 'ConfigMap' && d.metadata.name === 'apicore-backend-config');
assert.equal(configMaps.length, 1, 'expected one apicore-backend-config');
const data = configMaps[0].data ?? {};
assert.ok('TELEGRAM_BOT_USERNAME' in data, 'TELEGRAM_BOT_USERNAME missing in apicore-backend-config');
assert.ok(!(TELEGRAM_TOKEN_KEY in data), `${TELEGRAM_TOKEN_KEY} must not be in a ConfigMap`);
for (const doc of model.docs.filter((d) => d.kind === 'ConfigMap')) {
assert.ok(!(TELEGRAM_TOKEN_KEY in (doc.data ?? {})), `${doc.metadata.name} holds ${TELEGRAM_TOKEN_KEY}`);
}
// Both roles read the name from the one ConfigMap through envFrom.
// (The worker only exists from the stage that rolls out backend-roles.)
for (const pod of [...model.byApp('apicore-backend'), ...model.byApp('apicore-worker')]) {
assert.ok(
(mainContainer(pod).envFrom ?? []).some((f) => f.configMapRef?.name === 'apicore-backend-config'),
`${pod.owner} does not read apicore-backend-config`,
);
}
});
}
test('api and worker run prisma migrate deploy as initContainer; only api seeds', () => {
const api = onePod(target, 'apicore-backend');
const worker = onePod(target, 'apicore-worker');
assert.deepEqual(api.initContainers.map((c) => c.name), ['migrate', 'seed']);
assert.deepEqual(worker.initContainers.map((c) => c.name), ['migrate']);
for (const pod of [api, worker]) {
const [migrate] = pod.initContainers;
assert.deepEqual(migrate.command, MIGRATE, `${pod.owner}: migrate command`);
assert.equal(envOf(migrate).HOME, '/tmp', `${pod.owner}: migrate HOME`);
assert.equal(envOf(migrate).CHECKPOINT_DISABLE, '1', `${pod.owner}: migrate CHECKPOINT_DISABLE`);
for (const c of pod.initContainers) assert.equal(c.image, mainContainer(pod).image, `${pod.owner}: ${c.name} image`);
}
assert.deepEqual(api.initContainers[1].command, SEED);
const all = (p) => [...p.initContainers, ...p.containers];
const seeding = target.pods.filter((p) => all(p).some((c) => c.command?.includes('dist/seed.js')));
assert.deepEqual(seeding.map((p) => p.owner), [api.owner], 'only the api seeds');
const migrating = target.pods.filter((p) => p.initContainers.some((c) => c.command?.includes('migrate')));
assert.deepEqual(migrating.map((p) => p.owner).sort(), [api.owner, worker.owner].sort());
assert.equal(mainContainer(api).command, undefined, 'api runs the image CMD node dist/main.js');
assert.equal(envOf(mainContainer(api)).APICORE_ROLE, 'api');
assert.deepEqual(mainContainer(worker).command, ['node', 'dist/worker.js']);
assert.equal(envOf(mainContainer(worker)).APICORE_ROLE, 'worker');
assert.equal(envOf(mainContainer(worker)).WORKER_HEALTH_PORT, '3002');
});
test('every image is pinned by sha256 digest', () => {
for (const pod of target.pods) {
for (const c of [...pod.initContainers, ...pod.containers]) {
assert.match(c.image, /@sha256:[0-9a-f]{64}$/, `${pod.owner} container ${c.name}: ${c.image}`);
assert.doesNotMatch(c.image, /:latest(@|$)/, `${pod.owner} container ${c.name} uses latest`);
}
}
});
// --- consistency with the cluster and between objects ------------------------
test('workload and Service selectors equal the live cluster', () => {
for (const [owner, selector] of Object.entries(LIVE_SELECTORS)) {
const pod = target.byOwner(owner);
assert.ok(pod, `${owner} missing`);
assert.deepEqual(pod.doc.spec.selector, { matchLabels: selector }, `${owner}: selector differs from live`);
}
for (const [name, live] of Object.entries(LIVE_SERVICES)) {
const service = target.docs.find((d) => d.kind === 'Service' && d.metadata.name === name);
assert.ok(service, `Service ${name} missing`);
assert.deepEqual(service.spec.selector, live.selector, `Service ${name}: selector differs from live`);
const ports = service.spec.ports.map(({ name: n, port, targetPort }) => ({ ...(n ? { name: n } : {}), port, targetPort }));
assert.deepEqual(ports, live.ports, `Service ${name}: ports differ from live`);
}
});
test('postgres keeps the live hardening and its static volume in every stage', () => {
for (const [name, model] of [...stages, ['target', target]]) {
const postgres = postgresOf(model);
const { spec } = postgres;
assert.equal(postgres.doc.spec.volumeClaimTemplates, undefined, `${name}: volumeClaimTemplates instead of the static volume`);
assert.equal(spec.automountServiceAccountToken, false, `${name}: service account token mounted`);
assert.equal(spec.enableServiceLinks, false, `${name}: service links enabled`);
assert.deepEqual(spec.securityContext, { seccompProfile: { type: 'RuntimeDefault' } }, `${name}: pod securityContext`);
assert.equal(postgres.containers.length, 1, `${name}: one container`);
const [container] = postgres.containers;
assert.deepEqual(container.securityContext, POSTGRES_SECURITY, `${name}: container securityContext differs from live`);
assert.deepEqual(container.resources, res('30m', '1', '128Mi', '1Gi'), `${name}: resources differ from live`);
for (const probe of ['startupProbe', 'livenessProbe', 'readinessProbe']) {
assert.ok(container[probe]?.exec, `${name}: ${probe} missing`);
}
const volumes = Object.fromEntries((spec.volumes ?? []).map((v) => [v.name, v]));
const mountedAt = Object.fromEntries((container.volumeMounts ?? []).map((m) => [m.mountPath, volumes[m.name]]));
assert.equal(mountedAt['/var/lib/postgresql']?.persistentVolumeClaim?.claimName, 'apicore-postgres-data', `${name}: data volume`);
for (const path of ['/tmp', '/var/run/postgresql', '/dev/shm']) {
assert.ok(mountedAt[path]?.emptyDir?.sizeLimit, `${name}: ${path} is not a size-limited emptyDir`);
}
const claim = model.docs.find((d) => d.kind === 'PersistentVolumeClaim' && d.metadata.name === 'apicore-postgres-data');
assert.ok(claim, `${name}: PVC apicore-postgres-data missing`);
assert.deepEqual(
claim.spec,
{ accessModes: ['ReadWriteMany'], storageClassName: '', volumeName: 'pv-apicore-postgres', resources: { requests: { storage: '10Gi' } } },
`${name}: PVC differs from the live static binding`,
);
}
});
test('every NetworkPolicy selector names pods that exist', () => {
const existing = (selector) => target.pods.some((p) => matchesSelector(selector, p.labels));
const outsideRepo = (selector) => Object.entries(SOLVER_LABELS).every(([k, v]) => selector.matchLabels?.[k] === v);
for (const policy of target.policies) {
const { podSelector } = policy.spec;
if (!isEmptySelector(podSelector) && !outsideRepo(podSelector)) {
assert.ok(existing(podSelector), `${policy.metadata.name}: podSelector matches no rendered pod`);
}
const peers = [...(policy.spec.ingress ?? []).flatMap((r) => r.from ?? []), ...(policy.spec.egress ?? []).flatMap((r) => r.to ?? [])];
for (const peer of peers.filter((p) => p.podSelector && !p.namespaceSelector && !isEmptySelector(p.podSelector))) {
assert.ok(existing(peer.podSelector), `${policy.metadata.name}: peer ${JSON.stringify(peer.podSelector)} matches no rendered pod`);
}
}
});
// --- every rollout stage keeps the application reachable ---------------------
test('the stage file covers several rollout stages', () => {
assert.ok(stages.length >= 4, `expected the rollout stages, got ${stages.map(([n]) => n)}`);
assert.ok(stages.some(([, m]) => m.policies.length === 0), 'no stage without NetworkPolicies (Runbook step 1)');
});
for (const [name, model] of [...stages, ['target', target]]) {
test(`stage ${name}: every Service selects pods that expose its target port`, () => {
for (const service of model.docs.filter((d) => d.kind === 'Service')) {
const selected = model.pods.filter((p) => matchesSelector({ matchLabels: service.spec.selector }, p.labels));
assert.ok(selected.length > 0, `Service ${service.metadata.name} selects no rendered pod`);
for (const { port, targetPort = port } of service.spec.ports) {
const exposes = (p) =>
p.ports.some((c) => (typeof targetPort === 'string' ? c.name === targetPort : c.containerPort === targetPort));
assert.ok(selected.every(exposes), `Service ${service.metadata.name}: target port ${targetPort} not exposed`);
}
}
});
test(`stage ${name}: DNS, ingress, the api and every db client keep working`, () => {
const api = onePod(model, 'apicore-backend');
const frontend = onePod(model, 'apicore-frontend');
const postgres = postgresOf(model);
for (const pod of model.pods) {
for (const protocol of ['UDP', 'TCP']) {
assert.ok(model.canConnect(pod, KUBE_DNS, 53, protocol), `${pod.owner} cannot resolve names (${protocol})`);
}
}
assert.ok(model.canConnect(TRAEFIK, frontend, 3000), 'traefik cannot reach the frontend');
assert.ok(model.canConnect(TRAEFIK, api, 3001), 'traefik cannot reach the api');
assert.ok(model.canConnect(frontend, api, 3001), 'frontend cannot reach the api');
assert.ok(model.canConnect(TRAEFIK, SOLVER, 8089), 'traefik cannot reach the http01 solver');
assert.ok(model.canConnect(api, postgres, 5432), 'api cannot reach postgres');
assert.ok(model.canConnect(api, ip('1.1.1.1'), 443), 'api cannot reach the internet on 443');
const dbClients = model.pods.filter((p) => ['apicore-worker', 'apicore-job', 'apicore-db-backup'].includes(p.labels.app));
for (const client of dbClients) {
assert.ok(model.canConnect(client, postgres, 5432), `${client.owner} cannot reach postgres`);
}
for (const worker of model.byApp('apicore-worker')) {
assert.ok(model.canConnect(worker, ip('1.1.1.1'), 443), 'worker cannot reach the internet on 443');
}
});
}