#!/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 "$@"