"""Render elk app-sjabloon naar echte compose-bestanden, om te laten valideren. `yaml.safe_load` in de testsuite zegt alleen dat het geldige YAML is. Of `docker compose` het ook accepteert is een andere vraag: een `depends_on` in de verkeerde vorm, een poortnotatie die net niet klopt, een `network_mode` naast `networks` — dat zijn geldige YAML én kapotte stacks. Die fout merk je anders pas als iemand de app installeert. Docker draait niet overal waar Python draait (in CI zitten ze in verschillende containers), dus dit script rendert alleen. Het valideren doet `.forgejo/workflows/*.yml` erna met `docker compose config`. python3 tools/render_voor_validatie.py [doelmap] Schrijft /__/docker-compose.yml en meldt hoeveel het er zijn. Sjablonen met schakelbare onderdelen leveren meer dan één variant op: de standaardkeuze, alles aan en alles uit dekken de conditionele blokken af die met alleen de standaardwaarden nooit gerenderd worden. """ from __future__ import annotations import os import shutil import sys from pathlib import Path WORTEL = Path(__file__).resolve().parent.parent sys.path.insert(0, str(WORTEL / "server-up")) os.environ.setdefault("SU_CONFIG", "/tmp/su-render-validatie.json") from core import boilerplates as bp # noqa: E402 def _waarden(app: Path, overrides: dict) -> dict: velden = bp.fields(app) uit = {f["name"]: f["default"] for f in velden if f.get("default") is not None} uit.setdefault("service_name", app.name.replace("-", "")) # Een verplicht veld zonder standaardwaarde zou leeg renderen; compose ziet # dan bijvoorbeeld een lege image-naam en klaagt over iets anders. for f in velden: if f.get("required") and not uit.get(f["name"]): uit[f["name"]] = "testwaarde12345678901234567890ab" uit.update(overrides) return uit def varianten(app: Path) -> list[tuple[str, dict]]: """Welke combinaties zijn de moeite van het valideren waard?""" schakelaars = [f["name"] for f in bp.fields(app) if f.get("is_group_toggle")] if not schakelaars: return [("standaard", {})] uit = {s: False for s in schakelaars} combinaties = [ ("standaard", {}), ("alles-aan", {s: True for s in schakelaars}), ] # Alles uit levert een bestand zonder services op; dat is geen geldige # compose, maar wél precies wat de gebruiker krijgt als hij alles uitzet. # Dat hoort de app af te vangen, niet docker compose — dus overslaan. if "enable_gluetun" in schakelaars: # De VPN verandert de netwerkopzet van elke downloadclient; dat is de # combinatie waar het echt mis kan gaan. combinaties.append(("vpn", {s: True for s in schakelaars})) combinaties.append(("vpn-minimaal", uit | {"enable_gluetun": True, "enable_qbittorrent": True})) return combinaties def main() -> int: doel = Path(sys.argv[1] if len(sys.argv) > 1 else ".compose-check").resolve() if doel.exists(): shutil.rmtree(doel) doel.mkdir(parents=True) apps = sorted(d for d in (WORTEL / "apps").iterdir() if d.is_dir() and (d / "template.json").is_file()) n = 0 for app in apps: for naam, overrides in varianten(app): uit = doel / f"{app.name}__{naam}" try: bp.render_to_dir(app, uit, _waarden(app, overrides)) except Exception as e: print(f"FOUT {app.name} ({naam}): {e}", file=sys.stderr) return 1 n += 1 print(f"{n} compose-bestanden gerenderd uit {len(apps)} sjablonen → {doel}") return 0 if __name__ == "__main__": raise SystemExit(main())