Files
ApiCore/deploy/k8s/tests/netpol-model.mjs
T
Nick SzaunigandClaude Opus 5.5 c01a38835a feat: platform backbone and field capture, first part (checkpoint)
Platform backbone (plan 2026-10-13-plattform-rueckgrat), reviewed tasks:
- run roles api/worker/job/all with shared startup checks (Task 1)
- schema and migration for DomainEvent, deliveries, JobRun, ThrottlerHit (Task 2)
- audit, notifications and domain events written in the same transaction (Task 3)
- dispatcher with SKIP LOCKED claims, retries and stale release (Task 4)
- worker building blocks: tick, LISTEN/NOTIFY wake-up, health (Task 5)
- worker process and entry points (Task 6)
- throttler counters in Postgres (Task 19)
- separate keys per purpose with key ring (Task 20)
- public origin page without exact location (Task 22)
- shop keys split into read and write (Task 23)
- Kustomize base, components, overlay encry, CI validation (Task 26)

Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), reviewed tasks:
- brood frames, read endpoints, apiary overview and offline pack
- network basics, offline queue v3, navigation sheets, form building blocks
- new inspection with last-time hints and weather, varroa per day,
  treatment prefill, apiary page with round
- inspection photo (Task 10) is implemented, review pending

Not rolled out. Remaining tasks follow in later commits.

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

162 lines
6.2 KiB
JavaScript

