- 新增完整的 Python 实现,替代 Go 版本 - 添加 Web 登录界面和仪表板 - 实现 JWT 认证和 API 密钥管理 - 添加数据库存储功能 - 保持与 Go 版本一致的目录结构和启动脚本 - 包含完整的文档和测试脚本
438 lines
13 KiB
Python
438 lines
13 KiB
Python
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
|
|
) |