alembic fix
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user