Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user