CI: compose-bestanden valideren met docker compose config
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 2m32s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 2m32s
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
This commit is contained in:
parent
58d9be1f48
commit
279c1e36e3
5 changed files with 196 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,3 +5,5 @@ __pycache__/
|
|||
.pytest_cache/
|
||||
venv/
|
||||
node_modules/
|
||||
.compose-check/
|
||||
venv/
|
||||
|
|
|
|||
92
tools/render_voor_validatie.py
Normal file
92
tools/render_voor_validatie.py
Normal file
|
|
@ -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 <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())
|
||||
Loading…
Reference in a new issue