diff --git a/.env.example b/.env.example index cdf02b1..7b9a50c 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,6 @@ DATABASE_URL=postgresql+psycopg://rf4:rf4_local@localhost:5432/rf4_spotter +APP_VERSION=0.1.0 +APP_REVISION=dev PUBLIC_API_URL=http://localhost:8000 API_INTERNAL_URL=http://api:8000 ADMIN_TOKEN=change-me-in-production diff --git a/.env.production.example b/.env.production.example index d2ee2a1..ab88026 100644 --- a/.env.production.example +++ b/.env.production.example @@ -8,6 +8,8 @@ POSTGRES_USER=rf4 POSTGRES_PASSWORD=replace-with-long-random-value # URL-encode special characters from POSTGRES_PASSWORD in this URL. DATABASE_URL=postgresql+psycopg://rf4:replace-with-url-encoded-password@db:5432/rf4_spotter +APP_VERSION=0.1.0 +APP_REVISION=replace-with-git-commit-sha ADMIN_TOKEN=replace-with-at-least-32-random-characters RATE_LIMIT_SECRET=replace-with-at-least-32-random-characters diff --git a/README.md b/README.md index 5ceac0f..f2d0503 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ docker compose up --build - сайт: ; - OpenAPI: ; - liveness API: ; -- readiness PostgreSQL, MinIO и импорта: ; +- readiness PostgreSQL, MinIO и импорта с версией/revision сборки: ; - консоль MinIO: . -Контейнер API сам выполняет `alembic upgrade head`, затем идемпотентный seed. PostgreSQL хранит данные в именованном volume `postgres_data`, а MinIO — в `minio_data`. Compose ожидает readiness PostgreSQL и MinIO перед API, а API-контейнер проверяет `/ready`. Официальный импорт по умолчанию необязателен; при включённом scheduler установите `OFFICIAL_IMPORT_REQUIRED=true`, тогда отсутствующий, неуспешный или просроченный запуск сделает readiness отрицательным. +Контейнер API сам выполняет `alembic upgrade head`, затем идемпотентный seed. PostgreSQL хранит данные в именованном volume `postgres_data`, а MinIO — в `minio_data`. Compose ожидает readiness PostgreSQL и MinIO перед API, а API-контейнер проверяет `/ready`. Версия и commit SHA задаются через `APP_VERSION`/`APP_REVISION`; те же значения доступны администратору в `/api/v1/admin/diagnostics`. Официальный импорт по умолчанию необязателен; при включённом scheduler установите `OFFICIAL_IMPORT_REQUIRED=true`, тогда отсутствующий, неуспешный или просроченный запуск сделает readiness отрицательным. API и scheduler пишут по одной JSON-записи на событие. HTTP-лог содержит только сгенерированный `request_id`, метод, путь без query string, статус и длительность; IP, заголовок авторизации и пользовательский payload не журналируются. `X-Request-ID` возвращается клиенту. Стандартный access-log Uvicorn отключён. Уровень управляется `LOG_LEVEL`. diff --git a/apps/api/app/config.py b/apps/api/app/config.py index bc8f4d4..ff8a8e7 100644 --- a/apps/api/app/config.py +++ b/apps/api/app/config.py @@ -4,6 +4,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): deployment_environment: str = "development" + app_version: str = "0.1.0" + app_revision: str = "dev" database_url: str = "postgresql+psycopg://rf4:rf4_local@localhost:5432/rf4_spotter" admin_token: str = "change-me-in-production" s3_endpoint_url: str = "http://localhost:9000" diff --git a/apps/api/app/main.py b/apps/api/app/main.py index 0033416..59cd7fb 100644 --- a/apps/api/app/main.py +++ b/apps/api/app/main.py @@ -89,7 +89,7 @@ def ready(db: Db) -> JSONResponse: ) return JSONResponse( status_code=200 if is_ready else 503, - content={"status": "ready" if is_ready else "not_ready", "components": components}, + content={"status": "ready" if is_ready else "not_ready", "version": settings.app_version, "revision": settings.app_revision, "components": components}, ) @@ -257,6 +257,11 @@ def _admin(authorization: Annotated[str | None, Header()] = None) -> str: return "admin" +@app.get("/api/v1/admin/diagnostics") +def admin_diagnostics(_: Annotated[str, Depends(_admin)]) -> dict[str, str]: + return {"version": settings.app_version, "revision": settings.app_revision, "environment": settings.deployment_environment} + + @app.get("/api/v1/imports", response_model=list[ImportRunOut]) def imports(db: Db, limit: int = Query(20, ge=1, le=100), offset: int = Query(0, ge=0)) -> list[OfficialRecordImport]: return list(db.scalars(select(OfficialRecordImport).order_by(OfficialRecordImport.started_at.desc(), OfficialRecordImport.id.desc()).offset(offset).limit(limit))) diff --git a/apps/api/tests/test_api.py b/apps/api/tests/test_api.py index 2d1a91a..18d0a16 100644 --- a/apps/api/tests/test_api.py +++ b/apps/api/tests/test_api.py @@ -85,6 +85,13 @@ def test_liveness_does_not_probe_dependencies() -> None: assert response.headers["Cross-Origin-Opener-Policy"] == "same-origin" +def test_admin_diagnostics_exposes_build_identity_only_to_admin() -> None: + assert client.get("/api/v1/admin/diagnostics").status_code == 401 + response = client.get("/api/v1/admin/diagnostics", headers={"Authorization": "Bearer change-me-in-production"}) + assert response.status_code == 200 + assert response.json() == {"version": "0.1.0", "revision": "dev", "environment": "development"} + + def test_spot_detail_and_catches() -> None: spot_id = client.get("/api/v1/activity").json()[0]["spot_id"] detail = client.get(f"/api/v1/spots/{spot_id}") diff --git a/compose.production.yaml b/compose.production.yaml index 5daedb8..92c6696 100644 --- a/compose.production.yaml +++ b/compose.production.yaml @@ -105,6 +105,8 @@ services: restart: unless-stopped environment: DEPLOYMENT_ENVIRONMENT: production + APP_VERSION: ${APP_VERSION:-0.1.0} + APP_REVISION: ${APP_REVISION:-unknown} DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL} ADMIN_TOKEN: ${ADMIN_TOKEN:?Set ADMIN_TOKEN} CORS_ORIGINS: '["https://${SITE_DOMAIN:?Set SITE_DOMAIN}"]' diff --git a/deploy/README.md b/deploy/README.md index 82c7137..7099c15 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -31,6 +31,8 @@ docker run --rm caddy:2.10.2-alpine caddy hash-password --plaintext 'ОТДЕЛ ./deploy/preflight.sh ``` +Перед сборкой запишите текущий `git rev-parse --short HEAD` в `APP_REVISION` файла `.env.production`, чтобы `/ready` однозначно показывал развёрнутый commit. + ```bash docker compose --env-file .env.production -f compose.production.yaml config --quiet docker compose --env-file .env.production -f compose.production.yaml build diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 5bf402d..7a050b2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -146,7 +146,7 @@ - [ ] Добавить безопасный административный экспорт диагностики без персональных данных. - [ ] Контролировать рост PostgreSQL и MinIO с порогами предупреждений. - [ ] Добавить фоновую проверку битых исходных ссылок с соблюдением лимитов источников. -- [ ] Показывать commit SHA/версию в readiness и административной диагностике. +- [x] Показывать версию и commit SHA в readiness и защищённой административной диагностике (7 сентября 2026). - [x] Добавить публичную `/status` без внутренних адресов, секретов и текстов ошибок (7 сентября 2026). - [ ] После появления сервера подключить privacy-friendly аналитику без cookies либо собственные агрегированные счётчики. - [ ] Зафиксировать нагрузочный бюджет и проверить p95 публичных API на целевом сервере.