Three-way detection in entrypoint:
- alembic_version exists → normal incremental upgrade
- no tables → fresh upgrade from scratch
- tables exist but no alembic_version → stamp head (legacy DB created
by create_all(), mark all migrations as current without running them)
This fixes 'relation already exists' errors when Alembic tries to CREATE
TABLE on databases that already have tables from previous deployments.
186 lines
8.5 KiB
Bash
Executable File
186 lines
8.5 KiB
Bash
Executable File
#!/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
|
||
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) # managed → normal upgrade
|
||
if not insp.get_table_names():
|
||
exit(1) # empty → fresh upgrade
|
||
exit(2) # legacy → stamp head
|
||
" 2>/dev/null
|
||
_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 — stamping Alembic head (tables already exist)..."
|
||
alembic stamp head 2>&1 | sed 's/^/[entrypoint] /'
|
||
echo "[entrypoint] ✅ Alembic stamped head, schema assumed up to date"
|
||
fi
|
||
|
||
bootstrap_admin
|
||
|
||
echo "[entrypoint] Starting backend: $*"
|
||
exec "$@"
|