alembic fix

This commit is contained in:
2026-06-01 16:34:07 +03:00
parent e12a9ddfce
commit 4c5da7e4d9
15 changed files with 346 additions and 90 deletions

View File

@@ -2,14 +2,17 @@
set -euo pipefail
# #region docker.backend.entrypoint [C:4] [TYPE Module] [SEMANTICS docker,entrypoint,enterprise,certs]
# @BRIEF Container entrypoint — установка корп. сертификатов, bootstrap admin,
# затем запуск backend (exec "$@").
# @BRIEF Container entrypoint — установка корп. сертификатов, wait-for-DB,
# Alembic миграции, bootstrap admin, затем запуск backend (exec "$@").
# @PRE Python runtime и backend source доступны в /app/backend.
# Переменная CERTS_PATH указывает на директорию с .crt файлами (или пусто).
# DATABASE_URL установлен и указывает на доступный PostgreSQL.
# @POST Сертификаты установлены в системное хранилище и NSS базу Chromium.
# БД подтверждена как доступная. Alembic миграции применены.
# Admin создан (если INITIAL_ADMIN_CREATE=true). Backend запущен через exec "$@".
# @SIDE_EFFECT Модифицирует /usr/local/share/ca-certificates/ и вызывает update-ca-certificates.
# Импортирует CA в NSS DB (~/.pki/nssdb) для Chromium.
# Выполняет Alembic upgrade head.
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [backend/src/scripts/create_admin.py]
# @RELATION DEPENDS_ON -> [backend/src/scripts/init_auth_db.py]
@@ -18,6 +21,9 @@ set -euo pipefail
# (create_admin.py идемпотентен).
# @INVARIANT Повторный запуск install_llm_ca_certs не дублирует сертификаты в bundle.
# @INVARIANT Повторный запуск install_ca_to_nss не дублирует сертификаты в NSS DB.
# @INVARIANT wait_for_db exit(1) если БД недоступна после DB_WAIT_ATTEMPTS попыток —
# контейнер не продолжит запуск с недоступной БД.
# @INVARIANT Alembic миграции выполняются ТОЛЬКО в entrypoint, НЕ в app.py lifespan.
# #endregion docker.backend.entrypoint
# ======================================================================
@@ -420,6 +426,73 @@ install_ca_to_nss() {
}
# #endregion docker.backend.entrypoint.install_ca_to_nss
# ======================================================================
# wait_for_db — проверка доступности БД с retry
# ======================================================================
# #region docker.backend.entrypoint.wait_for_db [C:3] [TYPE Function]
# @BRIEF Проверяет доступность PostgreSQL с retry-логикой перед запуском миграций.
# @PRE DATABASE_URL установлен и указывает на валидный PostgreSQL.
# @POST Соединение с БД подтверждено (SELECT 1) или скрипт exit(1) после таймаута.
# @RATIONALE Без wait-for-db enterprise-деплой с внешним PostgreSQL падает
# мгновенно — entrypoint интерпретирует connection refused как "пустая БД".
# @REJECTED Использование pg_isready напрямую отвергнуто — клиент psql может
# отсутствовать в python:3.11-slim образе. SQLAlchemy-проверка универсальна.
wait_for_db() {
local max_attempts="${DB_WAIT_ATTEMPTS:-30}"
local delay="${DB_WAIT_DELAY:-2}"
local attempt=0
echo "[entrypoint] Waiting for database (max ${max_attempts} attempts, ${delay}s delay)..."
while [ $attempt -lt $max_attempts ]; do
attempt=$((attempt + 1))
# Capture stderr to detect permanent errors (KeyError, ArgumentError)
local _err_output
_err_output="$(python3 -c "
import os, sys
from sqlalchemy import create_engine, text
try:
url = os.environ['DATABASE_URL']
except KeyError:
print('FATAL: DATABASE_URL environment variable is not set', file=sys.stderr)
sys.exit(99)
try:
e = create_engine(url)
with e.connect() as c:
c.execute(text('SELECT 1'))
except Exception as exc:
# Permanent errors — no point retrying
err_str = str(exc).lower()
if 'database_url' in err_str or 'could not parse' in err_str or 'invalid' in err_str:
print(f'FATAL: {exc}', file=sys.stderr)
sys.exit(99)
sys.exit(1)
" 2>&1)"
local _rc=$?
if [ $_rc -eq 0 ]; then
echo "[entrypoint] ✅ Database is reachable"
return 0
fi
# Permanent error — exit immediately, don't waste retries
if [ $_rc -eq 99 ]; then
echo "[entrypoint] ❌ $_err_output" >&2
return 1
fi
echo "[entrypoint] Attempt ${attempt}/${max_attempts} — retrying in ${delay}s..."
sleep "$delay"
done
# P0: Mask credentials — never print full DATABASE_URL to logs
local _masked_url
_masked_url="$(echo "$DATABASE_URL" | sed -E 's|://([^:]+):([^@]+)@|://\1:***@|')"
echo "[entrypoint] ❌ Database not reachable after ${max_attempts} attempts" >&2
echo "[entrypoint] DATABASE_URL=${_masked_url}" >&2
return 1
}
# #endregion docker.backend.entrypoint.wait_for_db
# ======================================================================
# MAIN
# ======================================================================
@@ -428,8 +501,10 @@ install_certificates
install_llm_ca_certs
install_ca_to_nss
# Check DB state: empty, legacy (tables exist but no alembic), or managed
# || prevents set -e from killing the script on non-zero exit from detection script
# ── C1: Wait for database before any migration logic ──
wait_for_db
# ── C2: DB state detection (now safe — DB is confirmed reachable) ──
_db_state=0
python3 -c "
import os
@@ -452,25 +527,11 @@ elif [ $_db_state -eq 1 ]; then
alembic upgrade head 2>&1 | sed 's/^/[entrypoint] /'
echo "[entrypoint] ✅ Alembic initial migration applied"
else
echo "[entrypoint] Legacy database — adding critical migration columns..."
# Add boot-critical columns that Alembic migrations would add
python3 -c "
import os
from sqlalchemy import create_engine, inspect, text
e = create_engine(os.environ['DATABASE_URL'])
with e.begin() as conn:
inspector = inspect(conn)
roles_cols = {c['name'] for c in inspector.get_columns('roles')}
if 'is_admin' not in roles_cols:
conn.execute(text('ALTER TABLE roles ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE'))
conn.execute(text(\"UPDATE roles SET is_admin = TRUE WHERE name = 'Admin'\"))
print(' roles.is_admin column added')
else:
print(' roles.is_admin already exists')
"
echo "[entrypoint] Stamping Alembic head (tables already exist)..."
alembic stamp head 2>&1 | sed 's/^/[entrypoint] /'
echo "[entrypoint] ✅ Alembic stamped head, schema ready"
echo "[entrypoint] Legacy database — running Alembic upgrade (not stamp)..."
# H2: Запускаем полноценный upgrade вместо stamp head — legacy-БД может
# отсутствовать таблицы, добавленные миграциями после написания entrypoint.
alembic upgrade head 2>&1 | sed 's/^/[entrypoint] /'
echo "[entrypoint] ✅ Alembic upgrade complete for legacy database"
fi
bootstrap_admin