#!/usr/bin/env bash set -euo pipefail REPO_URL="https://git.lohmar.co.uk/lexton-it/NextWks.git" BUILD_DIR="/tmp/nextwks-build" TARGET_DIR="/opt/nextworkspace" BACKUP_DIR="/opt/backup" NETWORK_NAME="nextwks-net" HEALTH_CHECK_RETRIES=10 HEALTH_CHECK_INTERVAL=2 usage() { echo "Usage: $0 [--install|--update|--destroy]" echo " --install First-time setup on a bare VM (prompts for config)" echo " --update Smart update: pull, build, copy, bounce containers" echo " --destroy Full greenfield redeploy (uses saved secrets)" exit 1 } [ $# -eq 0 ] && usage MODE="${1#--}" case "$MODE" in install|update|destroy) ;; *) usage ;; esac # --- Auto-escalate to root if not already --- if [ "$(id -u)" -ne 0 ]; then echo "[*] Escalating to root..." exec sudo bash "$(realpath "$0")" "$@" fi # --- Load existing env (if any) --- if [ -f "$BACKUP_DIR/.env" ]; then set -a; source "$BACKUP_DIR/.env"; set +a elif [ -f "$TARGET_DIR/.env" ]; then set -a; source "$TARGET_DIR/.env"; set +a fi DOMAIN="${DOMAIN:-nextwks.eu}" echo "=== NextWorkspace ${MODE} ===" # ============================================================ # 1. INSTALL MODE — first-time setup # ============================================================ if [ "$MODE" = "install" ]; then echo "[*] Installing system dependencies..." apt-get update -qq && apt-get install -y -qq git build-essential curl podman podman-compose if ! command -v go &>/dev/null; then echo "[*] Installing Go..." GO_VERSION=$(curl -sL https://go.dev/VERSION?m=text) GO_URL="https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz" curl -sL "$GO_URL" -o /tmp/go.tar.gz rm -rf /usr/local/go tar -C /usr/local -xzf /tmp/go.tar.gz rm /tmp/go.tar.gz echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh chmod +x /etc/profile.d/go.sh export PATH=$PATH:/usr/local/go/bin fi echo "" echo "--- NextWorkspace Configuration ---" read -p "Domain [nextwks.eu]: " input; DOMAIN="${input:-$DOMAIN}" read -p "TLS email (Let's Encrypt): " TLS_EMAIL while [ -z "$TLS_EMAIL" ]; do read -p "TLS email (required): " TLS_EMAIL; done while echo "$TLS_EMAIL" | grep -qv '@'; do read -p "Invalid email: " TLS_EMAIL; done read -p "Admin username: " ADMIN_USERNAME while [ -z "$ADMIN_USERNAME" ]; do read -p "Admin username (required): " ADMIN_USERNAME; done ADMIN_PASSWORD=$(openssl rand -hex 6) # 12 chars, no pipefail issues echo "" echo "========================================" echo " Domain: $DOMAIN" echo " TLS email: $TLS_EMAIL" echo " Admin username: $ADMIN_USERNAME" echo " Admin password: $ADMIN_PASSWORD" echo " Save this password — it won't be shown again!" echo "========================================" echo "" read -p "SMTP host [smtp.openxchange.eu]: " SMTP_HOST; SMTP_HOST="${SMTP_HOST:-smtp.openxchange.eu}" read -p "SMTP port [587]: " SMTP_PORT; SMTP_PORT="${SMTP_PORT:-587}" read -p "SMTP user [post@nextwks.eu]: " SMTP_USER; SMTP_USER="${SMTP_USER:-post@nextwks.eu}" read -sp "SMTP password: " SMTP_PASS; echo "" [ -z "$SMTP_PASS" ] && echo "ERROR: SMTP password required" && exit 1 # Persist config to backup vault mkdir -p "$BACKUP_DIR" cat > "$BACKUP_DIR/.env" </dev/null || echo "$HOME") cp "$BUILD_DIR/tools/nextwks.sh" "$USER_HOME/nextwks.sh" chmod +x "$USER_HOME/nextwks.sh" echo "[*] Saved to $USER_HOME/nextwks.sh — use it for future updates" fi # ============================================================ # 3. BUILD static binary # ============================================================ echo "[*] Building static binary..." export PATH=$PATH:/usr/local/go/bin CGO_ENABLED=0 go build -o nextworkspace . # ============================================================ # 4. CREATE target directory structure # ============================================================ mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \ "$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \ "$TARGET_DIR/compose" "$TARGET_DIR/www" \ "$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs" # ============================================================ # 5. TEARDOWN (destroy mode only) # ============================================================ if [ "$MODE" = "destroy" ]; then echo "[*] Full teardown..." podman stop caddy authelia launcher 2>/dev/null || true podman rm caddy authelia launcher 2>/dev/null || true rm -rf "$TARGET_DIR" mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \ "$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \ "$TARGET_DIR/compose" "$TARGET_DIR/www" \ "$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs" fi # ============================================================ # 6. COPY artifacts to target # ============================================================ echo "[*] Copying artifacts..." cp nextworkspace "$TARGET_DIR/nextworkspace" cp "$BUILD_DIR/VERSION" "$TARGET_DIR/VERSION" if [ -d "$BUILD_DIR/config/www" ]; then cp -r "$BUILD_DIR/config/www"/* "$TARGET_DIR/www/" fi if [ -d "$BUILD_DIR/lng" ]; then rm -rf "$TARGET_DIR/lng" cp -r "$BUILD_DIR/lng" "$TARGET_DIR/lng" fi if [ -d "$BUILD_DIR/config/nextworkspace" ]; then cp -r "$BUILD_DIR/config/nextworkspace"/* "$TARGET_DIR/config/nextworkspace/" fi # Restore .env from backup if [ -f "$BACKUP_DIR/.env" ]; then cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env" chmod 644 "$TARGET_DIR/.env" fi # ============================================================ # 7. GENERATE config files with placeholder substitution # (install + destroy modes; update skips to keep secrets stable) # ============================================================ if [ "$MODE" != "update" ]; then echo "[*] Generating config files..." # Caddyfile sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \ "$BUILD_DIR/config/caddy/Caddyfile" > "$TARGET_DIR/config/caddy/Caddyfile" # Authelia config — preserve existing secrets if present JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}" SESSION_SECRET="${SESSION_SECRET:-$(openssl rand -hex 32)}" sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{JWT_SECRET}|$JWT_SECRET|g" \ -e "s|{SESSION_SECRET}|$SESSION_SECRET|g" \ -e "s|{SMTP_HOST}|${SMTP_HOST:-smtp.openxchange.eu}|g" \ -e "s|{SMTP_PORT}|${SMTP_PORT:-587}|g" \ -e "s|{SMTP_USER}|${SMTP_USER:-post@nextwks.eu}|g" \ -e "s|{SMTP_PASS}|$SMTP_PASS|g" \ "$BUILD_DIR/config/authelia/configuration.yml" > "$TARGET_DIR/config/authelia/configuration.yml" # Users database ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-}" if [ -z "$ADMIN_PASSWORD_HASH" ] && [ -n "${ADMIN_PASSWORD:-}" ]; then ADMIN_PASSWORD_HASH=$(cd "$BUILD_DIR" && go run ./tools/hash-password/ "$ADMIN_PASSWORD" 2>/dev/null || echo "") fi sed -e "s|{ADMIN_PASSWORD_HASH}|$ADMIN_PASSWORD_HASH|g" \ -e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \ "$BUILD_DIR/config/authelia/users_database.yml" > "$TARGET_DIR/config/authelia/users_database.yml" # Persist generated secrets so --destroy is idempotent if [ -f "$BACKUP_DIR/.env" ]; then # Update existing .env with any new secrets sed -i "/^JWT_SECRET=/d; /^SESSION_SECRET=/d; /^ADMIN_PASSWORD_HASH=/d" "$BACKUP_DIR/.env" 2>/dev/null || true fi echo "JWT_SECRET=$JWT_SECRET" >> "$BACKUP_DIR/.env" echo "SESSION_SECRET=$SESSION_SECRET" >> "$BACKUP_DIR/.env" [ -n "$ADMIN_PASSWORD_HASH" ] && echo "ADMIN_PASSWORD_HASH=$ADMIN_PASSWORD_HASH" >> "$BACKUP_DIR/.env" chmod 600 "$BACKUP_DIR/.env" fi # ============================================================ # 8. DEPLOY stack # ============================================================ echo "[*] Deploying containers on $NETWORK_NAME..." # Ensure network exists podman network create "$NETWORK_NAME" 2>/dev/null || true # AUTHELIA_SECRET is SESSION_SECRET (Authelia session.secret) AUTHELIA_SECRET="${SESSION_SECRET:-}" if [ -z "$AUTHELIA_SECRET" ]; then # Update mode — extract from existing config AUTHELIA_SECRET=$(sed -n '/^session:/,/^[a-z]/p' "$TARGET_DIR/config/authelia/configuration.yml" \ | grep 'secret:' | awk '{print $2}' 2>/dev/null || echo "") fi # Generate compose file with substituted secret sed -e "s|{AUTHELIA_SECRET}|$AUTHELIA_SECRET|g" \ "$BUILD_DIR/compose/stack.yaml" > "$TARGET_DIR/compose/stack.yaml" podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true sleep 1 podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues" # ============================================================ # 9. HEALTH CHECK # ============================================================ echo "[*] Running health check..." for i in $(seq 1 $HEALTH_CHECK_RETRIES); do HEALTH=$(podman exec launcher wget -qO- http://127.0.0.1:9000/health 2>/dev/null || echo "") if [ "$HEALTH" = "OK" ]; then echo "[OK] NextWorkspace launcher is healthy" echo "[OK] https://$DOMAIN/" exit 0 fi sleep $HEALTH_CHECK_INTERVAL done echo "[FAIL] Health check failed — launcher did not respond" exit 1