Welle 2 · Docker-Dienste unter Arcane einzeln: Stacks, Zustand, Gesundheit, Update-Stand; neue Stacks unter „Neu entdeckt“
Statt „Arcane (Docker) · 11 Images“ zeigt das Homelab-Cockpit je Stack jeden Container mit Zustand, Healthcheck und Update-Stand („Update verfügbar“, „lokal gebaut“ für NerdQuiz und Rippy, „aktuell“). Der Wächter meldet gelb, wenn ein Container ungesund ist oder ständig neu startet; beendete Container sind kein Befund. Neue Stacks erscheinen unter „Neu entdeckt“ (eigenes Register, nur „Übernehmen“), Lucy meldet sie einmal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c7c396f56d
commit
6af42bd42c
@@ -0,0 +1,92 @@
|
||||
"""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"
|
||||
@@ -221,7 +221,7 @@ print(json.dumps({"pruefungen": [p.__name__ for p in waechter.PRUEFUNGEN], "date
|
||||
""")
|
||||
assert ergebnis["pruefungen"] == ["pruefe_platte", "pruefe_partner", "pruefe_ausfuehrer", "pruefe_gaeste",
|
||||
"pruefe_gast_platten", "pruefe_paketlisten", "pruefe_host", "pruefe_speicher",
|
||||
"pruefe_zertifikate"]
|
||||
"pruefe_zertifikate", "pruefe_docker"]
|
||||
|
||||
|
||||
def test_homelab_rolle_hat_die_homelab_schnittstellen(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user