diff --git a/tools/nextwks.sh b/tools/nextwks.sh index 56d4b88..e5cba05 100755 --- a/tools/nextwks.sh +++ b/tools/nextwks.sh @@ -21,16 +21,10 @@ 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 - -# Determine the non-root user for file ownership -RUN_USER="${SUDO_USER:-${USER:-master}}" -RUN_UID=$(id -u "$RUN_USER" 2>/dev/null || echo 1000) -RUN_GID=$(id -g "$RUN_USER" 2>/dev/null || echo 1000) +# Helper: run with sudo if not already root +maybe_sudo() { + if [ "$(id -u)" -eq 0 ]; then "$@"; else sudo "$@"; fi +} # --- Load existing env (if any) --- if [ -f "$BACKUP_DIR/.env" ]; then @@ -47,18 +41,19 @@ echo "=== NextWorkspace ${MODE} ===" # ============================================================ if [ "$MODE" = "install" ]; then echo "[*] Installing system dependencies..." - apt-get update -qq && apt-get install -y -qq git build-essential curl podman podman-compose + maybe_sudo apt-get update -qq + maybe_sudo 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 + maybe_sudo rm -rf /usr/local/go + maybe_sudo 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 + maybe_sudo sh -c 'echo "export PATH=\$PATH:/usr/local/go/bin" > /etc/profile.d/go.sh' + maybe_sudo chmod +x /etc/profile.d/go.sh export PATH=$PATH:/usr/local/go/bin fi @@ -70,7 +65,7 @@ if [ "$MODE" = "install" ]; then 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 + ADMIN_PASSWORD=$(openssl rand -hex 6) echo "" echo "========================================" @@ -92,8 +87,8 @@ if [ "$MODE" = "install" ]; then read -p "IMAP port [993]: " IMAP_PORT; IMAP_PORT="${IMAP_PORT:-993}" # Persist config to backup vault - mkdir -p "$BACKUP_DIR" - cat > "$BACKUP_DIR/.env" < '$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" + cp "$BUILD_DIR/tools/nextwks.sh" "$HOME/nextwks.sh" + chmod +x "$HOME/nextwks.sh" + echo "[*] Saved to $HOME/nextwks.sh — use it for future updates" fi # ============================================================ @@ -133,58 +127,61 @@ export PATH=$PATH:/usr/local/go/bin CGO_ENABLED=0 go build -o nextworkspace . # ============================================================ -# 4. CREATE target directory structure +# 4. CREATE target & backup directories (as root) # ============================================================ -mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \ +maybe_sudo 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" + "$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs" \ + "$BACKUP_DIR" # ============================================================ # 5. TEARDOWN (destroy mode only — wipes target dir) # ============================================================ if [ "$MODE" = "destroy" ]; then echo "[*] Full teardown..." - rm -rf "$TARGET_DIR" - mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \ + maybe_sudo rm -rf "$TARGET_DIR" + maybe_sudo 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 +# 6. COPY artifacts to target (as root) # ============================================================ echo "[*] Copying artifacts..." -cp nextworkspace "$TARGET_DIR/nextworkspace" -cp "$BUILD_DIR/VERSION" "$TARGET_DIR/VERSION" +maybe_sudo cp nextworkspace "$TARGET_DIR/nextworkspace" +maybe_sudo cp "$BUILD_DIR/VERSION" "$TARGET_DIR/VERSION" if [ -d "$BUILD_DIR/config/www" ]; then - cp -r "$BUILD_DIR/config/www"/* "$TARGET_DIR/www/" + maybe_sudo 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" + maybe_sudo rm -rf "$TARGET_DIR/lng" + maybe_sudo 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/" + maybe_sudo 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" + maybe_sudo cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env" + maybe_sudo chmod 644 "$TARGET_DIR/.env" fi # ============================================================ # 7. GENERATE config files with placeholder substitution -# (install + destroy modes; update skips to keep secrets stable) +# Write to /tmp first, then sudo cp to target # ============================================================ +GEN_DIR=$(mktemp -d) +trap "rm -rf '$GEN_DIR'" EXIT + 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" + "$BUILD_DIR/config/caddy/Caddyfile" > "$GEN_DIR/Caddyfile" # Authelia config — preserve existing secrets if present JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}" @@ -195,7 +192,7 @@ if [ "$MODE" != "update" ]; then -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" + "$BUILD_DIR/config/authelia/configuration.yml" > "$GEN_DIR/configuration.yml" # Users database ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-}" @@ -204,53 +201,56 @@ if [ "$MODE" != "update" ]; then 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" + "$BUILD_DIR/config/authelia/users_database.yml" > "$GEN_DIR/users_database.yml" + + # Copy generated configs to target + maybe_sudo cp "$GEN_DIR/Caddyfile" "$TARGET_DIR/config/caddy/Caddyfile" + maybe_sudo cp "$GEN_DIR/configuration.yml" "$TARGET_DIR/config/authelia/configuration.yml" + maybe_sudo cp "$GEN_DIR/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 + maybe_sudo 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" + maybe_sudo bash -c "echo 'JWT_SECRET=$JWT_SECRET' >> '$BACKUP_DIR/.env'" + maybe_sudo bash -c "echo 'SESSION_SECRET=$SESSION_SECRET' >> '$BACKUP_DIR/.env'" + [ -n "$ADMIN_PASSWORD_HASH" ] && maybe_sudo bash -c "echo 'ADMIN_PASSWORD_HASH=$ADMIN_PASSWORD_HASH' >> '$BACKUP_DIR/.env'" + maybe_sudo chmod 600 "$BACKUP_DIR/.env" fi # ============================================================ -# 8. FIX OWNERSHIP — chown to the non-root user +# 8. FIX OWNERSHIP — all files in TARGET_DIR/BACKUP_DIR to user # ============================================================ +RUN_USER="${SUDO_USER:-${USER}}" echo "[*] Setting file ownership to $RUN_USER..." -chown -R "$RUN_UID:$RUN_GID" "$TARGET_DIR" 2>/dev/null || true -chown -R "$RUN_UID:$RUN_GID" "$BACKUP_DIR" 2>/dev/null || true +maybe_sudo chown -R "$RUN_USER:" "$TARGET_DIR" 2>/dev/null || true +maybe_sudo chown -R "$RUN_USER:" "$BACKUP_DIR" 2>/dev/null || true # ============================================================ -# 9. STOP old containers (all modes) +# 9. STOP old containers (all modes, as root) # ============================================================ echo "[*] Stopping any previous containers..." for c in caddy authelia launcher; do - podman stop "$c" 2>/dev/null && echo " stopped $c" || true - podman rm "$c" 2>/dev/null && echo " removed $c" || true + maybe_sudo podman stop "$c" 2>/dev/null && echo " stopped $c" || true + maybe_sudo podman rm "$c" 2>/dev/null && echo " removed $c" || true done sleep 1 # ============================================================ # 10. PORT CHECK — before deploy # ============================================================ -if ss -tlnp 2>/dev/null | grep -q ':80 '; then +if maybe_sudo ss -tlnp 2>/dev/null | grep -q ':80 '; then echo "==============================================" echo "[WARN] Port 80 is already in use." - ss -tlnp 2>/dev/null | grep ':80 ' + maybe_sudo ss -tlnp 2>/dev/null | grep ':80 ' echo "" echo " This is usually Caddy from a previous run." - echo " Run this to stop it, then re-run deploy:" - echo " podman stop caddy && podman rm caddy" echo "==============================================" echo "" read -p "Stop the container on port 80 now? [y/N]: " KILL if [ "$KILL" = "y" ] || [ "$KILL" = "Y" ]; then - podman stop caddy 2>/dev/null || true - podman rm caddy 2>/dev/null || true + maybe_sudo podman stop caddy 2>/dev/null || true + maybe_sudo podman rm caddy 2>/dev/null || true sleep 2 echo "[OK] Port 80 freed." else @@ -260,35 +260,34 @@ if ss -tlnp 2>/dev/null | grep -q ':80 '; then fi # ============================================================ -# 11. DEPLOY stack +# 11. DEPLOY stack (as root — needs port 80/443) # ============================================================ echo "[*] Deploying containers on $NETWORK_NAME..." -# Ensure network exists -podman network create "$NETWORK_NAME" 2>/dev/null || true +maybe_sudo 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" \ + AUTHELIA_SECRET=$(maybe_sudo 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" + "$BUILD_DIR/compose/stack.yaml" > "$GEN_DIR/stack.yaml" +maybe_sudo cp "$GEN_DIR/stack.yaml" "$TARGET_DIR/compose/stack.yaml" -podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true +maybe_sudo 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" +maybe_sudo podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues" # ============================================================ -# 11. HEALTH CHECK +# 12. HEALTH CHECK # ============================================================ echo "[*] Running health check..." for i in $(seq 1 $HEALTH_CHECK_RETRIES); do - HEALTH=$(podman exec launcher curl -sf http://127.0.0.1:9000/health 2>/dev/null || echo "") + HEALTH=$(maybe_sudo podman exec launcher curl -sf 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/" @@ -300,8 +299,8 @@ done echo "[FAIL] Health check failed — launcher did not respond" echo "" echo "--- Container status ---" -podman ps -a --filter "name=caddy|authelia|launcher" 2>/dev/null || true +maybe_sudo podman ps -a --filter "name=caddy|authelia|launcher" 2>/dev/null || true echo "" echo "--- Launcher logs (last 20 lines) ---" -podman logs launcher --tail 20 2>/dev/null || echo " (no logs)" +maybe_sudo podman logs launcher --tail 20 2>/dev/null || echo " (no logs)" exit 1