diff --git a/.env.example b/.env.example index e01f5de..f9b58d7 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 c33d0f7..ff88733 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 34d64d3..f766fe9 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(response: Response) -> dict: + """Liveness + readiness probe. Returns 503 if Postgres or Redis is unreachable.""" + 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() # type: ignore[misc] + except Exception as e: + logger.warning(f"Healthcheck Redis failed: {e}") + errors.append("redis") + + if errors: + response.status_code = 503 + return {"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 18b7e66..fa8174c 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 diff --git a/scripts/seed_admin.py b/scripts/seed_admin.py new file mode 100644 index 0000000..91f4a7b --- /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())