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