cve: only scan registry repositories that are actually running (#0051)
Decision by sorb: narrow the target set to what is operated rather than tidying the registry. A repository now counts only while at least one of its tags is running, which drops element-desktop-build and windows-vm and 61 of their 62 critical findings with them. Of those 62, exactly two had a fix available; they are build artefacts nobody runs, so remediation was never the right answer. The narrowing stays derived rather than maintained: the running set is the one the derivation already builds, so there is no second list to keep in step (ADR-0026). Order now matters. The registry selection needs the running estate to tell a rollback target from a build artefact, so it runs after the other two sources and is skipped when they yield nothing. That last part changes behaviour deliberately. A derivation where both estate sources answer successfully but empty used to count as complete and merely unusable; it now reports the registry as failed. If nothing is running at all, that is an outage rather than a normal state, and it should say so instead of hanging on a single boolean. The test carries the new contract with that reasoning written next to it, plus a counter-case proving the registry does run when the estate is known.
This commit is contained in:
@@ -196,16 +196,31 @@ def tag_erstellt(
|
||||
return None
|
||||
|
||||
|
||||
def ziele_registry(registry: str, je_repo: int = 3, timeout: float = 15.0) -> set:
|
||||
def ziele_registry(registry: str, je_repo: int = 3, laufend: Optional[set] = None,
|
||||
timeout: float = 15.0) -> set:
|
||||
"""Die juengsten `je_repo` Fassungen jedes Repos.
|
||||
|
||||
Reichweite laut Gate 1 (Entscheidung sorb, Option C): Bestand plus das, was
|
||||
ein Rollback realistisch treffen kann. Ein Rollback-Ziel mit bekannter
|
||||
Luecke ist der einzige Fall, in dem ein nicht laufendes Image betrieblich
|
||||
zaehlt - bei v0.1.0 rollt niemand mehr zurueck.
|
||||
|
||||
⚠️ EINENGUNG (#0051, Entscheidung sorb 2026-08-21): Beruecksichtigt werden nur
|
||||
Repos, von denen gerade MINDESTENS EIN Tag laeuft. Ohne diese Bedingung
|
||||
kamen reine Bau-Artefakte in die Zielmenge - `element-desktop-build` und
|
||||
`windows-vm` trugen zusammen 61 CRITICAL, davon 1 behebbar. Befunde ueber
|
||||
Images, die niemand betreibt, sind Rauschen, das wie Signal aussieht.
|
||||
|
||||
Die Einengung ist ABGELEITET, nicht gepflegt: `laufend` ist dieselbe Menge,
|
||||
die die Herleitung ohnehin bildet. Keine zweite Liste (ADR-0026).
|
||||
"""
|
||||
ziele = set()
|
||||
for repo in registry_repos(registry, timeout):
|
||||
if laufend is not None and not any(
|
||||
z.startswith(f"{registry}/{repo}:") for z in laufend
|
||||
):
|
||||
# Kein Tag dieses Repos laeuft - Bau-Artefakt, kein Rollback-Ziel.
|
||||
continue
|
||||
token = registry_token(registry, f"repository:{repo}:pull", timeout)
|
||||
kandidaten = [
|
||||
t
|
||||
@@ -252,20 +267,22 @@ class Herleitung(NamedTuple):
|
||||
|
||||
|
||||
def herleiten(cfg: dict, vorheriger_stand: Optional[dict] = None) -> Herleitung:
|
||||
"""Fragt alle drei Quellen. Ein Ausfall kostet nur die eigene Quelle."""
|
||||
"""Fragt alle drei Quellen. Ein Ausfall kostet nur die eigene Quelle.
|
||||
|
||||
⚠️ REIHENFOLGE IST BEDEUTSAM: Die Registry-Auswahl braucht den laufenden
|
||||
Bestand, um Bau-Artefakte auszuschliessen (#0051). Faellt der Bestand aus,
|
||||
faellt die Registry-Auswahl mit - und das ist richtig so: Eine
|
||||
Registry-Auswahl ohne Bestandswissen koennte nicht unterscheiden, was ein
|
||||
Rollback-Ziel ist und was ein Bau-Artefakt.
|
||||
"""
|
||||
stand = dict(vorheriger_stand or {})
|
||||
je_quelle, fehler = {}, {}
|
||||
jetzt = time.time()
|
||||
|
||||
aufgaben = (
|
||||
("cluster", lambda: ziele_cluster(cfg["prometheus"], cfg["timeout"])),
|
||||
("operating", lambda: ziele_operating(cfg["prometheus"], cfg["timeout"])),
|
||||
("registry", lambda: ziele_registry(cfg["registry"], cfg["je_repo"], cfg["timeout"])),
|
||||
)
|
||||
for name, hole in aufgaben:
|
||||
def versuche(name, hole):
|
||||
if not cfg.get(f"{name}_an", True):
|
||||
je_quelle[name] = set()
|
||||
continue
|
||||
return
|
||||
try:
|
||||
je_quelle[name] = hole()
|
||||
stand[name] = jetzt
|
||||
@@ -273,6 +290,21 @@ def herleiten(cfg: dict, vorheriger_stand: Optional[dict] = None) -> Herleitung:
|
||||
je_quelle[name] = set()
|
||||
fehler[name] = f"{type(e).__name__}: {e}"
|
||||
|
||||
versuche("cluster", lambda: ziele_cluster(cfg["prometheus"], cfg["timeout"]))
|
||||
versuche("operating", lambda: ziele_operating(cfg["prometheus"], cfg["timeout"]))
|
||||
|
||||
laufend = je_quelle.get("cluster", set()) | je_quelle.get("operating", set())
|
||||
if laufend:
|
||||
versuche("registry", lambda: ziele_registry(
|
||||
cfg["registry"], cfg["je_repo"], laufend, cfg["timeout"]))
|
||||
else:
|
||||
# Ohne Bestandswissen keine Registry-Auswahl - lieber eine kleinere
|
||||
# Menge als eine, die Bau-Artefakte mitschleppt. Als Fehler gemeldet,
|
||||
# damit es nicht wie Erfolg aussieht.
|
||||
je_quelle["registry"] = set()
|
||||
if "registry" not in fehler:
|
||||
fehler["registry"] = "uebersprungen: kein laufender Bestand bekannt"
|
||||
|
||||
ziele = set().union(*je_quelle.values()) if je_quelle else set()
|
||||
return Herleitung(ziele=ziele, je_quelle=je_quelle, fehler=fehler, stand=stand)
|
||||
|
||||
|
||||
@@ -266,11 +266,29 @@ class Ausfallverhalten(unittest.TestCase):
|
||||
|
||||
def test_gegenprobe_leere_menge_ist_NICHT_dasselbe_wie_erfolg(self):
|
||||
# Der Fall, der ohne diese Unterscheidung wie 100 % Deckung aussaehe.
|
||||
#
|
||||
# ⚠️ VERHALTEN GEAENDERT am 2026-08-21 (#0051): Antworten Cluster UND
|
||||
# Betriebs-Host erfolgreich LEER, wird die Registry-Auswahl jetzt
|
||||
# uebersprungen und als Fehler gemeldet. Frueher galt das als
|
||||
# "vollstaendig, nur unbrauchbar". Der Grund fuer die Aenderung: Ohne
|
||||
# laufenden Bestand kann die Registry-Auswahl Rollback-Ziele nicht von
|
||||
# Bau-Artefakten unterscheiden - und laeuft wirklich nichts, ist das ein
|
||||
# Ausfall und kein Normalzustand. Er soll deshalb als Fehler sichtbar
|
||||
# sein, nicht nur an `brauchbar` haengen.
|
||||
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
|
||||
self.assertFalse(h.brauchbar) # nicht verwertbar ...
|
||||
self.assertFalse(h.vollstaendig) # ... UND es wird gesagt
|
||||
self.assertIn("registry", h.fehler)
|
||||
self.assertIn("kein laufender Bestand", h.fehler["registry"])
|
||||
|
||||
def test_registry_ohne_laufenden_bestand_wird_uebersprungen(self):
|
||||
# Gegenprobe zur Zeile darueber: Mit Bestand laeuft die Registry sehr wohl.
|
||||
self._stelle({"a:1"}, set(), {"reg/x:v1"})
|
||||
h = targets.herleiten(self.CFG)
|
||||
self.assertIn("reg/x:v1", h.ziele)
|
||||
self.assertNotIn("registry", h.fehler)
|
||||
|
||||
|
||||
class Deckungsrechnung(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user