#!/usr/bin/env bash # # Aktualisiert eine vorhandene Syncova-Installation. # # Die Reihenfolge ist nicht verhandelbar und der Grund fuer dieses Skript: # # sichern -> anhalten -> Programme tauschen -> migrieren -> starten -> pruefen # # Laeuft der Dienst noch, waehrend sich das Schema aendert, arbeitet er auf # einem Stand, den er nicht kennt. Und ohne die Sicherung davor gibt es keinen # Weg zurueck: Ein Rollback nimmt genau eine Migration zuruck, und Daten in # Tabellen, die es vorher nicht gab, sind danach weg. # # Scheitert ein Schritt, wird die vorige Fassung zurueckgeholt. # # ./update.sh prueft, fragt, aktualisiert # ./update.sh --pruefen sieht nur nach und aendert nichts # ./update.sh --ja ohne Rueckfrage # # Das Repository wird nicht angefasst. Der Verschluesselungsschluessel auch # nicht — er bleibt, wie er ist, sonst waeren alle Geheimnisse unlesbar. set -euo pipefail # --------------------------------------------------------------------------- # Feste Orte — gleichlautend in setup.sh und uninstall.sh # --------------------------------------------------------------------------- readonly installationRoot="/opt/syncova" readonly configurationDirectory="/etc/syncova" readonly configurationFile="${configurationDirectory}/syncova.env" readonly serviceName="syncova-api" readonly backupDirectory="/var/backups/syncova" # previousReleaseRoot nimmt die abgeloeste Fassung auf. # # Sie bleibt liegen, bis die neue nachweislich laeuft. Ein Update, das die alte # Fassung sofort loescht, hat keinen Rueckweg. readonly previousReleaseRoot="${installationRoot}.vorherige" # --------------------------------------------------------------------------- # Ausgabe # --------------------------------------------------------------------------- if [[ -t 1 ]]; then readonly colorReset=$'\033[0m' readonly colorBold=$'\033[1m' readonly colorRed=$'\033[31m' readonly colorGreen=$'\033[32m' readonly colorYellow=$'\033[33m' else readonly colorReset='' colorBold='' colorRed='' colorGreen='' colorYellow='' fi writeStep() { printf '%s==>%s %s\n' "${colorBold}" "${colorReset}" "$1"; } writeDetail() { printf ' %s\n' "$1"; } writeSuccess() { printf ' %s✓%s %s\n' "${colorGreen}" "${colorReset}" "$1"; } writeWarning() { printf ' %s!%s %s\n' "${colorYellow}" "${colorReset}" "$1" >&2; } abortWithMessage() { printf '\n%sAbbruch:%s %s\n' "${colorRed}${colorBold}" "${colorReset}" "$1" >&2 exit 1 } # --------------------------------------------------------------------------- # Schalter # --------------------------------------------------------------------------- checkOnly="nein" assumeYes="nein" packageDirectory="" showUsage() { cat <<'USAGE' Verwendung: update.sh [--paket ] [--pruefen] [--ja] Schalter: --paket Entpacktes neues Paket (Vorgabe: Verzeichnis des Skripts) --pruefen Nur nachsehen: installierte und neue Fassung vergleichen --ja Ohne Rueckfrage aktualisieren --hilfe Diese Uebersicht Vor dem Tausch werden Datenbank und Konfiguration gesichert nach: /var/backups/syncova Nicht angetastet werden das Repository und der Verschluesselungsschluessel. USAGE } while [[ $# -gt 0 ]]; do case "$1" in --paket) packageDirectory="${2:-}"; shift 2 ;; --pruefen) checkOnly="ja"; shift ;; --ja) assumeYes="ja"; shift ;; --hilfe|-h|--help) showUsage; exit 0 ;; *) showUsage >&2; abortWithMessage "unbekannter Schalter: $1" ;; esac done # --------------------------------------------------------------------------- # 1. Was ist installiert? # --------------------------------------------------------------------------- printf '\n%sSyncova Backups — Aktualisierung%s\n\n' "${colorBold}" "${colorReset}" writeStep "Vorhandene Installation" if [[ "$(id -u)" -ne 0 ]]; then abortWithMessage "Dieses Skript braucht root-Rechte (sudo ./update.sh)." fi if [[ ! -x "${installationRoot}/bin/syncova-api" ]]; then abortWithMessage "Unter ${installationRoot} ist keine Installation zu finden. Zum Einrichten: ./setup.sh" fi if [[ ! -f "${configurationFile}" ]]; then abortWithMessage "Die Konfiguration ${configurationFile} fehlt. Ohne sie laesst sich weder sichern noch migrieren." fi installedVersion="$("${installationRoot}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}')" [[ -n "${installedVersion}" ]] || installedVersion="unbekannt" writeSuccess "Installiert: ${installedVersion}" # Der Schemastand gehoert zur Bestandsaufnahme: Steht er auf "unsauber", ist # eine fruehere Migration abgebrochen, und ein Update darauf waere fahrlaessig. set -a # shellcheck disable=SC1090 . "${configurationFile}" set +a schemaState="$("${installationRoot}/bin/syncova-migrate" status 2>&1 | tail -2 | tr '\n' ' ')" writeDetail "Schema: ${schemaState}" if printf '%s' "${schemaState}" | grep -qi "unsauber\|dirty"; then printf '\n' writeWarning "Der Schemastand ist als unsauber vermerkt." writeDetail "Eine fruehere Migration ist abgebrochen. Bringen Sie das Schema" writeDetail "zuerst in Ordnung ('syncova-migrate force ' nach Ruecksprache)," writeDetail "bevor Sie aktualisieren." abortWithMessage "Ein Update auf einem unsauberen Schema wird nicht durchgefuehrt." fi serviceWasRunning="nein" if systemctl is-active --quiet "${serviceName}" 2>/dev/null; then serviceWasRunning="ja" writeSuccess "Dienst laeuft" else writeDetail "Der Dienst laeuft derzeit nicht." fi # --------------------------------------------------------------------------- # 2. Was ist neu? # --------------------------------------------------------------------------- printf '\n' writeStep "Neues Paket" if [[ -z "${packageDirectory}" ]]; then packageDirectory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" fi if [[ ! -x "${packageDirectory}/bin/syncova-api" ]]; then abortWithMessage "Unter ${packageDirectory} liegt kein Auslieferungspaket. Geben Sie es mit --paket an." fi packageVersion="$("${packageDirectory}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}')" writeSuccess "Neu: ${packageVersion}" if [[ -f "${packageDirectory}/SHA256SUMS" ]] && command -v sha256sum >/dev/null 2>&1; then if ( cd "${packageDirectory}" && sha256sum --quiet -c SHA256SUMS >/dev/null 2>&1 ); then writeSuccess "Pruefsummen stimmen" else abortWithMessage "Die Pruefsummen des neuen Pakets stimmen nicht. Ein unvollstaendig uebertragenes Paket wird nicht eingespielt." fi fi if [[ "${installedVersion}" == "${packageVersion}" ]]; then printf '\n' writeDetail "Installiert und neu sind dieselbe Fassung (${packageVersion})." if [[ "${checkOnly}" == "ja" ]]; then exit 0 fi if [[ "${assumeYes}" != "ja" ]]; then read -r -p " Trotzdem neu einspielen? [j/N]: " reinstallAnswer [[ "${reinstallAnswer}" =~ ^[jJyY] ]] || { printf '\n Nichts zu tun.\n\n'; exit 0; } else printf '\n Nichts zu tun.\n\n' exit 0 fi fi if [[ "${checkOnly}" == "ja" ]]; then printf '\n' printf ' %s%s%s → %s%s%s\n\n' "${colorBold}" "${installedVersion}" "${colorReset}" \ "${colorBold}${colorGreen}" "${packageVersion}" "${colorReset}" printf ' Zum Aktualisieren: %s\n\n' "$0" exit 0 fi # --------------------------------------------------------------------------- # 3. Rueckfrage # --------------------------------------------------------------------------- if [[ "${assumeYes}" != "ja" ]]; then printf '\n' printf ' %s%s%s → %s%s%s\n\n' "${colorBold}" "${installedVersion}" "${colorReset}" \ "${colorBold}${colorGreen}" "${packageVersion}" "${colorReset}" writeDetail "Vor dem Tausch werden Datenbank und Konfiguration gesichert." writeDetail "Repository und Verschluesselungsschluessel bleiben unberuehrt." writeDetail "Der Dienst ist waehrend des Updates kurz nicht erreichbar." printf '\n' read -r -p " Jetzt aktualisieren? [j/N]: " updateAnswer [[ "${updateAnswer}" =~ ^[jJyY] ]] || { printf '\n Abgebrochen. Es wurde nichts geaendert.\n\n'; exit 0; } fi # --------------------------------------------------------------------------- # 4. Sichern # --------------------------------------------------------------------------- printf '\n' writeStep "Sicherung vor dem Update" install -d -m 0700 "${backupDirectory}" backupTimestamp="$(date -u '+%Y%m%d-%H%M%S')" backupPrefix="${backupDirectory}/${backupTimestamp}-vor-${packageVersion}" # Die Konfiguration zuerst: Sie ist klein und enthaelt den Schluessel. install -m 0600 "${configurationFile}" "${backupPrefix}.env" writeSuccess "Konfiguration: ${backupPrefix}.env" # Die Datenbank. Ohne pg_dump gibt es keinen Weg zurueck — deshalb bricht das # Skript hier ab, statt "auf gut Glueck" weiterzumachen. if ! command -v pg_dump >/dev/null 2>&1; then printf '\n' writeWarning "pg_dump ist nicht vorhanden." writeDetail "Ohne Datenbanksicherung gibt es nach einer misslungenen Migration" writeDetail "keinen Weg zurueck: Ein Rollback nimmt genau eine Migration" writeDetail "zurueck, und Daten in neuen Tabellen sind danach weg." printf '\n' writeDetail "Installieren Sie postgresql-client, oder sichern Sie von Hand und" writeDetail "starten Sie danach mit --ja erneut." abortWithMessage "Ohne Datenbanksicherung wird nicht aktualisiert." fi databaseDumpFile="${backupPrefix}.dump" if ! PGPASSWORD="${SYNCOVA_DB_PASSWORD:-}" pg_dump \ --host "${SYNCOVA_DB_HOST:-127.0.0.1}" \ --port "${SYNCOVA_DB_PORT:-5432}" \ --username "${SYNCOVA_DB_USER:-syncova}" \ --dbname "${SYNCOVA_DB_NAME:-syncova}" \ --format=custom --file "${databaseDumpFile}" 2>/tmp/syncova-pgdump.log; then printf '\n' writeWarning "Die Datenbanksicherung schlug fehl:" head -5 /tmp/syncova-pgdump.log | sed 's/^/ /' >&2 rm -f /tmp/syncova-pgdump.log abortWithMessage "Ohne Sicherung wird nicht aktualisiert." fi rm -f /tmp/syncova-pgdump.log chmod 0600 "${databaseDumpFile}" dumpSize="$(du -h "${databaseDumpFile}" | awk '{print $1}')" writeSuccess "Datenbank: ${databaseDumpFile} (${dumpSize})" # --------------------------------------------------------------------------- # 5. Tauschen # --------------------------------------------------------------------------- # updateFailed holt die vorige Fassung zurueck. # # Aufgerufen aus dem trap. Die Reihenfolge ist die Umkehrung des Updates: erst # den Dienst anhalten, dann die Programme zuruecktauschen, dann starten. updateFailed() { local exitCode=$? [[ ${exitCode} -eq 0 ]] && exit 0 printf '\n%sDas Update wird zurueckgenommen.%s\n' "${colorYellow}${colorBold}" "${colorReset}" >&2 systemctl stop "${serviceName}" 2>/dev/null || true if [[ -d "${previousReleaseRoot}" ]]; then rm -rf "${installationRoot}" mv "${previousReleaseRoot}" "${installationRoot}" writeDetail "Die vorige Fassung ${installedVersion} liegt wieder an ihrem Platz." fi if [[ "${serviceWasRunning}" == "ja" ]]; then systemctl start "${serviceName}" 2>/dev/null || \ writeWarning "Der Dienst liess sich nicht wieder starten: systemctl status ${serviceName}" fi printf '\n' >&2 writeDetail "Die Datenbank wurde NICHT zurueckgespielt — das geht nur von Hand:" writeDetail "" writeDetail " systemctl stop ${serviceName}" writeDetail " pg_restore --clean --if-exists -d ${SYNCOVA_DB_NAME:-syncova} ${databaseDumpFile}" writeDetail " systemctl start ${serviceName}" writeDetail "" writeDetail "Notwendig ist das nur, wenn die Migration bereits gelaufen war." writeDetail "Der Schemastand sagt es: ${installationRoot}/bin/syncova-migrate status" exit "${exitCode}" } trap updateFailed EXIT printf '\n' writeStep "Programme tauschen" if [[ "${serviceWasRunning}" == "ja" ]]; then systemctl stop "${serviceName}" writeSuccess "Dienst angehalten" fi rm -rf "${previousReleaseRoot}" mv "${installationRoot}" "${previousReleaseRoot}" install -d -m 0755 "${installationRoot}" cp -R "${packageDirectory}/bin" "${installationRoot}/" [[ -d "${packageDirectory}/web" ]] && cp -R "${packageDirectory}/web" "${installationRoot}/" [[ -d "${packageDirectory}/migrations" ]] && cp -R "${packageDirectory}/migrations" "${installationRoot}/" [[ -d "${packageDirectory}/docs" ]] && cp -R "${packageDirectory}/docs" "${installationRoot}/" [[ -d "${packageDirectory}/deployment" ]] && cp -R "${packageDirectory}/deployment" "${installationRoot}/" chmod -R 0755 "${installationRoot}/bin" printf '%s\n' "${packageVersion}" > "${installationRoot}/VERSION" writeSuccess "Programme ausgetauscht" # --------------------------------------------------------------------------- # 6. Migrieren # --------------------------------------------------------------------------- printf '\n' writeStep "Datenbankschema" # Mit dem NEUEN Programm und bei angehaltenem Dienst. Beides gehoert zusammen: # Die neue Fassung bringt ihre Migrationen mit, und ein laufender Dienst # arbeitete waehrenddessen auf einem Stand, den er nicht kennt. migrationOutput="$("${installationRoot}/bin/syncova-migrate" up 2>&1 | tail -1)" writeDetail "${migrationOutput}" if printf '%s' "${migrationOutput}" | grep -qi "error\|fehler"; then abortWithMessage "Die Migration schlug fehl." fi writeSuccess "Schema aktuell" # --------------------------------------------------------------------------- # 7. Starten und pruefen # --------------------------------------------------------------------------- printf '\n' writeStep "Dienst" systemctl daemon-reload systemctl start "${serviceName}" healthAddress="${SYNCOVA_HTTP_LISTEN_ADDRESS:-127.0.0.1:8080}" serviceIsReady="nein" for _attempt in $(seq 1 20); do if curl -fsS "http://${healthAddress}/health/ready" >/dev/null 2>&1; then serviceIsReady="ja" break fi sleep 1 done if [[ "${serviceIsReady}" != "ja" ]]; then printf '\n' writeWarning "Der Dienst meldet sich nach dem Update nicht als betriebsbereit." journalctl -u "${serviceName}" -n 15 --no-pager 2>/dev/null | sed 's/^/ /' >&2 || true abortWithMessage "Das Update wird zurueckgenommen." fi # Die laufende Fassung ist eine Beigabe, kein Pruefkriterium — sie wird aus dem # Programm gelesen, nicht aus der Gesundheitsantwort. # # Der erste Anlauf holte sie mit "grep -o" aus der Antwort von /health/ready. # Die enthaelt aber gar kein Versionsfeld; grep endet dann mit 1, und mit # "set -o pipefail" nahm das Skript ein **gelungenes** Update wieder zurueck. # Ein Rollback wegen einer Zeile, die nur der Ausgabe dient. runningVersion="$("${installationRoot}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}' || true)" writeSuccess "Dienst laeuft und ist betriebsbereit${runningVersion:+ (${runningVersion})}" # Ab hier ist das Update gelungen. trap - EXIT # --------------------------------------------------------------------------- # Abschluss # --------------------------------------------------------------------------- printf '\n%sDie Aktualisierung ist abgeschlossen.%s\n\n' "${colorGreen}${colorBold}" "${colorReset}" printf ' %s → %s\n\n' "${installedVersion}" "${packageVersion}" printf ' Sicherung %s.dump\n' "${backupPrefix}" printf ' Vorige Fassung %s\n' "${previousReleaseRoot}" printf '\n' writeDetail "Die vorige Fassung bleibt liegen, bis Sie sie entfernen:" writeDetail " rm -rf ${previousReleaseRoot}" printf '\n' writeDetail "Sehen Sie vorher nach, ob die neue Fassung im Betrieb traegt —" writeDetail "ein Update, das man am selben Tag entsorgt, hat keinen Rueckweg." printf '\n'