Merge branch agent/phase-a-dev-runtime
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
commit
550d405935
@ -25,6 +25,13 @@ services:
|
||||
- .env.dev
|
||||
environment:
|
||||
- REDIS_HOST=redis-dev
|
||||
- REDIS_DB_DEV=0
|
||||
- BACKEND_DEV_MODE=true
|
||||
- APP_ENV=development
|
||||
- ENVIRONMENT=development
|
||||
- START_MODE=dev
|
||||
- CC_COMPOSE_PROJECT=api-dev
|
||||
- CC_COMPOSE_SERVICE=backend-dev
|
||||
- RUN_INIT=false
|
||||
- INIT_MODE=infra
|
||||
ports:
|
||||
@ -45,6 +52,13 @@ services:
|
||||
- .env.dev
|
||||
environment:
|
||||
- REDIS_HOST=redis-dev
|
||||
- REDIS_DB_DEV=0
|
||||
- BACKEND_DEV_MODE=true
|
||||
- APP_ENV=development
|
||||
- ENVIRONMENT=development
|
||||
- START_MODE=dev
|
||||
- CC_COMPOSE_PROJECT=api-dev
|
||||
- CC_COMPOSE_SERVICE=backend-test
|
||||
- API_HEALTH_URL=http://192.168.0.64:18000/health
|
||||
depends_on:
|
||||
redis-dev:
|
||||
|
||||
@ -46,6 +46,11 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
- REDIS_HOST=redis
|
||||
- START_MODE=prod
|
||||
- APP_ENV=production
|
||||
- ENVIRONMENT=production
|
||||
- CC_COMPOSE_PROJECT=api
|
||||
- CC_COMPOSE_SERVICE=backend
|
||||
- RUN_INIT=${RUN_INIT:-false} # Set to 'true' to run init on startup
|
||||
- INIT_MODE=${INIT_MODE:-infra} # Which init tasks to run
|
||||
ports:
|
||||
|
||||
@ -98,11 +98,13 @@ if [ "$RUN_INIT" = "true" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start the production server (unless init-only mode)
|
||||
# Start the server (unless init-only mode). Default remains production, but
|
||||
# development Compose can set START_MODE=dev so cc-api-dev reports and uses
|
||||
# development Redis/config instead of silently booting as prod.
|
||||
START_MODE="${START_MODE:-prod}"
|
||||
if [ "$1" != "init-only" ] && [ -z "$INIT_ONLY" ]; then
|
||||
print_status "Starting production server..."
|
||||
exec ./start.sh prod
|
||||
print_status "Starting ${START_MODE} server..."
|
||||
exec ./start.sh "$START_MODE"
|
||||
else
|
||||
print_status "Init-only mode - not starting server"
|
||||
fi
|
||||
|
||||
|
||||
60
main.py
60
main.py
@ -10,6 +10,7 @@ logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH
|
||||
from fastapi import FastAPI, HTTPException
|
||||
import uvicorn
|
||||
import requests
|
||||
from urllib.parse import urlparse
|
||||
from typing import Dict, Any, Optional
|
||||
from modules.database.tools.neo4j_driver_tools import get_driver
|
||||
|
||||
@ -22,15 +23,59 @@ from modules.queue_system import ServiceType
|
||||
app = FastAPI()
|
||||
setup_cors(app)
|
||||
|
||||
def _truthy_env(name: str) -> bool:
|
||||
return os.getenv(name, "").lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _runtime_environment() -> str:
|
||||
"""Return the API runtime role used for dev/prod backing-service selection."""
|
||||
start_mode = os.getenv("START_MODE", "prod").lower()
|
||||
if start_mode == "dev" or _truthy_env("BACKEND_DEV_MODE"):
|
||||
return "dev"
|
||||
return "prod"
|
||||
|
||||
|
||||
def _url_host(url: Optional[str]) -> Optional[str]:
|
||||
if not url:
|
||||
return None
|
||||
parsed = urlparse(url)
|
||||
return parsed.hostname
|
||||
|
||||
|
||||
def _runtime_identity() -> Dict[str, Any]:
|
||||
"""Non-secret runtime identity for agents and smoke tests.
|
||||
|
||||
This intentionally exposes only modes, labels, and URL hosts. It must not
|
||||
include API keys, passwords, bearer tokens, or full URLs that may embed
|
||||
credentials.
|
||||
"""
|
||||
return {
|
||||
"api_runtime_role": _runtime_environment(),
|
||||
"start_mode": os.getenv("START_MODE", "prod"),
|
||||
"app_environment": os.getenv("APP_ENV"),
|
||||
"environment": os.getenv("ENVIRONMENT"),
|
||||
"backend_dev_mode": _truthy_env("BACKEND_DEV_MODE"),
|
||||
"compose_project": os.getenv("COMPOSE_PROJECT_NAME") or os.getenv("CC_COMPOSE_PROJECT"),
|
||||
"compose_service": os.getenv("COMPOSE_SERVICE") or os.getenv("CC_COMPOSE_SERVICE"),
|
||||
"supabase_url_host": _url_host(os.getenv("SUPABASE_URL")),
|
||||
}
|
||||
|
||||
|
||||
# Health check endpoint
|
||||
@app.get("/health")
|
||||
async def health_check() -> Dict[str, Any]:
|
||||
"""Health check endpoint that verifies all service dependencies"""
|
||||
runtime_identity = _runtime_identity()
|
||||
health_status = {
|
||||
"status": "healthy",
|
||||
"runtime": runtime_identity,
|
||||
"services": {
|
||||
"neo4j": {"status": "healthy", "message": "Connected"},
|
||||
"supabase": {"status": "healthy", "message": "Connected"},
|
||||
"supabase": {
|
||||
"status": "healthy",
|
||||
"message": "Connected",
|
||||
"url_host": runtime_identity["supabase_url_host"],
|
||||
},
|
||||
"redis": {"status": "healthy", "message": "Connected"}
|
||||
}
|
||||
}
|
||||
@ -45,9 +90,10 @@ async def health_check() -> Dict[str, Any]:
|
||||
}
|
||||
health_status["status"] = "unhealthy"
|
||||
except Exception as e:
|
||||
logger.warning(f"Neo4j health check failed: {e}")
|
||||
health_status["services"]["neo4j"] = {
|
||||
"status": "unhealthy",
|
||||
"message": f"Error checking Neo4j: {str(e)}"
|
||||
"message": "Error checking Neo4j"
|
||||
}
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
@ -67,9 +113,10 @@ async def health_check() -> Dict[str, Any]:
|
||||
}
|
||||
health_status["status"] = "unhealthy"
|
||||
except Exception as e:
|
||||
logger.warning(f"Supabase health check failed: {e}")
|
||||
health_status["services"]["supabase"] = {
|
||||
"status": "unhealthy",
|
||||
"message": f"Error checking Supabase Auth API: {str(e)}"
|
||||
"message": "Error checking Supabase Auth API"
|
||||
}
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
@ -77,8 +124,8 @@ async def health_check() -> Dict[str, Any]:
|
||||
# Check Redis using new Redis manager
|
||||
from modules.redis_manager import get_redis_manager
|
||||
|
||||
# Determine environment
|
||||
environment = 'dev' if os.getenv('BACKEND_DEV_MODE', 'true').lower() == 'true' else 'prod'
|
||||
# Determine environment from explicit startup/runtime identity.
|
||||
environment = runtime_identity["api_runtime_role"]
|
||||
redis_manager = get_redis_manager(environment)
|
||||
|
||||
# Get comprehensive health check
|
||||
@ -96,9 +143,10 @@ async def health_check() -> Dict[str, Any]:
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Redis health check failed: {e}")
|
||||
health_status["services"]["redis"] = {
|
||||
"status": "unhealthy",
|
||||
"message": f"Error checking Redis: {str(e)}"
|
||||
"message": "Error checking Redis"
|
||||
}
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
|
||||
@ -38,8 +38,21 @@ def test_dev_api_health_endpoint_is_healthy():
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload['status'] == 'healthy'
|
||||
|
||||
runtime = payload['runtime']
|
||||
assert runtime['api_runtime_role'] == 'dev'
|
||||
assert runtime['start_mode'] == 'dev'
|
||||
assert runtime['app_environment'] == 'development'
|
||||
assert runtime['environment'] == 'development'
|
||||
assert runtime['backend_dev_mode'] is True
|
||||
assert runtime['compose_project'] == 'api-dev'
|
||||
assert runtime['supabase_url_host'] == '192.168.0.94'
|
||||
|
||||
assert payload['services']['supabase']['status'] == 'healthy'
|
||||
assert payload['services']['supabase']['url_host'] == '192.168.0.94'
|
||||
assert payload['services']['redis']['status'] == 'healthy'
|
||||
assert payload['services']['redis']['environment'] == 'dev'
|
||||
assert payload['services']['redis']['database'] == 0
|
||||
|
||||
|
||||
def test_supabase_dev_seed_core_counts():
|
||||
@ -51,3 +64,19 @@ def test_supabase_dev_seed_core_counts():
|
||||
def test_supabase_dev_seed_timetable_counts():
|
||||
assert _rest_count('classes') == 17
|
||||
assert _rest_count('taught_lessons') == 1462
|
||||
|
||||
|
||||
def test_runtime_identity_does_not_expose_secret_values():
|
||||
health_url = os.getenv('API_HEALTH_URL', 'http://192.168.0.64:18000/health')
|
||||
response = requests.get(health_url, timeout=15)
|
||||
assert response.status_code == 200
|
||||
payload_text = response.text
|
||||
for secret_name in ('SERVICE_ROLE_KEY', 'ANON_KEY', 'SUPABASE_JWT_SECRET', 'REDIS_PASSWORD'):
|
||||
secret_value = os.getenv(secret_name)
|
||||
if secret_value:
|
||||
assert secret_value not in payload_text
|
||||
|
||||
runtime = response.json()['runtime']
|
||||
assert 'supabase_url' not in runtime
|
||||
assert 'service_role_key' not in runtime
|
||||
assert 'anon_key' not in runtime
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user