from fastapi import FastAPI, Request, Depends, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials import structlog from datetime import datetime, timedelta from typing import Dict, Any, Optional, List from pydantic import BaseModel import secrets from app.config import get_settings # 配置日志 structlog.configure( processors=[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.processors.JSONRenderer() ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) logger = structlog.get_logger() # 创建 FastAPI 应用 app = FastAPI( title="Gitea Webhook Ambassador (Demo)", description="高性能的 Gitea 到 Jenkins 的 Webhook 服务 - 演示版本", version="1.0.0" ) # 添加 CORS 中间件 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 安全配置 security = HTTPBearer(auto_error=False) # 演示数据存储 api_keys = { "demo_admin_key": { "name": "演示管理员密钥", "key_hash": "demo_admin_key", "key_prefix": "demo_adm", "created_at": datetime.utcnow(), "last_used": datetime.utcnow(), "is_active": True, "role": "admin" }, "demo_user_key": { "name": "演示用户密钥", "key_hash": "demo_user_key", "key_prefix": "demo_usr", "created_at": datetime.utcnow(), "last_used": datetime.utcnow(), "is_active": True, "role": "user" } } trigger_logs = [ { "id": 1, "repository_name": "freeleaps/test-project", "branch_name": "main", "commit_sha": "abc123def456", "job_name": "test-project-build", "status": "success", "error_message": None, "created_at": datetime.utcnow() - timedelta(hours=2) }, { "id": 2, "repository_name": "freeleaps/another-project", "branch_name": "dev", "commit_sha": "def456ghi789", "job_name": "another-project-dev", "status": "success", "error_message": None, "created_at": datetime.utcnow() - timedelta(hours=1) }, { "id": 3, "repository_name": "freeleaps/test-project", "branch_name": "feature/new-feature", "commit_sha": "ghi789jkl012", "job_name": "test-project-feature", "status": "failed", "error_message": "Build timeout", "created_at": datetime.utcnow() - timedelta(minutes=30) } ] project_mappings = { 1: { "repository_name": "freeleaps/test-project", "default_job": "test-project-build", "branch_jobs": [ {"branch": "dev", "job": "test-project-dev"}, {"branch": "staging", "job": "test-project-staging"} ], "branch_patterns": [ {"pattern": "feature/*", "job": "test-project-feature"}, {"pattern": "hotfix/*", "job": "test-project-hotfix"} ], "created_at": datetime.utcnow() - timedelta(days=1), "updated_at": datetime.utcnow() - timedelta(hours=6) } } # 请求/响应模型 class HealthResponse(BaseModel): status: str service: str version: str timestamp: datetime jenkins: Dict[str, Any] worker_pool: Dict[str, Any] database: Dict[str, Any] class TriggerLogResponse(BaseModel): id: int repository_name: str branch_name: str commit_sha: str job_name: str status: str error_message: Optional[str] = None created_at: datetime class APIKeyResponse(BaseModel): id: str name: str key_prefix: str created_at: datetime last_used: datetime is_active: bool role: str class ProjectMappingResponse(BaseModel): id: int repository_name: str default_job: str branch_jobs: List[dict] branch_patterns: List[dict] created_at: datetime updated_at: datetime # 认证函数 def verify_api_key(api_key: str): """验证 API 密钥""" for key_id, key_data in api_keys.items(): if key_data["key_hash"] == api_key and key_data["is_active"]: # 更新最后使用时间 key_data["last_used"] = datetime.utcnow() return key_data return None async def get_current_user( credentials: Optional[HTTPAuthorizationCredentials] = Depends(security) ): """获取当前用户(支持 API 密钥认证)""" if not credentials: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="需要认证令牌", headers={"WWW-Authenticate": "Bearer"}, ) token = credentials.credentials # 验证 API 密钥 api_key_data = verify_api_key(token) if api_key_data: return { "username": api_key_data["name"], "auth_type": "api_key", "role": api_key_data["role"] } # 认证失败 raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证令牌", headers={"WWW-Authenticate": "Bearer"}, ) # 公开端点 @app.get("/health", response_model=HealthResponse) async def health_check(): """健康检查端点""" settings = get_settings() return HealthResponse( status="healthy", service="Gitea Webhook Ambassador (Demo)", version=settings.version, timestamp=datetime.utcnow(), jenkins={"status": "connected"}, worker_pool={ "active_workers": 2, "queue_size": 0, "total_processed": len(trigger_logs), "total_failed": len([log for log in trigger_logs if log["status"] == "failed"]) }, database={"status": "connected"} ) @app.get("/") async def root(): """根路径""" return { "name": "Gitea Webhook Ambassador (Demo)", "version": "1.0.0", "description": "高性能的 Gitea 到 Jenkins 的 Webhook 服务 - 演示版本", "endpoints": { "webhook": "/webhook/gitea", "health": "/health", "logs": "/api/logs", "admin": "/api/admin" }, "demo_keys": { "admin": "demo_admin_key", "user": "demo_user_key" } } @app.post("/webhook/gitea") async def handle_gitea_webhook(request: Request): """处理 Gitea webhook 请求""" try: body = await request.body() # 记录 webhook 请求 logger.info("Received Gitea webhook", body_size=len(body), headers=dict(request.headers)) # 添加新的触发日志 log_entry = { "id": len(trigger_logs) + 1, "repository_name": "demo-repo", "branch_name": "main", "commit_sha": "demo123", "job_name": "demo-job", "status": "success", "error_message": None, "created_at": datetime.utcnow() } trigger_logs.append(log_entry) return { "success": True, "message": "Webhook received successfully", "data": { "body_size": len(body), "timestamp": datetime.utcnow().isoformat() } } except Exception as e: logger.error("Webhook processing failed", error=str(e)) return JSONResponse( status_code=500, content={ "success": False, "message": "Webhook processing failed", "error": str(e) } ) # 需要认证的端点 @app.get("/api/logs", response_model=List[TriggerLogResponse]) async def get_trigger_logs( repository: Optional[str] = None, branch: Optional[str] = None, limit: int = 100, current_user: dict = Depends(get_current_user) ): """获取触发日志""" print(f"用户 {current_user['username']} 访问日志端点") filtered_logs = trigger_logs.copy() if repository: filtered_logs = [log for log in filtered_logs if log["repository_name"] == repository] if branch: filtered_logs = [log for log in filtered_logs if log["branch_name"] == branch] # 按时间倒序排列并限制数量 filtered_logs.sort(key=lambda x: x["created_at"], reverse=True) return filtered_logs[:limit] @app.get("/api/logs/stats") async def get_log_stats(current_user: dict = Depends(get_current_user)): """获取日志统计信息""" print(f"用户 {current_user['username']} 访问日志统计") total_logs = len(trigger_logs) successful_logs = len([log for log in trigger_logs if log["status"] == "success"]) failed_logs = len([log for log in trigger_logs if log["status"] == "failed"]) # 最近24小时的日志数 yesterday = datetime.utcnow() - timedelta(days=1) recent_logs = len([log for log in trigger_logs if log["created_at"] >= yesterday]) # 按仓库分组的统计 repo_stats = {} for log in trigger_logs: repo = log["repository_name"] repo_stats[repo] = repo_stats.get(repo, 0) + 1 return { "total_logs": total_logs, "successful_logs": successful_logs, "failed_logs": failed_logs, "recent_logs_24h": recent_logs, "repository_stats": [ {"repository": repo, "count": count} for repo, count in repo_stats.items() ] } @app.get("/api/admin/api-keys", response_model=List[APIKeyResponse]) async def list_api_keys(current_user: dict = Depends(get_current_user)): """列出所有 API 密钥(仅管理员)""" if current_user["role"] != "admin": raise HTTPException(status_code=403, detail="需要管理员权限") print(f"管理员 {current_user['username']} 查看 API 密钥列表") return [ APIKeyResponse( id=key_id, name=key_data["name"], key_prefix=key_data["key_prefix"], created_at=key_data["created_at"], last_used=key_data["last_used"], is_active=key_data["is_active"], role=key_data["role"] ) for key_id, key_data in api_keys.items() ] @app.get("/api/admin/projects", response_model=List[ProjectMappingResponse]) async def list_project_mappings(current_user: dict = Depends(get_current_user)): """列出所有项目映射""" print(f"用户 {current_user['username']} 查看项目映射") return [ ProjectMappingResponse( id=mapping_id, repository_name=mapping_data["repository_name"], default_job=mapping_data["default_job"], branch_jobs=mapping_data["branch_jobs"], branch_patterns=mapping_data["branch_patterns"], created_at=mapping_data["created_at"], updated_at=mapping_data["updated_at"] ) for mapping_id, mapping_data in project_mappings.items() ] @app.get("/api/admin/stats") async def get_admin_stats(current_user: dict = Depends(get_current_user)): """获取管理统计信息""" print(f"用户 {current_user['username']} 查看管理统计") total_keys = len(api_keys) active_keys = len([key for key in api_keys.values() if key["is_active"]]) # 最近使用的密钥 week_ago = datetime.utcnow() - timedelta(days=7) recent_keys = len([ key for key in api_keys.values() if key["last_used"] >= week_ago ]) total_mappings = len(project_mappings) return { "api_keys": { "total": total_keys, "active": active_keys, "recently_used": recent_keys }, "project_mappings": { "total": total_mappings } } # 中间件 @app.middleware("http") async def log_requests(request: Request, call_next): """请求日志中间件""" start_time = datetime.utcnow() response = await call_next(request) process_time = (datetime.utcnow() - start_time).total_seconds() response.headers["X-Process-Time"] = str(process_time) return response if __name__ == "__main__": import uvicorn settings = get_settings() print("🚀 启动 Gitea Webhook Ambassador 演示版本") print("=" * 60) print("📋 演示 API 密钥:") print(" 管理员密钥: demo_admin_key") print(" 用户密钥: demo_user_key") print() print("🔧 使用示例:") print(" curl -H 'Authorization: Bearer demo_admin_key' http://localhost:8000/api/admin/api-keys") print(" curl -H 'Authorization: Bearer demo_user_key' http://localhost:8000/api/logs") print("=" * 60) uvicorn.run( "app.main_demo:app", host=settings.server.host, port=settings.server.port, reload=settings.server.reload )