// Kubernetes semantics for rendered manifests: pod templates, label
// selectors and NetworkPolicies (the union of all policies that select a
// pod, both the egress and the ingress side). Used by
// manifest-policy.test.mjs to ask "can A reach B on port P".
export const NAMESPACE = 'apicore';
/** Pod template of a workload, or undefined for anything else. */
export function podTemplateOf(doc) {
if (doc.kind === 'CronJob') return doc.spec?.jobTemplate?.spec?.template;
if (['Deployment', 'StatefulSet', 'DaemonSet', 'Job'].includes(doc.kind)) return doc.spec?.template;
return doc.kind === 'Pod' ? doc : undefined;
}
function podsOf(docs) {
return docs.flatMap((doc) => {
const template = podTemplateOf(doc);
if (!template) return [];
const namespace = doc.metadata.namespace ?? NAMESPACE;
const containers = template.spec.containers ?? [];
return [{
kind: 'pod',
owner: `${doc.kind}/${doc.metadata.name}`,
doc,
namespace,
nsLabels: { 'kubernetes.io/metadata.name': namespace },
labels: template.metadata?.labels ?? {},
spec: template.spec,
containers,
initContainers: template.spec.initContainers ?? [],
ports: containers.flatMap((c) => c.ports ?? []),
}];
});
}
/** A pod that is not rendered here (Traefik, CoreDNS, cert-manager solver). */
export function syntheticPod(namespace, labels, ports = []) {
return {
kind: 'pod', owner: `synthetic/${namespace}/${JSON.stringify(labels)}`, namespace,
nsLabels: { 'kubernetes.io/metadata.name': namespace }, labels, ports,
};
}
export const ip = (address) => ({ kind: 'ip', owner: `ip/${address}`, ip: address });
export function matchesSelector(selector, labels) {
if (!selector) return true;
const plain = Object.entries(selector.matchLabels ?? {}).every(([k, v]) => labels[k] === v);
const expressions = (selector.matchExpressions ?? []).every(({ key, operator, values = [] }) => {
if (operator === 'In') return values.includes(labels[key]);
if (operator === 'NotIn') return !values.includes(labels[key]);
if (operator === 'Exists') return key in labels;
if (operator === 'DoesNotExist') return !(key in labels);
throw new Error(`unsupported selector operator ${operator}`);
});
return plain && expressions;
}
export const isEmptySelector = (s) =>
!s || (Object.keys(s.matchLabels ?? {}).length === 0 && (s.matchExpressions ?? []).length === 0);
export function policyTypes(policy) {
return policy.spec.policyTypes ?? (policy.spec.egress ? ['Ingress', 'Egress'] : ['Ingress']);
}
function parseIp(address) {
if (address.includes(':')) {
const [head, tail] = address.includes('::') ? address.split('::') : [address, null];
const h = head ? head.split(':') : [];
const t = tail ? tail.split(':') : [];
const groups = tail === null ? h : [...h, ...Array(8 - h.length - t.length).fill('0'), ...t];
return { bits: 128, value: groups.reduce((acc, g) => (acc << 16n) + BigInt(parseInt(g, 16)), 0n) };
}
return { bits: 32, value: address.split('.').reduce((acc, o) => (acc << 8n) + BigInt(Number(o)), 0n) };
}
export function cidrContains(cidr, address) {
const [network, prefix] = cidr.split('/');
const net = parseIp(network);
const host = parseIp(address);
if (net.bits !== host.bits) return false;
const shift = BigInt(net.bits - Number(prefix));
return net.value >> shift === host.value >> shift;
}
function peerMatches(peer, endpoint, policyNamespace) {
if (peer.ipBlock) {
return (
endpoint.kind === 'ip' &&
cidrContains(peer.ipBlock.cidr, endpoint.ip) &&
!(peer.ipBlock.except ?? []).some((cidr) => cidrContains(cidr, endpoint.ip))
);
}
if (endpoint.kind !== 'pod') return false;
const namespaceOk = peer.namespaceSelector
? matchesSelector(peer.namespaceSelector, endpoint.nsLabels)
: endpoint.namespace === policyNamespace;
return namespaceOk && matchesSelector(peer.podSelector, endpoint.labels);
}
function portMatches(ports, port, protocol, destination) {
if (!ports || ports.length === 0) return true;
return ports.some((p) => {
if ((p.protocol ?? 'TCP') !== protocol) return false;
if (p.port === undefined) return true;
if (typeof p.port === 'string') {
return (destination.ports ?? []).some(
(c) => c.name === p.port && c.containerPort === port && (c.protocol ?? 'TCP') === protocol,
);
}
return p.endPort ? port >= p.port && port <= p.endPort : p.port === port;
});
}
/** Everything the tests need to reason about one rendered document set. */
export function modelOf(docs) {
const policies = docs.filter((d) => d.kind === 'NetworkPolicy');
const pods = podsOf(docs);
const policiesFor = (pod, type) =>
policies.filter(
(p) =>
(p.metadata.namespace ?? NAMESPACE) === pod.namespace &&
matchesSelector(p.spec.podSelector, pod.labels) &&
policyTypes(p).includes(type),
);
function allows(direction, pod, peer, port, protocol) {
const type = direction === 'egress' ? 'Egress' : 'Ingress';
const relevant = policiesFor(pod, type);
if (relevant.length === 0) return true;
const destination = direction === 'egress' ? peer : pod;
return relevant.some((policy) =>
(policy.spec[direction] ?? []).some((rule) => {
const peers = rule[direction === 'egress' ? 'to' : 'from'];
const peerOk =
!peers || peers.length === 0 ||
peers.some((p) => peerMatches(p, peer, policy.metadata.namespace ?? NAMESPACE));
return peerOk && portMatches(rule.ports, port, protocol, destination);
}),
);
}
const allowsEgress = (src, dst, port, protocol = 'TCP') => allows('egress', src, dst, port, protocol);
const allowsIngress = (dst, src, port, protocol = 'TCP') => allows('ingress', dst, src, port, protocol);
const canConnect = (src, dst, port, protocol = 'TCP') =>
(src.kind !== 'pod' || allowsEgress(src, dst, port, protocol)) &&
(dst.kind !== 'pod' || allowsIngress(dst, src, port, protocol));
return {
docs,
pods,
policies,
policiesFor,
allowsEgress,
allowsIngress,
canConnect,
byApp: (app) => pods.filter((p) => p.labels.app === app),
byOwner: (owner) => pods.find((p) => p.owner === owner),
};
}