Files
sso-mvk/install.sh
Дмитрий Мамедов 8bcfa7ec6f commit 9
2026-06-10 17:34:23 +03:00

346 lines
14 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
trap 'echo -e "\033[0;31m✘\033[0m ERROR on line $LINENO: exit code $?" >&2; exit 1' ERR
REPO="lendry/sso-mvk"
GIT_HOST="git.lendry.ru"
BRANCH="main"
INSTALL_DIR="${INSTALL_DIR:-$HOME/sso-mvk}"
SKIP_PREREQS="${SKIP_PREREQS:-false}"
PROGRESS_FILE="$INSTALL_DIR/.install-progress"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${GREEN}${NC} $1"; }
warn() { echo -e "${YELLOW}${NC} $1"; }
error() { echo -e "${RED}${NC} $1"; exit 1; }
step() { echo -e "\n${CYAN}━━━ $1 ━━━${NC}"; }
# ─── Mark progress ──────────────────────────────────────────────
mark_done() { echo "$1" >> "$PROGRESS_FILE"; }
is_done() { grep -qx "$1" "$PROGRESS_FILE" 2>/dev/null; }
# ─── Find free port (check + increment until free) ──────────────
find_free_port() {
local port=$1 max=$2
max="${max:-65535}"
while [ "$port" -le "$max" ]; do
if ! ss -tlnp "sport = :$port" 2>/dev/null | grep -q .; then
echo "$port"
return 0
fi
port=$((port + 1))
done
echo "$1"
return 1
}
# ─── Update or add a variable in .env ────────────────────────────
set_env() {
local key=$1 val=$2 file="${INSTALL_DIR}/.env"
if grep -q "^${key}=" "$file" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${val}|" "$file"
else
echo "${key}=${val}" >> "$file"
fi
}
echo -e "${CYAN}"
echo " ╔══════════════════════════════════════════════╗"
echo " ║ SSO Service — Auto Installer ║"
echo " ║ https://${GIT_HOST}/${REPO}"
echo " ╚══════════════════════════════════════════════╝"
echo -e "${NC}"
# ─── Проверка root ──────────────────────────────────────────────
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
else
SUDO="sudo"
fi
# ─── Detecting OS ────────────────────────────────────────────────
step "Detecting operating system"
OS=""
if [ -f /etc/os-release ]; then
. /etc/os-release
OS="$ID"
elif command -v lsb_release &>/dev/null; then
OS="$(lsb_release -si | tr '[:upper:]' '[:lower:]')"
else
OS="$(uname -s)"
fi
info "Detected: ${OS} ($(uname -m))"
# ─── 1. Docker ──────────────────────────────────────────────────
install_docker() {
step "1/8 Installing Docker"
if command -v docker &>/dev/null; then
info "Docker already installed ($($SUDO docker --version 2>/dev/null || docker --version))"
else
warn "Docker not found — installing..."
if command -v apt-get &>/dev/null || command -v yum &>/dev/null || command -v dnf &>/dev/null; then
curl -fsSL https://get.docker.com | $SUDO sh
else
error "Unsupported package manager. Install Docker manually: https://docs.docker.com/engine/install/"
fi
$SUDO usermod -aG docker "$USER" 2>/dev/null || true
info "Docker installed."
fi
# Ждём запуска Docker daemon (до 30 секунд)
if ! $SUDO docker info &>/dev/null; then
warn "Docker daemon not running — starting..."
$SUDO systemctl enable --now docker 2>/dev/null || $SUDO dockerd &>/dev/null &
for i in $(seq 1 15); do
sleep 2
if $SUDO docker info &>/dev/null; then
info "Docker daemon started"
break
fi
if [ "$i" -eq 15 ]; then
error "Docker daemon did not start. Check: $SUDO systemctl status docker"
fi
done
else
info "Docker daemon is running"
fi
# Docker Compose plugin
if ! $SUDO docker compose version &>/dev/null; then
warn "Docker Compose not found — installing plugin..."
$SUDO mkdir -p /usr/local/lib/docker/cli-plugins
$SUDO curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/lib/docker/cli-plugins/docker-compose
$SUDO chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
fi
info "Docker Compose: $($SUDO docker compose version 2>&1)"
}
# ─── 2. Node.js ─────────────────────────────────────────────────
install_node() {
step "2/8 Installing Node.js"
if command -v node &>/dev/null; then
NODE_MAJOR=$(node -v | sed 's/v//' | cut -d. -f1)
if [ "$NODE_MAJOR" -ge 22 ]; then
info "Node.js $(node -v) detected"
return 0
fi
warn "Node.js $(node -v) is too old — upgrading to v22..."
fi
warn "Installing Node.js 22 LTS..."
if command -v apt-get &>/dev/null; then
curl -fsSL https://deb.nodesource.com/setup_22.x | $SUDO bash -
$SUDO apt-get install -y nodejs
elif command -v dnf &>/dev/null; then
curl -fsSL https://rpm.nodesource.com/setup_22.x | $SUDO bash -
$SUDO dnf install -y nodejs
elif command -v yum &>/dev/null; then
curl -fsSL https://rpm.nodesource.com/setup_22.x | $SUDO bash -
$SUDO yum install -y nodejs
else
warn "Package manager not recognised — trying nvm..."
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 22
nvm alias default 22
fi
info "Node.js $(node -v) ready"
}
if [ "$SKIP_PREREQS" != "true" ]; then
install_docker
install_node
else
step "1-2/8 Skipping prerequisites (SKIP_PREREQS=true)"
fi
# ─── 3. Download project ─────────────────────────────────────────
step "3/8 Downloading SSO Service"
if ! is_done "download"; then
if [ -d "$INSTALL_DIR/.git" ]; then
cd "$INSTALL_DIR"
git pull --ff-only 2>/dev/null && info "Updated existing installation" || warn "Could not git pull, continuing"
else
mkdir -p "$INSTALL_DIR"
if command -v git &>/dev/null; then
git clone --depth 1 "https://$GIT_HOST/$REPO.git" "$INSTALL_DIR" 2>&1
else
warn "git not found, downloading archive..."
curl -fsSL "https://$GIT_HOST/$REPO/archive/$BRANCH.tar.gz" | tar xz --strip=1 -C "$INSTALL_DIR"
fi
info "Downloaded to $INSTALL_DIR"
fi
cd "$INSTALL_DIR"
mark_done "download"
else
cd "$INSTALL_DIR"
info "Already downloaded"
fi
# ─── 4. .env ─────────────────────────────────────────────────────
step "4/8 Configuring environment"
if ! is_done "env"; then
if [ ! -f .env ]; then
cp .env.example .env
JWT_ACCESS=$(openssl rand -hex 32 2>/dev/null || node -e "process.stdout.write(require('crypto').randomBytes(32).toString('hex'))")
JWT_REFRESH=$(openssl rand -hex 32 2>/dev/null || node -e "process.stdout.write(require('crypto').randomBytes(32).toString('hex'))")
if [ "$(uname -s)" = "Darwin" ]; then
sed -i '' "s/change-me-access-secret-at-least-32-chars/$JWT_ACCESS/" .env
sed -i '' "s/change-me-refresh-secret-at-least-32-chars/$JWT_REFRESH/" .env
else
sed -i "s/change-me-access-secret-at-least-32-chars/$JWT_ACCESS/" .env
sed -i "s/change-me-refresh-secret-at-least-32-chars/$JWT_REFRESH/" .env
fi
info "Created .env with random JWT secrets"
else
info ".env already exists, keeping it"
fi
mark_done "env"
else
info "Environment already configured"
fi
set -a; source .env; set +a
# ─── 5. Docker infrastructure ────────────────────────────────────
step "5/8 Starting infrastructure (PostgreSQL, Redis, RabbitMQ)"
# Проверка и переназначение занятых портов
PORT_MAP=""
for pair in "POSTGRES_PORT:5432" "REDIS_PORT:6379" "RABBITMQ_PORT:5672" "RABBITMQ_UI_PORT:15672"; do
var="${pair%%:*}"
default="${pair#*:}"
current=$(grep "^${var}=" "$INSTALL_DIR/.env" 2>/dev/null | cut -d= -f2) || true
current="${current:-$default}"
if ss -tlnp "sport = :$current" 2>/dev/null | grep -q .; then
warn "Port $current ($var) is busy — finding free port..."
new_port=$(find_free_port "$((current + 1))" 2>/dev/null) || new_port=$((current + 100))
set_env "$var" "$new_port"
info " $var changed: $current$new_port"
PORT_MAP="$PORT_MAP $var=$new_port"
else
info " $var$current"
fi
done
# Если порты изменились — перечитать .env
if [ -n "$PORT_MAP" ]; then
set -a; source "$INSTALL_DIR/.env"; set +a
info "Ports adjusted — re-sourced .env"
fi
# Обновить DATABASE_URL если POSTGRES_PORT изменился
POSTGRES_PORT_CUR=$(grep "^POSTGRES_PORT=" "$INSTALL_DIR/.env" 2>/dev/null | cut -d= -f2) || true
PG_USER=$(grep "^POSTGRES_USER=" "$INSTALL_DIR/.env" 2>/dev/null | cut -d= -f2) || true
PG_PASS=$(grep "^POSTGRES_PASSWORD=" "$INSTALL_DIR/.env" 2>/dev/null | cut -d= -f2) || true
PG_DB=$(grep "^POSTGRES_DB=" "$INSTALL_DIR/.env" 2>/dev/null | cut -d= -f2) || true
NEW_URL="postgresql://${PG_USER}:${PG_PASS}@localhost:${POSTGRES_PORT_CUR}/${PG_DB}"
set_env "DATABASE_URL" "$NEW_URL"
export DATABASE_URL="$NEW_URL"
# Остановить предыдущие контейнеры (если есть), потом запустить заново
$SUDO docker compose down --remove-orphans 2>/dev/null || true
if ! $SUDO docker compose up -d --force-recreate --remove-orphans; then
warn "Docker Compose output:"
$SUDO docker compose logs --tail=30 2>/dev/null || true
error "Containers failed to start. Check ports and run: ${SUDO}docker compose logs"
fi
info "Waiting for PostgreSQL (up to 60s)..."
DB_READY=false
for i in $(seq 1 30); do
if $SUDO docker compose exec -T postgres pg_isready -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" &>/dev/null; then
DB_READY=true
info "PostgreSQL is ready"
break
fi
if [ "$i" -eq 30 ]; then
warn "PostgreSQL logs (last 20 lines):"
$SUDO docker compose logs --tail=20 postgres 2>/dev/null || true
error "PostgreSQL did not start. Check ${INSTALL_DIR}/.env for DB credentials and port 5432 availability."
fi
sleep 2
done
info "Waiting for Redis..."
for i in $(seq 1 15); do
if $SUDO docker compose exec -T redis redis-cli ping 2>/dev/null | grep -q PONG; then
info "Redis is ready"
break
fi
if [ "$i" -eq 15 ]; then
warn "Redis did not respond — continuing anyway..."
fi
sleep 2
done
# ─── 6. Dependencies & Prisma ────────────────────────────────────
step "6/8 Installing dependencies"
npm ci --no-audit --no-fund || error "'npm ci' failed. Check network / ${INSTALL_DIR}/package-lock.json"
info "npm dependencies installed"
npx prisma generate || error "'prisma generate' failed."
info "Prisma client generated"
export DATABASE_URL
npx prisma db push --skip-generate || error "'prisma db push' failed. Is PostgreSQL accessible? Check ${INSTALL_DIR}/.env DATABASE_URL"
info "Database schema applied"
# ─── 7. Seed ─────────────────────────────────────────────────────
step "7/8 Seeding database"
npx ts-node -P tsconfig.json prisma/seed.ts || error "Seed failed."
info "Seed data created"
# ─── 8. Build & Start ────────────────────────────────────────────
step "8/8 Building and starting SSO Service"
npm run build || error "'npm run build' failed. Check TypeScript errors above."
info "Application built"
# Остановить предыдущий процесс, если был
if [ -f "$INSTALL_DIR/.sso.pid" ]; then
kill "$(cat "$INSTALL_DIR/.sso.pid")" 2>/dev/null || true
rm -f "$INSTALL_DIR/.sso.pid"
fi
pm2 delete sso-service 2>/dev/null || true
if command -v pm2 &>/dev/null; then
pm2 start npm --name "sso-service" -- run start:prod
pm2 save 2>/dev/null || true
info "Started with PM2"
else
PORT="${PORT:-3000}" nohup node dist/main > "$INSTALL_DIR/sso.log" 2>&1 &
echo $! > "$INSTALL_DIR/.sso.pid"
info "Started in background (PID $(cat "$INSTALL_DIR/.sso.pid")) — logs: $INSTALL_DIR/sso.log"
fi
sleep 3
if curl -sfo /dev/null "http://localhost:${PORT:-3000}/metrics" 2>/dev/null; then
info "Service is running on http://localhost:${PORT:-3000}"
else
warn "Service may still be starting... check: $INSTALL_DIR/sso.log"
fi
# ─── Done ────────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}════════════════════════════════════════════════${NC}"
echo -e "${GREEN} SSO Service installed and running!${NC}"
echo -e "${GREEN}════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${CYAN}Web panel:${NC} http://localhost:${PORT:-3000}"
echo -e " ${CYAN}Admin login:${NC} admin@sso.local / admin123!"
echo -e " ${CYAN}Metrics:${NC} http://localhost:${PORT:-3000}/metrics"
echo -e " ${CYAN}RabbitMQ UI:${NC} http://localhost:15672 (${RABBITMQ_USER:-sso} / ${RABBITMQ_PASSWORD:-sso_secret})"
echo ""
echo -e " ${YELLOW}Install dir:${NC} $INSTALL_DIR"
echo -e " ${YELLOW}Log file:${NC} $INSTALL_DIR/sso.log"
echo ""
echo -e " ${YELLOW}Stop:${NC} $(command -v pm2 &>/dev/null && echo 'pm2 stop sso-service' || echo "kill \$(cat $INSTALL_DIR/.sso.pid)")"
echo -e " ${YELLOW}Update:${NC} cd $INSTALL_DIR && git pull && npm ci && npm run build && npm run start:prod"
echo -e " ${YELLOW}Re-run:${NC} curl -fsSL https://${GIT_HOST}/${REPO}/raw/branch/${BRANCH}/install.sh | bash"
echo ""
echo -e " ${GREEN}Open http://localhost:${PORT:-3000} in your browser${NC}"
echo ""