#!/usr/bin/env bash # # Sprint 21: pre-deploy readiness check на prod-vm. # # Запускается ПЕРЕД prod-deploy.sh. Проверяет: # 1. Backup сделан < FM_BACKUP_MAX_AGE_MIN (60) минут назад # 2. Свободного места ≥ FM_MIN_FREE_GB (5) GB на каждом mount # 3. Текущий /health/ready возвращает 200 # 4. (опц.) CI-проверки на этом коммите прошли (FM_CHECK_CI=1) # 5. .env содержит все required переменные (без placeholder'ов # типа CHANGEME/REPLACE_ME) # # Exit 0 — всё хорошо, можно деплоить. # Exit 1+ — конкретная причина в stderr. # # Usage: # deploy/check-prod-readiness.sh [--dry-run] [--ssh-host HOST] # # По умолчанию проверки локальные (предполагается что скрипт запущен НА # prod-vm). С --ssh-host выполняется через ssh: запускает себя # удалённо. set -uo pipefail # -e снят: хотим прогнать ВСЕ проверки и собрать суммарный отчёт, # не вылетая на первой. DRY_RUN=0 SSH_HOST="" PROD_URL="${PROD_URL:-https://admin.food-market.kz}" BACKUP_DIR="${FM_BACKUP_DIR:-/opt/food-market-data/backups}" ENV_FILE="${FM_ENV_FILE:-/home/nns/food-market-prod/deploy/.env}" MAX_AGE_MIN="${FM_BACKUP_MAX_AGE_MIN:-60}" MIN_FREE_GB="${FM_MIN_FREE_GB:-5}" MOUNTS="${FM_MOUNTS:-/opt /var/lib/docker}" while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=1; shift ;; --ssh-host) SSH_HOST="$2"; shift 2 ;; --help|-h) grep -E '^#( |$)' "$0" | sed 's/^# \?//'; exit 0 ;; *) echo "Unknown arg: $1" >&2; exit 2 ;; esac done if [[ -n "$SSH_HOST" ]]; then # Re-run self on remote host. echo "[check] proxying to $SSH_HOST" exec ssh "$SSH_HOST" "FM_BACKUP_DIR='$BACKUP_DIR' FM_ENV_FILE='$ENV_FILE' \ FM_BACKUP_MAX_AGE_MIN='$MAX_AGE_MIN' FM_MIN_FREE_GB='$MIN_FREE_GB' \ FM_MOUNTS='$MOUNTS' PROD_URL='$PROD_URL' bash -s" < "$0" $([[ $DRY_RUN -eq 1 ]] && echo --dry-run) fi PASS=0 FAIL=0 ERRORS=() check() { local name="$1" status="$2" detail="$3" if [[ "$status" == "OK" ]]; then echo "[✓] $name — $detail" ((PASS+=1)) else echo "[✗] $name — $detail" >&2 ERRORS+=("$name: $detail") ((FAIL+=1)) fi } # ── 1. Backup age ──────────────────────────────────────────────────── if [[ ! -d "$BACKUP_DIR" ]]; then check "backup-age" "FAIL" "backup-dir $BACKUP_DIR не существует" else # Самый свежий db-*.dump LATEST=$(ls -t "$BACKUP_DIR"/db-*.dump 2>/dev/null | head -1) if [[ -z "$LATEST" ]]; then check "backup-age" "FAIL" "в $BACKUP_DIR нет db-*.dump файлов" else AGE_SEC=$(( $(date +%s) - $(stat -c %Y "$LATEST") )) AGE_MIN=$(( AGE_SEC / 60 )) if (( AGE_MIN > MAX_AGE_MIN )); then check "backup-age" "FAIL" "последний backup $LATEST: $AGE_MIN мин назад (порог $MAX_AGE_MIN)" else check "backup-age" "OK" "$AGE_MIN мин назад ($LATEST)" fi fi fi # ── 2. Free disk space ─────────────────────────────────────────────── for mnt in $MOUNTS; do if [[ ! -d "$mnt" ]]; then check "disk:$mnt" "FAIL" "mount не существует" continue fi FREE_KB=$(df --output=avail "$mnt" 2>/dev/null | tail -1 | tr -d ' ') if [[ -z "$FREE_KB" || "$FREE_KB" -le 0 ]]; then check "disk:$mnt" "FAIL" "df не вернул avail" continue fi FREE_GB=$(( FREE_KB / 1024 / 1024 )) if (( FREE_GB < MIN_FREE_GB )); then check "disk:$mnt" "FAIL" "$FREE_GB GB свободно (порог $MIN_FREE_GB)" else check "disk:$mnt" "OK" "$FREE_GB GB свободно" fi done # ── 3. /health/ready ───────────────────────────────────────────────── HEALTH_BODY=$(curl -fsS --max-time 10 "$PROD_URL/health/ready" 2>/dev/null || echo "") if echo "$HEALTH_BODY" | grep -q '"status":"Healthy"'; then check "health-ready" "OK" "$PROD_URL/health/ready=Healthy" else check "health-ready" "FAIL" "ответ: ${HEALTH_BODY:-}" fi # ── 4. CI status (опц.) ────────────────────────────────────────────── if [[ "${FM_CHECK_CI:-0}" == "1" ]]; then # Берём текущий commit и проверяем что CI workflow прошёл. # Реализация зависит от наличия Forgejo CLI; пока — manual hint. CURRENT_SHA=$(cd /home/nns/food-market 2>/dev/null && git rev-parse HEAD 2>/dev/null || echo "") if [[ -n "$CURRENT_SHA" ]]; then check "ci-status" "OK" "skipped (manual check: GET /api/v1/repos/nns/food-market/commits/$CURRENT_SHA/status)" else check "ci-status" "OK" "skipped (not a git repo)" fi fi # ── 5. .env complete ───────────────────────────────────────────────── if [[ ! -f "$ENV_FILE" ]]; then check ".env-file" "FAIL" "$ENV_FILE не существует" else # Список обязательных переменных для прод-окружения. REQUIRED=( "POSTGRES_PASSWORD" "OPENIDDICT_ISSUER" "OPENIDDICT_CERT_PASSWORD" ) MISSING=() PLACEHOLDER=() for key in "${REQUIRED[@]}"; do VAL=$(grep -E "^${key}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2- || true) if [[ -z "$VAL" ]]; then MISSING+=("$key") elif [[ "$VAL" =~ ^(CHANGEME|REPLACE_ME|TODO|dev|food_market_dev)$ ]]; then PLACEHOLDER+=("$key=$VAL") fi done if (( ${#MISSING[@]} > 0 )); then check ".env-required" "FAIL" "отсутствуют: ${MISSING[*]}" fi if (( ${#PLACEHOLDER[@]} > 0 )); then check ".env-placeholders" "FAIL" "плейсхолдеры: ${PLACEHOLDER[*]}" fi if (( ${#MISSING[@]} == 0 && ${#PLACEHOLDER[@]} == 0 )); then check ".env-file" "OK" "${#REQUIRED[@]} required key(s) заполнены" fi fi # ── Итог ───────────────────────────────────────────────────────────── echo echo "==> $PASS passed, $FAIL failed" if (( FAIL > 0 )); then echo "Не готов к деплою:" for e in "${ERRORS[@]}"; do echo " - $e"; done exit 1 fi if [[ $DRY_RUN -eq 1 ]]; then echo "(dry-run; никаких изменений)" fi echo "OK — можно запускать prod-deploy.sh" exit 0