server-up/tools/render_voor_validatie.py
Ramon 279c1e36e3
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 2m32s
CI: compose-bestanden valideren met docker compose config
De testsuite controleert alleen of een sjabloon geldige YAML oplevert. Een
depends_on in de verkeerde vorm of network_mode naast networks is prima YAML en
een kapotte stack — dat merkte je pas als iemand de app installeerde.

- tools/render_voor_validatie.py rendert elk sjabloon naar echte
  compose-bestanden; sjablonen met schakelbare onderdelen leveren meerdere
  varianten op (standaard, alles aan, met en zonder VPN), zodat ook de
  conditionele blokken langskomen die met alleen de standaardwaarden nooit
  gerenderd worden
- Nieuwe stap in build.yml, deploy.yml en deploy-prod.yml die er
  `docker compose config` overheen haalt. Renderen gebeurt in de
  python-container, valideren op de runner zelf: Python en Docker staan in CI
  niet op dezelfde plek

145 bestanden uit 142 sjablonen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
2026-07-31 23:05:50 +02:00

92 lines
3.7 KiB
Python

"""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 <doelmap>/<app>__<variant>/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())