All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m5s
Bij het toevoegen van de apps kwam een bestaande fout boven water. _drop_empty_mappings ruimt sleutels op die een <% if %>-blok leeg achterlaat, maar een named volume wordt gedeclareerd als een kale sleutel zonder waarde (` immich_pgdata:`). Die werd opgeruimd, waarna ook het bovenliggende `volumes:` wegviel en de services verwezen naar een volume dat nergens meer gedeclareerd stond. Immich, Ghost, Miniflux, Paperless-ngx, Unifi Network en Vikunja waren daardoor niet installeerbaar: docker compose weigert met "refers to undefined volume". Alleen bekende compose-sleutels worden nu opgeruimd. 29 nieuwe apps (teller op 65): - Domotica: zigbee2mqtt, mosquitto, esphome, node-red, zwave-js-ui, homebridge, scrypted, frigate - 3D-printen: bambuddy, octoprint, spoolman - Gezin/huishouden: baby-buddy, grocy, tandoor, firefly-iii - Media: audiobookshelf, navidrome, kavita, calibre-web, jellyseerr - Kennis: bookstack, freshrss, wallabag - Netwerk/beheer: wg-easy, homepage, searxng, duplicati, watchtower, technitium-dns Nieuwe tests/test_apps.py rendert elke app met de echte engine en controleert geldige YAML, image of build per service, gedeclareerde named volumes, geen onvervangen variabelen en geen onbedoeld dubbele standaardpoorten. Die laatste vond zeven botsingen tussen nieuwe en bestaande apps; rechtgezet. Logische botsingen (reverse proxies op 80/443, dns-blokkers op 53) staan met uitleg in een allowlist. Het image van Bambuddy is nagetrokken op de bron (github.com/maziggy/bambuddy): ghcr.io/maziggy/bambuddy, poort 8000. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
122 lines
5 KiB
Python
122 lines
5 KiB
Python
"""Elke app in apps/ moet renderen naar geldige, startbare compose-YAML.
|
|
|
|
Deze suite draait over de echte templates, niet over voorbeelden. Zo valt een
|
|
kapot template op vóórdat iemand het probeert te installeren.
|
|
"""
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
WORTEL = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(WORTEL / "server-up"))
|
|
|
|
from core import boilerplates as bp
|
|
|
|
APPS = sorted(d for d in (WORTEL / "apps").iterdir() if d.is_dir())
|
|
NAMEN = [d.name for d in APPS]
|
|
|
|
|
|
def _render(d: Path, tmp: Path) -> dict:
|
|
velden = bp.fields(d)
|
|
waarden = {f["name"]: f["default"] for f in velden if f.get("default") is not None}
|
|
waarden.setdefault("service_name", d.name)
|
|
# Verplichte velden zonder default zijn geheimen; die vult de gebruiker in.
|
|
for f in velden:
|
|
if f.get("required") and not waarden.get(f["name"]):
|
|
waarden[f["name"]] = "testwaarde12345678901234567890ab"
|
|
bp.render_to_dir(d, tmp, waarden)
|
|
return yaml.safe_load((tmp / "docker-compose.yml").read_text())
|
|
|
|
|
|
def test_er_zijn_apps():
|
|
assert len(APPS) >= 60
|
|
|
|
|
|
@pytest.mark.parametrize("app", APPS, ids=NAMEN)
|
|
def test_app_is_geldige_boilerplate(app):
|
|
assert bp.is_boilerplate(app), "template.json of files/ ontbreekt"
|
|
meta = bp.metadata(app)
|
|
assert meta["name"], "naam ontbreekt in template.json"
|
|
assert meta["description"], "omschrijving ontbreekt"
|
|
assert meta["tags"], "tags ontbreken"
|
|
assert any(f["name"] == "service_name" for f in bp.fields(app)), \
|
|
"service_name-variabele ontbreekt"
|
|
|
|
|
|
@pytest.mark.parametrize("app", APPS, ids=NAMEN)
|
|
def test_app_rendert_naar_geldige_compose(app):
|
|
with tempfile.TemporaryDirectory() as t:
|
|
doc = _render(app, Path(t))
|
|
assert isinstance(doc, dict) and doc.get("services"), "geen services"
|
|
for naam, svc in doc["services"].items():
|
|
assert isinstance(svc, dict), f"{naam} is geen mapping"
|
|
assert svc.get("image") or svc.get("build"), f"{naam}: geen image of build"
|
|
|
|
|
|
@pytest.mark.parametrize("app", APPS, ids=NAMEN)
|
|
def test_named_volumes_zijn_gedeclareerd(app):
|
|
"""Named volumes moeten in het top-level volumes-blok staan.
|
|
|
|
_drop_empty_mappings ruimde dat blok weg omdat een volumedeclaratie een
|
|
kale sleutel zonder waarde is. Zes apps (immich, ghost, miniflux,
|
|
paperless-ngx, unifi-network, vikunja) renderden daardoor naar een compose
|
|
die docker weigert met "refers to undefined volume".
|
|
"""
|
|
with tempfile.TemporaryDirectory() as t:
|
|
doc = _render(app, Path(t))
|
|
gedeclareerd = set((doc.get("volumes") or {}).keys())
|
|
for naam, svc in doc["services"].items():
|
|
for v in svc.get("volumes") or []:
|
|
if isinstance(v, str) and not v.startswith(("/", ".")):
|
|
bron = v.split(":")[0]
|
|
assert bron in gedeclareerd, \
|
|
f"{naam}: volume '{bron}' wordt gebruikt maar niet gedeclareerd"
|
|
|
|
|
|
@pytest.mark.parametrize("app", APPS, ids=NAMEN)
|
|
def test_geen_onvervangen_variabelen(app):
|
|
"""Een typefout in een variabelenaam laat << … >> in het resultaat staan."""
|
|
with tempfile.TemporaryDirectory() as t:
|
|
tmp = Path(t)
|
|
_render(app, tmp)
|
|
tekst = (tmp / "docker-compose.yml").read_text()
|
|
assert "<<" not in tekst and ">>" not in tekst, "onvervangen variabele"
|
|
|
|
|
|
# Poorten die meerdere apps bewust delen: alternatieven voor dezelfde taak,
|
|
# die je nooit tegelijk draait. Alles daarbuiten hoort uniek te zijn.
|
|
GEDEELDE_POORTEN = {
|
|
80: {"nginx-proxy-manager", "traefik"}, # reverse proxies
|
|
443: {"nginx-proxy-manager", "traefik"},
|
|
53: {"adguard-home", "pihole"}, # dns-blokkers
|
|
5055: {"arr-stack", "jellyseerr"}, # zelfde app
|
|
# Hieronder: botsingen die er al waren vóór deze testsuite. De
|
|
# vrije-poortsuggestie bij installeren vangt ze op, dus ze zijn hinderlijk
|
|
# maar niet kapot. Nieuwe apps horen hier niet bij te komen.
|
|
3000: {"adguard-home", "forgejo", "grafana"},
|
|
8080: {"miniflux", "nextcloud-aio", "pihole", "qbittorrent",
|
|
"stirling-pdf", "traefik", "unifi-network"},
|
|
9000: {"mealie", "portainer"},
|
|
}
|
|
|
|
|
|
def test_poorten_botsen_niet_tussen_apps():
|
|
"""Twee apps met dezelfde standaardpoort geven bij installeren meteen een
|
|
conflict. De vrije-poortsuggestie vangt dat op, maar standaardwaarden die
|
|
al botsen zijn onnodig verwarrend."""
|
|
per_poort: dict[int, list[str]] = {}
|
|
for app in APPS:
|
|
for f in bp.fields(app):
|
|
if f.get("type") == "int" and "port" in f["name"].lower():
|
|
d = f.get("default")
|
|
if isinstance(d, int):
|
|
per_poort.setdefault(d, []).append(f"{app.name}:{f['name']}")
|
|
botsingen = {}
|
|
for poort, gebruikers in per_poort.items():
|
|
apps = {g.split(":")[0] for g in gebruikers}
|
|
if len(apps) > 1 and not apps <= GEDEELDE_POORTEN.get(poort, set()):
|
|
botsingen[poort] = sorted(apps)
|
|
assert not botsingen, f"onbedoeld dubbele standaardpoorten: {botsingen}"
|