"""Docker-Dienste unter Arcane einzeln (Ausbauplan Welle 2, Punkt 8, 25.09.2026). Die Container stammen aus Arcanes Antwort vom 25.09. (sieben Container in drei Stacks; NerdQuiz und Rippy baut der User selbst).""" import json import pytest from kern import einstellungen as einstellungen_mod from services.homelab import arcane, docker, einstellungen, entdecken JETZT = 1_790_340_000.0 def _c(name: str, stack: str, image: str, status: str = "Up 3 hours", state: str = "running", **info) -> dict: return {"id": f"{name}0123456789ab", "names": [f"/{name}"], "image": image, "state": state, "status": status, "labels": {"com.docker.compose.project": stack, "com.docker.compose.service": name.split("-")[1] if "-" in name else name}, "updateInfo": info} ARCANE = [ _c("arcane", "arcane", "ghcr.io/getarcaneapp/manager:latest", updateType="digest", currentVersion="latest"), _c("nerdquiz", "deploy", "nerdquiz:latest", "Up 3 hours (healthy)", currentVersion="1.18.2", error="distribution inspect of docker.io/library/nerdquiz:1.18.2 failed: Error response from daemon"), _c("rippy-api-1", "rippy", "rippy-api", "Up 3 hours (healthy)", updateType="local"), _c("rippy-postgres-1", "rippy", "postgres:16-alpine", "Up 3 hours (healthy)", updateType="digest", hasUpdate=True, currentVersion="16-alpine"), ] def test_container_zeile(): zeilen = {z["name"]: z for z in map(docker.container_zeile, ARCANE)} assert zeilen["arcane"]["update"] == "aktuell" and zeilen["arcane"]["gesund"] is None assert zeilen["nerdquiz"]["update"] == "lokal" and zeilen["nerdquiz"]["gesund"] is True # kein Fehler, lokal gebaut assert zeilen["rippy-api-1"]["update"] == "lokal" and zeilen["rippy-api-1"]["stack"] == "rippy" assert zeilen["rippy-postgres-1"]["update"] == "neu" and zeilen["rippy-postgres-1"]["dienst"] == "postgres" krank = docker.container_zeile(_c("x", "s", "i", "Up 2 minutes (unhealthy)", error="timeout")) assert (krank["gesund"], krank["update"]) == (False, "unbekannt") def test_lage_je_stack(monkeypatch): monkeypatch.setattr(einstellungen, "arcane_url", lambda: None) assert docker.lage() == {"verfuegbar": False, "grund": "Arcane steht nicht im Bericht des Ausführers.", "stacks": []} monkeypatch.setattr(einstellungen, "arcane_url", lambda: "http://192.168.178.28:3552") monkeypatch.setattr(arcane, "schluessel", lambda: "") assert docker.lage()["grund"].startswith("Es fehlt der Arcane-API-Schlüssel") monkeypatch.setattr(arcane, "schluessel", lambda: "schluessel") monkeypatch.setattr(arcane, "container_liste", lambda url: None) assert docker.lage()["grund"] == "Arcane antwortet gerade nicht." monkeypatch.setattr(arcane, "container_liste", lambda url: ARCANE) lage = docker.lage() assert [(s["name"], [c["name"] for c in s["container"]]) for s in lage["stacks"]] == [ ("arcane", ["arcane"]), ("deploy", ["nerdquiz"]), ("rippy", ["rippy-api-1", "rippy-postgres-1"])] def test_waechter_meldet_ungesunde_und_neustartende(monkeypatch): monkeypatch.setattr(einstellungen, "arcane_url", lambda: "http://arcane") monkeypatch.setattr(arcane, "schluessel", lambda: "schluessel") monkeypatch.setattr(docker, "arcane_ziel", lambda: "vm-106") krank = [*ARCANE, _c("rippy-worker-1", "rippy", "rippy-worker", "Up 1 minute (unhealthy)"), _c("einmal", "jobs", "busybox", "Exited (0) 2 hours ago", state="exited"), _c("hakt", "jobs", "busybox", "Restarting (1) 5 seconds ago", state="restarting")] monkeypatch.setattr(arcane, "container_liste", lambda url: krank) befunde = {b.id: b for b in docker.pruefe_docker()} assert set(befunde) == {"docker:rippy-worker-1", "docker:hakt"} # beendet ist kein Befund assert befunde["docker:rippy-worker-1"].titel == "Docker-Dienst rippy-worker-1 ist ungesund" assert befunde["docker:hakt"].quelle == "vm-106" and befunde["docker:hakt"].stufe == "gelb" @pytest.fixture def daten(tmp_path, monkeypatch): monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path)) einstellungen_mod.einstellungen.cache_clear() yield tmp_path einstellungen_mod.einstellungen.cache_clear() def test_neue_stacks(daten, monkeypatch): meldungen: list[str] = [] stacks = [{"name": "arcane", "container": [{"name": "arcane"}]}, {"name": "rippy", "container": [{"name": "rippy-api-1"}]}] assert entdecken.docker_gesehen(stacks, JETZT, meldungen.append) == [] # erster Abgleich: alles bekannt neu = [*stacks, {"name": "immich", "container": [{"name": "immich-server"}, {"name": "immich-ml"}]}] assert entdecken.docker_gesehen(neu, JETZT + 600, meldungen.append) == ["docker:immich"] assert meldungen == [("Neu unter Arcane: der Docker-Stack „immich“ (immich-server, immich-ml). Im Homelab-Cockpit " "unter „Neu entdeckt“ übernehmen.")] monkeypatch.setattr(docker, "lage", lambda: {"verfuegbar": True, "grund": None, "stacks": [ {"name": s["name"], "container": [{"name": c["name"]} for c in s["container"]]} for s in neu]}) (stack,) = entdecken.neue_docker() assert (stack["id"], stack["container"]) == ("docker:immich", ["immich-server", "immich-ml"]) assert entdecken.uebernehmen("docker:immich", "freigeben")["ok"] is False # Etiketten gibt es dort nicht assert entdecken.uebernehmen("docker:immich", "beobachten") == {"ok": True, "text": "Der Stack immich ist übernommen."} assert entdecken.neue_docker() == [] register = json.loads((daten / "docker-bekannt.json").read_text(encoding="utf-8")) assert register["docker:immich"]["zustand"] == "bekannt"