Files
ss-tools/docker/backend.entrypoint.sh
busya 21e32ac4cc fix: legacy DB support + QA audit fixes
- Entrypoint: for legacy DBs, add is_admin via raw SQL then stamp head
  (instead of running full migration chain which fails on existing tables)
- Fixed admin_api_keys.py Pydantic config (dict → ConfigDict)
- Added tests/test_alembic_migrations.py (PostgreSQL-only integration tests)
- Fixed infinite recursion in _create_table_if_not_exists (QA catch)
2026-05-26 20:35:36 +03:00

203 lines
9.2 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
# #region docker.backend.entrypoint [C:4] [TYPE Module] [SEMANTICS docker,entrypoint,enterprise,certs]
# @BRIEF Container entrypoint — установка корп. сертификатов, Playwright (lazy), bootstrap admin,
# затем запуск backend (exec "$@").
# @PRE Python runtime и backend source доступны в /app/backend.
# Переменная CERTS_PATH указывает на директорию с .crt файлами (или пусто).
# @POST Сертификаты установлены в системное хранилище. Playwright Chromium установлен (lazy).
# Admin создан (если INITIAL_ADMIN_CREATE=true). Backend запущен через exec "$@".
# @SIDE_EFFECT Модифицирует /usr/local/share/ca-certificates/ и вызывает update-ca-certificates.
# Скачивает Chromium ~377MB при первом запуске.
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [backend/src/scripts/create_admin.py]
# @RELATION DEPENDS_ON -> [backend/src/scripts/init_auth_db.py]
# @INVARIANT Если CERTS_PATH пуст или нет .crt файлов — install_certificates не падает.
# @INVARIANT Существующий admin аккаунт НЕ перезаписывается при перезапуске контейнера
# (create_admin.py идемпотентен).
# @INVARIANT Playwright Chromium устанавливается только если отсутствует в кеше.
# #endregion docker.backend.entrypoint
# ======================================================================
# install_certificates — корпоративные CA-сертификаты
# ======================================================================
# #region docker.backend.entrypoint.install_certificates [C:3] [TYPE Function]
# @BRIEF Устанавливает корпоративные .crt сертификаты из CERTS_PATH в системное хранилище Debian.
# @PRE CERTS_PATH указывает на директорию (может быть пуста или не существовать).
# В системе установлен пакет ca-certificates.
# @POST .crt файлы скопированы в /usr/local/share/ca-certificates/custom/ и
# добавлены в системное хранилище через update-ca-certificates.
# @SIDE_EFFECT Запускает update-ca-certificates — модифицирует /etc/ssl/certs/.
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [ca-certificates (system package)]
# @EXAMPLE:
# CERTS_PATH=/opt/certs docker run ... # монтируем ./certs в /opt/certs
# В ./certs кладём: my-corporate-ca.crt, another-ca.crt
# entrypoint установит оба в доверенные.
install_certificates() {
local certs_dir="${CERTS_PATH:-/opt/certs}"
if [ ! -d "$certs_dir" ]; then
echo "[entrypoint] CERTS_PATH=${certs_dir} не существует — пропускаем установку сертификатов"
return 0
fi
local cert_count
cert_count="$(find "$certs_dir" -maxdepth 1 -name '*.crt' -o -name '*.pem' 2>/dev/null | wc -l)"
if [ "$cert_count" -eq 0 ]; then
echo "[entrypoint] Нет .crt/.pem файлов в ${certs_dir} — пропускаем установку сертификатов"
return 0
fi
local target="/usr/local/share/ca-certificates/custom"
mkdir -p "$target"
echo "[entrypoint] Устанавливаем корпоративные сертификаты из ${certs_dir}..."
local copied=0
for cert in "$certs_dir"/*.crt "$certs_dir"/*.pem; do
if [ -f "$cert" ]; then
cp -v "$cert" "$target/" 2>&1 | sed 's/^/[entrypoint] /'
copied=$((copied + 1))
fi
done
if [ "$copied" -eq 0 ]; then
echo "[entrypoint] Нет файлов для копирования — пропускаем"
return 0
fi
echo "[entrypoint] Обновляем системное хранилище CA-сертификатов..."
update-ca-certificates --fresh 2>&1 | sed 's/^/[entrypoint] /'
echo "[entrypoint] ✅ Установлено ${copied} корпоративных сертификатов"
}
# #endregion docker.backend.entrypoint.install_certificates
# ======================================================================
# bootstrap_admin — создание admin пользователя
# ======================================================================
# #region docker.backend.entrypoint.bootstrap_admin [C:2] [TYPE Function]
# @BRIEF Создание admin пользователя из переменных окружения (идемпотентно).
# @PRE INITIAL_ADMIN_CREATE=true, INITIAL_ADMIN_USERNAME и INITIAL_ADMIN_PASSWORD заданы.
# @POST Admin пользователь создан в БД (если ещё не существовал).
# @LAYER Infrastructure
bootstrap_admin() {
local create_flag="${INITIAL_ADMIN_CREATE:-false}"
local username="${INITIAL_ADMIN_USERNAME:-}"
local password="${INITIAL_ADMIN_PASSWORD:-}"
local email="${INITIAL_ADMIN_EMAIL:-}"
case "${create_flag,,}" in
true|1|yes|y)
;;
*)
echo "[entrypoint] INITIAL_ADMIN_CREATE disabled — пропускаем bootstrap admin"
return 0
;;
esac
if [[ -z "${username}" ]]; then
echo "[entrypoint] INITIAL_ADMIN_USERNAME required when INITIAL_ADMIN_CREATE=true" >&2
return 1
fi
if [[ -z "${password}" ]]; then
echo "[entrypoint] INITIAL_ADMIN_PASSWORD required when INITIAL_ADMIN_CREATE=true" >&2
return 1
fi
echo "[entrypoint] Initializing auth database..."
python3 src/scripts/init_auth_db.py
echo "[entrypoint] Creating admin user '${username}' (idempotent)..."
if [[ -n "${email}" ]]; then
python3 src/scripts/create_admin.py --username "${username}" --password "${password}" --email "${email}"
else
python3 src/scripts/create_admin.py --username "${username}" --password "${password}"
fi
echo "[entrypoint] ✅ Admin bootstrap complete"
}
# #endregion docker.backend.entrypoint.bootstrap_admin
# ======================================================================
# install_playwright — lazy установка Chromium
# ======================================================================
# #region docker.backend.entrypoint.install_playwright [C:2] [TYPE Function]
# @BRIEF Lazy установка Playwright Chromium (~377 MB) при первом запуске.
# @PRE ~/.cache/ms-playwright доступен (можно закешировать volume).
# @POST Chromium установлен в PLAYWRIGHT_BROWSERS_PATH.
# @LAYER Infrastructure
install_playwright() {
local pw_dir="${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}"
if ls "${pw_dir}"/chromium-*/chrome-linux64/chrome 1>/dev/null 2>&1; then
echo "[entrypoint] Playwright Chromium already installed at ${pw_dir}"
return 0
fi
echo "[entrypoint] Installing Playwright Chromium (first start only, ~377 MB)..."
echo "[entrypoint] Cache with: -v playwright_cache:/root/.cache/ms-playwright"
python3 -m playwright install chromium --with-deps 2>&1
echo "[entrypoint] ✅ Playwright Chromium installed"
}
# #endregion docker.backend.entrypoint.install_playwright
# ======================================================================
# MAIN
# ======================================================================
install_certificates
install_playwright
# 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
_db_state=0
python3 -c "
import os
from sqlalchemy import create_engine, inspect
e = create_engine(os.environ['DATABASE_URL'])
insp = inspect(e)
if 'alembic_version' in insp.get_table_names():
exit(0)
if not insp.get_table_names():
exit(1)
exit(2)
" 2>&1 || _db_state=$?
if [ $_db_state -eq 0 ]; then
echo "[entrypoint] Running Alembic migrations (incremental)..."
alembic upgrade head 2>&1 | sed 's/^/[entrypoint] /'
echo "[entrypoint] ✅ Alembic migrations applied"
elif [ $_db_state -eq 1 ]; then
echo "[entrypoint] Empty database — running full Alembic upgrade..."
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"
fi
bootstrap_admin
echo "[entrypoint] Starting backend: $*"
exec "$@"