From 279c1e36e3d807a5195a9bc1d360d90f1fa8e270 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 31 Jul 2026 23:05:50 +0200 Subject: [PATCH] CI: compose-bestanden valideren met docker compose config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy --- .forgejo/workflows/build.yml | 34 +++++++++++ .forgejo/workflows/deploy-prod.yml | 34 +++++++++++ .forgejo/workflows/deploy.yml | 34 +++++++++++ .gitignore | 2 + tools/render_voor_validatie.py | 92 ++++++++++++++++++++++++++++++ 5 files changed, 196 insertions(+) create mode 100644 tools/render_voor_validatie.py diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index ec99593..cf653f2 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -69,6 +69,40 @@ jobs: sh -c "pip install -q -r server-up/requirements.txt pytest \ && python -m pytest tests -q -p no:cacheprovider" + - name: Compose-bestanden valideren + run: | + set -euo pipefail + # De testsuite controleert of de sjablonen geldige YAML opleveren. + # Of `docker compose` ze ook accepteert is een andere vraag: een + # depends_on in de verkeerde vorm of network_mode naast networks is + # prima YAML en een kapotte stack. Dat merk je anders pas als iemand + # de app installeert. + # + # Renderen kan alleen waar Python staat (in de container), valideren + # alleen waar Docker staat (op de runner). Vandaar twee helften. + docker run --rm \ + -v "$PWD:/w" -w /w \ + -v su-pip-cache:/root/.cache/pip \ + -e PYTHONDONTWRITEBYTECODE=1 \ + python:3.12-slim \ + sh -c "pip install -q -r server-up/requirements.txt \ + && python tools/render_voor_validatie.py .compose-check \ + && chown -R $(id -u):$(id -g) .compose-check" + fouten=0 + for f in .compose-check/*/docker-compose.yml; do + if ! uitvoer=$(docker compose -f "$f" config -q 2>&1); then + echo "✖ $(basename "$(dirname "$f")")" + echo "$uitvoer" | sed 's/^/ /' + fouten=$((fouten + 1)) + fi + done + rm -rf .compose-check + if [ "$fouten" -ne 0 ]; then + echo "$fouten sjablo(o)n(en) leveren een compose-bestand op dat docker weigert" + exit 1 + fi + echo "Alle compose-bestanden zijn geldig" + - name: Inloggen op de registry run: | set -euo pipefail diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml index cbdb591..e749177 100644 --- a/.forgejo/workflows/deploy-prod.yml +++ b/.forgejo/workflows/deploy-prod.yml @@ -57,6 +57,40 @@ jobs: sh -c "pip install -q -r server-up/requirements.txt pytest \ && python -m pytest tests -q -p no:cacheprovider" + - name: Compose-bestanden valideren + run: | + set -euo pipefail + # De testsuite controleert of de sjablonen geldige YAML opleveren. + # Of `docker compose` ze ook accepteert is een andere vraag: een + # depends_on in de verkeerde vorm of network_mode naast networks is + # prima YAML en een kapotte stack. Dat merk je anders pas als iemand + # de app installeert. + # + # Renderen kan alleen waar Python staat (in de container), valideren + # alleen waar Docker staat (op de runner). Vandaar twee helften. + docker run --rm \ + -v "$PWD:/w" -w /w \ + -v su-pip-cache:/root/.cache/pip \ + -e PYTHONDONTWRITEBYTECODE=1 \ + python:3.12-slim \ + sh -c "pip install -q -r server-up/requirements.txt \ + && python tools/render_voor_validatie.py .compose-check \ + && chown -R $(id -u):$(id -g) .compose-check" + fouten=0 + for f in .compose-check/*/docker-compose.yml; do + if ! uitvoer=$(docker compose -f "$f" config -q 2>&1); then + echo "✖ $(basename "$(dirname "$f")")" + echo "$uitvoer" | sed 's/^/ /' + fouten=$((fouten + 1)) + fi + done + rm -rf .compose-check + if [ "$fouten" -ne 0 ]; then + echo "$fouten sjablo(o)n(en) leveren een compose-bestand op dat docker weigert" + exit 1 + fi + echo "Alle compose-bestanden zijn geldig" + - name: Sync code naar deploy-directory run: | set -euo pipefail diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 68bf184..291bd21 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -57,6 +57,40 @@ jobs: sh -c "pip install -q -r server-up/requirements.txt pytest \ && python -m pytest tests -q -p no:cacheprovider" + - name: Compose-bestanden valideren + run: | + set -euo pipefail + # De testsuite controleert of de sjablonen geldige YAML opleveren. + # Of `docker compose` ze ook accepteert is een andere vraag: een + # depends_on in de verkeerde vorm of network_mode naast networks is + # prima YAML en een kapotte stack. Dat merk je anders pas als iemand + # de app installeert. + # + # Renderen kan alleen waar Python staat (in de container), valideren + # alleen waar Docker staat (op de runner). Vandaar twee helften. + docker run --rm \ + -v "$PWD:/w" -w /w \ + -v su-pip-cache:/root/.cache/pip \ + -e PYTHONDONTWRITEBYTECODE=1 \ + python:3.12-slim \ + sh -c "pip install -q -r server-up/requirements.txt \ + && python tools/render_voor_validatie.py .compose-check \ + && chown -R $(id -u):$(id -g) .compose-check" + fouten=0 + for f in .compose-check/*/docker-compose.yml; do + if ! uitvoer=$(docker compose -f "$f" config -q 2>&1); then + echo "✖ $(basename "$(dirname "$f")")" + echo "$uitvoer" | sed 's/^/ /' + fouten=$((fouten + 1)) + fi + done + rm -rf .compose-check + if [ "$fouten" -ne 0 ]; then + echo "$fouten sjablo(o)n(en) leveren een compose-bestand op dat docker weigert" + exit 1 + fi + echo "Alle compose-bestanden zijn geldig" + - name: Sync code naar deploy-directory run: | set -euo pipefail diff --git a/.gitignore b/.gitignore index 1501d07..ac7dd1a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__/ .pytest_cache/ venv/ node_modules/ +.compose-check/ +venv/ diff --git a/tools/render_voor_validatie.py b/tools/render_voor_validatie.py new file mode 100644 index 0000000..37d29bc --- /dev/null +++ b/tools/render_voor_validatie.py @@ -0,0 +1,92 @@ +"""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())