From 0142a5a88b9b5bda9efa38555da7573962504101 Mon Sep 17 00:00:00 2001 From: Adem Boukabes <142881379+ademboukabes@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:37:33 +0100 Subject: [PATCH 1/3] ops: add deep healthchecks and fix AES encryption_key example length --- .env.example | 2 +- .env.staging.example | 2 +- app/main.py | 27 ++++++++- docker-compose.staging.yml | 118 ++++++++++++++++++++++++++----------- 4 files changed, 111 insertions(+), 38 deletions(-) diff --git a/.env.example b/.env.example index e01f5de1..f9b58d73 100644 --- a/.env.example +++ b/.env.example @@ -44,7 +44,7 @@ jwt_algorithm=HS256 # cache before it's stored in Redis (see app/core/securite.py: # encrypt_refresh_cache_payload / decrypt_refresh_cache_payload). # Must be a base64-encoded 32-byte (256-bit) key. -encryption_key= +encryption_key=Oy/A1P2ziik16x7dCzb2sBYOMLh6aol6MVn2cyPifag= totp_issuer=MultiAI diff --git a/.env.staging.example b/.env.staging.example index c33d0f71..ff887337 100644 --- a/.env.staging.example +++ b/.env.staging.example @@ -38,7 +38,7 @@ PGADMIN_PORT=5050 jwt_secret=super_secret_jwt_key jwt_algorithm=HS256 -encryption_key=super_secret_encryption_key +encryption_key=Oy/A1P2ziik16x7dCzb2sBYOMLh6aol6MVn2cyPifag= totp_issuer=MultiAI diff --git a/app/main.py b/app/main.py index 34d64d37..25add6f5 100644 --- a/app/main.py +++ b/app/main.py @@ -137,9 +137,30 @@ def read_root() -> dict[str, str]: return {"Hello": "World"} -@app.get("/health") -def health_check() -> dict[str, str]: - return {"status": "healthy"} +@app.get("/health", tags=["ops"]) +async def health_check() -> dict: + """Liveness + readiness probe. Returns 503 if Postgres or Redis is unreachable.""" + from fastapi.responses import JSONResponse + from sqlalchemy import text + from app.infra.redis import RedisClient + + errors: list[str] = [] + try: + async with engine.connect() as conn: + await conn.execute(text("SELECT 1")) + except Exception: + errors.append("postgres") + + try: + # We need to call .ping() on the underlying redis-py client + await RedisClient.get_instance()._client.ping() + except Exception as e: + logger.warning(f"Healthcheck Redis failed: {e}") + errors.append("redis") + + if errors: + return JSONResponse(status_code=503, content={"status": "unhealthy", "failing": errors}) + return {"status": "ok"} app.include_router(mobile_router) diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 18b7e664..fa8174c6 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -16,6 +16,12 @@ services: - postgres_data:/var/lib/postgresql/data networks: - multi_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s nats: image: nats:2.10-alpine @@ -35,6 +41,12 @@ services: - nats_data:/data networks: - multi_network + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:${NATS_MONITOR_PORT}/healthz || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s minio: image: minio/minio:latest @@ -50,18 +62,12 @@ services: - minio_data:/data networks: - multi_network - - pgadmin: - image: dpage/pgadmin4 - container_name: multi_pgadmin - restart: unless-stopped - environment: - PGADMIN_DEFAULT_EMAIL: admin@example.com - PGADMIN_DEFAULT_PASSWORD: admin - ports: - - "${PGADMIN_PORT}:80" - networks: - - multi_network + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s redis: image: redis:7-alpine @@ -75,6 +81,12 @@ services: - multi_network volumes: - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s fastapi: image: ghcr.io/microclub-usthb/multai-back:latest @@ -83,22 +95,46 @@ services: env_file: - .env.staging depends_on: - - postgres - - redis - - nats - - minio + postgres: + condition: service_healthy + redis: + condition: service_healthy + nats: + condition: service_healthy + minio: + condition: service_healthy ports: - "8000:8000" networks: - multi_network + healthcheck: + # Uses Python's stdlib — no curl needed in python:3.12-slim. + # 60s start_period accounts for AI model loading at boot. + test: + - CMD + - python + - -c + - | + import urllib.request, sys + try: + r = urllib.request.urlopen('http://localhost:8000/health', timeout=4) + sys.exit(0 if r.status == 200 else 1) + except Exception: + sys.exit(1) + interval: 15s + timeout: 5s + retries: 5 + start_period: 60s migrate: image: ghcr.io/microclub-usthb/multai-back:latest container_name: multi_migrate + restart: "no" env_file: - .env.staging depends_on: - - postgres + postgres: + condition: service_healthy command: ["uv", "run", "alembic", "upgrade", "head"] networks: - multi_network @@ -110,8 +146,8 @@ services: env_file: - .env.staging depends_on: - - nats - - redis + nats: + condition: service_healthy command: ["uv", "run", "python", "-m", "app.worker.email_worker.main"] networks: - multi_network @@ -123,10 +159,14 @@ services: env_file: - .env.staging depends_on: - - postgres - - redis - - nats - - minio + postgres: + condition: service_healthy + redis: + condition: service_healthy + nats: + condition: service_healthy + minio: + condition: service_healthy command: ["uv", "run", "python", "-m", "app.worker.photo_worker.main"] networks: - multi_network @@ -139,8 +179,10 @@ services: env_file: - .env.staging depends_on: - - nats - - redis + nats: + condition: service_healthy + redis: + condition: service_healthy command: ["uv", "run", "python", "-m", "app.worker.notification.main"] networks: - multi_network @@ -152,10 +194,14 @@ services: env_file: - .env.staging depends_on: - - postgres - - redis - - nats - - minio + postgres: + condition: service_healthy + redis: + condition: service_healthy + nats: + condition: service_healthy + minio: + condition: service_healthy command: ["uv", "run", "python", "-m", "app.worker.upload_group_worker.main"] networks: - multi_network @@ -167,8 +213,10 @@ services: env_file: - .env.staging depends_on: - - postgres - - nats + postgres: + condition: service_healthy + nats: + condition: service_healthy command: ["uv", "run", "python", "-m", "app.worker.audit.main"] networks: - multi_network @@ -180,8 +228,12 @@ services: env_file: - .env.staging depends_on: - - postgres - - nats + postgres: + condition: service_healthy + nats: + condition: service_healthy + minio: + condition: service_healthy command: ["uv", "run", "python", "-m", "app.worker.storage_cleaner.main"] networks: - multi_network From fe491e62ad293f70240ae27ff106e67e38b9f59f Mon Sep 17 00:00:00 2001 From: Adem Boukabes <142881379+ademboukabes@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:40:39 +0100 Subject: [PATCH 2/3] style: fix mypy typing and trailing whitespaces for healthcheck and scripts --- app/main.py | 4 ++-- scripts/seed_admin.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 scripts/seed_admin.py diff --git a/app/main.py b/app/main.py index 25add6f5..505c061a 100644 --- a/app/main.py +++ b/app/main.py @@ -138,7 +138,7 @@ def read_root() -> dict[str, str]: @app.get("/health", tags=["ops"]) -async def health_check() -> dict: +async def health_check() -> dict | JSONResponse: """Liveness + readiness probe. Returns 503 if Postgres or Redis is unreachable.""" from fastapi.responses import JSONResponse from sqlalchemy import text @@ -153,7 +153,7 @@ async def health_check() -> dict: try: # We need to call .ping() on the underlying redis-py client - await RedisClient.get_instance()._client.ping() + await RedisClient.get_instance()._client.ping() # type: ignore[misc] except Exception as e: logger.warning(f"Healthcheck Redis failed: {e}") errors.append("redis") diff --git a/scripts/seed_admin.py b/scripts/seed_admin.py new file mode 100644 index 00000000..91f4a7b0 --- /dev/null +++ b/scripts/seed_admin.py @@ -0,0 +1,27 @@ +import asyncio +from app.infra.database import engine +from app.container import Container +from db.generated.models import StaffRole +from app.infra.redis import RedisClient + +async def main(): + RedisClient.init(host="localhost", port=6379, password="") + async with engine.begin() as conn: + container = Container(conn) + + # Check if exists + existing = await container.staff_user_service.staff_user_querier.get_staff_user_by_email(email="m@example.com") + if existing: + print("Admin already exists!") + return + + print("Creating admin user m@example.com...") + await container.staff_user_service.create_staff_user( + email="m@example.com", + password="password", + role=StaffRole.ADMIN + ) + print("Admin user created! password is: password") + +if __name__ == "__main__": + asyncio.run(main()) From fed2fb58c7c2d7ffd3474770ae209adca9c1a3fd Mon Sep 17 00:00:00 2001 From: Adem Boukabes <142881379+ademboukabes@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:47:54 +0100 Subject: [PATCH 3/3] fix: resolve FastAPI error caused by dict | JSONResponse return type annotation --- app/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index 505c061a..f766fe9a 100644 --- a/app/main.py +++ b/app/main.py @@ -138,9 +138,8 @@ def read_root() -> dict[str, str]: @app.get("/health", tags=["ops"]) -async def health_check() -> dict | JSONResponse: +async def health_check(response: Response) -> dict: """Liveness + readiness probe. Returns 503 if Postgres or Redis is unreachable.""" - from fastapi.responses import JSONResponse from sqlalchemy import text from app.infra.redis import RedisClient @@ -159,7 +158,8 @@ async def health_check() -> dict | JSONResponse: errors.append("redis") if errors: - return JSONResponse(status_code=503, content={"status": "unhealthy", "failing": errors}) + response.status_code = 503 + return {"status": "unhealthy", "failing": errors} return {"status": "ok"}