Files
site/apps/api/scripts/docker_entrypoint.py
T

136 lines
4.3 KiB
Python

"""Docker entrypoint: wait for DB, reconcile Alembic state, migrate, start API."""
from __future__ import annotations
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from alembic import command
from alembic.config import Config
from sqlalchemy import create_engine, inspect, text
from app.core.install_secrets import ensure_install_secrets
from app.core.config import settings
from app.core.server_logging import append_server_log_line, build_uvicorn_log_config, ensure_server_log_file
INITIAL_REVISION = "20260711_0001"
HEAD_REVISION = "20260716_0009"
SCHEMA_TABLES = (
"content_pages",
"email_verification_tokens",
"password_reset_tokens",
"refresh_tokens",
"user_profiles",
"users",
"enterprises",
"enterprise_members",
"farm_hubs",
"user_farm_access",
"hub_credentials",
"hub_pairing_sessions",
"sync_outbox",
"sync_event_log",
"sync_applied_events",
"sync_cursors",
"sync_record_state",
"sync_conflicts",
"sync_reconcile_runs",
"report_sync_state",
"zootech_component",
"zootech_recipe",
"zootech_ingredient",
)
def wait_for_database(max_attempts: int = 30, delay_seconds: float = 1.0):
if settings.database_url.startswith("sqlite"):
raise RuntimeError(
"DATABASE_URL points to SQLite — Docker API requires PostgreSQL.\n"
"Your apps/api/data/secrets/install.env was likely overwritten by a local test.\n"
"Fix:\n"
" rm apps/api/data/secrets/install.env\n"
" python apps/api/scripts/bootstrap_install.py\n"
" docker compose --profile docker-web down -v\n"
" docker compose --profile docker-web up -d --build"
)
engine = create_engine(settings.database_url)
last_error: Exception | None = None
for _ in range(max_attempts):
try:
with engine.connect() as connection:
connection.execute(text("SELECT 1"))
return engine
except Exception as exc:
last_error = exc
time.sleep(delay_seconds)
detail = str(last_error or "unknown error")
hint = ""
if "password authentication failed" in detail or "does not exist" in detail:
hint = (
"\n\nPostgres credentials in install.env do not match the existing database volume "
"(common if docker compose ran before bootstrap_install.py).\n"
"Local dev fix — deletes Docker DB data:\n"
" docker compose --profile docker-web down -v\n"
" docker compose --profile docker-web up -d --build\n"
"See docs/deploy.md#восстановление-секретов"
)
raise RuntimeError(f"Database is unavailable: {detail}{hint}") from last_error
def current_revision(engine) -> str | None:
inspector = inspect(engine)
if "alembic_version" not in inspector.get_table_names():
return None
with engine.connect() as connection:
return connection.execute(text("SELECT version_num FROM alembic_version")).scalar()
def _reset_schema(engine) -> None:
cascade = " CASCADE" if engine.dialect.name == "postgresql" else ""
with engine.begin() as connection:
for table in SCHEMA_TABLES:
connection.execute(text(f'DROP TABLE IF EXISTS "{table}"{cascade}'))
connection.execute(text(f'DROP TABLE IF EXISTS "alembic_version"{cascade}'))
def run_migrations(engine) -> None:
config = Config("alembic.ini")
tables = set(inspect(engine).get_table_names())
revision = current_revision(engine)
schema_tables = set(SCHEMA_TABLES)
existing_schema = tables & schema_tables
if existing_schema:
if schema_tables.issubset(tables):
if revision is None:
command.stamp(config, INITIAL_REVISION)
else:
_reset_schema(engine)
command.upgrade(config, "head")
def main() -> None:
ensure_install_secrets()
engine = wait_for_database()
run_migrations(engine)
ensure_server_log_file()
append_server_log_line("API starting (docker entrypoint)")
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
log_config=build_uvicorn_log_config(),
)
if __name__ == "__main__":
main()