feat(deploy): healthcheck api и worker, отдельный migrate-сервис, лимит загрузки отчёта

Миграции вынесены из команды api в one-shot сервис migrate, api и worker стартуют после него. /health отвечает 503 при недоступной БД. Отчёт больше MAX_UPLOAD_BYTES (25 МиБ) получает 413, Caddy режет на 30 МБ раньше. Том uploads убран, секреты env_file передаются сервисам явно. В CI добавлена сборка образа бэкенда без push, test_migrations сверяет модели с историей Alembic.
This commit is contained in:
Dmitry
2026-09-19 21:55:33 +03:00
parent 600496048f
commit 4a0b4e0532
11 changed files with 139 additions and 21 deletions
+4 -2
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from fastapi import APIRouter
from fastapi import APIRouter, Response, status
from pydantic import BaseModel
from sqlalchemy import text
@@ -17,10 +17,12 @@ class Health(BaseModel):
@router.get("/health", name="check")
async def check(session: SessionDep) -> Health:
async def check(session: SessionDep, response: Response) -> Health:
"""503 when the database cannot be reached, so a container healthcheck can trust the code."""
try:
await session.execute(text("SELECT 1"))
db = "ok"
except Exception as exc:
db = f"error: {type(exc).__name__}"
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
return Health(status="ok" if db == "ok" else "degraded", version=__version__, database=db)
@@ -20,7 +20,7 @@ from typing import Annotated
from fastapi import APIRouter, File, Form, Query, UploadFile
from sqlalchemy import select
from fintracker.api.deps import CurrentUser, SessionDep
from fintracker.api.deps import CurrentUser, SessionDep, SettingsDep
from fintracker.api.errors import Problem
from fintracker.api.schemas.imports import (
AccountSuggestion,
@@ -64,13 +64,19 @@ def _problem(exc: ImportProblem) -> Problem:
@router.post("", name="create")
async def create_import(
session: SessionDep,
settings: SettingsDep,
_: CurrentUser,
file: Annotated[UploadFile, File(description="the report itself")],
account_id: Annotated[int | None, Form(description="target account, if known")] = None,
parser: Annotated[str | None, Form(description="force a parser from registry.names()")] = None,
) -> ImportPreview:
"""Upload a report, parse it and show what committing it would do. Writes no event."""
data = await file.read()
limit = settings.max_upload_bytes
data = await file.read(limit + 1)
if len(data) > limit:
raise Problem(
413, "Payload Too Large", f"Report is larger than {limit // (1024 * 1024)} MiB"
)
try:
outcome = await report_import.upload(
session,