#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" DOCKER_CMD=() SECRETS_FILE="$SCRIPT_DIR/.mongo-secrets.env" RESET_MONGO_DATA=0 for arg in "$@"; do case "$arg" in --reset-mongo-data|-r) RESET_MONGO_DATA=1 ;; esac done ensure_mongo_secrets() { if [ -f "$SECRETS_FILE" ]; then # shellcheck disable=SC1090 source "$SECRETS_FILE" fi if [ -z "${MONGO_INITDB_ROOT_PASSWORD:-}" ]; then MONGO_INITDB_ROOT_PASSWORD="$(openssl rand -hex 24)" fi if [ -z "${MONGO_APP_PASSWORD:-}" ]; then MONGO_APP_PASSWORD="$(openssl rand -hex 24)" fi cat > "$SECRETS_FILE" <&2 return 0 } wait_for_mongo_ready() { local timeout_seconds=180 local interval=2 local elapsed=0 local mongo_container_id="" local container_state="" local health_status="" mongo_container_id="$("${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" ps -q mongodb 2>/dev/null || true)" if [ -z "$mongo_container_id" ]; then echo "[FEHLER] MongoDB-Container wurde nicht gefunden." >&2 return 1 fi while [ $elapsed -lt $timeout_seconds ]; do container_state="$(${DOCKER_CMD[@]} inspect -f '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}' "$mongo_container_id" 2>/dev/null || true)" health_status="${container_state#* }" if [ "$health_status" = "healthy" ]; then return 0 fi if [ "${container_state%% *}" = "running" ] && [ -z "$health_status" ]; then return 0 fi if [ "${container_state%% *}" = "exited" ] || [ "${container_state%% *}" = "dead" ]; then break fi sleep $interval elapsed=$((elapsed + interval)) done echo "[FEHLER] MongoDB wurde nicht rechtzeitig gestartet. Status: ${container_state:-unbekannt}" >&2 "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" ps mongodb >&2 || true "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" logs --tail 80 mongodb >&2 || true return 1 } resolve_docker_cmd() { if docker info >/dev/null 2>&1; then DOCKER_CMD=(docker) return 0 fi if sudo -n docker info >/dev/null 2>&1; then DOCKER_CMD=(sudo docker) return 0 fi if command -v sudo >/dev/null 2>&1 && [ -t 0 ]; then echo "Docker-Zugriff ohne Gruppe erkannt. Verwende sudo docker (ggf. Passwortabfrage)." if sudo docker info >/dev/null 2>&1; then DOCKER_CMD=(sudo docker) return 0 fi fi echo "[FEHLER] Kein Zugriff auf den Docker Daemon (/var/run/docker.sock)." >&2 echo "Führe das Script mit sudo aus oder füge deinen User zur docker-Gruppe hinzu:" >&2 echo " sudo usermod -aG docker $USER" >&2 echo "Danach neu einloggen und Script erneut starten." >&2 exit 3 } compose() { "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" "$@" } is_truthy() { case "${1:-}" in 1|true|TRUE|yes|YES|on|ON) return 0 ;; *) return 1 ;; esac } reset_mongo_data_if_requested() { if ! is_truthy "${MONGO_RESET_DATA:-0}" && [ "$RESET_MONGO_DATA" -ne 1 ]; then return 0 fi echo "[WARNUNG] Setze den MongoDB-Volume-Stand zurück, damit die aktuellen Secrets neu initialisiert werden." >&2 "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" down -v --remove-orphans } build_website_image() { if compose build website; then return 0 fi echo "[WARNUNG] Standard-Build fehlgeschlagen, versuche Legacy-Build ohne BuildKit erneut..." >&2 DOCKER_BUILDKIT=0 COMPOSE_DOCKER_CLI_BUILD=0 compose build website } # ============================================================================ # WATCHER LOGIK FÜR ALLE TENANT AKTIONEN (add, remove, restart) # ============================================================================ TARGET_SYSTEM_DIR="/opt/Inventarsystem" TARGET_SCRIPT="$TARGET_SYSTEM_DIR/manage-tenant.sh" get_free_port() { local requested_port="${1:-10002}" local used_ports # Belegte Ports aus manage-tenant.sh list auslesen used_ports=$( (cd "$TARGET_SYSTEM_DIR" && "$TARGET_SCRIPT" list 2>/dev/null) | grep -oE '[0-9]{4,5}' || true ) local check_port="$requested_port" if ! [[ "$check_port" =~ ^[0-9]+$ ]]; then check_port=10002 fi # Falls Port belegt ist, den nächsten freien ermitteln while echo "$used_ports" | grep -q -w "$check_port"; do echo "[INFO] Port $check_port ist belegt. Prüfe $(($check_port + 1))..." >&2 check_port=$((check_port + 1)) done echo "$check_port" } watcher_loop() { local trigger_dir="$1" echo "[INFO] Tenant Watcher gestartet. Lausche in $trigger_dir..." while true; do for file in "$trigger_dir"/*.json; do [ -f "$file" ] || continue echo "[INFO] Verarbeite Trigger: $file" if command -v jq >/dev/null 2>&1; then ACTION=$(jq -r '.action // "add"' "$file" 2>/dev/null || echo "add") SLUG=$(jq -r '.slug // empty' "$file" 2>/dev/null || true) PORT=$(jq -r '.port // empty' "$file" 2>/dev/null || true) PASS=$(jq -r '.password // empty' "$file" 2>/dev/null || true) MODULES=$(jq -r '.module_config // empty' "$file" 2>/dev/null || true) else ACTION=$(grep -o '"action": *"[^"]*"' "$file" | cut -d'"' -f4 || echo "add") [ -z "$ACTION" ] && ACTION="add" SLUG=$(grep -o '"slug": *"[^"]*"' "$file" | cut -d'"' -f4 || true) PORT=$(grep -o '"port": *[0-9]*' "$file" | cut -d':' -f2 | tr -d ' ' || true) PASS=$(grep -o '"password": *"[^"]*"' "$file" | cut -d'"' -f4 || true) MODULES=$(grep -o '"module_config": *"[^"]*"' "$file" | cut -d'"' -f4 || true) fi if [ -z "$SLUG" ]; then echo "[ERROR] Ungültige Payload in $file (Slug fehlt). Wird gelöscht." >&2 rm -f "$file" continue fi case "$ACTION" in "add") if [ -n "$PASS" ]; then # 1. Port prüfen & ermitteln ACTUAL_PORT=$(get_free_port "$PORT") echo "[INFO] Verwende Port $ACTUAL_PORT für Tenant $SLUG" # 2. Tenant anlegen if (cd "$TARGET_SYSTEM_DIR" && "$TARGET_SCRIPT" add "$SLUG" "$ACTUAL_PORT" "$PASS"); then echo "[SUCCESS] Tenant bereitgestellt: $SLUG (Port: $ACTUAL_PORT)" # WORKAROUND für Docker: Warte kurz, damit der Docker Daemon den Container-Namen # vom Neustart freigibt und wir nicht in einen Konflikt laufen. echo "[INFO] Warte 8 Sekunden auf Docker-Cleanup (Vermeidung von Namenskonflikten)..." sleep 8 # 3. Module konfigurieren if [ -n "$MODULES" ]; then echo "[INFO] Konfiguriere Module für $SLUG: $MODULES" # shellcheck disable=SC2086 if (cd "$TARGET_SYSTEM_DIR" && "$TARGET_SCRIPT" module "$SLUG" $MODULES); then echo "[SUCCESS] Module für $SLUG erfolgreich konfiguriert." else echo "[ERROR] Fehler beim Konfigurieren der Module für $SLUG." >&2 fi echo "[INFO] Warte weitere 8 Sekunden auf Docker..." sleep 8 fi # Um sicherzugehen, löschen wir verwaiste Container, bevor das Skript greift echo "[INFO] Bereinige verwaiste App-Container..." sudo docker rm -f inventarsystem-app-1 2>/dev/null || true # 4. System neustarten echo "[INFO] Führe sudo ./restart.sh nach Tenant-Generierung aus..." if (cd "$TARGET_SYSTEM_DIR" && sudo ./restart.sh); then echo "[SUCCESS] System erfolgreich neu gestartet für $SLUG." else echo "[ERROR] Fehler beim Ausführen von sudo ./restart.sh" >&2 fi else echo "[ERROR] Fehler beim Bereitstellen von: $SLUG" >&2 fi else echo "[ERROR] Fehlendes Passwort für add in $file" >&2 fi ;; "remove") if (cd "$TARGET_SYSTEM_DIR" && "$TARGET_SCRIPT" remove "$SLUG" -y); then echo "[SUCCESS] Tenant gelöscht: $SLUG" else echo "[ERROR] Fehler beim Löschen von: $SLUG" >&2 fi ;; "restart") if (cd "$TARGET_SYSTEM_DIR" && "$TARGET_SCRIPT" restart-tenant "$SLUG"); then echo "[SUCCESS] Tenant neu gestartet: $SLUG" else echo "[ERROR] Fehler beim Neustart von: $SLUG" >&2 fi ;; *) echo "[ERROR] Unbekannte Aktion ($ACTION) in $file" >&2 ;; esac rm -f "$file" done sleep 2 done } start_tenant_watcher() { local trigger_dir="$TARGET_SYSTEM_DIR/triggers" local pid_file="$SCRIPT_DIR/.watcher.pid" local log_file="$SCRIPT_DIR/watcher.log" if [ ! -d "$trigger_dir" ]; then mkdir -p "$trigger_dir" chmod 777 "$trigger_dir" fi if [ -f "$pid_file" ]; then local old_pid old_pid=$(cat "$pid_file") if kill -0 "$old_pid" 2>/dev/null; then echo "Beende alten Watcher-Prozess (PID: $old_pid)..." kill "$old_pid" || true fi fi echo "Starte asynchronen Tenant-Watcher im Hintergrund..." watcher_loop "$trigger_dir" >> "$log_file" 2>&1 & echo $! > "$pid_file" echo "Watcher läuft! Logs findest du in: $log_file" } # ============================================================================ export SESSION_COOKIE_SECURE="0" export INSTANCE_TLS_MODE="development" export INSTANCE_PARENT_DOMAIN="${INSTANCE_PARENT_DOMAIN:-meine-domain}" export DEV_HOSTS_REFRESH_INTERVAL="${DEV_HOSTS_REFRESH_INTERVAL:-30}" echo "Launching WEBSITE stack" echo " SESSION_COOKIE_SECURE=$SESSION_COOKIE_SECURE" echo " INSTANCE_TLS_MODE=$INSTANCE_TLS_MODE" echo " INSTANCE_PARENT_DOMAIN=$INSTANCE_PARENT_DOMAIN" echo " DEV_HOSTS_REFRESH_INTERVAL=$DEV_HOSTS_REFRESH_INTERVAL" ensure_mongo_secrets if [ ! -f "$SCRIPT_DIR/gunicorn.conf.py" ]; then echo "[FEHLER] gunicorn.conf.py fehlt in $SCRIPT_DIR. Bitte prüfen." >&2 exit 2 fi resolve_docker_cmd reset_mongo_data_if_requested build_website_image compose up -d mongodb wait_for_mongo_ready ensure_mongo_app_user compose up -d --no-deps website check_url="http://localhost:4999" timeout_seconds=60 interval=2 elapsed=0 echo "Warte auf Website (${check_url}) bis ${timeout_seconds}s..." while [ $elapsed -lt $timeout_seconds ]; do if curl -sS --max-time 2 "$check_url" >/dev/null 2>&1; then echo "Website erreichbar nach ${elapsed}s" break fi sleep $interval elapsed=$((elapsed + interval)) done if [ $elapsed -ge $timeout_seconds ]; then echo "[FEHLER] Website nicht erreichbar nach ${timeout_seconds}s. Sammle Diagnosedaten..." echo "--- docker compose ps ---" compose ps || true echo "--- docker logs website (tail 200) ---" "${DOCKER_CMD[@]}" logs --tail 200 website-website-1 || true echo "Bitte prüfe Container-Logs und nginx-Konfiguration." fi echo "Website stack is running on http://localhost:4999" echo "Provisioning creates self-signed certs per subdomain when needed." start_tenant_watcher