Files
Key-service-Server/Website/launch.sh
T

456 lines
16 KiB
Bash
Executable File

#!/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" <<EOF
MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME:-website_root}
MONGO_INITDB_ROOT_PASSWORD=$MONGO_INITDB_ROOT_PASSWORD
MONGO_APP_USER=${MONGO_APP_USER:-website_app}
MONGO_APP_PASSWORD=$MONGO_APP_PASSWORD
EOF
chmod 600 "$SECRETS_FILE"
# shellcheck disable=SC1090
source "$SECRETS_FILE"
export MONGO_INITDB_ROOT_USERNAME MONGO_INITDB_ROOT_PASSWORD MONGO_APP_USER MONGO_APP_PASSWORD
}
ensure_mongo_app_user() {
if "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" exec -T mongodb mongosh --quiet \
--username "$MONGO_INITDB_ROOT_USERNAME" \
--password "$MONGO_INITDB_ROOT_PASSWORD" \
--authenticationDatabase admin \
--eval '
const appDatabase = process.env.MONGO_DB_NAME || "Invario_Website";
const appUser = process.env.MONGO_APP_USER || "website_app";
const appPassword = process.env.MONGO_APP_PASSWORD;
if (!appPassword) {
throw new Error("Missing MONGO_APP_PASSWORD");
}
const appDb = db.getSiblingDB(appDatabase);
if (!appDb.getUser(appUser)) {
appDb.createUser({
user: appUser,
pwd: appPassword,
roles: [{ role: "readWrite", db: appDatabase }],
});
print("created app user");
} else {
appDb.updateUser(appUser, {
pwd: appPassword,
roles: [{ role: "readWrite", db: appDatabase }],
});
print("updated app user");
}
'
then
return 0
fi
echo "[WARNUNG] App-User konnte nicht synchronisiert werden; der bestehende Mongo-Volume-Stand verwendet wahrscheinlich andere Root-Credentials. Falls das ein frischer Dev-Stand ist, starte mit MONGO_RESET_DATA=1 neu." >&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"
TRIAL_EXPIRATION_DIR="$TARGET_SYSTEM_DIR/trial-expirations"
TRIAL_MODULES_OFF="library=off inventory=off student_cards=off terminplan=off"
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"
}
register_trial_expiration() {
local slug="$1"
local expires_at="$2"
local expires_epoch
expires_epoch=$(date -d "$expires_at" +%s 2>/dev/null || true)
if [ -z "$expires_epoch" ]; then
echo "[ERROR] Ungültiges Trial-Ablaufdatum für $slug: $expires_at" >&2
return 1
fi
mkdir -p "$TRIAL_EXPIRATION_DIR"
printf '%s\n' "$expires_epoch" > "$TRIAL_EXPIRATION_DIR/$slug.expiry"
}
deactivate_expired_trials() {
local now_epoch
now_epoch=$(date +%s)
for expiry_file in "$TRIAL_EXPIRATION_DIR"/*.expiry; do
[ -f "$expiry_file" ] || continue
local slug expires_epoch
slug="${expiry_file##*/}"
slug="${slug%.expiry}"
expires_epoch=$(cat "$expiry_file" 2>/dev/null || true)
if ! [[ "$expires_epoch" =~ ^[0-9]+$ ]]; then
echo "[ERROR] Ungültige Ablaufdatei: $expiry_file" >&2
continue
fi
if [ "$expires_epoch" -gt "$now_epoch" ]; then
continue
fi
echo "[INFO] Deaktiviere abgelaufene Testversion: $slug"
if (cd "$TARGET_SYSTEM_DIR" && "$TARGET_SCRIPT" module "$slug" $TRIAL_MODULES_OFF); then
rm -f "$expiry_file"
echo "[SUCCESS] Testversion deaktiviert: $slug"
else
echo "[ERROR] Testversion konnte nicht deaktiviert werden: $slug" >&2
fi
done
}
watcher_loop() {
local trigger_dir="$1"
echo "[INFO] Tenant Watcher gestartet. Lausche in $trigger_dir..."
mkdir -p "$TRIAL_EXPIRATION_DIR"
while true; do
deactivate_expired_trials
for file in "$trigger_dir"/*.json; do
[ -f "$file" ] || continue
echo "[INFO] Verarbeite Trigger: $file"
EXPIRES_AT=""
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)
EXPIRES_AT=$(jq -r '.expires_at // 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)
EXPIRES_AT=$(grep -o '"expires_at": *"[^"]*"' "$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
if [ -n "${EXPIRES_AT:-}" ]; then
register_trial_expiration "$SLUG" "$EXPIRES_AT" || true
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
;;
"deactivate")
if (cd "$TARGET_SYSTEM_DIR" && "$TARGET_SCRIPT" module "$SLUG" $TRIAL_MODULES_OFF); then
echo "[SUCCESS] Testversion deaktiviert: $SLUG"
else
echo "[ERROR] Testversion konnte nicht deaktiviert werden: $SLUG" >&2
fi
;;
"trial")
echo "[ERROR] Veralteter trial-Trigger für $SLUG: Testversionen müssen über add erstellt werden." >&2
;;
*)
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