#!/usr/bin/env bash # ============================================================================= # Aktualisierung: laedt das aktuelle Release und ruft dessen install.sh auf. # Enthaelt selbst keinerlei Installationslogik. # # Eine einzelne Datei genuegt -- sie holt sich alles Weitere: # # curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/update.sh # sudo bash update.sh tesm # # Auf einer bestehenden Installation liegt sie schon bereit: # # sudo bash /srv/tesm/deploy/update.sh tesm # # Laeuft hier die Master-Ausgabe des Lizenzservers, kommt das Release aus dem # privaten Repositorium tesm-license-master -- dafuer braucht es einen # Zugriffstoken in TESM_RELEASE_TOKEN oder in /etc/tesm-release.token. # ============================================================================= set -euo pipefail # Ein Lauf dieses Skripts gehoert in eine definierte Sprachumgebung -- nicht in # die, die das Terminal des Administrators zufaellig ankuendigt. Per SSH kommt # LC_ALL mit (sshd: "AcceptEnv LANG LC_*"), und ist diese Locale auf dem Ziel # nicht erzeugt, warnt **jede neu gestartete Shell**: # # bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8) # # Mitten in einer Aktualisierung sieht das nach einem Fehler aus und ist # keiner. Nebenbei sind damit auch die Meldungen der Werkzeuge, die hier # laufen, in einer Sprache -- ein Protokoll, das je nach Arbeitsplatz anders # klingt, liest sich schlecht. if locale -a 2>/dev/null | grep -qix 'c\.utf-\?8'; then export LC_ALL=C.UTF-8 LANG=C.UTF-8 else export LC_ALL=C LANG=C fi APP="${1:-}" TAG="${2:-latest}" # Alles nach dem Tag geht unveraendert an install.sh weiter. Ohne das liesse # sich eine benannte Instanz nicht aktualisieren: install.sh leitet den Namen # aus "--instance" ab, und ohne den Schalter entstuende neben "tesm-test" eine # zweite Installation namens "tesm" -- mit eigener Datenbank, eigener # nginx-Site und demselben Port. shift $(( $# > 2 ? 2 : $# )) EXTRA=("$@") [[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { echo "Verwendung: update.sh [tag] [install.sh-Argumente ...]" >&2 echo "Beispiel : update.sh tesm latest --instance test" >&2 echo "Ohne Rueckfrage: --yes anhaengen (ohne Terminal zwingend)." >&2 echo "Laeuft die Fassung schon, endet der Aufruf ohne Arbeit; --force setzt" >&2 echo "sie trotzdem neu auf." >&2 exit 2 } # Repositorium: die Master-Ausgabe des Lizenzservers liegt in einem eigenen, # privaten Repositorium, heisst als Installation aber weiter "tesm-license". # Erkannt wird sie an der Installation und nicht an einem Schalter -- wer # aktualisiert, soll nicht erst wissen muessen, welche Ausgabe hier laeuft. # Sonst holte der Aufruf die Ausgabe fuer Sub-Lizenzserver, und der Installer # muesste sie abweisen. REPO="$APP" if [[ "$APP" == "tesm-license" ]]; then NAME="$APP" for ((i = 0; i < ${#EXTRA[@]}; i++)); do [[ "${EXTRA[i]}" == "--instance" ]] && NAME="$APP-${EXTRA[i + 1]:-}" done # Die Wurzel ist ueberschreibbar, damit ein Test die Erkennung wirklich # ausfuehren kann und nicht nur den Quelltext liest. if [[ -d "${TESM_SRV_ROOT:-/srv}/$NAME/apps/tesm-license/src/tesm_license/master" ]]; then REPO="tesm-license-master" echo "==> Master-Ausgabe erkannt -- Release aus $REPO." fi fi # --8<-- Bootstrap: Release holen -------------------------------------------- # Wortgleich in update.sh und upgrade.sh; ein Hygienetest vergleicht die beiden # Bloecke. Zwei eigenstaendige Skripte koennen sich keine Datei teilen, und # beide sollen als **einzelne Datei** genuegen: auf das Zielsystem legen, # ausfuehren, fertig -- auf einer 1.2.8 liegt nichts von diesem Projekt. # # Erwartet: $APP und $TAG, wahlweise $REPO (Vorgabe: $APP). Setzt: $TAG # (aufgeloest) und $SRC (entpacktes Release). Raeumt sein Arbeitsverzeichnis # beim Beenden auf. # # $REPO gibt es, weil Installationsname und Repositorium auseinanderfallen # koennen: die Master-Ausgabe des Lizenzservers liegt in # "tesm-license-master", die Installation heisst weiter "tesm-license". # # Zwei Fallen des Vorgaengers sind hier vermieden: # * Der Tag "latest" wird ueber die API aufgeloest, nie direkt in die # Download-URL geschrieben. Gitea liefert sonst mit HTTP 200 ein # automatisch erzeugtes Quell-Snapshot statt des echten Release-Anhangs. # * Nach dem Entpacken wird geprueft, dass ein install.sh existiert. BASE="${TESM_RELEASE_BASE:-https://gitea.int.eertmoed.net}" OWNER="${TESM_RELEASE_OWNER:-alientim}" # Ein privates Repositorium -- tesm-license-master -- gibt sein Release nur mit # Zugriffstoken heraus. Reihenfolge: Umgebungsvariable, dann eine Datei, die # der Betreiber selbst anlegt. Der Installer legt sie nie an: ob ein Token auf # dem Server liegt, ist keine Entscheidung eines Skripts. TOKEN="${TESM_RELEASE_TOKEN:-}" if [[ -z "$TOKEN" && -r /etc/tesm-release.token ]]; then TOKEN="$(tr -d '[:space:]' < /etc/tesm-release.token)" fi # Ein Wagenruecklauf im Paket macht jedes Shell-Skript auf dem Ziel unbrauchbar # ("bad interpreter"). Die Pruefung darf dabei nicht selbst von einem # Zusatzpaket abhaengen: mit "file" -- das auf einem schlanken Server fehlt -- # endete sie in "command not found", grep bekam nichts zu sehen, und das Skript # machte weiter, als waere alles in Ordnung. hat_crlf() { local cr cr="$(printf '\r')" # -U (binaer lesen) ist unter Linux ohne Wirkung, auf einer # Windows-Buildmaschine aber entscheidend: dort entfernt grep den # Wagenruecklauf beim Lesen und findet deshalb nie einen. LC_ALL=C grep -qU "$cr" "$1" } BOOTSTRAP_WORK="" bootstrap_aufraeumen() { # Den Ausstiegscode zuerst festhalten: alles danach ueberschreibt ihn. local code=$? [[ -n "$BOOTSTRAP_WORK" ]] && rm -rf "$BOOTSTRAP_WORK" # Wer einen eigenen Abschluss braucht -- etwa einen Rueckweg nach einem # Fehlschlag --, definiert "beim_beenden". Ein zweiter "trap ... EXIT" waere # der falsche Weg: er wuerde diesen hier ersetzen, und das entpackte Release # bliebe unter /tmp liegen. declare -F beim_beenden >/dev/null 2>&1 && beim_beenden "$code" return "$code" } trap bootstrap_aufraeumen EXIT release_holen() { local repo="${TESM_RELEASE_REPO:-${REPO:-$APP}}" asset # Leeres Array und "set -u" vertragen sich nur mit dieser Schreibweise. local -a kopf=() [[ -n "$TOKEN" ]] && kopf=(-H "Authorization: token $TOKEN") for werkzeug in curl tar python3 find; do command -v "$werkzeug" >/dev/null 2>&1 || { echo "Auf diesem System fehlt $werkzeug -- ohne das geht es nicht." >&2 return 1 } done BOOTSTRAP_WORK="$(mktemp -d /tmp/tesm-release-XXXXXX)" if [[ "$TAG" == "latest" ]]; then echo "==> Ermittle aktuellen Release-Tag ..." TAG="$(curl -fsS ${kopf[0]+"${kopf[@]}"} "$BASE/api/v1/repos/$OWNER/$repo/releases/latest" \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" || { echo "Kein Release gefunden unter $BASE/$OWNER/$repo." >&2 [[ -z "$TOKEN" ]] && echo "Ist das Repositorium privat? Dann TESM_RELEASE_TOKEN setzen." >&2 echo "Andere Quelle: TESM_RELEASE_BASE, TESM_RELEASE_OWNER, TESM_RELEASE_REPO." >&2 return 1 } echo " $TAG" fi asset="$repo-$TAG.tar.gz" echo "==> Lade $asset ..." curl -fsSL ${kopf[0]+"${kopf[@]}"} -o "$BOOTSTRAP_WORK/$asset" \ "$BASE/$OWNER/$repo/releases/download/$TAG/$asset" || { echo "Download fehlgeschlagen: $BASE/$OWNER/$repo/releases/download/$TAG/$asset" >&2 return 1 } echo "==> Entpacke ..." tar -xzf "$BOOTSTRAP_WORK/$asset" -C "$BOOTSTRAP_WORK" # Drei Ebenen, nicht zwei: release.sh packt mit dem Praefix "/", also # liegt das Skript unter /deploy/install.sh. Mit "-maxdepth 2" fand die # Suche nie etwas -- und die Fehlermeldung schob es auf den Download. SRC="$(find "$BOOTSTRAP_WORK" -maxdepth 3 -name install.sh -path '*/deploy/*' \ -printf '%h/..\n' | head -1)" [[ -n "$SRC" ]] || { echo "Im Paket fehlt deploy/install.sh -- Download unbrauchbar." >&2 return 1 } SRC="$(cd "$SRC" && pwd)" echo "==> Pruefe auf CRLF (Windows-Buildmaschinen) ..." if hat_crlf "$SRC/deploy/install.sh"; then echo "Das Paket enthaelt CRLF-Zeilenenden. Beim Paketieren fehlte" >&2 echo "'git -c core.autocrlf=false archive'." >&2 return 1 fi } # --8<-- Ende Bootstrap ------------------------------------------------------ release_holen # Schon installiert? Dann gar nicht erst fragen. # # Dieselbe Pruefung steht in install.sh und gilt dort endgueltig (sie # vergleicht auch den Kern). Hier steht sie noch einmal, weil ein Update, das # ohne Wirkung bleibt, gar nicht erst nach einer Bestaetigung fragen soll -- # und weil "es lief durch" sonst wie eine Aktualisierung aussieht. ERZWINGEN=0 for ((i = 0; i < ${#EXTRA[@]}; i++)); do [[ "${EXTRA[i]}" == "--force" ]] && ERZWINGEN=1 done INSTALLATION="$APP" for ((i = 0; i < ${#EXTRA[@]}; i++)); do [[ "${EXTRA[i]}" == "--instance" ]] && INSTALLATION="$APP-${EXTRA[i + 1]:-}" done MODUL="tesm" [[ "$APP" == "tesm-license" ]] && MODUL="tesm_license" if [[ $ERZWINGEN -eq 0 && -x "/srv/$INSTALLATION/venv/bin/python" ]]; then LAEUFT="$("/srv/$INSTALLATION/venv/bin/python" -c \ "import $MODUL; print($MODUL.__version__)" 2>/dev/null || true)" if [[ -n "$LAEUFT" && "v$LAEUFT" == "$TAG" ]]; then echo "==> $APP $LAEUFT laeuft hier bereits -- nichts zu tun." echo " Trotzdem neu einrichten: --force" exit 0 fi fi # --yes darf auch hier stehen, nicht nur bei install.sh und upgrade.sh. Es # bleibt in EXTRA: install.sh bekommt es ohnehin, doppelt schadet nicht. OHNE_RUECKFRAGE=0 for ((i = 0; i < ${#EXTRA[@]}; i++)); do [[ "${EXTRA[i]}" == "--yes" || "${EXTRA[i]}" == "-y" ]] && OHNE_RUECKFRAGE=1 done if [[ $OHNE_RUECKFRAGE -eq 1 ]]; then echo "==> Version $TAG wird ohne Rueckfrage installiert." elif [[ -t 0 ]]; then read -r -p "Version $TAG fuer $APP${EXTRA[0]+ (${EXTRA[*]})} installieren? [j/N] " answer [[ "$answer" =~ ^[jJyY]$ ]] || exit 0 else # Ohne Terminal las "read" hier EOF, die Antwort blieb leer, und das Skript # endete mit 0 -- wer es aus einem anderen Skript aufrief, hielt das Update # fuer erledigt. Lieber laut abbrechen. echo "Kein Terminal fuer die Rueckfrage, und --yes fehlt -- es wurde nichts" >&2 echo "installiert. Aufruf mit --yes wiederholen." >&2 exit 3 fi # Kein exec: sonst laeuft die Aufraeumfalle nie, und jedes Update liesse ein # entpacktes Release unter /tmp liegen. bash "$SRC/deploy/install.sh" --app "$APP" --yes ${EXTRA[0]+"${EXTRA[@]}"}