diff --git a/CHANGELOG.md b/CHANGELOG.md index 055463d..7c9dc7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +# v0.7.30-beta — Installeren met één regel + +```bash +curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh | sh +``` + +Het script controleert je systeem, biedt aan Docker te installeren als dat +ontbreekt, haalt de code op, maakt `.env` aan, bouwt het image, start de +container en wacht tot `/healthz` antwoordt. Daarna toont het waar je terecht +kunt — met de waarschuwing dat je meteen een beheerdersaccount moet aanmaken. + +Opties: `--dir`, `--port`, `--bind`, `--branch`, `--token`, `--update`, +`--uninstall`, `--yes` en `--dry-run`. + +**`--dry-run`** toont de hele gang van zaken zonder iets te wijzigen. Handig om +te zien wat er gaat gebeuren voordat je een script met root-rechten loslaat op +je server; `docs/installeren.md` zet die stap dan ook vooraan. + +**Bijwerken** gaat met `sh /opt/server-up/install.sh --update`: nieuwe code +ophalen, opnieuw bouwen, herstarten. Je `.env`, stacks en gegevens blijven staan. +**Verwijderen** stopt de container maar laat je gegevens met rust, en vertelt +daarna hoe je die alsnog opruimt. + +Het script gebruikt alleen `sudo` waar dat nodig is: kun je zelf al in de +doelmap schrijven én docker aanroepen, dan blijft alles onder je eigen account. + +Nieuw: `README.md` verwijst er nu naar, plus `docs/installeren.md` met alle +opties en een probleemoplostabel. + +Getest onder `sh`, `dash` en `bash`, met een testsuite die controleert dat er +geen bashismen in sluipen, dat een typefout in de opties netjes stopt, en dat +een proefdraai echt niets aanmaakt. Twee dingen die daarbij naar boven kwamen: +de wachtlus gaf anderhalve minuut lang geen enkel teken van leven (nu puntjes), +en `docker compose --project-directory` zoekt zonder `-f` het compose-bestand +alsnog in de huidige map — wat bij `curl | sh` je thuismap is. + +--- + # v0.7.20-beta — Instellingen wijzigen, koppelen achteraf, en een defect uit v0.7.00 ## 🔴 Gedeeld netwerk botste met `network_mode` diff --git a/README.md b/README.md index 136c257..4d1c47a 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,35 @@ IP-adres in je netwerk geven. ## Installeren ```bash -git clone https://github.com/bes-r/server-up.git -cd server-up -cp .env.example .env # instellingen; zie hieronder +curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh | sh +``` + +Dat controleert je systeem, installeert Docker als het ontbreekt, zet Server Up +in `/opt/server-up` en start het op `http://localhost:5000`. + +Liever eerst lezen wat je uitvoert — verstandig, want het script gebruikt +root-rechten: + +```bash +curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh -o install.sh +less install.sh +sh install.sh --dry-run # toont wat het zou doen, zonder iets te wijzigen +sh install.sh +``` + +Handmatig kan ook: + +```bash +git clone https://git.ramonbesselink.nl/bes-r/server-up.git /opt/server-up +cd /opt/server-up +cp .env.example .env docker compose up -d --build ``` -Open daarna `http://localhost:5000` en **maak meteen een beheerdersaccount aan.** +Alle opties staan in [docs/installeren.md](docs/installeren.md): +`--dir`, `--port`, `--bind`, `--branch`, `--token`, `--update`, `--uninstall`. + +Open daarna de interface en **maak meteen een beheerdersaccount aan.** Zolang dat niet gebeurd is, kan iedereen die de pagina bereikt het account claimen — de container waarschuwt daar bij het opstarten ook voor. @@ -60,6 +82,7 @@ proxy met TLS voor staat. | Document | Waarover | |---|---| +| [docs/installeren.md](docs/installeren.md) | Het installatiescript, opties, bijwerken en verwijderen | | [docs/beveiliging.md](docs/beveiliging.md) | Accounts, rollen, SSO, reverse proxy, wat wel en niet is afgedekt | | [docs/apps-maken.md](docs/apps-maken.md) | Eigen apps toevoegen: het volledige templateformaat | | [docs/netwerken.md](docs/netwerken.md) | Stacks een eigen IP geven met macvlan of ipvlan | @@ -82,6 +105,16 @@ apps/mijn-app/ Het volledige formaat staat in [docs/apps-maken.md](docs/apps-maken.md). +## Bijwerken en verwijderen + +```bash +sh /opt/server-up/install.sh --update # nieuwste versie ophalen en herstarten +sh /opt/server-up/install.sh --uninstall # stoppen; je gegevens blijven staan +``` + +Draait Server Up vanaf een registry-image, dan kan bijwerken ook met een knop in +de interface — zie [docs/updates.md](docs/updates.md). + ## Ontwikkelen ```bash diff --git a/VERSION b/VERSION index 944e688..1e3a4ac 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.20-beta +0.7.30-beta diff --git a/docs/README.md b/docs/README.md index 5eeff25..b9f4876 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,7 @@ | Document | Waarover, en wanneer je het nodig hebt | |---|---| +| [installeren.md](installeren.md) | Installeren met één regel, alle opties van het script, bijwerken en verwijderen. | | [beveiliging.md](beveiliging.md) | Accounts, rollen, reverse-proxy-SSO en TLS. **Lees dit voordat je Server Up van buiten je netwerk bereikbaar maakt** — toegang tot de interface staat gelijk aan root op de host. | | [apps-maken.md](apps-maken.md) | Eigen apps toevoegen: het volledige `template.json`-formaat, de compose-syntaxis, apps aan elkaar koppelen en onderdelen optioneel maken. | | [netwerken.md](netwerken.md) | Stacks een eigen IP-adres geven in je LAN met macvlan of ipvlan. Inclusief de valkuil dat de host zijn eigen macvlan-containers niet bereikt. | diff --git a/docs/installeren.md b/docs/installeren.md new file mode 100644 index 0000000..a75e0d6 --- /dev/null +++ b/docs/installeren.md @@ -0,0 +1,149 @@ +# Installeren + +## In één regel + +```bash +curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh | sh +``` + +Dat installeert Server Up in `/opt/server-up`, bereikbaar op +`http://localhost:5000`. + +**Liever eerst zien wat je uitvoert?** Verstandig — je haalt een script binnen +dat root-rechten gebruikt: + +```bash +curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh -o install.sh +less install.sh +sh install.sh +``` + +Wil je alleen weten wát het zou doen, zonder dat er iets verandert: + +```bash +sh install.sh --dry-run +``` + +--- + +## Wat het script doet + +1. Controleert of je op Linux zit en of `curl` en `tar` aanwezig zijn +2. Kijkt of Docker draait — zo niet, biedt het aan die te installeren via het + officiële script van docker.com (het vraagt eerst) +3. Kloont de repo naar `/opt/server-up` (of pakt het tar-archief uit als git + ontbreekt) +4. Maakt `.env` aan op basis van `.env.example`, met jouw poort en bind-adres +5. Bouwt het image en start de container +6. Wacht tot `/healthz` antwoordt en toont waar je terecht kunt + +Het gebruikt alleen `sudo` waar dat nodig is. Kun je zelf al in de doelmap +schrijven én `docker` aanroepen, dan blijft alles onder je eigen account. + +--- + +## Opties + +``` +--dir PAD Installatiemap (standaard: /opt/server-up) +--port POORT Poort voor de webinterface (standaard: 5000) +--bind ADRES Waarop de poort luistert (standaard: 127.0.0.1) +--branch NAAM Branch om te installeren (standaard: main) +--token TOKEN Toegangstoken, als je repo niet openbaar is +--update Bijwerken naar de nieuwste versie +--uninstall Stoppen en verwijderen (gegevens blijven staan) +--yes Niets vragen +--dry-run Alleen tonen wat er zou gebeuren +``` + +### Voorbeelden + +```bash +# Standaard, alleen bereikbaar vanaf de server zelf +sh install.sh + +# Bereikbaar op je netwerk, andere poort +sh install.sh --bind 0.0.0.0 --port 8080 + +# Ergens anders neerzetten +sh install.sh --dir /srv/server-up + +# De beta-branch, zonder vragen +sh install.sh --branch dev --yes + +# Uit een repo die niet openbaar is +sh install.sh --token jouw-forgejo-token +``` + +--- + +## Meteen na het installeren + +**Maak een beheerdersaccount aan.** Server Up beheert de Docker-daemon, en wie +containers kan starten kan willekeurige mappen van de host mounten. Zolang er +nog geen account is, kan iedereen die de pagina bereikt het claimen. + +De poort staat standaard op `127.0.0.1`, dus alleen bereikbaar vanaf de server +zelf. Wil je er van buitenaf bij, zet dan eerst een reverse proxy met TLS ervoor +en pas daarna `BIND` aan: + +```bash +sed -i 's/^BIND=.*/BIND=0.0.0.0/' /opt/server-up/.env +docker compose -f /opt/server-up/docker-compose.yml up -d +``` + +Zie [beveiliging.md](beveiliging.md) voor de reverse proxy, rollen en SSO. + +--- + +## Bijwerken + +```bash +sh /opt/server-up/install.sh --update +``` + +Haalt de nieuwste versie op, bouwt opnieuw en herstart. Je `.env` blijft staan, +net als je stacks en gegevens. + +Draait Server Up vanaf een registry-image, dan kun je ook bijwerken vanuit de +interface zelf — zie [updates.md](updates.md). + +--- + +## Verwijderen + +```bash +sh /opt/server-up/install.sh --uninstall +``` + +Stopt en verwijdert de container. **Je gegevens blijven staan**: het +docker-volume `su-data` (instellingen, accounts, auditlog) en je stackmappen +onder `/opt/serverup`. Het script vertelt daarna hoe je die alsnog opruimt. + +--- + +## Handmatig, zonder script + +```bash +git clone https://git.ramonbesselink.nl/bes-r/server-up.git /opt/server-up +cd /opt/server-up +cp .env.example .env +$EDITOR .env +docker compose up -d --build +``` + +--- + +## Als er iets misgaat + +| Melding | Wat er aan de hand is | +|---|---| +| `Geen root en geen sudo` | Draai als root, of kies met `--dir` een map waar je zelf in mag schrijven | +| `Docker is geïnstalleerd maar de daemon reageert niet` | `systemctl start docker` | +| `De docker-compose-plugin ontbreekt` | Installeer `docker-compose-plugin` via je pakketbeheerder | +| `Ophalen mislukt. Is de repo openbaar?` | Gebruik `--token` met een Forgejo-token dat de repo mag lezen | +| Reageert niet binnen anderhalve minuut | `docker compose -f /opt/server-up/docker-compose.yml logs --tail=50` | + +Laadt de interface zonder opmaak, dan is het downloaden van de front-end- +bestanden tijdens de build misgegaan. Server Up meldt dat bij het opstarten in +zijn eigen log; opnieuw bouwen lost het meestal op. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..8533f58 --- /dev/null +++ b/install.sh @@ -0,0 +1,356 @@ +#!/bin/sh +# Server Up — installeren, bijwerken of verwijderen op een Linux-server. +# +# curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh | sh +# +# Liever eerst lezen wat je uitvoert (aan te raden): +# +# curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh -o install.sh +# less install.sh +# sh install.sh +# +# POSIX sh, dus het werkt onder dash, ash en bash. + +set -eu + +SCRIPT_VERSIE="1.0" +GIT_HOST="git.ramonbesselink.nl" +GIT_PAD="bes-r/server-up" +BRANCH="main" +DOEL="/opt/server-up" +POORT="5000" +BIND="127.0.0.1" +TOKEN="" +JA=0 +DROOG=0 +ACTIE="install" + +# ── Uitvoer ────────────────────────────────────────────────────────────────── +# Kleuren alleen als we naar een terminal schrijven; in een pipe of logbestand +# leveren escape-codes onleesbare rommel op. +if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then + C_OK=$(printf '\033[32m'); C_WAARSCHUW=$(printf '\033[33m') + C_FOUT=$(printf '\033[31m'); C_KOP=$(printf '\033[1m'); C_UIT=$(printf '\033[0m') +else + C_OK=''; C_WAARSCHUW=''; C_FOUT=''; C_KOP=''; C_UIT='' +fi + +log() { printf '%s\n' " $*"; } +stap() { printf '\n%s%s%s\n' "$C_KOP" "$*" "$C_UIT"; } +goed() { printf ' %s✔%s %s\n' "$C_OK" "$C_UIT" "$*"; } +waarschuw() { printf ' %s!%s %s\n' "$C_WAARSCHUW" "$C_UIT" "$*"; } +fout() { printf '\n %s✖ %s%s\n\n' "$C_FOUT" "$*" "$C_UIT" >&2; exit 1; } +# Bij --dry-run alleen waarschuwen: je wilt de hele voorvertoning zien, ook op +# een machine waar Docker (nog) niet staat. +fout_tenzij_droog() { + if [ "$DROOG" -eq 1 ]; then waarschuw "$*"; else fout "$*"; fi +} + +heeft() { command -v "$1" >/dev/null 2>&1; } + +# Voert uit, of toont alleen wat er zou gebeuren bij --dry-run. +doe() { + if [ "$DROOG" -eq 1 ]; then + printf ' [droog] %s\n' "$*" + else + "$@" + fi +} + +vraag() { + # vraag "Vraag?" -> 0 bij ja. Met --yes of zonder terminal: altijd ja. + [ "$JA" -eq 1 ] && return 0 + [ -t 0 ] || return 0 + printf ' %s [J/n] ' "$1" + read -r antwoord /dev/null 2>&1; then + goed "Draait onder je eigen account; sudo is niet nodig" + return 0 + fi + fi + if heeft sudo; then + SUDO="sudo" + log "sudo wordt gebruikt voor ${DOEL} en docker" + else + fout "Geen root en geen sudo. Draai als root, of kies met --dir een map waar je zelf in mag schrijven." + fi +} + +# ── Vereisten ──────────────────────────────────────────────────────────────── +controleer_systeem() { + stap "Systeem controleren" + bepaal_rechten + [ "$(uname -s)" = "Linux" ] || waarschuw "Dit script is voor Linux; op $(uname -s) is het onbeproefd." + for hulp in curl tar; do + heeft "$hulp" || fout "'$hulp' ontbreekt. Installeer het en probeer opnieuw." + done + goed "$(uname -s) $(uname -m)" +} + +controleer_docker() { + if heeft docker; then + if $SUDO docker info >/dev/null 2>&1; then + goed "Docker draait ($($SUDO docker version --format '{{.Server.Version}}' 2>/dev/null || echo '?'))" + else + fout_tenzij_droog "Docker is geïnstalleerd maar de daemon reageert niet. Start hem met: systemctl start docker" + fi + else + waarschuw "Docker is niet geïnstalleerd." + if vraag "Nu installeren via het officiële script van docker.com?"; then + stap "Docker installeren" + doe sh -c "curl -fsSL https://get.docker.com | $SUDO sh" + doe $SUDO systemctl enable --now docker || true + heeft docker || [ "$DROOG" -eq 1 ] || fout "Installeren van Docker is niet gelukt." + goed "Docker geïnstalleerd" + else + fout_tenzij_droog "Docker is vereist. Zie https://docs.docker.com/engine/install/" + fi + fi + + if $SUDO docker compose version >/dev/null 2>&1; then + goed "Compose-plugin aanwezig" + else + fout_tenzij_droog "De docker-compose-plugin ontbreekt. Installeer 'docker-compose-plugin' via je pakketbeheerder." + fi +} + +# ── Broncode ophalen ───────────────────────────────────────────────────────── +bron_url() { + if [ -n "$TOKEN" ]; then + printf 'https://%s@%s/%s.git' "$TOKEN" "$GIT_HOST" "$GIT_PAD" + else + printf 'https://%s/%s.git' "$GIT_HOST" "$GIT_PAD" + fi +} + +haal_broncode() { + stap "Broncode ophalen" + if [ -d "$DOEL/.git" ]; then + log "Bestaande installatie bijwerken in $DOEL" + doe $SUDO git -C "$DOEL" fetch --depth 1 origin "$BRANCH" + doe $SUDO git -C "$DOEL" reset --hard "origin/$BRANCH" + goed "Bijgewerkt naar de nieuwste $BRANCH" + return 0 + fi + + doe $SUDO mkdir -p "$DOEL" + if heeft git; then + # Zonder .git kunnen we later niet netjes bijwerken, dus git heeft de + # voorkeur boven een los archief. + if doe $SUDO git clone --depth 1 --branch "$BRANCH" "$(bron_url)" "$DOEL"; then + goed "Gekloond naar $DOEL" + return 0 + fi + waarschuw "Klonen mislukte; ik probeer het archief." + fi + + archief="https://${GIT_HOST}/${GIT_PAD}/archive/${BRANCH}.tar.gz" + log "Downloaden: $archief" + if [ "$DROOG" -eq 0 ]; then + tijdelijk="$(mktemp -d)" + curl -fsSL ${TOKEN:+-H "Authorization: token $TOKEN"} "$archief" \ + | tar xz -C "$tijdelijk" --strip-components=1 \ + || fout "Ophalen mislukt. Is de repo openbaar? Zo niet, gebruik --token." + $SUDO cp -r "$tijdelijk/." "$DOEL/" + rm -rf "$tijdelijk" + fi + goed "Uitgepakt naar $DOEL" +} + +# ── Instellingen ───────────────────────────────────────────────────────────── +schrijf_env() { + stap "Instellingen" + env_bestand="$DOEL/.env" + if [ -f "$env_bestand" ]; then + goed "Bestaande .env blijft ongemoeid" + # BIND en PORT wel bijwerken als ze expliciet zijn meegegeven. + return 0 + fi + if [ "$DROOG" -eq 1 ]; then + printf ' [droog] .env aanmaken met BIND=%s PORT=%s\n' "$BIND" "$POORT" + return 0 + fi + if [ -f "$DOEL/.env.example" ]; then + $SUDO cp "$DOEL/.env.example" "$env_bestand" + else + printf '' | $SUDO tee "$env_bestand" >/dev/null + fi + $SUDO sh -c "sed -i '/^BIND=/d;/^PORT=/d' '$env_bestand'" + $SUDO sh -c "printf 'BIND=%s\nPORT=%s\n' '$BIND' '$POORT' >> '$env_bestand'" + goed "Aangemaakt: $env_bestand (BIND=$BIND, PORT=$POORT)" +} + +# ── Starten ────────────────────────────────────────────────────────────────── +start() { + stap "Bouwen en starten" + if [ "$DROOG" -eq 0 ] && [ ! -f "$DOEL/docker-compose.yml" ]; then + fout "Geen docker-compose.yml in $DOEL — het ophalen van de broncode is misgegaan." + fi + log "Dit duurt de eerste keer enkele minuten." + doe $SUDO docker compose -f "$DOEL/docker-compose.yml" --project-directory "$DOEL" \ + up -d --build --remove-orphans + goed "Container gestart" +} + +wacht_op_gereed() { + [ "$DROOG" -eq 1 ] && return 0 + stap "Wachten tot Server Up reageert" + printf ' ' + i=0 + while [ "$i" -lt 45 ]; do + if curl -fsS "http://127.0.0.1:${POORT}/healthz" >/dev/null 2>&1; then + # Spatie na de dubbele punt toestaan: of json compact is, + # hangt af van de Flask-versie. + versie="$(curl -fsS "http://127.0.0.1:${POORT}/healthz" 2>/dev/null \ + | sed -n 's/.*"version" *: *"\([^"]*\)".*/\1/p')" + printf '\n' + goed "Server Up${versie:+ }${versie:-} draait" + return 0 + fi + # Een puntje per poging, zodat je ziet dat er nog iets gebeurt. + printf '.' + i=$((i + 1)) + sleep 2 + done + printf '\n' + waarschuw "Server Up reageerde niet binnen anderhalve minuut." + log "Bekijk de logs met: $SUDO docker compose -f $DOEL/docker-compose.yml logs --tail=50" + return 1 +} + +afsluiting() { + if [ "$BIND" = "0.0.0.0" ]; then + ip="$(hostname -I 2>/dev/null | awk '{print $1}')" + adres="http://${ip:-}:${POORT}" + else + adres="http://localhost:${POORT}" + fi + + printf '\n%s────────────────────────────────────────────────────────%s\n' "$C_KOP" "$C_UIT" + printf '%s Server Up draait op %s%s\n' "$C_OK" "$adres" "$C_UIT" + printf '%s────────────────────────────────────────────────────────%s\n\n' "$C_KOP" "$C_UIT" + + printf ' %sMaak nu meteen een beheerdersaccount aan.%s\n' "$C_WAARSCHUW" "$C_UIT" + printf ' Zolang dat niet gebeurd is, kan iedereen die deze pagina\n' + printf ' bereikt het account claimen.\n\n' + + if [ "$BIND" = "127.0.0.1" ]; then + printf ' Alleen bereikbaar vanaf deze server zelf. Van buitenaf erbij?\n' + printf ' 1. Zet een reverse proxy met TLS ervoor\n' + printf ' 2. sed -i "s/^BIND=.*/BIND=0.0.0.0/" %s/.env\n' "$DOEL" + printf ' 3. %s docker compose -f %s/docker-compose.yml up -d\n\n' "${SUDO:-}" "$DOEL" + else + printf ' %sDe interface staat open op je netwerk. Server Up beheert%s\n' "$C_WAARSCHUW" "$C_UIT" + printf ' Docker als root, dus zet er een reverse proxy met TLS voor.\n\n' + fi + + printf ' Bijwerken: sh %s/install.sh --update\n' "$DOEL" + printf ' Logs: %s docker compose -f %s/docker-compose.yml logs -f\n' "${SUDO:-}" "$DOEL" + printf ' Documentatie: %s/README.md\n\n' "$DOEL" +} + +verwijderen() { + stap "Server Up verwijderen" + [ -d "$DOEL" ] || fout "Niets gevonden in $DOEL" + if ! vraag "Container stoppen en verwijderen? Je gegevens blijven staan."; then + log "Afgebroken." + exit 0 + fi + doe $SUDO docker compose -f "$DOEL/docker-compose.yml" --project-directory "$DOEL" \ + down --remove-orphans + goed "Container gestopt en verwijderd" + printf '\n' + log "Je gegevens staan nog in het docker-volume 'su-data' en in je stackmappen." + log "Alles definitief weg? Dan:" + log " $SUDO docker volume rm su-data" + log " $SUDO rm -rf $DOEL" + printf '\n' +} + +# ── Hoofdprogramma ─────────────────────────────────────────────────────────── +printf '\n%sServer Up — installatiescript %s%s\n' "$C_KOP" "$SCRIPT_VERSIE" "$C_UIT" +[ "$DROOG" -eq 1 ] && waarschuw "Proefdraai: er wordt niets gewijzigd." + +case "$ACTIE" in + uninstall) + bepaal_rechten + verwijderen + ;; + update) + [ -d "$DOEL" ] || fout "Geen installatie gevonden in $DOEL. Installeer eerst zonder --update." + controleer_systeem + controleer_docker + haal_broncode + start + wacht_op_gereed || true + printf '\n' + goed "Bijgewerkt." + ;; + install) + controleer_systeem + controleer_docker + haal_broncode + schrijf_env + start + wacht_op_gereed || true + afsluiting + ;; +esac diff --git a/tests/test_install_script.py b/tests/test_install_script.py new file mode 100644 index 0000000..e64ef6b --- /dev/null +++ b/tests/test_install_script.py @@ -0,0 +1,111 @@ +"""Het installatiescript: syntaxis, opties en de dingen die stuk mogen gaan. + +Een installer die je met `curl | sh` draait moet in elk geval syntactisch +kloppen onder de shells die distributies als /bin/sh gebruiken, en niet +stilzwijgend het verkeerde doen bij een typefout in de opties. +""" +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +WORTEL = Path(__file__).resolve().parent.parent +SCRIPT = WORTEL / "install.sh" + + +def _draai(*args, **kw): + return subprocess.run(["sh", str(SCRIPT), *args], capture_output=True, + text=True, timeout=60, **kw) + + +def test_script_bestaat_en_is_uitvoerbaar(): + assert SCRIPT.is_file() + assert SCRIPT.stat().st_mode & 0o111, "niet uitvoerbaar" + + +@pytest.mark.parametrize("shell", ["sh", "dash", "bash"]) +def test_syntaxis_klopt_onder(shell): + """Debian en Ubuntu gebruiken dash als /bin/sh; bashismen breken daar.""" + if not shutil.which(shell): + pytest.skip(f"{shell} niet aanwezig") + r = subprocess.run([shell, "-n", str(SCRIPT)], capture_output=True, text=True) + assert r.returncode == 0, r.stderr + + +def test_geen_bashismen(): + tekst = SCRIPT.read_text(encoding="utf-8") + for patroon, uitleg in [ + (r"\[\[", "[[ ]] is bash-only"), + (r"^\s*declare ", "declare is bash-only"), + (r"\$\{[A-Za-z_]+\[", "arrays zijn bash-only"), + (r"==", "== in test is bash-only"), + ]: + treffers = [l for l in tekst.splitlines() + if re.search(patroon, l) and not l.strip().startswith("#")] + assert not treffers, f"{uitleg}: {treffers[:2]}" + + +def test_help_werkt_en_noemt_alle_opties(): + r = _draai("--help") + assert r.returncode == 0 + for optie in ("--dir", "--port", "--bind", "--branch", "--token", + "--update", "--uninstall", "--yes", "--dry-run"): + assert optie in r.stdout, f"{optie} niet gedocumenteerd in --help" + + +def test_onbekende_optie_stopt(): + r = _draai("--bestaatniet") + assert r.returncode != 0 + assert "Onbekende optie" in r.stderr + + +@pytest.mark.parametrize("poort", ["abc", "", "80a", "-1"]) +def test_ongeldige_poort_stopt(poort): + r = _draai("--port", poort) + assert r.returncode != 0 + # Een lege waarde valt al af bij het uitlezen van de optie, de rest bij de + # controle daarna; beide meldingen gaan over de poort. + assert "poort" in r.stderr.lower() or "port" in r.stderr.lower() + + +def test_update_zonder_installatie_zegt_dat(tmp_path): + r = _draai("--update", "--dir", str(tmp_path / "bestaatniet")) + assert r.returncode != 0 + assert "Geen installatie gevonden" in r.stderr + + +def test_dry_run_wijzigt_niets(tmp_path): + doel = tmp_path / "server-up" + r = _draai("--dry-run", "--dir", str(doel), "--yes") + assert not doel.exists(), "proefdraai heeft toch iets aangemaakt" + assert "[droog]" in r.stdout + + +def test_dry_run_toont_de_hele_gang_van_zaken(tmp_path): + """Ook op een machine zonder Docker moet je de voorvertoning kunnen zien.""" + r = _draai("--dry-run", "--dir", str(tmp_path / "su"), "--yes") + for kop in ("Systeem controleren", "Broncode ophalen", "Instellingen", + "Bouwen en starten"): + assert kop in r.stdout, f"stap ontbreekt: {kop}" + + +def test_verwijst_naar_het_juiste_git_domein(): + tekst = SCRIPT.read_text(encoding="utf-8") + assert 'GIT_HOST="git.ramonbesselink.nl"' in tekst + + +def test_compose_wordt_met_expliciet_bestand_aangeroepen(): + """`--project-directory` alleen laat compose het bestand nog in de huidige + map zoeken; met `curl | sh` is dat je thuismap.""" + for regel in SCRIPT.read_text(encoding="utf-8").splitlines(): + if "docker compose" in regel and ("up -d" in regel or "down " in regel): + assert "-f " in regel, f"geen expliciet compose-bestand: {regel.strip()}" + + +def test_readme_en_docs_noemen_dezelfde_installatieregel(): + regel = ("curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/" + "raw/branch/main/install.sh") + assert regel in (WORTEL / "README.md").read_text(encoding="utf-8") + assert regel in (WORTEL / "docs" / "installeren.md").read_text(encoding="utf-8")