freeleaps-ops/apps/gitea-webhook-ambassador-python/app/services/webhook_service.py
Nicolas f6c515157c feat: 添加 Python 版本的 Gitea Webhook Ambassador
- 新增完整的 Python 实现,替代 Go 版本
- 添加 Web 登录界面和仪表板
- 实现 JWT 认证和 API 密钥管理
- 添加数据库存储功能
- 保持与 Go 版本一致的目录结构和启动脚本
- 包含完整的文档和测试脚本
2025-07-20 21:17:10 +08:00

280 lines
10 KiB
Python

"""
Webhook 处理服务
实现智能分发、任务排队和防抖策略
"""
import asyncio
from typing import Optional, Dict, Any
from datetime import datetime
import structlog
from celery import Celery
from app.config import get_settings
from app.models.gitea import GiteaWebhook, WebhookResponse
from app.services.dedup_service import DeduplicationService
from app.services.jenkins_service import JenkinsService
from app.services.database_service import get_database_service
from app.tasks.jenkins_tasks import trigger_jenkins_job
logger = structlog.get_logger()
class WebhookService:
"""Webhook 处理服务"""
def __init__(
self,
dedup_service: DeduplicationService,
jenkins_service: JenkinsService,
celery_app: Celery
):
self.dedup_service = dedup_service
self.jenkins_service = jenkins_service
self.celery_app = celery_app
self.settings = get_settings()
self.db_service = get_database_service()
async def process_webhook(self, webhook: GiteaWebhook) -> WebhookResponse:
"""
处理 Webhook 事件
Args:
webhook: Gitea Webhook 数据
Returns:
WebhookResponse: 处理结果
"""
try:
# 1. 验证事件类型
if not webhook.is_push_event():
return WebhookResponse(
success=True,
message="Non-push event ignored",
event_id=webhook.get_event_id()
)
# 2. 提取关键信息
branch = webhook.get_branch_name()
commit_hash = webhook.get_commit_hash()
repository = webhook.repository.full_name
logger.info("Processing webhook",
repository=repository,
branch=branch,
commit_hash=commit_hash)
# 3. 防抖检查
dedup_key = self.dedup_service.generate_dedup_key(commit_hash, branch)
if await self.dedup_service.is_duplicate(dedup_key):
return WebhookResponse(
success=True,
message="Duplicate event ignored",
event_id=webhook.get_event_id()
)
# 4. 获取项目映射和任务名
job_name = await self._determine_job_name(repository, branch)
if not job_name:
return WebhookResponse(
success=True,
message=f"No Jenkins job mapping for repository: {repository}, branch: {branch}",
event_id=webhook.get_event_id()
)
# 5. 准备任务参数
job_params = self._prepare_job_parameters(webhook, job_name)
# 6. 提交任务到队列
task_result = await self._submit_job_to_queue(
webhook, job_name, job_params
)
if task_result:
return WebhookResponse(
success=True,
message="Job queued successfully",
event_id=webhook.get_event_id(),
job_name=job_name
)
else:
return WebhookResponse(
success=False,
message="Failed to queue job",
event_id=webhook.get_event_id()
)
except Exception as e:
logger.error("Error processing webhook",
repository=webhook.repository.full_name,
error=str(e))
return WebhookResponse(
success=False,
message=f"Internal server error: {str(e)}",
event_id=webhook.get_event_id()
)
async def _determine_job_name(self, repository: str, branch: str) -> Optional[str]:
"""根据仓库和分支确定任务名"""
# 首先尝试从数据库获取项目映射
job_name = await self.db_service.determine_job_name(repository, branch)
if job_name:
return job_name
# 如果数据库中没有映射,使用配置文件中的环境分发
environment = self.settings.get_environment_for_branch(branch)
if environment:
return environment.jenkins_job
return None
def _prepare_job_parameters(self, webhook: GiteaWebhook, job_name: str) -> Dict[str, str]:
"""准备 Jenkins 任务参数"""
author_info = webhook.get_author_info()
return {
"BRANCH_NAME": webhook.get_branch_name(),
"COMMIT_SHA": webhook.get_commit_hash(),
"REPOSITORY_URL": webhook.repository.clone_url,
"REPOSITORY_NAME": webhook.repository.full_name,
"PUSHER_NAME": author_info["name"],
"PUSHER_EMAIL": author_info["email"],
"PUSHER_USERNAME": author_info["username"],
"COMMIT_MESSAGE": webhook.get_commit_message(),
"JOB_NAME": job_name,
"WEBHOOK_EVENT_ID": webhook.get_event_id(),
"TRIGGER_TIME": datetime.utcnow().isoformat()
}
async def _submit_job_to_queue(
self,
webhook: GiteaWebhook,
job_name: str,
job_params: Dict[str, str]
) -> bool:
"""提交任务到 Celery 队列"""
try:
# 创建任务
task_kwargs = {
"job_name": job_name,
"jenkins_url": self.settings.jenkins.url,
"parameters": job_params,
"event_id": webhook.get_event_id(),
"repository": webhook.repository.full_name,
"branch": webhook.get_branch_name(),
"commit_hash": webhook.get_commit_hash(),
"priority": 1 # 默认优先级
}
# 提交到 Celery 队列
task = self.celery_app.send_task(
"app.tasks.jenkins_tasks.trigger_jenkins_job",
kwargs=task_kwargs,
priority=environment.priority
)
logger.info("Job submitted to queue",
task_id=task.id,
job_name=job_name,
repository=webhook.repository.full_name,
branch=webhook.get_branch_name())
return True
except Exception as e:
logger.error("Failed to submit job to queue",
job_name=job_name,
error=str(e))
return False
async def get_webhook_stats(self) -> Dict[str, Any]:
"""获取 Webhook 处理统计"""
try:
# 获取队列统计
queue_stats = await self._get_queue_stats()
# 获取防抖统计
dedup_stats = await self.dedup_service.get_stats()
# 获取环境配置
environments = {}
for name, config in self.settings.environments.items():
environments[name] = {
"branches": config.branches,
"jenkins_job": config.jenkins_job,
"jenkins_url": config.jenkins_url,
"priority": config.priority
}
return {
"queue": queue_stats,
"deduplication": dedup_stats,
"environments": environments,
"config": {
"max_concurrent": self.settings.queue.max_concurrent,
"max_retries": self.settings.queue.max_retries,
"retry_delay": self.settings.queue.retry_delay
},
"timestamp": datetime.utcnow().isoformat()
}
except Exception as e:
logger.error("Error getting webhook stats", error=str(e))
return {"error": str(e)}
async def _get_queue_stats(self) -> Dict[str, Any]:
"""获取队列统计信息"""
try:
# 获取 Celery 队列统计
inspect = self.celery_app.control.inspect()
# 活跃任务
active = inspect.active()
active_count = sum(len(tasks) for tasks in active.values()) if active else 0
# 等待任务
reserved = inspect.reserved()
reserved_count = sum(len(tasks) for tasks in reserved.values()) if reserved else 0
# 注册的 worker
registered = inspect.registered()
worker_count = len(registered) if registered else 0
return {
"active_tasks": active_count,
"queued_tasks": reserved_count,
"worker_count": worker_count,
"queue_length": active_count + reserved_count
}
except Exception as e:
logger.error("Error getting queue stats", error=str(e))
return {"error": str(e)}
async def clear_queue(self) -> Dict[str, Any]:
"""清空队列"""
try:
# 撤销所有活跃任务
inspect = self.celery_app.control.inspect()
active = inspect.active()
revoked_count = 0
if active:
for worker, tasks in active.items():
for task in tasks:
self.celery_app.control.revoke(task["id"], terminate=True)
revoked_count += 1
logger.info("Queue cleared", revoked_count=revoked_count)
return {
"success": True,
"revoked_count": revoked_count,
"message": f"Cleared {revoked_count} tasks from queue"
}
except Exception as e:
logger.error("Error clearing queue", error=str(e))
return {
"success": False,
"error": str(e)
}