Task 14: Port StructuredModel mit Anthropic-Adapter (@anthropic-ai/sdk 0.131.0 exakt, Lieferkette geprüft), fester Endpunkt https://api.anthropic.com (Umgebungs-Header werden entfernt), feste Systemregeln ohne Datum/Volk/Person, Transkript als Daten in Tags, strukturierte Ausgabe per JSON-Schema mit effort low, serverseitiger Fallback per VOICE_SERVER_FALLBACK abschaltbar. Jeder Vorschlag wird geprüft: bekanntes Feld, Wert in den Formulargrenzen, Zitat steht wörtlich im Gesagten (Dezimalzahl als ein Wort), Korrektur gewinnt. Netzwächter: nur die Worker-Verdrahtung erreicht den Adapter (transitiv geprüft). Task 15: Worker-Schleife voice-extractions (LISTEN + 15-s-Abfrage, ein Lauf zugleich, höchstens 20 Notizen je Lauf), Modellaufruf außerhalb jeder Transaktion mit 240-s-Obergrenze, Ergebnis versiegelt mit Ereignis nur bei gehaltenem Claim; ohne Schlüssel EXTRACT_DISABLED ohne Versuch, Tageslimit zählt jeden bezahlten Aufruf über das neue Journal VoiceExtractionCall (nur IDs, Modell, Tokens, Ergebnis; 90 Tage, nicht im Backup). Nur der Worker-Container erhält ANTHROPIC_API_KEY aus dem Secret apicore-anthropic (optional); API- und Job-Rollen warnen, wenn der Schlüssel bei ihnen gesetzt ist. Gates: Backend lint, typecheck, 4589 Unit-Tests, build; e2e 104 Suites (nur der Wetter-Test gegen die echte API schwankt); Manifest-Policy 55. Refs #27 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
763 lines
42 KiB
JavaScript
763 lines
42 KiB
JavaScript
// 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', 'voice-transcribe'];
|
||
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}`);
|
||
// The shared job patch (backend-roles) selects CronJob objects labelled app=apicore-job;
|
||
// the voice CronJob is deliberately not one of them (see the voice tests below).
|
||
const appJobs = cronJobs.filter((cj) => 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');
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- voice transcription CronJob (spec T7 6.3, 9.2, 11.1; plan task 13) ------
|
||
|
||
const VOICE_CRONJOB = 'apicore-job-voice-transcribe';
|
||
const WHISPER_IMAGE = 'git.encry.de/nick.rosenloecher/apicore-whisper';
|
||
const ANTHROPIC_KEY = 'ANTHROPIC_API_KEY';
|
||
const ANTHROPIC_SECRET = 'apicore-anthropic';
|
||
|
||
function voiceCronJob(model) {
|
||
const found = model.docs.filter((d) => d.kind === 'CronJob' && d.metadata.name === VOICE_CRONJOB);
|
||
assert.equal(found.length, 1, `expected exactly one CronJob ${VOICE_CRONJOB}`);
|
||
return found[0];
|
||
}
|
||
const voicePodOf = (model) => {
|
||
const pod = model.byOwner(`CronJob/${VOICE_CRONJOB}`);
|
||
assert.ok(pod, 'voice pod template missing');
|
||
return pod;
|
||
};
|
||
const voiceContainer = (pod, name) => {
|
||
const found = [...pod.initContainers, ...pod.containers].filter((c) => c.name === name);
|
||
assert.equal(found.length, 1, `expected exactly one container ${name}`);
|
||
return found[0];
|
||
};
|
||
|
||
test('whisper container has no env from secrets, no secretKeyRef and no service account token', () => {
|
||
const pod = voicePodOf(target);
|
||
const whisper = voiceContainer(pod, 'whisper');
|
||
assert.equal(pod.spec.automountServiceAccountToken, false, 'service account token mounted');
|
||
assert.equal(whisper.envFrom, undefined, 'whisper takes envFrom');
|
||
assert.deepEqual(whisper.env, [{ name: 'WHISPER_CPU_THREADS', value: '2' }], 'whisper env is only the thread count');
|
||
assert.ok(!(whisper.env ?? []).some((e) => e.valueFrom), 'whisper env uses valueFrom');
|
||
assert.equal(whisper.image.split('@')[0], WHISPER_IMAGE);
|
||
// Every volume of the pod is an emptyDir: no secret, configMap or projected token.
|
||
for (const volume of pod.spec.volumes ?? []) {
|
||
assert.ok(volume.emptyDir, `volume ${volume.name} is not an emptyDir`);
|
||
}
|
||
assert.deepEqual(
|
||
(whisper.volumeMounts ?? []).map((m) => m.mountPath).sort(),
|
||
['/tmp', '/work/in', '/work/out'],
|
||
'whisper mounts more than the work volume and /tmp',
|
||
);
|
||
assert.equal(whisper.securityContext.runAsNonRoot, true);
|
||
assert.equal(whisper.securityContext.runAsUser, 1000);
|
||
assert.equal(whisper.securityContext.readOnlyRootFilesystem, true);
|
||
assert.equal(whisper.securityContext.allowPrivilegeEscalation, false);
|
||
assert.deepEqual(whisper.securityContext.capabilities.drop, ['ALL']);
|
||
assert.deepEqual(whisper.securityContext.seccompProfile, { type: 'RuntimeDefault' });
|
||
for (const secret of ['apicore-backend-secret', ANTHROPIC_SECRET, TELEGRAM_SECRET]) {
|
||
assert.deepEqual(
|
||
secretReferences(pod, secret).filter((r) => r.container === 'whisper'),
|
||
[],
|
||
`whisper references secret ${secret}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('the voice pod matches the jobs network policy (egress only postgres and dns)', () => {
|
||
const pod = voicePodOf(target);
|
||
const postgres = postgresOf(target);
|
||
assert.equal(pod.labels.app, 'apicore-job', 'the pod template must carry app=apicore-job for apicore-jobs');
|
||
assert.equal(pod.labels['apicore.io/job'], 'voice-transcribe');
|
||
assert.equal(pod.labels['db-client'], 'true');
|
||
assert.ok(target.canConnect(pod, postgres, 5432), 'voice pod cannot reach postgres');
|
||
assert.ok(!target.canConnect(pod, postgres, 22), 'voice pod reaches postgres on another port');
|
||
for (const protocol of ['UDP', 'TCP']) {
|
||
assert.ok(target.canConnect(pod, KUBE_DNS, 53, protocol), `voice pod cannot resolve names (${protocol})`);
|
||
}
|
||
for (const address of ['1.1.1.1', '10.43.0.1', '169.254.169.254']) {
|
||
for (const port of [80, 443]) {
|
||
assert.ok(!target.canConnect(pod, ip(address), port), `voice pod reaches ${address}:${port}`);
|
||
}
|
||
}
|
||
for (const other of target.pods.filter((p) => p !== postgres && p !== pod)) {
|
||
assert.ok(!target.canConnect(pod, other, 3001) && !target.canConnect(pod, other, 3000), `voice pod reaches ${other.owner}`);
|
||
}
|
||
for (const policy of target.policiesFor(pod, 'Ingress')) {
|
||
assert.equal((policy.spec.ingress ?? []).length, 0, `${policy.metadata.name} opens ingress to the voice pod`);
|
||
}
|
||
});
|
||
|
||
test('work volume is a memory emptyDir of at most 32Mi, mounted in all three containers', () => {
|
||
const pod = voicePodOf(target);
|
||
const work = (pod.spec.volumes ?? []).find((v) => v.name === 'work');
|
||
assert.ok(work?.emptyDir, 'work volume missing or not an emptyDir');
|
||
assert.equal(work.emptyDir.medium, 'Memory', 'work volume is not memory-backed');
|
||
assert.equal(work.emptyDir.sizeLimit, '32Mi');
|
||
for (const name of ['fetch', 'ingest']) {
|
||
const mount = (voiceContainer(pod, name).volumeMounts ?? []).find((m) => m.name === 'work');
|
||
assert.equal(mount?.mountPath, '/work', `${name} does not mount work on /work`);
|
||
assert.ok(!mount.readOnly, `${name} mounts work read-only`);
|
||
}
|
||
// whisper: /work/in read-only, /work/out writable, nothing else of the work volume.
|
||
const whisperWork = (voiceContainer(pod, 'whisper').volumeMounts ?? []).filter((m) => m.name === 'work');
|
||
assert.deepEqual(
|
||
whisperWork.map((m) => [m.mountPath, m.subPath, m.readOnly ?? false]).sort(),
|
||
[['/work/in', 'in', true], ['/work/out', 'out', false]].sort(),
|
||
'whisper must see /work/in read-only and /work/out writable only',
|
||
);
|
||
// fetch, whisper and ingest share the files (0700/0600): one uid, one fsGroup.
|
||
assert.equal(pod.spec.securityContext.runAsUser, 1000);
|
||
assert.equal(pod.spec.securityContext.fsGroup, 1000);
|
||
for (const name of ['fetch', 'whisper', 'ingest']) {
|
||
const own = voiceContainer(pod, name).securityContext.runAsUser;
|
||
assert.ok(own === undefined || own === 1000, `${name} runs as another uid`);
|
||
}
|
||
});
|
||
|
||
test('voice resources match spec 11.1', () => {
|
||
const pod = voicePodOf(target);
|
||
assert.deepEqual(voiceContainer(pod, 'fetch').resources, res('50m', '500m', '192Mi', '512Mi'));
|
||
assert.deepEqual(voiceContainer(pod, 'ingest').resources, res('50m', '500m', '192Mi', '512Mi'));
|
||
assert.deepEqual(voiceContainer(pod, 'whisper').resources, res('50m', '2000m', '1280Mi', '1280Mi'));
|
||
});
|
||
|
||
test('the voice CronJob is not matched by the shared job patch and has exactly fetch, whisper, ingest', () => {
|
||
const cronJob = voiceCronJob(target);
|
||
assert.notEqual(cronJob.metadata.labels?.app, 'apicore-job', 'the shared job patch would add a container `job`');
|
||
const pod = voicePodOf(target);
|
||
assert.deepEqual(pod.initContainers.map((c) => c.name), ['fetch', 'whisper']);
|
||
assert.deepEqual(pod.containers.map((c) => c.name), ['ingest']);
|
||
assert.ok(![...pod.initContainers, ...pod.containers].some((c) => c.name === 'job'), 'a container `job` was patched in');
|
||
// Everything the shared patch would have set is spelled out in the manifest.
|
||
const { spec } = cronJob;
|
||
assert.equal(spec.schedule, '*/2 * * * *');
|
||
assert.equal(spec.timeZone, 'Europe/Berlin');
|
||
assert.equal(spec.concurrencyPolicy, 'Forbid');
|
||
assert.equal(spec.startingDeadlineSeconds, 120);
|
||
assert.equal(spec.suspend ?? false, false);
|
||
assert.equal(spec.jobTemplate.spec.activeDeadlineSeconds, 1500);
|
||
assert.equal(spec.jobTemplate.spec.backoffLimit, 0);
|
||
assert.equal(spec.jobTemplate.spec.ttlSecondsAfterFinished, 86400);
|
||
assert.equal(pod.spec.restartPolicy, 'Never');
|
||
assert.equal(pod.spec.securityContext.runAsNonRoot, true);
|
||
assert.deepEqual(pod.spec.securityContext.seccompProfile, { type: 'RuntimeDefault' });
|
||
// fetch and ingest are the backend image in the job role; both phases are exact commands.
|
||
const fetch = voiceContainer(pod, 'fetch');
|
||
const ingest = voiceContainer(pod, 'ingest');
|
||
assert.deepEqual(fetch.command, ['node', 'dist/cli.js', 'voice-transcribe', '--phase=fetch']);
|
||
assert.deepEqual(ingest.command, ['node', 'dist/cli.js', 'voice-transcribe', '--phase=ingest']);
|
||
assert.ok(KNOWN_JOBS.includes(fetch.command[2]));
|
||
for (const c of [fetch, ingest]) {
|
||
assert.equal(envOf(c).APICORE_ROLE, 'job', `${c.name}: APICORE_ROLE`);
|
||
assert.equal(c.image.split('@')[0], 'git.encry.de/nick.rosenloecher/apicore-backend');
|
||
assert.ok((c.envFrom ?? []).some((f) => f.secretRef?.name === 'apicore-backend-secret'), `${c.name}: backend secret`);
|
||
}
|
||
});
|
||
|
||
// Claude extraction (spec T7 6.4, 11.3): only the worker's main container carries
|
||
// the key, as one optional secretKeyRef (no envFrom, no volume, no other secret
|
||
// reference to apicore-anthropic).
|
||
for (const [label, model] of [['target', target], ...stages]) {
|
||
const hasKey = label === 'target' || label === 'step6-voice';
|
||
test(`${label}: ${ANTHROPIC_KEY} reaches only the worker container through an optional secretKeyRef`, () => {
|
||
for (const pod of model.pods) {
|
||
const isWorker = pod.labels.app === 'apicore-worker';
|
||
for (const c of [...pod.containers, ...pod.initContainers]) {
|
||
const entries = (c.env ?? []).filter((e) => e.name === ANTHROPIC_KEY);
|
||
if (isWorker && c.name === 'apicore-worker' && hasKey) {
|
||
assert.equal(entries.length, 1, `${c.name} must define ${ANTHROPIC_KEY} exactly once`);
|
||
assert.deepEqual(
|
||
entries[0].valueFrom,
|
||
{ secretKeyRef: { name: ANTHROPIC_SECRET, key: ANTHROPIC_KEY, optional: true } },
|
||
`${c.name}: ${ANTHROPIC_KEY} must be an optional secretKeyRef to ${ANTHROPIC_SECRET}`,
|
||
);
|
||
assert.equal(entries[0].value, undefined, `${c.name}: ${ANTHROPIC_KEY} has a literal value`);
|
||
} else if (hasKey || !isWorker) {
|
||
assert.equal(entries.length, 0, `${pod.owner}/${c.name} defines ${ANTHROPIC_KEY}`);
|
||
}
|
||
const viaEnvFrom = (c.envFrom ?? []).some((f) => f.secretRef?.name === ANTHROPIC_SECRET);
|
||
assert.ok(!viaEnvFrom, `${pod.owner}/${c.name} takes ${ANTHROPIC_SECRET} through envFrom`);
|
||
}
|
||
const refs = secretReferences(pod, ANTHROPIC_SECRET);
|
||
if (isWorker && hasKey) {
|
||
assert.deepEqual(refs.map((r) => r.container), ['apicore-worker'], 'only the worker main container may reference the secret');
|
||
} else if (!isWorker) {
|
||
assert.deepEqual(refs, [], `${pod.owner} must not use secret ${ANTHROPIC_SECRET}`);
|
||
}
|
||
for (const volume of pod.spec.volumes ?? []) {
|
||
assert.notEqual(volume.secret?.secretName, ANTHROPIC_SECRET, `${pod.owner} mounts ${ANTHROPIC_SECRET}`);
|
||
}
|
||
}
|
||
for (const doc of model.docs.filter((d) => d.kind === 'ConfigMap')) {
|
||
assert.ok(!(ANTHROPIC_KEY in (doc.data ?? {})), `${doc.metadata.name} holds ${ANTHROPIC_KEY}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
test('the voice settings sit in the shared ConfigMap with the values the backend accepts', () => {
|
||
const configMap = target.docs.find((d) => d.kind === 'ConfigMap' && d.metadata.name === 'apicore-backend-config');
|
||
assert.equal(configMap.data.VOICE_RECORDING_ENABLED, 'false');
|
||
assert.equal(configMap.data.VOICE_MODEL, 'claude-sonnet-5-5');
|
||
assert.equal(configMap.data.VOICE_DAILY_EXTRACTIONS, '60');
|
||
});
|
||
|
||
test('apicore-whisper is pinned by digest in the overlay', () => {
|
||
const overlay = readFileSync(new URL('../overlays/encry/kustomization.yaml', import.meta.url), 'utf8');
|
||
assert.match(overlay, new RegExp(`name: ${WHISPER_IMAGE}\\s+digest: sha256:[0-9a-f]{64}`), 'no digest entry for apicore-whisper');
|
||
const whisper = voiceContainer(voicePodOf(target), 'whisper');
|
||
assert.match(whisper.image, new RegExp(`^${WHISPER_IMAGE}@sha256:[0-9a-f]{64}$`));
|
||
assert.doesNotMatch(whisper.image, /:latest/);
|
||
});
|
||
|
||
test('no volume except work is mounted by both whisper and fetch/ingest; /tmp volumes are size-limited', () => {
|
||
const pod = voicePodOf(target);
|
||
const mounted = (name) => new Set((voiceContainer(pod, name).volumeMounts ?? []).map((m) => m.name));
|
||
const whisper = mounted('whisper');
|
||
for (const other of ['fetch', 'ingest']) {
|
||
const shared = [...mounted(other)].filter((v) => whisper.has(v));
|
||
assert.deepEqual(shared, ['work'], `whisper and ${other} share volumes other than work: ${shared}`);
|
||
}
|
||
const tmpOf = (name) => voiceContainer(pod, name).volumeMounts.find((m) => m.mountPath === '/tmp').name;
|
||
assert.notEqual(tmpOf('whisper'), tmpOf('ingest'), 'whisper and ingest share /tmp');
|
||
for (const volume of pod.spec.volumes) {
|
||
assert.ok(volume.emptyDir?.sizeLimit, `volume ${volume.name} has no sizeLimit`);
|
||
}
|
||
assert.equal(pod.spec.volumes.find((v) => v.name === tmpOf('ingest')).emptyDir.sizeLimit, '64Mi');
|
||
});
|