Files
ss-tools/docker/backend.entrypoint.sh
busya e2a473aa98 fix: prevent set -e from killing entrypoint on DB detection
The python3 DB detection script exits with code 1 (empty) or 2 (legacy),
but entrypoint has set -e which kills the script on any non-zero exit.
Fixed by using '|| _db_state=$?' pattern to safely capture exit code
without triggering set -e.
2026-05-26 20:15:32 +03:00

187 lines
8.5 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 — 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 "$@"