#!/usr/bin/env python3 # Zusicherungen fuer targets.py (#0106). Stdlib, laeuft ohne Netz: # python3 test_targets.py # # Die Antworten sind AUFGEZEICHNET, nicht erfunden - abgenommen am 2026-08-21 # von der laufenden Anlage (Prometheus auf dem Betriebs-Host, rohana /v2). # # ⚠️ Jede Zusicherung hat ihre GEGENPROBE. Eine Pruefung, die nur je "gleich" # gesagt hat, ist eine Vermutung - das ist in diesem Projekt die haeufigste # Fehlerklasse ("meldet Erfolg, ist aber blind"), zuletzt belegt in der AAR vom # 2026-08-01. Deshalb steht neben jedem "muss zusammenfallen" ein "darf NICHT # zusammenfallen". import unittest from datetime import datetime, timezone import targets def ts(jahr, monat, tag): return datetime(jahr, monat, tag, tzinfo=timezone.utc) # --- Aufgezeichnete Antworten ------------------------------------------------ # Auszug aus `count by (image) (kube_pod_container_info)`, 2026-08-21. PROM_CLUSTER = [ {"metric": {"image": "docker.io/library/postgres:17-alpine"}}, {"metric": {"image": "docker.io/rancher/mirrored-library-traefik:3.6.10"}}, {"metric": {"image": "rohana.axion1337.de/sorb/threadnet-web:v0.6.0"}}, {"metric": {"image": "oci.element.io/synapse:v1.151.0-ess.1"}}, ] # Auszug aus `container_last_seen{...}` - BEIDE Jobs, damit die Abgrenzung # ueberhaupt etwas zu tun hat. PROM_CADVISOR_GEMISCHT = [ {"metric": {"image": "gitea/gitea:1.27.0", "job": "operating_cadvisor"}}, {"metric": {"image": "traefik:v3.7.9", "job": "operating_cadvisor"}}, {"metric": {"image": "ghcr.io/coollabsio/coolify:4.3.9", "job": "gameserver_cadvisor"}}, {"metric": {"image": "redis:7-alpine", "job": "gameserver_cadvisor"}}, ] class Normalisierung(unittest.TestCase): def test_docker_hub_schreibweisen_fallen_zusammen(self): formen = [ "docker.io/library/postgres:17-alpine", "index.docker.io/library/postgres:17-alpine", "library/postgres:17-alpine", "postgres:17-alpine", ] ergebnisse = {targets.normalisiere(f) for f in formen} self.assertEqual(ergebnisse, {"postgres:17-alpine"}) def test_gegenprobe_verschiedene_fassungen_fallen_NICHT_zusammen(self): # Ohne diese Zusicherung koennte normalisiere() alles auf denselben Wert # abbilden und der Test oben waere trotzdem gruen. self.assertNotEqual( targets.normalisiere("postgres:15-alpine"), targets.normalisiere("postgres:17-alpine"), ) self.assertNotEqual( targets.normalisiere("rohana.axion1337.de/sorb/threadnet-web:v0.3.0"), targets.normalisiere("rohana.axion1337.de/sorb/threadnet-web:v0.6.0"), ) def test_fremde_registry_bleibt_unangetastet(self): for ref in ( "rohana.axion1337.de/sorb/threadnet-web:v0.6.0", "ghcr.io/fluxcd/source-controller:v1.8.2", "quay.io/jetstack/cert-manager-webhook:v1.14.0", ): self.assertEqual(targets.normalisiere(ref), ref) def test_ohne_tag_wird_verworfen_und_NICHT_zu_latest(self): for ref in ("postgres", "docker.io/library/postgres", "ghcr.io/a/b"): self.assertIsNone(targets.normalisiere(ref)) def test_digest_wird_abgetrennt_tag_bleibt(self): self.assertEqual( targets.normalisiere("postgres:17-alpine@sha256:" + "ab" * 32), "postgres:17-alpine", ) # Nur Digest, kein Tag -> kein Ziel. self.assertIsNone(targets.normalisiere("postgres@sha256:" + "ab" * 32)) def test_registry_mit_port_wird_nicht_als_tag_gelesen(self): self.assertEqual(targets.normalisiere("reg.lan:5000/a/b:v1"), "reg.lan:5000/a/b:v1") self.assertIsNone(targets.normalisiere("reg.lan:5000/a/b")) def test_leeres_und_muell(self): for ref in ("", " ", None): self.assertIsNone(targets.normalisiere(ref)) class Quellentrennung(unittest.TestCase): def setUp(self): self._echt = targets.promql def tearDown(self): targets.promql = self._echt def test_operating_nimmt_gameserver_nicht_mit(self): # Der Ausdruck filtert serverseitig; hier wird geprueft, dass er den # Filter ueberhaupt traegt - sonst faellt die Abgrenzung still weg. self.assertIn('job="operating_cadvisor"', targets.AUSDRUCK_OPERATING) gesehen = {} def fake(basis, ausdruck, timeout=10.0): gesehen["ausdruck"] = ausdruck # Prometheus wuerde bereits gefiltert antworten - wir simulieren das, # indem wir den Filter anwenden. job = "operating_cadvisor" if 'job="operating_cadvisor"' in ausdruck else None return [r for r in PROM_CADVISOR_GEMISCHT if r["metric"]["job"] == job] targets.promql = fake ziele = targets.ziele_operating("http://x") self.assertEqual(ziele, {"gitea/gitea:1.27.0", "traefik:v3.7.9"}) def test_gegenprobe_ohne_filter_kaeme_gameserver_mit(self): # Beweist, dass die Zusicherung oben ueberhaupt etwas prueft. def fake_ohne_filter(basis, ausdruck, timeout=10.0): return PROM_CADVISOR_GEMISCHT targets.promql = fake_ohne_filter ziele = targets.ziele_operating("http://x") self.assertIn("redis:7-alpine", ziele) def test_cluster_normalisiert_beim_einlesen(self): targets.promql = lambda b, a, timeout=10.0: PROM_CLUSTER ziele = targets.ziele_cluster("http://x") self.assertIn("postgres:17-alpine", ziele) self.assertIn("rancher/mirrored-library-traefik:3.6.10", ziele) self.assertNotIn("docker.io/library/postgres:17-alpine", ziele) class RegistryAuswahl(unittest.TestCase): def setUp(self): self._repos, self._tags, self._erstellt = ( targets.registry_repos, targets.registry_tags, targets.tag_erstellt, ) targets.registry_token = lambda *a, **k: "egal" def tearDown(self): targets.registry_repos, targets.registry_tags, targets.tag_erstellt = ( self._repos, self._tags, self._erstellt, ) def _stelle(self, tags, zeiten): targets.registry_repos = lambda reg, timeout=15.0: ["sorb/x"] targets.registry_tags = lambda reg, repo, token="", timeout=15.0: tags targets.tag_erstellt = lambda reg, repo, tag, token="", timeout=15.0: zeiten.get(tag) def test_juengste_drei_nach_ZEIT_nicht_nach_name(self): # ⚠️ Der Kern: lexikografisch stuende v0.9.0 vor v0.10.0. tags = ["v0.9.0", "v0.10.0", "v0.8.0", "v0.7.0"] zeiten = { "v0.7.0": ts(2026, 1, 1), "v0.8.0": ts(2026, 2, 1), "v0.9.0": ts(2026, 3, 1), "v0.10.0": ts(2026, 4, 1), } self._stelle(tags, zeiten) ziele = targets.ziele_registry("reg.example", je_repo=3) self.assertEqual( ziele, {"reg.example/sorb/x:v0.10.0", "reg.example/sorb/x:v0.9.0", "reg.example/sorb/x:v0.8.0"}, ) def test_gegenprobe_namenssortierung_haette_anders_gewaehlt(self): # Belegt, dass der Test oben nicht zufaellig gruen ist: nach Namen # sortiert faellt die JUENGSTE Fassung heraus und eine alte kommt mit. tags = ["v0.9.0", "v0.10.0", "v0.8.0", "v0.7.0"] nach_name = sorted(tags, reverse=True)[:3] self.assertNotIn("v0.10.0", nach_name) # die juengste - waere verloren self.assertIn("v0.7.0", nach_name) # die aelteste - waere mitgekommen def test_bau_artefakte_fallen_raus(self): tags = ["v1.0.0", "sha-13547676", "latest-ci", "latest"] zeiten = {t: ts(2026, 5, 10) for t in tags} self._stelle(tags, zeiten) ziele = targets.ziele_registry("reg.example", je_repo=5) self.assertEqual(ziele, {"reg.example/sorb/x:v1.0.0"}) def test_tag_ohne_zeitstempel_wird_uebersprungen_nicht_geraten(self): tags = ["v1.0.0", "v2.0.0"] zeiten = {"v1.0.0": ts(2026, 5, 10)} # v2.0.0 liefert None self._stelle(tags, zeiten) ziele = targets.ziele_registry("reg.example", je_repo=5) self.assertEqual(ziele, {"reg.example/sorb/x:v1.0.0"}) class Ausfallverhalten(unittest.TestCase): """⚠️ Der eigentliche Kern (ADR-0026): Ein Ausfall der Herleitung darf nicht wie vollstaendige Deckung aussehen.""" CFG = { "prometheus": "http://x", "registry": "reg.example", "je_repo": 3, "timeout": 1.0, "cluster_an": True, "operating_an": True, "registry_an": True, } def setUp(self): self._c, self._o, self._r = ( targets.ziele_cluster, targets.ziele_operating, targets.ziele_registry, ) def tearDown(self): targets.ziele_cluster, targets.ziele_operating, targets.ziele_registry = ( self._c, self._o, self._r, ) def _stelle(self, cluster=None, operating=None, registry=None): def macher(wert): if isinstance(wert, Exception): def f(*a, **k): raise wert return f return lambda *a, **k: (wert or set()) targets.ziele_cluster = macher(cluster) targets.ziele_operating = macher(operating) targets.ziele_registry = macher(registry) def test_alles_gut(self): self._stelle({"a:1"}, {"b:1"}, {"c:1"}) h = targets.herleiten(self.CFG) self.assertEqual(h.ziele, {"a:1", "b:1", "c:1"}) self.assertTrue(h.vollstaendig) self.assertTrue(h.brauchbar) self.assertEqual(set(h.stand), {"cluster", "operating", "registry"}) def test_eine_quelle_faellt_aus_die_anderen_liefern_weiter(self): self._stelle({"a:1"}, OSError("Prometheus weg"), {"c:1"}) h = targets.herleiten(self.CFG) self.assertEqual(h.ziele, {"a:1", "c:1"}) self.assertFalse(h.vollstaendig) self.assertTrue(h.brauchbar) self.assertIn("operating", h.fehler) self.assertNotIn("operating", h.stand) # Frische bleibt aus self.assertIn("cluster", h.stand) def test_stand_der_ausgefallenen_quelle_bleibt_auf_dem_ALTEN_wert(self): # Damit cve_target_source_stale altert, statt zu verschwinden. self._stelle(OSError("weg"), {"b:1"}, {"c:1"}) h = targets.herleiten(self.CFG, vorheriger_stand={"cluster": 1000.0}) self.assertEqual(h.stand["cluster"], 1000.0) self.assertGreater(h.stand["operating"], 1000.0) def test_alle_quellen_weg_ergibt_leere_und_UNBRAUCHBARE_menge(self): self._stelle(OSError("a"), OSError("b"), OSError("c")) h = targets.herleiten(self.CFG) self.assertEqual(h.ziele, set()) self.assertFalse(h.brauchbar) # -> nichts schreiben, nichts loeschen self.assertEqual(set(h.fehler), {"cluster", "operating", "registry"}) def test_gegenprobe_leere_menge_ist_NICHT_dasselbe_wie_erfolg(self): # Der Fall, der ohne diese Unterscheidung wie 100 % Deckung aussaehe. self._stelle(set(), set(), set()) h = targets.herleiten(self.CFG) self.assertEqual(h.ziele, set()) self.assertTrue(h.vollstaendig) # kein Fehler gemeldet ... self.assertFalse(h.brauchbar) # ... aber trotzdem nicht verwertbar class Deckungsrechnung(unittest.TestCase): """Die Mengenarithmetik, auf der die Kriterien 2 und 3 beruhen.""" def test_zahlen_der_handmessung_vom_2026_08_21(self): soll = {f"i{n}:v1" for n in range(51)} ist = {f"i{n}:v1" for n in range(27)} | {"alt:v1", "weg:v1"} fehlend = soll - ist verwaist = ist - soll self.assertEqual(len(fehlend), 24) self.assertEqual(len(verwaist), 2) self.assertAlmostEqual(len(soll & ist) / len(soll), 27 / 51, places=4) def test_gegenprobe_die_rechnung_kann_rot_werden(self): soll = {"a:1", "b:1"} ist = {"a:1", "b:1"} self.assertEqual(len(soll - ist), 0) ist_kaputt = {"a:1"} self.assertEqual(len(soll - ist_kaputt), 1) # merkt es if __name__ == "__main__": unittest.main(verbosity=2)