From 7ed1312015ba69e50b588b9befa6a0c77590b83c Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 11 Jul 2025 11:31:24 +0800 Subject: [PATCH 01/26] =?UTF-8?q?feat(ops):=20=E6=96=B0=E5=A2=9E/=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20freeleaps-ops=20=E7=9B=B8=E5=85=B3=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freeleaps/CICD_IMPLEMENTATION_SUMMARY.md | 1 + freeleaps/DEVOPS_RECONCILER_CICD.md | 1 + .../alpha/ci/freeleaps2-devops/Jenkinsfile | 1 + .../ci/freeleaps2-reconciler/Jenkinsfile | 1 + .../prod/ci/freeleaps2-devops/Jenkinsfile | 35 +++++++++++++++++++ .../prod/ci/freeleaps2-reconciler/Jenkinsfile | 35 +++++++++++++++++++ 6 files changed, 74 insertions(+) create mode 100644 freeleaps/CICD_IMPLEMENTATION_SUMMARY.md create mode 100644 freeleaps/DEVOPS_RECONCILER_CICD.md create mode 100644 freeleaps/alpha/ci/freeleaps2-devops/Jenkinsfile create mode 100644 freeleaps/alpha/ci/freeleaps2-reconciler/Jenkinsfile create mode 100644 freeleaps/prod/ci/freeleaps2-devops/Jenkinsfile create mode 100644 freeleaps/prod/ci/freeleaps2-reconciler/Jenkinsfile diff --git a/freeleaps/CICD_IMPLEMENTATION_SUMMARY.md b/freeleaps/CICD_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/freeleaps/CICD_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/freeleaps/DEVOPS_RECONCILER_CICD.md b/freeleaps/DEVOPS_RECONCILER_CICD.md new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/freeleaps/DEVOPS_RECONCILER_CICD.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/freeleaps/alpha/ci/freeleaps2-devops/Jenkinsfile b/freeleaps/alpha/ci/freeleaps2-devops/Jenkinsfile new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/freeleaps/alpha/ci/freeleaps2-devops/Jenkinsfile @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/freeleaps/alpha/ci/freeleaps2-reconciler/Jenkinsfile b/freeleaps/alpha/ci/freeleaps2-reconciler/Jenkinsfile new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/freeleaps/alpha/ci/freeleaps2-reconciler/Jenkinsfile @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/freeleaps/prod/ci/freeleaps2-devops/Jenkinsfile b/freeleaps/prod/ci/freeleaps2-devops/Jenkinsfile new file mode 100644 index 00000000..64e7f9d1 --- /dev/null +++ b/freeleaps/prod/ci/freeleaps2-devops/Jenkinsfile @@ -0,0 +1,35 @@ +library 'first-class-pipeline' + +executeFreeleapsPipeline { + serviceName = 'freeleaps' + environmentSlug = 'prod' + serviceGitBranch = 'master' + serviceGitRepo = "https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps2-devops.git" + serviceGitRepoType = 'monorepo' + serviceGitCredentialsId = 'freeleaps-repos-gitea-credentails' + executeMode = 'fully' + commitMessageLintEnabled = false + components = [ + [ + name: 'devops', + root: 'apps/devops', + language: 'python', + dependenciesManager: 'pip', + requirementsFile: 'requirements.txt', + buildCacheEnabled: true, + buildAgentImage: 'python:3.12-slim', + buildArtifacts: ['.'], + lintEnabled: true, + sastEnabled: true, + imageRegistry: 'docker.io', + imageRepository: 'freeleaps', + imageName: 'devops', + imageBuilder: 'dind', + dockerfilePath: 'Dockerfile', + imageBuildRoot: '.', + imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'], + registryCredentialsId: 'freeleaps-devops-docker-hub-credentials', + semanticReleaseEnabled: true + ] + ] +} \ No newline at end of file diff --git a/freeleaps/prod/ci/freeleaps2-reconciler/Jenkinsfile b/freeleaps/prod/ci/freeleaps2-reconciler/Jenkinsfile new file mode 100644 index 00000000..3564192a --- /dev/null +++ b/freeleaps/prod/ci/freeleaps2-reconciler/Jenkinsfile @@ -0,0 +1,35 @@ +library 'first-class-pipeline' + +executeFreeleapsPipeline { + serviceName = 'freeleaps' + environmentSlug = 'prod' + serviceGitBranch = 'master' + serviceGitRepo = "https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps2-reconciler.git" + serviceGitRepoType = 'monorepo' + serviceGitCredentialsId = 'freeleaps-repos-gitea-credentails' + executeMode = 'fully' + commitMessageLintEnabled = false + components = [ + [ + name: 'reconciler', + root: 'apps/reconciler', + language: 'python', + dependenciesManager: 'pip', + requirementsFile: 'requirements.txt', + buildCacheEnabled: true, + buildAgentImage: 'python:3.12-slim', + buildArtifacts: ['.'], + lintEnabled: true, + sastEnabled: true, + imageRegistry: 'docker.io', + imageRepository: 'freeleaps', + imageName: 'reconciler', + imageBuilder: 'dind', + dockerfilePath: 'Dockerfile', + imageBuildRoot: '.', + imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'], + registryCredentialsId: 'freeleaps-devops-docker-hub-credentials', + semanticReleaseEnabled: true + ] + ] +} \ No newline at end of file From 843a73ef80e25bd23be497fbc1a772033203a0e6 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 14 Jul 2025 14:54:16 +0800 Subject: [PATCH 02/26] Remove CICD_IMPLEMENTATION_SUMMARY.md file --- freeleaps/CICD_IMPLEMENTATION_SUMMARY.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 freeleaps/CICD_IMPLEMENTATION_SUMMARY.md diff --git a/freeleaps/CICD_IMPLEMENTATION_SUMMARY.md b/freeleaps/CICD_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 0519ecba..00000000 --- a/freeleaps/CICD_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From f6c515157c1ae95918a0bf9cf1714032bceaab94 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 20 Jul 2025 21:12:10 +0800 Subject: [PATCH 03/26] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Python=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=9A=84=20Gitea=20Webhook=20Ambassador?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增完整的 Python 实现,替代 Go 版本 - 添加 Web 登录界面和仪表板 - 实现 JWT 认证和 API 密钥管理 - 添加数据库存储功能 - 保持与 Go 版本一致的目录结构和启动脚本 - 包含完整的文档和测试脚本 --- .../.gitignore | 63 +++ apps/gitea-webhook-ambassador-python/Makefile | 143 ++++++ .../gitea-webhook-ambassador-python/README.md | 189 ++++++++ .../README_ENHANCED.md | 339 ++++++++++++++ apps/gitea-webhook-ambassador-python/USAGE.md | 379 +++++++++++++++ .../app/auth/middleware.py | 93 ++++ .../app/config.py | 189 ++++++++ .../app/handlers/__init__.py | 10 + .../app/handlers/admin.py | 287 ++++++++++++ .../app/handlers/auth.py | 122 +++++ .../app/handlers/health.py | 145 ++++++ .../app/handlers/logs.py | 106 +++++ .../app/handlers/projects.py | 161 +++++++ .../app/handlers/webhook.py | 42 ++ .../app/main.py | 383 +++++++++++++++ .../app/main_demo.py | 438 ++++++++++++++++++ .../app/main_enhanced.py | 313 +++++++++++++ .../app/main_simple.py | 145 ++++++ .../app/models/api_key.py | 20 + .../app/models/database.py | 92 ++++ .../app/models/gitea.py | 130 ++++++ .../app/models/project_mapping.py | 20 + .../app/models/trigger_log.py | 22 + .../app/services/database_service.py | 398 ++++++++++++++++ .../app/services/dedup_service.py | 223 +++++++++ .../app/services/jenkins_service.py | 100 ++++ .../app/services/queue_service.py | 70 +++ .../app/services/webhook_service.py | 280 +++++++++++ .../app/static/js/dashboard.js | 373 +++++++++++++++ .../app/tasks/jenkins_tasks.py | 306 ++++++++++++ .../app/templates/dashboard.html | 326 +++++++++++++ .../app/templates/login.html | 174 +++++++ .../check_version.sh | 133 ++++++ .../config/environments.yaml | 39 ++ .../configs/config.yaml | 32 ++ apps/gitea-webhook-ambassador-python/devbox | 220 +++++++++ .../docker-compose.yml | 176 +++++++ .../env.example | 42 ++ .../fix_pid.sh | 41 ++ .../gitea-webhook-ambassador.service | 15 + .../quick_check.sh | 31 ++ .../requirements.txt | 15 + .../scripts/setup.sh | 61 +++ .../scripts/start.sh | 64 +++ apps/gitea-webhook-ambassador-python/start.sh | 154 ++++++ .../test_auth.py | 172 +++++++ .../test_enhanced.py | 225 +++++++++ .../test_enhanced_features.py | 226 +++++++++ .../test_webhook.py | 307 ++++++++++++ 49 files changed, 8034 insertions(+) create mode 100644 apps/gitea-webhook-ambassador-python/.gitignore create mode 100644 apps/gitea-webhook-ambassador-python/Makefile create mode 100644 apps/gitea-webhook-ambassador-python/README.md create mode 100644 apps/gitea-webhook-ambassador-python/README_ENHANCED.md create mode 100644 apps/gitea-webhook-ambassador-python/USAGE.md create mode 100644 apps/gitea-webhook-ambassador-python/app/auth/middleware.py create mode 100644 apps/gitea-webhook-ambassador-python/app/config.py create mode 100644 apps/gitea-webhook-ambassador-python/app/handlers/__init__.py create mode 100644 apps/gitea-webhook-ambassador-python/app/handlers/admin.py create mode 100644 apps/gitea-webhook-ambassador-python/app/handlers/auth.py create mode 100644 apps/gitea-webhook-ambassador-python/app/handlers/health.py create mode 100644 apps/gitea-webhook-ambassador-python/app/handlers/logs.py create mode 100644 apps/gitea-webhook-ambassador-python/app/handlers/projects.py create mode 100644 apps/gitea-webhook-ambassador-python/app/handlers/webhook.py create mode 100644 apps/gitea-webhook-ambassador-python/app/main.py create mode 100644 apps/gitea-webhook-ambassador-python/app/main_demo.py create mode 100644 apps/gitea-webhook-ambassador-python/app/main_enhanced.py create mode 100644 apps/gitea-webhook-ambassador-python/app/main_simple.py create mode 100644 apps/gitea-webhook-ambassador-python/app/models/api_key.py create mode 100644 apps/gitea-webhook-ambassador-python/app/models/database.py create mode 100644 apps/gitea-webhook-ambassador-python/app/models/gitea.py create mode 100644 apps/gitea-webhook-ambassador-python/app/models/project_mapping.py create mode 100644 apps/gitea-webhook-ambassador-python/app/models/trigger_log.py create mode 100644 apps/gitea-webhook-ambassador-python/app/services/database_service.py create mode 100644 apps/gitea-webhook-ambassador-python/app/services/dedup_service.py create mode 100644 apps/gitea-webhook-ambassador-python/app/services/jenkins_service.py create mode 100644 apps/gitea-webhook-ambassador-python/app/services/queue_service.py create mode 100644 apps/gitea-webhook-ambassador-python/app/services/webhook_service.py create mode 100644 apps/gitea-webhook-ambassador-python/app/static/js/dashboard.js create mode 100644 apps/gitea-webhook-ambassador-python/app/tasks/jenkins_tasks.py create mode 100644 apps/gitea-webhook-ambassador-python/app/templates/dashboard.html create mode 100644 apps/gitea-webhook-ambassador-python/app/templates/login.html create mode 100755 apps/gitea-webhook-ambassador-python/check_version.sh create mode 100644 apps/gitea-webhook-ambassador-python/config/environments.yaml create mode 100644 apps/gitea-webhook-ambassador-python/configs/config.yaml create mode 100755 apps/gitea-webhook-ambassador-python/devbox create mode 100644 apps/gitea-webhook-ambassador-python/docker-compose.yml create mode 100644 apps/gitea-webhook-ambassador-python/env.example create mode 100755 apps/gitea-webhook-ambassador-python/fix_pid.sh create mode 100644 apps/gitea-webhook-ambassador-python/gitea-webhook-ambassador.service create mode 100755 apps/gitea-webhook-ambassador-python/quick_check.sh create mode 100644 apps/gitea-webhook-ambassador-python/requirements.txt create mode 100755 apps/gitea-webhook-ambassador-python/scripts/setup.sh create mode 100755 apps/gitea-webhook-ambassador-python/scripts/start.sh create mode 100755 apps/gitea-webhook-ambassador-python/start.sh create mode 100644 apps/gitea-webhook-ambassador-python/test_auth.py create mode 100644 apps/gitea-webhook-ambassador-python/test_enhanced.py create mode 100644 apps/gitea-webhook-ambassador-python/test_enhanced_features.py create mode 100755 apps/gitea-webhook-ambassador-python/test_webhook.py diff --git a/apps/gitea-webhook-ambassador-python/.gitignore b/apps/gitea-webhook-ambassador-python/.gitignore new file mode 100644 index 00000000..8fd4b48b --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/.gitignore @@ -0,0 +1,63 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual Environment +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Logs +*.log +logs/*.log + +# Database +*.db +*.sqlite +*.sqlite3 + +# PID files +*.pid + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# OS +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/Makefile b/apps/gitea-webhook-ambassador-python/Makefile new file mode 100644 index 00000000..4a9f6993 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/Makefile @@ -0,0 +1,143 @@ +.PHONY: build clean test lint docker-build docker-push run help install init + +# Variables +APP_NAME := gitea-webhook-ambassador +VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +PYTHON := python3 +PIP := pip +VENV := venv +CONFIG_FILE := config.yaml + +# Python commands +PYTHON_FILES := $(shell find . -name "*.py" -type f) +REQUIREMENTS := requirements.txt + +# Default target +.DEFAULT_GOAL := help + +# Install dependencies +install: + @echo "Installing dependencies..." + $(PYTHON) -m venv $(VENV) + . $(VENV)/bin/activate && $(PIP) install -r $(REQUIREMENTS) + +# Initialize database +init: + @echo "Initializing database..." + . $(VENV)/bin/activate && $(PYTHON) -c "from app.models.database import create_tables; create_tables(); print('Database initialized')" + +# Build (for Python, this means installing dependencies) +build: install + @echo "Python application ready" + +# Clean build artifacts +clean: + @echo "Cleaning up..." + @rm -rf $(VENV) + @rm -f *.db + @rm -rf logs/ + @rm -f *.pid + +# Run tests +test: + @echo "Running tests..." + . $(VENV)/bin/activate && $(PYTHON) test_enhanced_features.py + +# Run linter +lint: + @echo "Running linter..." + . $(VENV)/bin/activate && flake8 app/ --max-line-length=120 --ignore=E501,W503 + +# Build Docker image +docker-build: + @echo "Building Docker image $(APP_NAME):$(VERSION)..." + docker build -t $(APP_NAME):$(VERSION) . + docker tag $(APP_NAME):$(VERSION) $(APP_NAME):latest + +# Push Docker image to registry +docker-push: docker-build + @echo "Pushing Docker image $(APP_NAME):$(VERSION)..." + docker push $(APP_NAME):$(VERSION) + docker push $(APP_NAME):latest + +# Run locally +run: build init + @echo "Starting $(APP_NAME)..." + . $(VENV)/bin/activate && $(PYTHON) -m uvicorn app.main_enhanced:app --host 0.0.0.0 --port 8000 + +# Run in background +start: build init + @echo "Starting $(APP_NAME) in background..." + . $(VENV)/bin/activate && nohup $(PYTHON) -m uvicorn app.main_enhanced:app --host 0.0.0.0 --port 8000 > logs/service.log 2>&1 & + @echo $$! > service.pid + @echo "Service started with PID $$(cat service.pid)" + +# Stop service +stop: + @if [ -f service.pid ]; then \ + echo "Stopping $(APP_NAME)..."; \ + kill $$(cat service.pid) 2>/dev/null || true; \ + rm -f service.pid; \ + echo "Service stopped"; \ + else \ + echo "No service.pid found"; \ + fi + +# Restart service +restart: stop start + +# Show service status +status: + @if [ -f service.pid ]; then \ + PID=$$(cat service.pid); \ + if ps -p $$PID > /dev/null 2>&1; then \ + echo "✅ $(APP_NAME) is running (PID: $$PID)"; \ + echo "📝 Log file: logs/service.log"; \ + echo "🌐 Access: http://localhost:8000"; \ + else \ + echo "❌ $(APP_NAME) is not running (PID file exists but process not found)"; \ + rm -f service.pid; \ + fi; \ + else \ + echo "❌ $(APP_NAME) is not running"; \ + fi + +# Show logs +logs: + @if [ -f logs/service.log ]; then \ + echo "📝 Latest logs (last 50 lines):"; \ + echo "----------------------------------------"; \ + tail -n 50 logs/service.log; \ + echo "----------------------------------------"; \ + else \ + echo "❌ No log file found"; \ + fi + +# Follow logs +follow: + @if [ -f logs/service.log ]; then \ + echo "📝 Following logs (Ctrl+C to exit):"; \ + tail -f logs/service.log; \ + else \ + echo "❌ No log file found"; \ + fi + +# Show help +help: + @echo "Gitea Webhook Ambassador (Python) - Makefile commands:" + @echo " install - Install Python dependencies" + @echo " init - Initialize database" + @echo " build - Install dependencies (alias for install)" + @echo " clean - Remove build artifacts and logs" + @echo " test - Run tests" + @echo " lint - Run linter" + @echo " docker-build - Build Docker image" + @echo " docker-push - Build and push Docker image to registry" + @echo " run - Install, init and run locally (foreground)" + @echo " start - Install, init and start in background" + @echo " stop - Stop background service" + @echo " restart - Restart background service" + @echo " status - Show service status" + @echo " logs - Show latest logs" + @echo " follow - Follow logs in real-time" + @echo " help - Show this help message" \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/README.md b/apps/gitea-webhook-ambassador-python/README.md new file mode 100644 index 00000000..f3f5a952 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/README.md @@ -0,0 +1,189 @@ +# Gitea Webhook Ambassador (Python) + +一个高性能的 Python webhook 服务,用于连接 Gitea 和 Jenkins,支持智能分发、高并发处理和防抖策略。 + +## 🚀 新特性 + +### 1. 智能分发策略 +- **dev 分支** → 触发 alpha 环境构建 +- **prod 分支** → 触发生产环境构建 +- **其他分支** → 可配置的默认策略 + +### 2. 高并发处理 +- **异步任务队列**: 使用 Celery + Redis 处理高并发 +- **任务排队机制**: 防止构建丢失,确保任务按序执行 +- **负载均衡**: 支持多 worker 实例 + +### 3. 防抖策略 +- **基于 commit hash + 分支的去重**: 防止重复触发 +- **时间窗口防抖**: 在指定时间窗口内的相同提交只触发一次 +- **智能去重**: 支持配置去重策略 + +## 🏗️ 架构设计 + +``` +Gitea Webhook → FastAPI → Celery Queue → Jenkins Workers + ↓ ↓ ↓ ↓ + 验证签名 路由分发 任务排队 并发执行 + ↓ ↓ ↓ ↓ + 防抖检查 环境判断 持久化存储 状态反馈 +``` + +## 📁 项目结构 + +``` +gitea-webhook-ambassador-python/ +├── app/ +│ ├── __init__.py +│ ├── main.py # FastAPI 应用入口 +│ ├── config.py # 配置管理 +│ ├── models/ # 数据模型 +│ │ ├── __init__.py +│ │ ├── gitea.py # Gitea webhook 模型 +│ │ └── jenkins.py # Jenkins 任务模型 +│ ├── services/ # 业务逻辑 +│ │ ├── __init__.py +│ │ ├── webhook_service.py # Webhook 处理服务 +│ │ ├── jenkins_service.py # Jenkins 集成服务 +│ │ ├── queue_service.py # 队列管理服务 +│ │ └── dedup_service.py # 防抖服务 +│ ├── api/ # API 路由 +│ │ ├── __init__.py +│ │ ├── webhook.py # Webhook 端点 +│ │ ├── health.py # 健康检查 +│ │ └── admin.py # 管理接口 +│ ├── core/ # 核心组件 +│ │ ├── __init__.py +│ │ ├── security.py # 安全验证 +│ │ ├── database.py # 数据库连接 +│ │ └── cache.py # 缓存管理 +│ └── tasks/ # Celery 任务 +│ ├── __init__.py +│ └── jenkins_tasks.py # Jenkins 任务处理 +├── tests/ # 测试文件 +├── docker/ # Docker 配置 +├── requirements.txt # Python 依赖 +├── docker-compose.yml # 开发环境 +└── README.md +``` + +## 🛠️ 技术栈 + +- **Web 框架**: FastAPI +- **任务队列**: Celery + Redis +- **数据库**: PostgreSQL (生产) / SQLite (开发) +- **缓存**: Redis +- **监控**: Prometheus + Grafana +- **日志**: Structured logging with JSON +- **测试**: pytest + pytest-asyncio + +## 🚀 快速开始 + +### 1. 安装依赖 +```bash +pip install -r requirements.txt +``` + +### 2. 配置环境 +```bash +cp .env.example .env +# 编辑 .env 文件配置 Jenkins 和数据库连接 +``` + +### 3. 启动服务 +```bash +# 启动 Redis +docker run -d -p 6379:6379 redis:alpine + +# 启动 Celery worker +celery -A app.tasks worker --loglevel=info + +# 启动 FastAPI 应用 +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +## 📋 配置说明 + +### 环境分发配置 +```yaml +environments: + dev: + branches: ["dev", "develop", "development"] + jenkins_job: "alpha-build" + jenkins_url: "https://jenkins-alpha.example.com" + prod: + branches: ["prod", "production", "main", "master"] + jenkins_job: "production-build" + jenkins_url: "https://jenkins-prod.example.com" + default: + jenkins_job: "default-build" + jenkins_url: "https://jenkins-default.example.com" +``` + +### 防抖配置 +```yaml +deduplication: + enabled: true + window_seconds: 300 # 5分钟防抖窗口 + strategy: "commit_branch" # commit_hash + branch + cache_ttl: 3600 # 缓存1小时 +``` + +### 队列配置 +```yaml +queue: + max_concurrent: 10 + max_retries: 3 + retry_delay: 60 # 秒 + priority_levels: 3 +``` + +## 🔧 API 接口 + +### Webhook 端点 +``` +POST /webhook/gitea +``` + +### 健康检查 +``` +GET /health +GET /health/queue +GET /health/jenkins +``` + +### 管理接口 +``` +GET /admin/queue/status +GET /admin/queue/stats +POST /admin/queue/clear +``` + +## 🧪 测试 + +```bash +# 运行所有测试 +pytest + +# 运行特定测试 +pytest tests/test_webhook_service.py + +# 运行性能测试 +pytest tests/test_performance.py +``` + +## 📊 监控指标 + +- Webhook 接收率 +- 任务队列长度 +- Jenkins 构建成功率 +- 响应时间分布 +- 防抖命中率 + +## 🔒 安全特性 + +- Webhook 签名验证 +- API 密钥认证 +- 请求频率限制 +- 输入验证和清理 +- 安全日志记录 \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/README_ENHANCED.md b/apps/gitea-webhook-ambassador-python/README_ENHANCED.md new file mode 100644 index 00000000..f1056855 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/README_ENHANCED.md @@ -0,0 +1,339 @@ +# Gitea Webhook Ambassador (Python Enhanced Version) + +这是一个用 Python 重写的 Gitea Webhook Ambassador 服务,提供与 Go 版本相同的功能,但增加了 Web 界面和更多管理功能。 + +## 🚀 快速开始 + +### 方式一:使用 devbox 脚本(推荐,与 Go 版本一致) + +```bash +# 安装依赖 +./devbox install + +# 初始化数据库 +./devbox init + +# 启动服务 +./devbox start + +# 查看状态 +./devbox status + +# 查看日志 +./devbox logs + +# 停止服务 +./devbox stop +``` + +### 方式二:使用 Makefile + +```bash +# 安装依赖 +make install + +# 初始化数据库 +make init + +# 启动服务(前台运行) +make run + +# 启动服务(后台运行) +make start + +# 查看状态 +make status + +# 查看日志 +make logs + +# 停止服务 +make stop +``` + +### 方式三:直接使用 Python + +```bash +# 创建虚拟环境 +python3 -m venv venv +source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt + +# 初始化数据库 +python -c "from app.models.database import create_tables; create_tables()" + +# 启动服务 +python -m uvicorn app.main_enhanced:app --host 0.0.0.0 --port 8000 +``` + +## 📁 目录结构(与 Go 版本一致) + +``` +gitea-webhook-ambassador-python/ +├── app/ # 应用代码 +│ ├── auth/ # 认证模块 +│ ├── handlers/ # API 处理器 +│ ├── models/ # 数据模型 +│ ├── templates/ # HTML 模板 +│ ├── static/ # 静态文件 +│ └── main_enhanced.py # 主应用入口 +├── cmd/ # 命令行工具(与 Go 版本一致) +│ └── server/ # 服务器启动 +├── configs/ # 配置文件(与 Go 版本一致) +│ └── config.yaml # 主配置文件 +├── data/ # 数据目录(与 Go 版本一致) +│ └── *.db # SQLite 数据库文件 +├── logs/ # 日志目录(与 Go 版本一致) +│ └── service.log # 服务日志 +├── devbox # 启动脚本(与 Go 版本一致) +├── Makefile # 构建脚本(与 Go 版本一致) +├── requirements.txt # Python 依赖 +└── README_ENHANCED.md # 本文档 +``` + +## 🔧 配置 + +编辑 `configs/config.yaml` 文件: + +```yaml +server: + port: 8000 + webhookPath: "/webhook" + secretHeader: "X-Gitea-Signature" + secretKey: "admin-secret-key-change-in-production" + +jenkins: + url: "http://jenkins.example.com" + username: "jenkins-user" + token: "jenkins-api-token" + timeout: 30 + +admin: + token: "admin-api-token" + +database: + path: "data/gitea-webhook-ambassador.db" + +logging: + level: "info" + format: "text" + file: "logs/service.log" + +worker: + poolSize: 10 + queueSize: 100 + maxRetries: 3 + retryBackoff: 1 + +eventCleanup: + interval: 3600 + expireAfter: 7200 +``` + +## 🌐 Web 界面 + +启动服务后,访问以下地址: + +- **登录页面**: http://localhost:8000 +- **仪表板**: http://localhost:8000/dashboard +- **API 文档**: http://localhost:8000/docs + +### 默认登录凭据 +- **用户名**: admin +- **密码**: admin-secret-key-change-in-production + +## 📊 功能特性 + +### ✅ 与 Go 版本相同的功能 +- Gitea Webhook 接收和处理 +- Jenkins 任务触发 +- 项目映射配置 +- 分支模式匹配 +- 重试机制 +- 日志记录 + +### 🆕 Python 版本增强功能 +- **Web 登录界面**: 基于 Bootstrap 5 的现代化界面 +- **数据库存储**: SQLite 数据库存储 API 密钥和配置 +- **JWT 认证**: 7 天有效期的 JWT 令牌 +- **前端仪表板**: 多标签页管理界面 +- **自动重定向**: 未认证用户自动跳转到登录页 +- **健康检查**: 服务状态监控 +- **统计信息**: 请求统计和性能指标 + +## 🔌 API 端点 + +### 认证相关 +- `POST /api/auth/login` - 用户登录 +- `GET /api/auth/verify` - 验证 JWT 令牌 + +### 项目管理 +- `GET /api/projects` - 获取项目列表 +- `POST /api/projects` - 创建新项目 +- `PUT /api/projects/{id}` - 更新项目 +- `DELETE /api/projects/{id}` - 删除项目 + +### API 密钥管理 +- `GET /api/keys` - 获取 API 密钥列表 +- `POST /api/keys` - 创建新 API 密钥 +- `DELETE /api/keys/{id}` - 删除 API 密钥 + +### 系统监控 +- `GET /api/health` - 健康检查 +- `GET /api/stats` - 统计信息 +- `GET /api/logs` - 日志查看 + +### Webhook 处理 +- `POST /webhook` - Gitea Webhook 接收端点 + +## 🛠️ 开发 + +### 运行测试 +```bash +# 使用 devbox +./devbox test + +# 使用 Makefile +make test + +# 直接运行 +python test_enhanced_features.py +``` + +### 代码检查 +```bash +# 使用 Makefile +make lint + +# 直接运行 +flake8 app/ --max-line-length=120 --ignore=E501,W503 +``` + +### 清理 +```bash +# 使用 devbox +./devbox clean + +# 使用 Makefile +make clean +``` + +## 🐳 Docker 部署 + +### 构建镜像 +```bash +# 使用 Makefile +make docker-build + +# 直接构建 +docker build -t gitea-webhook-ambassador:latest . +``` + +### 运行容器 +```bash +docker run -d \ + --name gitea-webhook-ambassador \ + -p 8000:8000 \ + -v $(pwd)/configs:/app/configs \ + -v $(pwd)/data:/app/data \ + -v $(pwd)/logs:/app/logs \ + gitea-webhook-ambassador:latest +``` + +## 📈 与 Go 版本对比 + +| 特性 | Go 版本 | Python 版本 | +|------|---------|-------------| +| **启动方式** | `./devbox start` | `./devbox start` | +| **目录结构** | 标准 Go 项目结构 | 与 Go 版本一致 | +| **配置文件** | `configs/config.yaml` | `configs/config.yaml` | +| **日志目录** | `logs/` | `logs/` | +| **数据目录** | `data/` | `data/` | +| **Web 界面** | ❌ 无 | ✅ 完整仪表板 | +| **数据库** | ❌ 无 | ✅ SQLite | +| **JWT 认证** | ❌ 无 | ✅ 7天有效期 | +| **API 密钥管理** | ❌ 无 | ✅ 数据库存储 | +| **健康检查** | ✅ 基础 | ✅ 增强版 | +| **性能** | 🚀 极高 | 🚀 高 | + +## 🔄 迁移指南 + +### 从 Go 版本迁移到 Python 版本 + +1. **停止 Go 服务** + ```bash + cd /path/to/go-version + ./devbox stop + ``` + +2. **启动 Python 服务** + ```bash + cd /path/to/python-version + ./devbox install + ./devbox init + ./devbox start + ``` + +3. **验证服务** + ```bash + ./devbox status + curl http://localhost:8000/api/health + ``` + +4. **配置 Webhook** + - 更新 Gitea Webhook URL 为新的 Python 服务地址 + - 确保 Jenkins 配置正确 + +## 🆘 故障排除 + +### 常见问题 + +**1. 端口被占用** +```bash +# 检查端口占用 +lsof -i :8000 + +# 停止占用进程 +sudo kill -9 +``` + +**2. 虚拟环境问题** +```bash +# 重新创建虚拟环境 +rm -rf venv +./devbox install +``` + +**3. 数据库问题** +```bash +# 重新初始化数据库 +./devbox init +``` + +**4. 权限问题** +```bash +# 设置脚本权限 +chmod +x devbox +``` + +### 日志查看 +```bash +# 查看实时日志 +./devbox follow + +# 查看最新日志 +./devbox logs + +# 查看完整日志 +tail -f logs/service.log +``` + +## 📞 支持 + +如有问题,请检查: +1. 服务状态:`./devbox status` +2. 日志信息:`./devbox logs` +3. 配置文件:`configs/config.yaml` +4. 网络连接:`curl http://localhost:8000/api/health` \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/USAGE.md b/apps/gitea-webhook-ambassador-python/USAGE.md new file mode 100644 index 00000000..12991400 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/USAGE.md @@ -0,0 +1,379 @@ +# 🚀 Gitea Webhook Ambassador 使用指南 + +## 📋 目录 +1. [快速开始](#快速开始) +2. [配置说明](#配置说明) +3. [API 接口](#api-接口) +4. [数据库管理](#数据库管理) +5. [监控和日志](#监控和日志) +6. [故障排除](#故障排除) + +## 🚀 快速开始 + +### 1. 环境准备 + +```bash +# 克隆项目 +cd freeleaps-ops/apps/gitea-webhook-ambassador-python + +# 运行快速设置脚本 +chmod +x scripts/setup.sh +./scripts/setup.sh +``` + +### 2. 配置环境 + +编辑 `.env` 文件,配置必要的参数: + +```bash +# 编辑配置文件 +nano .env +``` + +**必需配置**: +```env +# Jenkins 配置 +JENKINS_USERNAME=your_jenkins_username +JENKINS_TOKEN=your_jenkins_api_token + +# 安全配置 +SECURITY_SECRET_KEY=your-secret-key-here-make-it-long-and-random +``` + +### 3. 启动服务 + +```bash +# 方法1: 使用启动脚本 +chmod +x scripts/start.sh +./scripts/start.sh + +# 方法2: 手动启动 +# 启动 Redis +docker run -d --name webhook-redis -p 6379:6379 redis:alpine + +# 激活虚拟环境 +source venv/bin/activate + +# 启动 API 服务 +python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + +# 新终端启动 Celery worker +celery -A app.tasks.jenkins_tasks worker --loglevel=info --concurrency=4 + +# 新终端启动定时任务 +celery -A app.tasks.jenkins_tasks beat --loglevel=info +``` + +### 4. 验证安装 + +访问以下地址验证服务是否正常: + +- **API 文档**: http://localhost:8000/docs +- **健康检查**: http://localhost:8000/health +- **监控指标**: http://localhost:8000/metrics + +## ⚙️ 配置说明 + +### 环境分发配置 + +编辑 `config/environments.yaml` 文件: + +```yaml +environments: + dev: + branches: ["dev", "develop", "development", "feature/*"] + jenkins_job: "alpha-build" + jenkins_url: "https://jenkins-alpha.freeleaps.com" + priority: 2 + + prod: + branches: ["prod", "production", "main", "master", "release/*"] + jenkins_job: "production-build" + jenkins_url: "https://jenkins-prod.freeleaps.com" + priority: 1 +``` + +### 防抖配置 + +```yaml +deduplication: + enabled: true + window_seconds: 300 # 5分钟防抖窗口 + strategy: "commit_branch" # commit_hash + branch + cache_ttl: 3600 # 缓存1小时 +``` + +## 🔧 API 接口 + +### Webhook 端点 + +**POST** `/webhook/gitea` + +接收 Gitea webhook 事件: + +```bash +curl -X POST "http://localhost:8000/webhook/gitea" \ + -H "Content-Type: application/json" \ + -H "X-Gitea-Signature: your-secret-key" \ + -d '{ + "ref": "refs/heads/dev", + "before": "abc123", + "after": "def456", + "repository": { + "full_name": "freeleaps/my-project", + "clone_url": "https://gitea.freeleaps.com/freeleaps/my-project.git" + }, + "pusher": { + "login": "developer", + "email": "dev@freeleaps.com" + } + }' +``` + +### 健康检查 + +**GET** `/health` + +```bash +curl http://localhost:8000/health +``` + +响应示例: +```json +{ + "status": "healthy", + "timestamp": 1640995200.0, + "services": { + "redis": "healthy", + "celery": "healthy" + } +} +``` + +### 队列状态 + +**GET** `/health/queue` + +```bash +curl http://localhost:8000/health/queue +``` + +响应示例: +```json +{ + "status": "healthy", + "queue_stats": { + "active_tasks": 2, + "queued_tasks": 5, + "worker_count": 4, + "total_queue_length": 7 + } +} +``` + +### 监控指标 + +**GET** `/metrics` + +```bash +curl http://localhost:8000/metrics +``` + +返回 Prometheus 格式的监控指标。 + +## 🗄️ 数据库管理 + +### 创建项目映射 + +使用 Python 脚本创建项目映射: + +```python +# create_mapping.py +import asyncio +from app.services.database_service import get_database_service + +async def create_mapping(): + db_service = get_database_service() + + mapping_data = { + "repository_name": "freeleaps/my-project", + "default_job": "default-build", + "branch_jobs": [ + {"branch_name": "dev", "job_name": "alpha-build"}, + {"branch_name": "main", "job_name": "production-build"} + ], + "branch_patterns": [ + {"pattern": r"feature/.*", "job_name": "feature-build"}, + {"pattern": r"hotfix/.*", "job_name": "hotfix-build"} + ] + } + + success = await db_service.create_project_mapping(mapping_data) + print(f"创建映射: {'成功' if success else '失败'}") + +if __name__ == "__main__": + asyncio.run(create_mapping()) +``` + +运行脚本: +```bash +python create_mapping.py +``` + +### 查看触发日志 + +```python +# view_logs.py +import asyncio +from app.services.database_service import get_database_service + +async def view_logs(): + db_service = get_database_service() + + logs = await db_service.get_trigger_logs( + repository_name="freeleaps/my-project", + limit=10 + ) + + for log in logs: + print(f"[{log['created_at']}] {log['repository_name']} - {log['branch_name']} - {log['status']}") + +if __name__ == "__main__": + asyncio.run(view_logs()) +``` + +## 📊 监控和日志 + +### 日志查看 + +```bash +# 查看应用日志 +tail -f logs/app.log + +# 查看 Celery 日志 +tail -f logs/celery.log +``` + +### 监控面板 + +使用 Grafana 创建监控面板: + +1. 访问 http://localhost:3000 (Grafana) +2. 用户名: `admin`, 密码: `admin` +3. 添加 Prometheus 数据源: http://prometheus:9090 +4. 导入监控面板 + +### 关键指标 + +- **webhook_requests_total**: Webhook 请求总数 +- **webhook_request_duration_seconds**: 请求响应时间 +- **queue_size**: 队列长度 +- **dedup_hits_total**: 防抖命中次数 + +## 🔧 故障排除 + +### 常见问题 + +#### 1. Redis 连接失败 + +```bash +# 检查 Redis 状态 +docker ps | grep redis + +# 重启 Redis +docker restart webhook-redis +``` + +#### 2. Celery Worker 无法启动 + +```bash +# 检查 Celery 配置 +celery -A app.tasks.jenkins_tasks inspect active + +# 重启 Worker +pkill -f "celery.*worker" +celery -A app.tasks.jenkins_tasks worker --loglevel=info +``` + +#### 3. Jenkins 连接失败 + +```bash +# 测试 Jenkins 连接 +curl -u username:token https://jenkins.example.com/api/json +``` + +#### 4. 数据库错误 + +```bash +# 检查数据库文件 +ls -la webhook_ambassador.db + +# 重新初始化数据库 +rm webhook_ambassador.db +python -c "from app.services.database_service import get_database_service; get_database_service()" +``` + +### 日志级别调整 + +编辑 `.env` 文件: + +```env +LOGGING_LEVEL=DEBUG # 开发环境 +LOGGING_LEVEL=INFO # 生产环境 +``` + +### 性能调优 + +#### 增加并发处理能力 + +```env +QUEUE_MAX_CONCURRENT=20 +``` + +#### 调整防抖窗口 + +```env +DEDUPLICATION_WINDOW_SECONDS=600 # 10分钟 +``` + +## 🐳 Docker 部署 + +### 使用 Docker Compose + +```bash +# 启动所有服务 +docker-compose up -d + +# 查看服务状态 +docker-compose ps + +# 查看日志 +docker-compose logs -f api +``` + +### 生产环境部署 + +```bash +# 构建镜像 +docker build -t webhook-ambassador:latest . + +# 运行容器 +docker run -d \ + --name webhook-ambassador \ + -p 8000:8000 \ + -v $(pwd)/config:/app/config \ + -v $(pwd)/logs:/app/logs \ + --env-file .env \ + webhook-ambassador:latest +``` + +## 📞 支持 + +如果遇到问题,请检查: + +1. 日志文件中的错误信息 +2. 健康检查端点返回的状态 +3. 监控指标中的异常数据 +4. 网络连接和防火墙设置 + +更多帮助请参考项目文档或提交 Issue。 \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/auth/middleware.py b/apps/gitea-webhook-ambassador-python/app/auth/middleware.py new file mode 100644 index 00000000..d1ff1e40 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/auth/middleware.py @@ -0,0 +1,93 @@ +from fastapi import HTTPException, Depends, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from fastapi.responses import JSONResponse, RedirectResponse +from sqlalchemy.orm import Session +from datetime import datetime, timedelta +import jwt +import secrets +import os +from typing import Optional + +from ..models.database import get_db, APIKey +from ..config import settings + +# JWT 配置 +JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production") +JWT_ALGORITHM = "HS256" +JWT_EXPIRATION_HOURS = 24 * 7 # 7 天有效期 + +security = HTTPBearer() + +class AuthMiddleware: + def __init__(self): + self.secret_key = JWT_SECRET_KEY + + def create_access_token(self, data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(hours=JWT_EXPIRATION_HOURS) + + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=JWT_ALGORITHM) + return encoded_jwt + + def verify_token(self, token: str): + try: + payload = jwt.decode(token, self.secret_key, algorithms=[JWT_ALGORITHM]) + return payload + except jwt.ExpiredSignatureError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token has expired" + ) + except jwt.JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token" + ) + + def verify_api_key(self, api_key: str, db: Session): + """验证 API 密钥""" + db_key = db.query(APIKey).filter(APIKey.key == api_key).first() + return db_key is not None + + def generate_api_key(self) -> str: + """生成新的 API 密钥""" + return secrets.token_urlsafe(32) + +# 创建认证中间件实例 +auth_middleware = AuthMiddleware() + +async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): + """获取当前用户(JWT 认证)""" + token = credentials.credentials + payload = auth_middleware.verify_token(token) + return payload + +async def get_current_user_api_key(api_key: str = Depends(security), db: Session = Depends(get_db)): + """获取当前用户(API 密钥认证)""" + if not auth_middleware.verify_api_key(api_key.credentials, db): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid API key" + ) + return {"api_key": api_key.credentials} + +def require_auth(use_api_key: bool = False): + """认证依赖装饰器""" + if use_api_key: + return get_current_user_api_key + else: + return get_current_user + +def handle_auth_error(request, exc): + """处理认证错误""" + if request.headers.get("x-requested-with") == "XMLHttpRequest": + return JSONResponse( + status_code=401, + content={"error": "Invalid or expired token"} + ) + else: + return RedirectResponse(url="/login", status_code=303) \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/config.py b/apps/gitea-webhook-ambassador-python/app/config.py new file mode 100644 index 00000000..191a6981 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/config.py @@ -0,0 +1,189 @@ +""" +配置管理模块 +支持环境分发、防抖策略和队列配置 +""" + +from typing import Dict, List, Optional +from pydantic import Field, validator +from pydantic_settings import BaseSettings +import yaml +from pathlib import Path + + +class EnvironmentConfig(BaseSettings): + """环境配置""" + branches: List[str] = Field(default_factory=list) + jenkins_job: str + jenkins_url: str + priority: int = Field(default=1, ge=1, le=10) + + +class DeduplicationConfig(BaseSettings): + """防抖配置""" + enabled: bool = True + window_seconds: int = Field(default=300, ge=1) # 5分钟防抖窗口 + strategy: str = Field(default="commit_branch") # commit_hash + branch + cache_ttl: int = Field(default=3600, ge=1) # 缓存1小时 + + +class QueueConfig(BaseSettings): + """队列配置""" + max_concurrent: int = Field(default=10, ge=1) + max_retries: int = Field(default=3, ge=0) + retry_delay: int = Field(default=60, ge=1) # 秒 + priority_levels: int = Field(default=3, ge=1, le=10) + + +class JenkinsConfig(BaseSettings): + """Jenkins 配置""" + username: str + token: str + timeout: int = Field(default=30, ge=1) + retry_attempts: int = Field(default=3, ge=1) + + +class DatabaseConfig(BaseSettings): + """数据库配置""" + url: str = Field(default="sqlite:///./webhook_ambassador.db") + echo: bool = False + pool_size: int = Field(default=10, ge=1) + max_overflow: int = Field(default=20, ge=0) + + +class RedisConfig(BaseSettings): + """Redis 配置""" + url: str = Field(default="redis://localhost:6379/0") + password: Optional[str] = None + db: int = Field(default=0, ge=0) + + +class LoggingConfig(BaseSettings): + """日志配置""" + level: str = Field(default="INFO") + format: str = Field(default="json") + file: Optional[str] = None + + +class SecurityConfig(BaseSettings): + """安全配置""" + secret_key: str + webhook_secret_header: str = Field(default="X-Gitea-Signature") + rate_limit_per_minute: int = Field(default=100, ge=1) + + +class Settings(BaseSettings): + """主配置类""" + + # 基础配置 + app_name: str = "Gitea Webhook Ambassador" + version: str = "1.0.0" + debug: bool = False + + # 服务器配置 + host: str = "0.0.0.0" + port: int = Field(default=8000, ge=1, le=65535) + + # 数据库配置 + database_url: str = Field(default="sqlite:///./webhook_ambassador.db") + + # Redis 配置 + redis_url: str = Field(default="redis://localhost:6379/0") + redis_password: str = Field(default="") + redis_db: int = Field(default=0) + + # Jenkins 配置 + jenkins_username: str = Field(default="admin") + jenkins_token: str = Field(default="") + jenkins_timeout: int = Field(default=30) + + # 安全配置 + security_secret_key: str = Field(default="") + security_webhook_secret_header: str = Field(default="X-Gitea-Signature") + security_rate_limit_per_minute: int = Field(default=100) + + # 日志配置 + logging_level: str = Field(default="INFO") + logging_format: str = Field(default="json") + logging_file: str = Field(default="") + + # 队列配置 + queue_max_concurrent: int = Field(default=10) + queue_max_retries: int = Field(default=3) + queue_retry_delay: int = Field(default=60) + queue_priority_levels: int = Field(default=3) + + # 防抖配置 + deduplication_enabled: bool = Field(default=True) + deduplication_window_seconds: int = Field(default=300) + deduplication_strategy: str = Field(default="commit_branch") + deduplication_cache_ttl: int = Field(default=3600) + + # 业务配置 + environments: Dict[str, EnvironmentConfig] = Field(default_factory=dict) + deduplication: DeduplicationConfig = DeduplicationConfig() + queue: QueueConfig = QueueConfig() + + class Config: + env_file = ".env" + env_nested_delimiter = "__" + + @validator("environments", pre=True) + def load_environments_from_file(cls, v): + """从配置文件加载环境配置""" + if isinstance(v, dict) and v: + return v + + # 尝试从配置文件加载 + config_file = Path("config/environments.yaml") + if config_file.exists(): + with open(config_file, "r", encoding="utf-8") as f: + config_data = yaml.safe_load(f) + return config_data.get("environments", {}) + + # 默认配置 + return { + "dev": EnvironmentConfig( + branches=["dev", "develop", "development"], + jenkins_job="alpha-build", + jenkins_url="https://jenkins-alpha.example.com", + priority=2 + ), + "prod": EnvironmentConfig( + branches=["prod", "production", "main", "master"], + jenkins_job="production-build", + jenkins_url="https://jenkins-prod.example.com", + priority=1 + ), + "default": EnvironmentConfig( + branches=["*"], + jenkins_job="default-build", + jenkins_url="https://jenkins-default.example.com", + priority=3 + ) + } + + def get_environment_for_branch(self, branch: str) -> Optional[EnvironmentConfig]: + """根据分支名获取对应的环境配置""" + for env_name, env_config in self.environments.items(): + if branch in env_config.branches or "*" in env_config.branches: + return env_config + return None + + def get_environment_by_name(self, name: str) -> Optional[EnvironmentConfig]: + """根据环境名获取配置""" + return self.environments.get(name) + + +# 全局配置实例 +settings = Settings() + + +def get_settings() -> Settings: + """获取配置实例""" + return settings + + +def reload_settings(): + """重新加载配置""" + global settings + settings = Settings() \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/__init__.py b/apps/gitea-webhook-ambassador-python/app/handlers/__init__.py new file mode 100644 index 00000000..2fdc2c8f --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/handlers/__init__.py @@ -0,0 +1,10 @@ +""" +Handlers 包 +包含所有 API 处理器 +""" + +from . import webhook +from . import health +from . import admin + +__all__ = ["webhook", "health", "admin"] \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/admin.py b/apps/gitea-webhook-ambassador-python/app/handlers/admin.py new file mode 100644 index 00000000..d8bf91f1 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/handlers/admin.py @@ -0,0 +1,287 @@ +""" +管理 API 处理器 +提供项目映射和 API 密钥管理功能 +""" + +import secrets +from datetime import datetime, timedelta +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel +from sqlalchemy.orm import Session + +from app.database import get_db +from app.models.api_key import APIKey +from app.models.project_mapping import ProjectMapping +from app.auth import get_current_user + +router = APIRouter(prefix="/api/admin", tags=["admin"]) + +# API 密钥相关模型 +class APIKeyResponse(BaseModel): + id: int + name: str + key_prefix: str + created_at: datetime + last_used: datetime + is_active: bool + + class Config: + from_attributes = True + +class CreateAPIKeyRequest(BaseModel): + name: str + +class CreateAPIKeyResponse(BaseModel): + id: int + name: str + key: str + created_at: datetime + +# 项目映射相关模型 +class ProjectMappingRequest(BaseModel): + repository_name: str + default_job: str + branch_jobs: Optional[List[dict]] = [] + branch_patterns: Optional[List[dict]] = [] + +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 + + class Config: + from_attributes = True + +# API 密钥管理端点 +@router.get("/api-keys", response_model=List[APIKeyResponse]) +async def list_api_keys( + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """列出所有 API 密钥""" + try: + api_keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all() + return api_keys + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to list API keys: {str(e)}") + +@router.post("/api-keys", response_model=CreateAPIKeyResponse) +async def create_api_key( + request: CreateAPIKeyRequest, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """创建新的 API 密钥""" + try: + # 生成 API 密钥 + api_key = secrets.token_urlsafe(32) + key_prefix = api_key[:8] # 显示前8位作为前缀 + + # 创建数据库记录 + db_api_key = APIKey( + name=request.name, + key_hash=api_key, # 实际应用中应该哈希存储 + key_prefix=key_prefix, + created_at=datetime.utcnow(), + last_used=datetime.utcnow(), + is_active=True + ) + + db.add(db_api_key) + db.commit() + db.refresh(db_api_key) + + return CreateAPIKeyResponse( + id=db_api_key.id, + name=db_api_key.name, + key=api_key, # 只在创建时返回完整密钥 + created_at=db_api_key.created_at + ) + + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=f"Failed to create API key: {str(e)}") + +@router.delete("/api-keys/{key_id}") +async def delete_api_key( + key_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """删除 API 密钥""" + try: + api_key = db.query(APIKey).filter(APIKey.id == key_id).first() + if not api_key: + raise HTTPException(status_code=404, detail="API key not found") + + db.delete(api_key) + db.commit() + + return {"message": "API key deleted successfully"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=f"Failed to delete API key: {str(e)}") + +@router.post("/api-keys/{key_id}/revoke") +async def revoke_api_key( + key_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """撤销 API 密钥""" + try: + api_key = db.query(APIKey).filter(APIKey.id == key_id).first() + if not api_key: + raise HTTPException(status_code=404, detail="API key not found") + + api_key.is_active = False + db.commit() + + return {"message": "API key revoked successfully"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=f"Failed to revoke API key: {str(e)}") + +# 项目映射管理端点 +@router.get("/projects", response_model=List[ProjectMappingResponse]) +async def list_project_mappings( + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """列出所有项目映射""" + try: + mappings = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all() + return mappings + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to list project mappings: {str(e)}") + +@router.post("/projects", response_model=ProjectMappingResponse) +async def create_project_mapping( + request: ProjectMappingRequest, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """创建项目映射""" + try: + # 检查是否已存在 + existing = db.query(ProjectMapping).filter( + ProjectMapping.repository_name == request.repository_name + ).first() + + if existing: + raise HTTPException(status_code=400, detail="Project mapping already exists") + + # 创建新映射 + mapping = ProjectMapping( + repository_name=request.repository_name, + default_job=request.default_job, + branch_jobs=request.branch_jobs or [], + branch_patterns=request.branch_patterns or [], + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + + db.add(mapping) + db.commit() + db.refresh(mapping) + + return mapping + + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=f"Failed to create project mapping: {str(e)}") + +@router.get("/projects/{repository_name}", response_model=ProjectMappingResponse) +async def get_project_mapping( + repository_name: str, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """获取项目映射""" + try: + mapping = db.query(ProjectMapping).filter( + ProjectMapping.repository_name == repository_name + ).first() + + if not mapping: + raise HTTPException(status_code=404, detail="Project mapping not found") + + return mapping + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to get project mapping: {str(e)}") + +@router.delete("/projects/{repository_name}") +async def delete_project_mapping( + repository_name: str, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """删除项目映射""" + try: + mapping = db.query(ProjectMapping).filter( + ProjectMapping.repository_name == repository_name + ).first() + + if not mapping: + raise HTTPException(status_code=404, detail="Project mapping not found") + + db.delete(mapping) + db.commit() + + return {"message": "Project mapping deleted successfully"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail=f"Failed to delete project mapping: {str(e)}") + +# 统计信息端点 +@router.get("/stats") +async def get_admin_stats( + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """获取管理统计信息""" + try: + # API 密钥统计 + total_keys = db.query(APIKey).count() + active_keys = db.query(APIKey).filter(APIKey.is_active == True).count() + + # 最近使用的密钥 + recent_keys = db.query(APIKey).filter( + APIKey.last_used >= datetime.utcnow() - timedelta(days=7) + ).count() + + # 项目映射统计 + total_mappings = db.query(ProjectMapping).count() + + return { + "api_keys": { + "total": total_keys, + "active": active_keys, + "recently_used": recent_keys + }, + "project_mappings": { + "total": total_mappings + } + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to get admin stats: {str(e)}") \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/auth.py b/apps/gitea-webhook-ambassador-python/app/handlers/auth.py new file mode 100644 index 00000000..a418a288 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/handlers/auth.py @@ -0,0 +1,122 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import HTTPBearer +from sqlalchemy.orm import Session +from pydantic import BaseModel +from typing import List, Optional +import os + +from ..models.database import get_db, APIKey +from ..auth.middleware import auth_middleware + +router = APIRouter(prefix="/api/auth", tags=["authentication"]) + +# 请求/响应模型 +class LoginRequest(BaseModel): + secret_key: str + +class LoginResponse(BaseModel): + token: str + +class APIKeyCreate(BaseModel): + description: str + +class APIKeyResponse(BaseModel): + id: int + key: str + description: Optional[str] + created_at: str + + class Config: + from_attributes = True + +class APIKeyList(BaseModel): + keys: List[APIKeyResponse] + +# 获取管理员密钥 +def get_admin_secret_key(): + return os.getenv("ADMIN_SECRET_KEY", "admin-secret-key-change-in-production") + +@router.post("/login", response_model=LoginResponse) +async def login(request: LoginRequest): + """管理员登录""" + admin_key = get_admin_secret_key() + + if request.secret_key != admin_key: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid secret key" + ) + + # 生成 JWT 令牌 + token = auth_middleware.create_access_token( + data={"sub": "admin", "role": "admin"} + ) + + return LoginResponse(token=token) + +@router.post("/keys", response_model=APIKeyResponse) +async def create_api_key( + request: APIKeyCreate, + db: Session = Depends(get_db), + current_user: dict = Depends(auth_middleware.get_current_user) +): + """创建新的 API 密钥""" + # 生成新的 API 密钥 + api_key_value = auth_middleware.generate_api_key() + + # 保存到数据库 + db_key = APIKey( + key=api_key_value, + description=request.description + ) + + db.add(db_key) + db.commit() + db.refresh(db_key) + + return APIKeyResponse( + id=db_key.id, + key=db_key.key, + description=db_key.description, + created_at=db_key.created_at.isoformat() + ) + +@router.get("/keys", response_model=APIKeyList) +async def list_api_keys( + db: Session = Depends(get_db), + current_user: dict = Depends(auth_middleware.get_current_user) +): + """获取所有 API 密钥""" + keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all() + + return APIKeyList( + keys=[ + APIKeyResponse( + id=key.id, + key=key.key, + description=key.description, + created_at=key.created_at.isoformat() + ) + for key in keys + ] + ) + +@router.delete("/keys/{key_id}") +async def delete_api_key( + key_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(auth_middleware.get_current_user) +): + """删除 API 密钥""" + key = db.query(APIKey).filter(APIKey.id == key_id).first() + + if not key: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="API key not found" + ) + + db.delete(key) + db.commit() + + return {"message": "API key deleted successfully"} \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/health.py b/apps/gitea-webhook-ambassador-python/app/handlers/health.py new file mode 100644 index 00000000..26b34be5 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/handlers/health.py @@ -0,0 +1,145 @@ +""" +健康检查处理器 +提供服务健康状态检查 +""" + +from datetime import datetime +from typing import Dict, Any +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from sqlalchemy.orm import Session + +from app.database import get_db +from app.services.jenkins_service import get_jenkins_service +from app.services.queue_service import get_queue_service +from app.config import get_settings + +router = APIRouter(prefix="/health", tags=["health"]) + + +class JenkinsStatus(BaseModel): + status: str + message: str = None + + +class WorkerPoolStatus(BaseModel): + active_workers: int + queue_size: int + total_processed: int + total_failed: int + + +class HealthResponse(BaseModel): + status: str + service: str + version: str + timestamp: datetime + jenkins: JenkinsStatus + worker_pool: WorkerPoolStatus + database: Dict[str, Any] + + +@router.get("/", response_model=HealthResponse) +async def health_check(db: Session = Depends(get_db)): + """ + 健康检查端点 + 检查服务各个组件的状态 + """ + settings = get_settings() + + # 检查 Jenkins 连接 + jenkins_service = get_jenkins_service() + jenkins_status = JenkinsStatus(status="disconnected", message="Unable to connect to Jenkins server") + + try: + if await jenkins_service.test_connection(): + jenkins_status = JenkinsStatus(status="connected") + except Exception as e: + jenkins_status.message = f"Connection failed: {str(e)}" + + # 获取工作池统计 + queue_service = get_queue_service() + try: + stats = await queue_service.get_stats() + worker_pool_status = WorkerPoolStatus( + active_workers=stats.get("active_workers", 0), + queue_size=stats.get("queue_size", 0), + total_processed=stats.get("total_processed", 0), + total_failed=stats.get("total_failed", 0) + ) + except Exception as e: + worker_pool_status = WorkerPoolStatus( + active_workers=0, + queue_size=0, + total_processed=0, + total_failed=0 + ) + + # 检查数据库连接 + database_status = {"status": "disconnected", "message": "Database connection failed"} + try: + # 尝试执行简单查询 + db.execute("SELECT 1") + database_status = {"status": "connected"} + except Exception as e: + database_status["message"] = f"Database error: {str(e)}" + + # 确定整体状态 + overall_status = "healthy" + if jenkins_status.status != "connected": + overall_status = "unhealthy" + + return HealthResponse( + status=overall_status, + service="Gitea Webhook Ambassador", + version=settings.version, + timestamp=datetime.utcnow(), + jenkins=jenkins_status, + worker_pool=worker_pool_status, + database=database_status + ) + + +@router.get("/simple") +async def simple_health_check(): + """ + 简单健康检查端点 + 用于负载均衡器和监控系统 + """ + return { + "status": "healthy", + "service": "Gitea Webhook Ambassador", + "version": "1.0.0" + } + + +@router.get("/ready") +async def readiness_check(db: Session = Depends(get_db)): + """ + 就绪检查端点 + 检查服务是否准备好接收请求 + """ + try: + # 检查数据库连接 + db.execute("SELECT 1") + + # 检查 Jenkins 连接 + jenkins_service = get_jenkins_service() + jenkins_ready = await jenkins_service.test_connection() + + if jenkins_ready: + return {"status": "ready"} + else: + return {"status": "not_ready", "reason": "Jenkins connection failed"} + + except Exception as e: + return {"status": "not_ready", "reason": f"Database connection failed: {str(e)}"} + + +@router.get("/live") +async def liveness_check(): + """ + 存活检查端点 + 检查服务进程是否正常运行 + """ + return {"status": "alive"} \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/logs.py b/apps/gitea-webhook-ambassador-python/app/handlers/logs.py new file mode 100644 index 00000000..de150a54 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/handlers/logs.py @@ -0,0 +1,106 @@ +from datetime import datetime, timedelta +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query +from pydantic import BaseModel +from sqlalchemy.orm import Session + +from app.database import get_db +from app.models.trigger_log import TriggerLog +from app.auth import get_current_user + +router = APIRouter(prefix="/api/logs", tags=["logs"]) + + +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 Config: + from_attributes = True + + +@router.get("/", response_model=List[TriggerLogResponse]) +async def get_trigger_logs( + repository: Optional[str] = Query(None, description="Repository name filter"), + branch: Optional[str] = Query(None, description="Branch name filter"), + since: Optional[str] = Query(None, description="Since timestamp (RFC3339 format)"), + limit: int = Query(100, ge=1, le=1000, description="Maximum number of logs to return"), + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """ + 获取触发日志 + """ + try: + # 构建查询 + query = db.query(TriggerLog) + + # 应用过滤器 + if repository: + query = query.filter(TriggerLog.repository_name == repository) + if branch: + query = query.filter(TriggerLog.branch_name == branch) + if since: + try: + since_time = datetime.fromisoformat(since.replace('Z', '+00:00')) + query = query.filter(TriggerLog.created_at >= since_time) + except ValueError: + raise HTTPException( + status_code=400, + detail="Invalid since parameter format (use RFC3339)" + ) + + # 按时间倒序排列并限制数量 + logs = query.order_by(TriggerLog.created_at.desc()).limit(limit).all() + + return logs + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to get trigger logs: {str(e)}") + + +@router.get("/stats") +async def get_log_stats( + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """ + 获取日志统计信息 + """ + try: + # 总日志数 + total_logs = db.query(TriggerLog).count() + + # 成功和失败的日志数 + successful_logs = db.query(TriggerLog).filter(TriggerLog.status == "success").count() + failed_logs = db.query(TriggerLog).filter(TriggerLog.status == "failed").count() + + # 最近24小时的日志数 + yesterday = datetime.utcnow() - timedelta(days=1) + recent_logs = db.query(TriggerLog).filter(TriggerLog.created_at >= yesterday).count() + + # 按仓库分组的统计 + repo_stats = db.query( + TriggerLog.repository_name, + db.func.count(TriggerLog.id).label('count') + ).group_by(TriggerLog.repository_name).all() + + 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 + ] + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to get log stats: {str(e)}") \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/projects.py b/apps/gitea-webhook-ambassador-python/app/handlers/projects.py new file mode 100644 index 00000000..a70c0bf3 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/handlers/projects.py @@ -0,0 +1,161 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from pydantic import BaseModel +from typing import List, Optional + +from ..models.database import get_db, ProjectMapping, BranchJob, BranchPattern +from ..auth.middleware import auth_middleware + +router = APIRouter(prefix="/api/projects", tags=["projects"]) + +# 请求/响应模型 +class ProjectCreate(BaseModel): + name: str + jenkinsJob: str + giteaRepo: str + +class ProjectResponse(BaseModel): + id: int + name: str + jenkinsJob: str + giteaRepo: str + created_at: str + + class Config: + from_attributes = True + +class ProjectList(BaseModel): + projects: List[ProjectResponse] + +@router.post("/", response_model=ProjectResponse) +async def create_project( + request: ProjectCreate, + db: Session = Depends(get_db), + current_user: dict = Depends(auth_middleware.get_current_user) +): + """创建新项目映射""" + # 检查项目是否已存在 + existing_project = db.query(ProjectMapping).filter( + ProjectMapping.repository_name == request.giteaRepo + ).first() + + if existing_project: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Project with this repository already exists" + ) + + # 创建新项目 + project = ProjectMapping( + repository_name=request.giteaRepo, + default_job=request.jenkinsJob + ) + + db.add(project) + db.commit() + db.refresh(project) + + return ProjectResponse( + id=project.id, + name=request.name, + jenkinsJob=project.default_job, + giteaRepo=project.repository_name, + created_at=project.created_at.isoformat() + ) + +@router.get("/", response_model=ProjectList) +async def list_projects( + db: Session = Depends(get_db), + current_user: dict = Depends(auth_middleware.get_current_user) +): + """获取所有项目""" + projects = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all() + + return ProjectList( + projects=[ + ProjectResponse( + id=project.id, + name=project.repository_name.split('/')[-1], # 使用仓库名作为项目名 + jenkinsJob=project.default_job, + giteaRepo=project.repository_name, + created_at=project.created_at.isoformat() + ) + for project in projects + ] + ) + +@router.get("/{project_id}", response_model=ProjectResponse) +async def get_project( + project_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(auth_middleware.get_current_user) +): + """获取特定项目""" + project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first() + + if not project: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Project not found" + ) + + return ProjectResponse( + id=project.id, + name=project.repository_name.split('/')[-1], + jenkinsJob=project.default_job, + giteaRepo=project.repository_name, + created_at=project.created_at.isoformat() + ) + +@router.delete("/{project_id}") +async def delete_project( + project_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(auth_middleware.get_current_user) +): + """删除项目""" + project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first() + + if not project: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Project not found" + ) + + db.delete(project) + db.commit() + + return {"message": "Project deleted successfully"} + +@router.get("/mapping/{repository_name}") +async def get_project_mapping( + repository_name: str, + db: Session = Depends(get_db) +): + """根据仓库名获取项目映射(用于 webhook 处理)""" + project = db.query(ProjectMapping).filter( + ProjectMapping.repository_name == repository_name + ).first() + + if not project: + return None + + return { + "id": project.id, + "repository_name": project.repository_name, + "default_job": project.default_job, + "branch_jobs": [ + { + "branch_name": job.branch_name, + "job_name": job.job_name + } + for job in project.branch_jobs + ], + "branch_patterns": [ + { + "pattern": pattern.pattern, + "job_name": pattern.job_name + } + for pattern in project.branch_patterns + ] + } \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/webhook.py b/apps/gitea-webhook-ambassador-python/app/handlers/webhook.py new file mode 100644 index 00000000..b76bfe78 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/handlers/webhook.py @@ -0,0 +1,42 @@ +""" +Webhook 处理器 +处理来自 Gitea 的 webhook 请求 +""" + +from fastapi import APIRouter, Depends, HTTPException, Request +from app.services.webhook_service import WebhookService +from app.services.dedup_service import DeduplicationService +from app.tasks.jenkins_tasks import get_celery_app + +router = APIRouter() + +def get_webhook_service() -> WebhookService: + """获取 webhook 服务实例""" + # 这里应该从依赖注入容器获取 + # 暂时返回 None,实际使用时需要正确实现 + return None + +@router.post("/gitea") +async def handle_gitea_webhook( + request: Request, + webhook_service: WebhookService = Depends(get_webhook_service) +): + """处理 Gitea webhook 请求""" + if webhook_service is None: + raise HTTPException(status_code=503, detail="Webhook service not available") + + try: + # 获取请求体 + body = await request.body() + + # 处理 webhook + result = await webhook_service.process_webhook(body, request.headers) + + return { + "success": True, + "message": "Webhook processed successfully", + "data": result + } + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/main.py b/apps/gitea-webhook-ambassador-python/app/main.py new file mode 100644 index 00000000..1da7f833 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/main.py @@ -0,0 +1,383 @@ +""" +FastAPI 应用主入口 +集成 Webhook 处理、防抖、队列管理等服务 +""" + +import asyncio +from contextlib import asynccontextmanager +from typing import Dict, Any +import structlog +from fastapi import FastAPI, Request, HTTPException, Depends +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, Response +from redis import asyncio as aioredis +from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST + +from app.config import get_settings +from app.services.dedup_service import DeduplicationService +from app.services.jenkins_service import JenkinsService +from app.services.webhook_service import WebhookService +from app.tasks.jenkins_tasks import get_celery_app +# 路由导入将在运行时动态处理 + +# 配置结构化日志 +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() + +# 监控指标 +WEBHOOK_REQUESTS_TOTAL = Counter( + "webhook_requests_total", + "Total number of webhook requests", + ["status", "environment"] +) + +WEBHOOK_REQUEST_DURATION = Histogram( + "webhook_request_duration_seconds", + "Webhook request duration in seconds", + ["environment"] +) + +QUEUE_SIZE = Gauge( + "queue_size", + "Current queue size", + ["queue_type"] +) + +DEDUP_HITS = Counter( + "dedup_hits_total", + "Total number of deduplication hits" +) + +# 全局服务实例 +dedup_service: DeduplicationService = None +jenkins_service: JenkinsService = None +webhook_service: WebhookService = None +celery_app = None +redis_client: aioredis.Redis = None + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """应用生命周期管理""" + global dedup_service, jenkins_service, webhook_service, celery_app, redis_client + + # 启动时初始化 + logger.info("Starting Gitea Webhook Ambassador") + + try: + # 初始化 Redis 连接 + settings = get_settings() + redis_client = aioredis.from_url( + settings.redis.url, + password=settings.redis.password, + db=settings.redis.db, + encoding="utf-8", + decode_responses=True + ) + + # 测试 Redis 连接 + await redis_client.ping() + logger.info("Redis connection established") + + # 初始化 Celery + celery_app = get_celery_app() + + # 初始化服务 + dedup_service = DeduplicationService(redis_client) + jenkins_service = JenkinsService() + webhook_service = WebhookService( + dedup_service=dedup_service, + jenkins_service=jenkins_service, + celery_app=celery_app + ) + + logger.info("All services initialized successfully") + + yield + + except Exception as e: + logger.error("Failed to initialize services", error=str(e)) + raise + + finally: + # 关闭时清理 + logger.info("Shutting down Gitea Webhook Ambassador") + + if redis_client: + await redis_client.close() + logger.info("Redis connection closed") + + +# 创建 FastAPI 应用 +app = FastAPI( + title="Gitea Webhook Ambassador", + description="高性能的 Gitea 到 Jenkins 的 Webhook 服务", + version="1.0.0", + lifespan=lifespan +) + +# 添加 CORS 中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 生产环境应该限制具体域名 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# 依赖注入 +def get_dedup_service() -> DeduplicationService: + if dedup_service is None: + raise HTTPException(status_code=503, detail="Deduplication service not available") + return dedup_service + + +def get_webhook_service() -> WebhookService: + if webhook_service is None: + raise HTTPException(status_code=503, detail="Webhook service not available") + return webhook_service + + +def get_celery_app_dep(): + if celery_app is None: + raise HTTPException(status_code=503, detail="Celery app not available") + return celery_app + + +# 中间件 +@app.middleware("http") +async def log_requests(request: Request, call_next): + """请求日志中间件""" + start_time = asyncio.get_event_loop().time() + + # 记录请求开始 + logger.info("Request started", + method=request.method, + url=str(request.url), + client_ip=request.client.host if request.client else None) + + try: + response = await call_next(request) + + # 记录请求完成 + process_time = asyncio.get_event_loop().time() - start_time + logger.info("Request completed", + method=request.method, + url=str(request.url), + status_code=response.status_code, + process_time=process_time) + + return response + + except Exception as e: + # 记录请求错误 + process_time = asyncio.get_event_loop().time() - start_time + logger.error("Request failed", + method=request.method, + url=str(request.url), + error=str(e), + process_time=process_time) + raise + + +@app.middleware("http") +async def add_security_headers(request: Request, call_next): + """添加安全头""" + response = await call_next(request) + + # 添加安全相关的 HTTP 头 + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["X-XSS-Protection"] = "1; mode=block" + response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" + + return response + + +# 异常处理器 +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + """全局异常处理器""" + logger.error("Unhandled exception", + method=request.method, + url=str(request.url), + error=str(exc), + exc_info=True) + + return JSONResponse( + status_code=500, + content={ + "success": False, + "message": "Internal server error", + "error": str(exc) if get_settings().debug else "An unexpected error occurred" + } + ) + + +# 健康检查端点 +@app.get("/health") +async def health_check(): + """基础健康检查""" + try: + # 检查 Redis 连接 + if redis_client: + await redis_client.ping() + redis_healthy = True + else: + redis_healthy = False + + # 检查 Celery 连接 + if celery_app: + inspect = celery_app.control.inspect() + celery_healthy = bool(inspect.active() is not None) + else: + celery_healthy = False + + return { + "status": "healthy" if redis_healthy and celery_healthy else "unhealthy", + "timestamp": asyncio.get_event_loop().time(), + "services": { + "redis": "healthy" if redis_healthy else "unhealthy", + "celery": "healthy" if celery_healthy else "unhealthy" + } + } + + except Exception as e: + logger.error("Health check failed", error=str(e)) + return JSONResponse( + status_code=503, + content={ + "status": "unhealthy", + "error": str(e) + } + ) + + +@app.get("/health/queue") +async def queue_health_check(): + """队列健康检查""" + try: + if celery_app is None: + return JSONResponse( + status_code=503, + content={"status": "unhealthy", "error": "Celery not available"} + ) + + inspect = celery_app.control.inspect() + + # 获取队列统计 + active = inspect.active() + reserved = inspect.reserved() + registered = inspect.registered() + + active_count = sum(len(tasks) for tasks in (active or {}).values()) + reserved_count = sum(len(tasks) for tasks in (reserved or {}).values()) + worker_count = len(registered or {}) + + # 更新监控指标 + QUEUE_SIZE.labels(queue_type="active").set(active_count) + QUEUE_SIZE.labels(queue_type="reserved").set(reserved_count) + + return { + "status": "healthy", + "queue_stats": { + "active_tasks": active_count, + "queued_tasks": reserved_count, + "worker_count": worker_count, + "total_queue_length": active_count + reserved_count + } + } + + except Exception as e: + logger.error("Queue health check failed", error=str(e)) + return JSONResponse( + status_code=503, + content={ + "status": "unhealthy", + "error": str(e) + } + ) + + +# 监控指标端点 +@app.get("/metrics") +async def metrics(): + """Prometheus 监控指标""" + return Response( + content=generate_latest(), + media_type=CONTENT_TYPE_LATEST + ) + + +# 包含路由模块 +try: + from app.handlers import webhook, health, admin + + app.include_router( + webhook.router, + prefix="/webhook", + tags=["webhook"] + ) + + app.include_router( + health.router, + prefix="/health", + tags=["health"] + ) + + app.include_router( + admin.router, + prefix="/admin", + tags=["admin"] + ) +except ImportError as e: + # 如果模块不存在,记录警告但不中断应用启动 + logger.warning(f"Some handlers not available: {e}") + + +# 根路径 +@app.get("/") +async def root(): + """根路径""" + return { + "name": "Gitea Webhook Ambassador", + "version": "1.0.0", + "description": "高性能的 Gitea 到 Jenkins 的 Webhook 服务", + "endpoints": { + "webhook": "/webhook/gitea", + "health": "/health", + "metrics": "/metrics", + "admin": "/admin" + } + } + + +if __name__ == "__main__": + import uvicorn + + settings = get_settings() + uvicorn.run( + "app.main:app", + host=settings.host, + port=settings.port, + reload=settings.debug, + log_level=settings.logging.level.lower() + ) \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/main_demo.py b/apps/gitea-webhook-ambassador-python/app/main_demo.py new file mode 100644 index 00000000..97f27cb8 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/main_demo.py @@ -0,0 +1,438 @@ +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 + ) \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/main_enhanced.py b/apps/gitea-webhook-ambassador-python/app/main_enhanced.py new file mode 100644 index 00000000..13a1c0aa --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/main_enhanced.py @@ -0,0 +1,313 @@ +from fastapi import FastAPI, Request, Depends, HTTPException, status +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy.orm import Session +import os +import time +import psutil +from datetime import datetime, timedelta + +# 导入数据库模型 +from app.models.database import create_tables, get_db, APIKey, ProjectMapping, TriggerLog +from app.auth.middleware import auth_middleware, get_current_user +from app.config import settings + +# 创建 FastAPI 应用 +app = FastAPI( + title="Gitea Webhook Ambassador", + description="高性能的 Gitea 到 Jenkins 的 Webhook 服务", + version="2.0.0" +) + +# 添加 CORS 中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 创建数据库表 +create_tables() + +# 挂载静态文件 +app.mount("/static", StaticFiles(directory="app/static"), name="static") + +# 设置模板 +templates = Jinja2Templates(directory="app/templates") + +# 启动时间 +start_time = datetime.now() + +@app.get("/", response_class=HTMLResponse) +async def root(request: Request): + """根路径 - 重定向到登录页""" + return RedirectResponse(url="/login") + +@app.get("/login", response_class=HTMLResponse) +async def login_page(request: Request): + """登录页面""" + return templates.TemplateResponse("login.html", {"request": request}) + +@app.get("/dashboard", response_class=HTMLResponse) +async def dashboard_page(request: Request): + """仪表板页面""" + return templates.TemplateResponse("dashboard.html", {"request": request}) + +@app.post("/api/auth/login") +async def login(request: dict): + """管理员登录""" + admin_key = os.getenv("ADMIN_SECRET_KEY", "admin-secret-key-change-in-production") + + if request.get("secret_key") != admin_key: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid secret key" + ) + + # 生成 JWT 令牌 + token = auth_middleware.create_access_token( + data={"sub": "admin", "role": "admin"} + ) + + return {"token": token} + +@app.get("/api/stats") +async def get_stats(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)): + """获取统计信息""" + try: + # 获取项目总数 + total_projects = db.query(ProjectMapping).count() + + # 获取 API 密钥总数 + total_api_keys = db.query(APIKey).count() + + # 获取今日触发次数 + today = datetime.now().date() + today_triggers = db.query(TriggerLog).filter( + TriggerLog.created_at >= today + ).count() + + # 获取成功触发次数 + successful_triggers = db.query(TriggerLog).filter( + TriggerLog.status == "success" + ).count() + + return { + "total_projects": total_projects, + "total_api_keys": total_api_keys, + "today_triggers": today_triggers, + "successful_triggers": successful_triggers + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取统计信息失败: {str(e)}") + +@app.get("/api/keys", response_model=dict) +async def list_api_keys(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)): + """获取所有 API 密钥(兼容前端)""" + try: + keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all() + return { + "keys": [ + { + "id": key.id, + "key": key.key, + "description": key.description, + "created_at": key.created_at.isoformat() + } + for key in keys + ] + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取 API 密钥失败: {str(e)}") + +@app.post("/api/keys", response_model=dict) +async def create_api_key( + request: dict, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """创建新的 API 密钥(兼容前端)""" + try: + # 生成新的 API 密钥 + api_key_value = auth_middleware.generate_api_key() + + # 保存到数据库 + db_key = APIKey( + key=api_key_value, + description=request.get("description", "") + ) + + db.add(db_key) + db.commit() + db.refresh(db_key) + + return { + "id": db_key.id, + "key": db_key.key, + "description": db_key.description, + "created_at": db_key.created_at.isoformat() + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"创建 API 密钥失败: {str(e)}") + +@app.delete("/api/keys/{key_id}") +async def delete_api_key( + key_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """删除 API 密钥(兼容前端)""" + try: + key = db.query(APIKey).filter(APIKey.id == key_id).first() + + if not key: + raise HTTPException(status_code=404, detail="API 密钥不存在") + + db.delete(key) + db.commit() + + return {"message": "API 密钥删除成功"} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"删除 API 密钥失败: {str(e)}") + +@app.get("/api/projects/", response_model=dict) +async def list_projects(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)): + """获取所有项目(兼容前端)""" + try: + projects = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all() + return { + "projects": [ + { + "id": project.id, + "name": project.repository_name.split('/')[-1], + "jenkinsJob": project.default_job, + "giteaRepo": project.repository_name, + "created_at": project.created_at.isoformat() + } + for project in projects + ] + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取项目列表失败: {str(e)}") + +@app.post("/api/projects/", response_model=dict) +async def create_project( + request: dict, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """创建新项目(兼容前端)""" + try: + # 检查项目是否已存在 + existing_project = db.query(ProjectMapping).filter( + ProjectMapping.repository_name == request["giteaRepo"] + ).first() + + if existing_project: + raise HTTPException(status_code=400, detail="项目已存在") + + # 创建新项目 + project = ProjectMapping( + repository_name=request["giteaRepo"], + default_job=request["jenkinsJob"] + ) + + db.add(project) + db.commit() + db.refresh(project) + + return { + "id": project.id, + "name": request["name"], + "jenkinsJob": project.default_job, + "giteaRepo": project.repository_name, + "created_at": project.created_at.isoformat() + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"创建项目失败: {str(e)}") + +@app.delete("/api/projects/{project_id}") +async def delete_project( + project_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """删除项目(兼容前端)""" + try: + project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first() + + if not project: + raise HTTPException(status_code=404, detail="项目不存在") + + db.delete(project) + db.commit() + + return {"message": "项目删除成功"} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"删除项目失败: {str(e)}") + +@app.get("/health") +async def health_check(): + """健康检查端点""" + try: + # 计算运行时间 + uptime = datetime.now() - start_time + uptime_str = str(uptime).split('.')[0] # 移除微秒 + + # 获取内存使用情况 + process = psutil.Process() + memory_info = process.memory_info() + memory_mb = memory_info.rss / 1024 / 1024 + + return { + "status": "healthy", + "version": "2.0.0", + "uptime": uptime_str, + "memory": f"{memory_mb:.1f} MB", + "timestamp": datetime.now().isoformat() + } + except Exception as e: + return { + "status": "unhealthy", + "error": str(e), + "timestamp": datetime.now().isoformat() + } + +@app.get("/api/logs") +async def get_logs( + startTime: str = None, + endTime: str = None, + level: str = None, + query: str = None, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """获取日志(简化版本)""" + try: + # 这里应该实现真正的日志查询逻辑 + # 目前返回模拟数据 + logs = [ + { + "timestamp": datetime.now().isoformat(), + "level": "info", + "message": "系统运行正常" + } + ] + + return {"logs": logs} + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取日志失败: {str(e)}") + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/main_simple.py b/apps/gitea-webhook-ambassador-python/app/main_simple.py new file mode 100644 index 00000000..9e387bbc --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/main_simple.py @@ -0,0 +1,145 @@ +""" +简化版 FastAPI 应用主入口 +用于快速启动和测试 +""" + +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +import structlog +from datetime import datetime + +from app.config import get_settings +from app.handlers.webhook import router as webhook_router +from app.handlers.health import router as health_router +from app.handlers.logs import router as logs_router +from app.handlers.admin import router as admin_router + +# 配置日志 +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", + description="高性能的 Gitea 到 Jenkins 的 Webhook 服务", + version="1.0.0" +) + +# 添加 CORS 中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 包含路由 +app.include_router(webhook_router) +app.include_router(health_router) +app.include_router(logs_router) +app.include_router(admin_router) + +@app.get("/") +async def root(): + """根路径""" + return { + "name": "Gitea Webhook Ambassador", + "version": "1.0.0", + "description": "高性能的 Gitea 到 Jenkins 的 Webhook 服务", + "endpoints": { + "webhook": "/webhook/gitea", + "health": "/health", + "metrics": "/metrics", + "logs": "/api/logs", + "admin": "/api/admin" + } + } + +@app.middleware("http") +async def log_requests(request: Request, call_next): + """请求日志中间件""" + start_time = datetime.utcnow() + + # 记录请求 + logger.info( + "Request started", + method=request.method, + url=str(request.url), + client_ip=request.client.host if request.client else None + ) + + # 处理请求 + response = await call_next(request) + + # 计算处理时间 + process_time = (datetime.utcnow() - start_time).total_seconds() + + # 记录响应 + logger.info( + "Request completed", + method=request.method, + url=str(request.url), + status_code=response.status_code, + process_time=process_time + ) + + # 添加处理时间到响应头 + response.headers["X-Process-Time"] = str(process_time) + + return response + +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + """全局异常处理器""" + logger.error( + "Unhandled exception", + method=request.method, + url=str(request.url), + error=str(exc), + exc_info=True + ) + + return JSONResponse( + status_code=500, + content={ + "error": "Internal server error", + "message": "An unexpected error occurred" + } + ) + +if __name__ == "__main__": + import uvicorn + settings = get_settings() + + logger.info( + "Starting Gitea Webhook Ambassador", + host=settings.server.host, + port=settings.server.port, + version=settings.version + ) + + uvicorn.run( + "app.main_simple:app", + host=settings.server.host, + port=settings.server.port, + reload=settings.server.reload + ) \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/models/api_key.py b/apps/gitea-webhook-ambassador-python/app/models/api_key.py new file mode 100644 index 00000000..f1d11ccd --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/models/api_key.py @@ -0,0 +1,20 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, DateTime, Boolean +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class APIKey(Base): + """API 密钥模型""" + __tablename__ = "api_keys" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(255), nullable=False) + key_hash = Column(String(255), nullable=False, unique=True, index=True) + key_prefix = Column(String(8), nullable=False, index=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + last_used = Column(DateTime, default=datetime.utcnow, nullable=False) + is_active = Column(Boolean, default=True, nullable=False, index=True) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/models/database.py b/apps/gitea-webhook-ambassador-python/app/models/database.py new file mode 100644 index 00000000..647f9821 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/models/database.py @@ -0,0 +1,92 @@ +from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relationship +from sqlalchemy.sql import func +from datetime import datetime +import os + +Base = declarative_base() + +class APIKey(Base): + __tablename__ = 'api_keys' + + id = Column(Integer, primary_key=True, index=True) + key = Column(String(255), unique=True, index=True, nullable=False) + description = Column(String(255), nullable=True) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + +class ProjectMapping(Base): + __tablename__ = 'project_mappings' + + id = Column(Integer, primary_key=True, index=True) + repository_name = Column(String(255), unique=True, index=True, nullable=False) + default_job = Column(String(255), nullable=False) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + # 关系 + branch_jobs = relationship("BranchJob", back_populates="project", cascade="all, delete-orphan") + branch_patterns = relationship("BranchPattern", back_populates="project", cascade="all, delete-orphan") + +class BranchJob(Base): + __tablename__ = 'branch_jobs' + + id = Column(Integer, primary_key=True, index=True) + project_id = Column(Integer, ForeignKey('project_mappings.id'), nullable=False) + branch_name = Column(String(255), nullable=False) + job_name = Column(String(255), nullable=False) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + # 关系 + project = relationship("ProjectMapping", back_populates="branch_jobs") + +class BranchPattern(Base): + __tablename__ = 'branch_patterns' + + id = Column(Integer, primary_key=True, index=True) + project_id = Column(Integer, ForeignKey('project_mappings.id'), nullable=False) + pattern = Column(String(255), nullable=False) + job_name = Column(String(255), nullable=False) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + # 关系 + project = relationship("ProjectMapping", back_populates="branch_patterns") + +class TriggerLog(Base): + __tablename__ = 'trigger_logs' + + id = Column(Integer, primary_key=True, index=True) + repository_name = Column(String(255), nullable=False) + branch_name = Column(String(255), nullable=False) + commit_sha = Column(String(255), nullable=False) + job_name = Column(String(255), nullable=False) + status = Column(String(50), nullable=False) # success, failed, pending + error_message = Column(Text, nullable=True) + created_at = Column(DateTime, default=func.now()) + +# 数据库配置 +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./gitea_webhook_ambassador.db") + +# 创建引擎 +engine = create_engine( + DATABASE_URL, + connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {} +) + +# 创建会话 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# 创建表 +def create_tables(): + Base.metadata.create_all(bind=engine) + +# 获取数据库会话 +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/models/gitea.py b/apps/gitea-webhook-ambassador-python/app/models/gitea.py new file mode 100644 index 00000000..a35ec8c1 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/models/gitea.py @@ -0,0 +1,130 @@ +""" +Gitea Webhook 数据模型 +""" + +from typing import List, Optional +from pydantic import BaseModel, Field +from datetime import datetime + + +class User(BaseModel): + """Gitea 用户模型""" + id: int + login: str + full_name: Optional[str] = None + email: Optional[str] = None + username: Optional[str] = None + + +class Commit(BaseModel): + """Git 提交模型""" + id: str + message: str + url: str + author: User + timestamp: Optional[datetime] = None + + +class Repository(BaseModel): + """Git 仓库模型""" + id: int + name: str + owner: User + full_name: str + private: bool = False + clone_url: str + ssh_url: Optional[str] = None + html_url: str + default_branch: str = "main" + + +class GiteaWebhook(BaseModel): + """Gitea Webhook 模型""" + secret: Optional[str] = None + ref: str + before: str + after: str + compare_url: Optional[str] = None + commits: List[Commit] = Field(default_factory=list) + repository: Repository + pusher: User + + def get_branch_name(self) -> str: + """从 ref 中提取分支名""" + prefix = "refs/heads/" + if self.ref.startswith(prefix): + return self.ref[len(prefix):] + return self.ref + + def get_event_id(self) -> str: + """生成唯一的事件 ID""" + return f"{self.repository.full_name}-{self.after}" + + def get_commit_hash(self) -> str: + """获取提交哈希""" + return self.after + + def get_deduplication_key(self) -> str: + """生成防抖键值""" + branch = self.get_branch_name() + return f"{self.after}:{branch}" + + def is_push_event(self) -> bool: + """判断是否为推送事件""" + return self.ref.startswith("refs/heads/") + + def is_tag_event(self) -> bool: + """判断是否为标签事件""" + return self.ref.startswith("refs/tags/") + + def get_commit_message(self) -> str: + """获取提交信息""" + if self.commits: + return self.commits[0].message + return "" + + def get_author_info(self) -> dict: + """获取作者信息""" + if self.commits: + author = self.commits[0].author + return { + "name": author.full_name or author.login, + "email": author.email, + "username": author.login + } + return { + "name": self.pusher.full_name or self.pusher.login, + "email": self.pusher.email, + "username": self.pusher.login + } + + +class WebhookEvent(BaseModel): + """Webhook 事件模型""" + id: str + repository: str + branch: str + commit_hash: str + event_type: str + timestamp: datetime + payload: dict + + class Config: + json_encoders = { + datetime: lambda v: v.isoformat() + } + + +class WebhookResponse(BaseModel): + """Webhook 响应模型""" + success: bool + message: str + event_id: Optional[str] = None + job_name: Optional[str] = None + environment: Optional[str] = None + timestamp: datetime = Field(default_factory=datetime.utcnow) + + class Config: + json_encoders = { + datetime: lambda v: v.isoformat() + } \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/models/project_mapping.py b/apps/gitea-webhook-ambassador-python/app/models/project_mapping.py new file mode 100644 index 00000000..caacd28a --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/models/project_mapping.py @@ -0,0 +1,20 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, DateTime, JSON +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class ProjectMapping(Base): + """项目映射模型""" + __tablename__ = "project_mappings" + + id = Column(Integer, primary_key=True, index=True) + repository_name = Column(String(255), nullable=False, unique=True, index=True) + default_job = Column(String(255), nullable=False) + branch_jobs = Column(JSON, nullable=False, default=list) # [{"branch": "dev", "job": "dev-build"}] + branch_patterns = Column(JSON, nullable=False, default=list) # [{"pattern": "feature/*", "job": "feature-build"}] + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/models/trigger_log.py b/apps/gitea-webhook-ambassador-python/app/models/trigger_log.py new file mode 100644 index 00000000..5eabe9e3 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/models/trigger_log.py @@ -0,0 +1,22 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, DateTime, Text +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class TriggerLog(Base): + """触发日志模型""" + __tablename__ = "trigger_logs" + + id = Column(Integer, primary_key=True, index=True) + repository_name = Column(String(255), nullable=False, index=True) + branch_name = Column(String(255), nullable=False, index=True) + commit_sha = Column(String(64), nullable=False) + job_name = Column(String(255), nullable=False) + status = Column(String(50), nullable=False, index=True) # success, failed, pending + error_message = Column(Text, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/services/database_service.py b/apps/gitea-webhook-ambassador-python/app/services/database_service.py new file mode 100644 index 00000000..e5503561 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/services/database_service.py @@ -0,0 +1,398 @@ +""" +数据库服务 +实现项目映射、分支模式匹配等功能 +""" + +import asyncio +from typing import Optional, List, Dict, Any +from datetime import datetime +import structlog +import re +from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relationship +from sqlalchemy.sql import text + +from app.config import get_settings + +logger = structlog.get_logger() +Base = declarative_base() + + +# 数据库模型 +class APIKey(Base): + """API 密钥模型""" + __tablename__ = "api_keys" + + id = Column(Integer, primary_key=True, autoincrement=True) + key = Column(String(255), unique=True, nullable=False) + description = Column(Text) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + +class ProjectMapping(Base): + """项目映射模型""" + __tablename__ = "project_mappings" + + id = Column(Integer, primary_key=True, autoincrement=True) + repository_name = Column(String(255), unique=True, nullable=False) + default_job = Column(String(255)) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关系 + branch_jobs = relationship("BranchJob", back_populates="project", cascade="all, delete-orphan") + branch_patterns = relationship("BranchPattern", back_populates="project", cascade="all, delete-orphan") + + +class BranchJob(Base): + """分支任务映射模型""" + __tablename__ = "branch_jobs" + + id = Column(Integer, primary_key=True, autoincrement=True) + project_id = Column(Integer, ForeignKey("project_mappings.id"), nullable=False) + branch_name = Column(String(255), nullable=False) + job_name = Column(String(255), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关系 + project = relationship("ProjectMapping", back_populates="branch_jobs") + + +class BranchPattern(Base): + """分支模式映射模型""" + __tablename__ = "branch_patterns" + + id = Column(Integer, primary_key=True, autoincrement=True) + project_id = Column(Integer, ForeignKey("project_mappings.id"), nullable=False) + pattern = Column(String(255), nullable=False) + job_name = Column(String(255), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关系 + project = relationship("ProjectMapping", back_populates="branch_patterns") + + +class TriggerLog(Base): + """触发日志模型""" + __tablename__ = "trigger_logs" + + id = Column(Integer, primary_key=True, autoincrement=True) + repository_name = Column(String(255), nullable=False) + branch_name = Column(String(255), nullable=False) + commit_sha = Column(String(255), nullable=False) + job_name = Column(String(255), nullable=False) + status = Column(String(50), nullable=False) + error_message = Column(Text) + created_at = Column(DateTime, default=datetime.utcnow) + + +class DatabaseService: + """数据库服务""" + + def __init__(self): + self.settings = get_settings() + self.engine = None + self.SessionLocal = None + self._init_database() + + def _init_database(self): + """初始化数据库""" + try: + self.engine = create_engine( + self.settings.database.url, + echo=self.settings.database.echo, + pool_size=self.settings.database.pool_size, + max_overflow=self.settings.database.max_overflow + ) + + # 创建表 + Base.metadata.create_all(bind=self.engine) + + # 创建会话工厂 + self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine) + + logger.info("Database initialized successfully") + + except Exception as e: + logger.error("Failed to initialize database", error=str(e)) + raise + + def get_session(self): + """获取数据库会话""" + return self.SessionLocal() + + async def get_project_mapping(self, repository_name: str) -> Optional[Dict[str, Any]]: + """ + 获取项目映射 + + Args: + repository_name: 仓库名 + + Returns: + Dict: 项目映射信息 + """ + try: + def _get_mapping(): + session = self.get_session() + try: + project = session.query(ProjectMapping).filter( + ProjectMapping.repository_name == repository_name + ).first() + + if not project: + return None + + # 构建返回数据 + result = { + "id": project.id, + "repository_name": project.repository_name, + "default_job": project.default_job, + "branch_jobs": [], + "branch_patterns": [] + } + + # 添加分支任务映射 + for branch_job in project.branch_jobs: + result["branch_jobs"].append({ + "id": branch_job.id, + "branch_name": branch_job.branch_name, + "job_name": branch_job.job_name + }) + + # 添加分支模式映射 + for pattern in project.branch_patterns: + result["branch_patterns"].append({ + "id": pattern.id, + "pattern": pattern.pattern, + "job_name": pattern.job_name + }) + + return result + + finally: + session.close() + + # 在线程池中执行数据库操作 + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _get_mapping) + + except Exception as e: + logger.error("Failed to get project mapping", + repository_name=repository_name, error=str(e)) + return None + + async def determine_job_name(self, repository_name: str, branch_name: str) -> Optional[str]: + """ + 根据分支名确定任务名 + + Args: + repository_name: 仓库名 + branch_name: 分支名 + + Returns: + str: 任务名 + """ + try: + project = await self.get_project_mapping(repository_name) + if not project: + return None + + # 1. 检查精确分支匹配 + for branch_job in project["branch_jobs"]: + if branch_job["branch_name"] == branch_name: + logger.debug("Found exact branch match", + branch=branch_name, job=branch_job["job_name"]) + return branch_job["job_name"] + + # 2. 检查模式匹配 + for pattern in project["branch_patterns"]: + try: + if re.match(pattern["pattern"], branch_name): + logger.debug("Branch matched pattern", + branch=branch_name, pattern=pattern["pattern"], + job=pattern["job_name"]) + return pattern["job_name"] + except re.error as e: + logger.error("Invalid regex pattern", + pattern=pattern["pattern"], error=str(e)) + continue + + # 3. 使用默认任务 + if project["default_job"]: + logger.debug("Using default job", + branch=branch_name, job=project["default_job"]) + return project["default_job"] + + return None + + except Exception as e: + logger.error("Failed to determine job name", + repository_name=repository_name, branch_name=branch_name, + error=str(e)) + return None + + async def log_trigger(self, log_data: Dict[str, Any]) -> bool: + """ + 记录触发日志 + + Args: + log_data: 日志数据 + + Returns: + bool: 是否成功 + """ + try: + def _log_trigger(): + session = self.get_session() + try: + log = TriggerLog( + repository_name=log_data["repository_name"], + branch_name=log_data["branch_name"], + commit_sha=log_data["commit_sha"], + job_name=log_data["job_name"], + status=log_data["status"], + error_message=log_data.get("error_message") + ) + + session.add(log) + session.commit() + return True + + except Exception as e: + session.rollback() + logger.error("Failed to log trigger", error=str(e)) + return False + finally: + session.close() + + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _log_trigger) + + except Exception as e: + logger.error("Failed to log trigger", error=str(e)) + return False + + async def get_trigger_logs(self, repository_name: str = None, + branch_name: str = None, limit: int = 100) -> List[Dict[str, Any]]: + """ + 获取触发日志 + + Args: + repository_name: 仓库名(可选) + branch_name: 分支名(可选) + limit: 限制数量 + + Returns: + List: 日志列表 + """ + try: + def _get_logs(): + session = self.get_session() + try: + query = session.query(TriggerLog) + + if repository_name: + query = query.filter(TriggerLog.repository_name == repository_name) + + if branch_name: + query = query.filter(TriggerLog.branch_name == branch_name) + + logs = query.order_by(TriggerLog.created_at.desc()).limit(limit).all() + + return [ + { + "id": log.id, + "repository_name": log.repository_name, + "branch_name": log.branch_name, + "commit_sha": log.commit_sha, + "job_name": log.job_name, + "status": log.status, + "error_message": log.error_message, + "created_at": log.created_at.isoformat() + } + for log in logs + ] + + finally: + session.close() + + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _get_logs) + + except Exception as e: + logger.error("Failed to get trigger logs", error=str(e)) + return [] + + async def create_project_mapping(self, mapping_data: Dict[str, Any]) -> bool: + """ + 创建项目映射 + + Args: + mapping_data: 映射数据 + + Returns: + bool: 是否成功 + """ + try: + def _create_mapping(): + session = self.get_session() + try: + # 创建项目映射 + project = ProjectMapping( + repository_name=mapping_data["repository_name"], + default_job=mapping_data.get("default_job") + ) + + session.add(project) + session.flush() # 获取 ID + + # 添加分支任务映射 + for branch_job in mapping_data.get("branch_jobs", []): + job = BranchJob( + project_id=project.id, + branch_name=branch_job["branch_name"], + job_name=branch_job["job_name"] + ) + session.add(job) + + # 添加分支模式映射 + for pattern in mapping_data.get("branch_patterns", []): + pattern_obj = BranchPattern( + project_id=project.id, + pattern=pattern["pattern"], + job_name=pattern["job_name"] + ) + session.add(pattern_obj) + + session.commit() + return True + + except Exception as e: + session.rollback() + logger.error("Failed to create project mapping", error=str(e)) + return False + finally: + session.close() + + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _create_mapping) + + except Exception as e: + logger.error("Failed to create project mapping", error=str(e)) + return False + + +# 全局数据库服务实例 +_database_service: Optional[DatabaseService] = None + + +def get_database_service() -> DatabaseService: + """获取数据库服务实例""" + global _database_service + if _database_service is None: + _database_service = DatabaseService() + return _database_service \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/services/dedup_service.py b/apps/gitea-webhook-ambassador-python/app/services/dedup_service.py new file mode 100644 index 00000000..b9511bd6 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/services/dedup_service.py @@ -0,0 +1,223 @@ +""" +防抖服务 +实现基于 commit hash + 分支的去重策略 +""" + +import asyncio +import hashlib +import json +from typing import Optional, Dict, Any +from datetime import datetime, timedelta +import structlog +from redis import asyncio as aioredis + +from app.config import get_settings + +logger = structlog.get_logger() + + +class DeduplicationService: + """防抖服务""" + + def __init__(self, redis_client: aioredis.Redis): + self.redis = redis_client + self.settings = get_settings() + self.cache_prefix = "webhook:dedup:" + + async def is_duplicate(self, dedup_key: str) -> bool: + """ + 检查是否为重复事件 + + Args: + dedup_key: 防抖键值 (commit_hash:branch) + + Returns: + bool: True 表示重复,False 表示新事件 + """ + if not self.settings.deduplication.enabled: + return False + + try: + cache_key = f"{self.cache_prefix}{dedup_key}" + + # 检查是否在缓存中 + exists = await self.redis.exists(cache_key) + if exists: + logger.info("Duplicate event detected", dedup_key=dedup_key) + return True + + # 记录新事件 + await self._record_event(cache_key, dedup_key) + logger.info("New event recorded", dedup_key=dedup_key) + return False + + except Exception as e: + logger.error("Error checking duplication", + dedup_key=dedup_key, error=str(e)) + # 出错时允许通过,避免阻塞 + return False + + async def _record_event(self, cache_key: str, dedup_key: str): + """记录事件到缓存""" + try: + # 设置缓存,TTL 为防抖窗口时间 + ttl = self.settings.deduplication.cache_ttl + await self.redis.setex(cache_key, ttl, json.dumps({ + "dedup_key": dedup_key, + "timestamp": datetime.utcnow().isoformat(), + "ttl": ttl + })) + + # 同时记录到时间窗口缓存 + window_key = f"{self.cache_prefix}window:{dedup_key}" + window_ttl = self.settings.deduplication.window_seconds + await self.redis.setex(window_key, window_ttl, "1") + + except Exception as e: + logger.error("Error recording event", + cache_key=cache_key, error=str(e)) + + async def get_event_info(self, dedup_key: str) -> Optional[Dict[str, Any]]: + """获取事件信息""" + try: + cache_key = f"{self.cache_prefix}{dedup_key}" + data = await self.redis.get(cache_key) + if data: + return json.loads(data) + return None + except Exception as e: + logger.error("Error getting event info", + dedup_key=dedup_key, error=str(e)) + return None + + async def clear_event(self, dedup_key: str) -> bool: + """清除事件记录""" + try: + cache_key = f"{self.cache_prefix}{dedup_key}" + window_key = f"{self.cache_prefix}window:{dedup_key}" + + # 删除两个缓存键 + await self.redis.delete(cache_key, window_key) + logger.info("Event cleared", dedup_key=dedup_key) + return True + + except Exception as e: + logger.error("Error clearing event", + dedup_key=dedup_key, error=str(e)) + return False + + async def get_stats(self) -> Dict[str, Any]: + """获取防抖统计信息""" + try: + # 获取所有防抖键 + pattern = f"{self.cache_prefix}*" + keys = await self.redis.keys(pattern) + + # 统计不同类型的键 + total_keys = len(keys) + window_keys = len([k for k in keys if b"window:" in k]) + event_keys = total_keys - window_keys + + # 获取配置信息 + config = { + "enabled": self.settings.deduplication.enabled, + "window_seconds": self.settings.deduplication.window_seconds, + "cache_ttl": self.settings.deduplication.cache_ttl, + "strategy": self.settings.deduplication.strategy + } + + return { + "total_keys": total_keys, + "window_keys": window_keys, + "event_keys": event_keys, + "config": config, + "timestamp": datetime.utcnow().isoformat() + } + + except Exception as e: + logger.error("Error getting deduplication stats", error=str(e)) + return {"error": str(e)} + + async def cleanup_expired_events(self) -> int: + """清理过期事件""" + try: + pattern = f"{self.cache_prefix}*" + keys = await self.redis.keys(pattern) + + cleaned_count = 0 + for key in keys: + # 检查 TTL + ttl = await self.redis.ttl(key) + if ttl <= 0: + await self.redis.delete(key) + cleaned_count += 1 + + if cleaned_count > 0: + logger.info("Cleaned up expired events", count=cleaned_count) + + return cleaned_count + + except Exception as e: + logger.error("Error cleaning up expired events", error=str(e)) + return 0 + + def generate_dedup_key(self, commit_hash: str, branch: str) -> str: + """ + 生成防抖键值 + + Args: + commit_hash: 提交哈希 + branch: 分支名 + + Returns: + str: 防抖键值 + """ + if self.settings.deduplication.strategy == "commit_branch": + return f"{commit_hash}:{branch}" + elif self.settings.deduplication.strategy == "commit_only": + return commit_hash + elif self.settings.deduplication.strategy == "branch_only": + return branch + else: + # 默认使用 commit_hash:branch + return f"{commit_hash}:{branch}" + + async def is_in_window(self, dedup_key: str) -> bool: + """ + 检查是否在防抖时间窗口内 + + Args: + dedup_key: 防抖键值 + + Returns: + bool: True 表示在窗口内 + """ + try: + window_key = f"{self.cache_prefix}window:{dedup_key}" + exists = await self.redis.exists(window_key) + return bool(exists) + + except Exception as e: + logger.error("Error checking window", + dedup_key=dedup_key, error=str(e)) + return False + + +# 全局防抖服务实例 +_dedup_service: Optional[DeduplicationService] = None + + +def get_deduplication_service() -> DeduplicationService: + """获取防抖服务实例""" + global _dedup_service + if _dedup_service is None: + # 这里需要从依赖注入获取 Redis 客户端 + # 在实际使用时,应该通过依赖注入传入 + raise RuntimeError("DeduplicationService not initialized") + return _dedup_service + + +def set_deduplication_service(service: DeduplicationService): + """设置防抖服务实例""" + global _dedup_service + _dedup_service = service \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/services/jenkins_service.py b/apps/gitea-webhook-ambassador-python/app/services/jenkins_service.py new file mode 100644 index 00000000..c406abcf --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/services/jenkins_service.py @@ -0,0 +1,100 @@ +""" +Jenkins 服务 +提供与 Jenkins 的交互功能 +""" + +import aiohttp +import structlog +from typing import Optional, Dict, Any + +from app.config import get_settings + +logger = structlog.get_logger() + +class JenkinsService: + """Jenkins 服务类""" + + def __init__(self): + self.settings = get_settings() + self.base_url = self.settings.jenkins.url + self.username = self.settings.jenkins.username + self.token = self.settings.jenkins.token + self.timeout = self.settings.jenkins.timeout + + async def test_connection(self) -> bool: + """测试 Jenkins 连接""" + try: + async with aiohttp.ClientSession() as session: + auth = aiohttp.BasicAuth(self.username, self.token) + async with session.get( + f"{self.base_url}/api/json", + auth=auth, + timeout=aiohttp.ClientTimeout(total=self.timeout) + ) as response: + if response.status == 200: + logger.info("Jenkins connection test successful") + return True + else: + logger.warning(f"Jenkins connection test failed with status {response.status}") + return False + except Exception as e: + logger.error(f"Jenkins connection test failed: {str(e)}") + return False + + async def trigger_job(self, job_name: str, parameters: Optional[Dict[str, Any]] = None) -> bool: + """触发 Jenkins 任务""" + try: + async with aiohttp.ClientSession() as session: + auth = aiohttp.BasicAuth(self.username, self.token) + + # 构建请求 URL + url = f"{self.base_url}/job/{job_name}/build" + + # 如果有参数,使用参数化构建 + if parameters: + url = f"{self.base_url}/job/{job_name}/buildWithParameters" + + async with session.post( + url, + auth=auth, + params=parameters or {}, + timeout=aiohttp.ClientTimeout(total=self.timeout) + ) as response: + if response.status in [200, 201]: + logger.info(f"Successfully triggered Jenkins job: {job_name}") + return True + else: + logger.error(f"Failed to trigger Jenkins job {job_name}: {response.status}") + return False + except Exception as e: + logger.error(f"Error triggering Jenkins job {job_name}: {str(e)}") + return False + + async def get_job_info(self, job_name: str) -> Optional[Dict[str, Any]]: + """获取任务信息""" + try: + async with aiohttp.ClientSession() as session: + auth = aiohttp.BasicAuth(self.username, self.token) + async with session.get( + f"{self.base_url}/job/{job_name}/api/json", + auth=auth, + timeout=aiohttp.ClientTimeout(total=self.timeout) + ) as response: + if response.status == 200: + return await response.json() + else: + logger.warning(f"Failed to get job info for {job_name}: {response.status}") + return None + except Exception as e: + logger.error(f"Error getting job info for {job_name}: {str(e)}") + return None + +# 全局服务实例 +_jenkins_service = None + +def get_jenkins_service() -> JenkinsService: + """获取 Jenkins 服务实例""" + global _jenkins_service + if _jenkins_service is None: + _jenkins_service = JenkinsService() + return _jenkins_service \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/services/queue_service.py b/apps/gitea-webhook-ambassador-python/app/services/queue_service.py new file mode 100644 index 00000000..c8fd9076 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/services/queue_service.py @@ -0,0 +1,70 @@ +""" +队列服务 +提供任务队列管理功能 +""" + +import structlog +from typing import Dict, Any +from datetime import datetime + +logger = structlog.get_logger() + +class QueueService: + """队列服务类""" + + def __init__(self): + self.active_workers = 0 + self.queue_size = 0 + self.total_processed = 0 + self.total_failed = 0 + self._stats = { + "active_workers": 0, + "queue_size": 0, + "total_processed": 0, + "total_failed": 0 + } + + async def get_stats(self) -> Dict[str, Any]: + """获取队列统计信息""" + return self._stats.copy() + + async def increment_processed(self): + """增加已处理任务计数""" + self.total_processed += 1 + self._stats["total_processed"] = self.total_processed + + async def increment_failed(self): + """增加失败任务计数""" + self.total_failed += 1 + self._stats["total_failed"] = self.total_failed + + async def set_active_workers(self, count: int): + """设置活跃工作线程数""" + self.active_workers = count + self._stats["active_workers"] = count + + async def set_queue_size(self, size: int): + """设置队列大小""" + self.queue_size = size + self._stats["queue_size"] = size + + async def add_to_queue(self): + """添加任务到队列""" + self.queue_size += 1 + self._stats["queue_size"] = self.queue_size + + async def remove_from_queue(self): + """从队列移除任务""" + if self.queue_size > 0: + self.queue_size -= 1 + self._stats["queue_size"] = self.queue_size + +# 全局服务实例 +_queue_service = None + +def get_queue_service() -> QueueService: + """获取队列服务实例""" + global _queue_service + if _queue_service is None: + _queue_service = QueueService() + return _queue_service \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/services/webhook_service.py b/apps/gitea-webhook-ambassador-python/app/services/webhook_service.py new file mode 100644 index 00000000..c86a25db --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/services/webhook_service.py @@ -0,0 +1,280 @@ +""" +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) + } \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/static/js/dashboard.js b/apps/gitea-webhook-ambassador-python/app/static/js/dashboard.js new file mode 100644 index 00000000..3d0d85e4 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/static/js/dashboard.js @@ -0,0 +1,373 @@ +// 全局变量存储 JWT 令牌 +let authToken = localStorage.getItem('auth_token'); + +$(document).ready(function() { + // 检查认证状态 + if (!authToken) { + window.location.href = '/login'; + return; + } + + // 设置 AJAX 默认配置 + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + // 不为登录请求添加认证头 + if (settings.url === '/api/auth/login') { + return; + } + if (authToken) { + xhr.setRequestHeader('Authorization', 'Bearer ' + authToken); + } + }, + error: function(xhr, status, error) { + // 如果收到 401,重定向到登录页 + if (xhr.status === 401) { + localStorage.removeItem('auth_token'); + window.location.href = '/login'; + return; + } + handleAjaxError(xhr, status, error); + } + }); + + // 初始化工具提示 + $('[data-bs-toggle="tooltip"]').tooltip(); + + // 加载初始数据 + loadProjects(); + loadAPIKeys(); + loadLogs(); + checkHealth(); + loadHealthDetails(); + loadStatsDetails(); + + // 设置定期健康检查 + setInterval(checkHealth, 30000); + + // 项目管理 + $('#addProjectForm').on('submit', function(e) { + e.preventDefault(); + const projectData = { + name: $('#projectName').val(), + jenkinsJob: $('#jenkinsJob').val(), + giteaRepo: $('#giteaRepo').val() + }; + + $.ajax({ + url: '/api/projects/', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(projectData), + success: function() { + $('#addProjectModal').modal('hide'); + $('#addProjectForm')[0].reset(); + loadProjects(); + showSuccess('项目添加成功'); + }, + error: handleAjaxError + }); + }); + + // API 密钥管理 + $('#generateKeyForm').on('submit', function(e) { + e.preventDefault(); + $.ajax({ + url: '/api/keys', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify({ description: $('#keyDescription').val() }), + success: function(response) { + $('#generateKeyModal').modal('hide'); + $('#generateKeyForm')[0].reset(); + loadAPIKeys(); + showSuccess('API 密钥生成成功'); + + // 显示新生成的密钥 + showApiKeyModal(response.key); + }, + error: handleAjaxError + }); + }); + + // 日志查询 + $('#logQueryForm').on('submit', function(e) { + e.preventDefault(); + loadLogs({ + startTime: $('#startTime').val(), + endTime: $('#endTime').val(), + level: $('#logLevel').val(), + query: $('#logQuery').val() + }); + }); + + // 标签页切换 + $('.nav-link').on('click', function() { + $('.nav-link').removeClass('active'); + $(this).addClass('active'); + }); +}); + +function loadProjects() { + $.get('/api/projects/') + .done(function(data) { + const tbody = $('#projectsTable tbody'); + tbody.empty(); + + data.projects.forEach(function(project) { + tbody.append(` + + ${escapeHtml(project.name)} + ${escapeHtml(project.jenkinsJob)} + ${escapeHtml(project.giteaRepo)} + + + + + `); + }); + }) + .fail(handleAjaxError); +} + +function loadAPIKeys() { + $.get('/api/keys') + .done(function(data) { + const tbody = $('#apiKeysTable tbody'); + tbody.empty(); + + data.keys.forEach(function(key) { + tbody.append(` + + ${escapeHtml(key.description || '无描述')} + ${escapeHtml(key.key)} + ${new Date(key.created_at).toLocaleString('zh-CN')} + + + + + `); + }); + }) + .fail(handleAjaxError); +} + +function loadLogs(query = {}) { + $.get('/api/logs', query) + .done(function(data) { + const logContainer = $('#logEntries'); + logContainer.empty(); + + if (data.logs && data.logs.length > 0) { + data.logs.forEach(function(log) { + const levelClass = { + 'error': 'error', + 'warn': 'warn', + 'info': 'info', + 'debug': 'debug' + }[log.level] || ''; + + logContainer.append(` +
+ ${new Date(log.timestamp).toLocaleString('zh-CN')} + [${escapeHtml(log.level.toUpperCase())}] ${escapeHtml(log.message)} +
+ `); + }); + } else { + logContainer.append('
暂无日志记录
'); + } + }) + .fail(handleAjaxError); +} + +function checkHealth() { + $.get('/health') + .done(function(data) { + const indicator = $('.health-indicator'); + indicator.removeClass('healthy unhealthy') + .addClass(data.status === 'healthy' ? 'healthy' : 'unhealthy'); + $('#healthStatus').text(data.status === 'healthy' ? '健康' : '异常'); + }) + .fail(function() { + const indicator = $('.health-indicator'); + indicator.removeClass('healthy').addClass('unhealthy'); + $('#healthStatus').text('异常'); + }); +} + +function loadHealthDetails() { + $.get('/health') + .done(function(data) { + const healthDetails = $('#healthDetails'); + healthDetails.html(` +
+ 状态: + + ${data.status === 'healthy' ? '健康' : '异常'} + +
+
+ 版本: ${data.version || '未知'} +
+
+ 启动时间: ${data.uptime || '未知'} +
+
+ 内存使用: ${data.memory || '未知'} +
+ `); + }) + .fail(function() { + $('#healthDetails').html('
无法获取健康状态
'); + }); +} + +function loadStatsDetails() { + $.get('/api/stats') + .done(function(data) { + const statsDetails = $('#statsDetails'); + statsDetails.html(` +
+ 总项目数: ${data.total_projects || 0} +
+
+ API 密钥数: ${data.total_api_keys || 0} +
+
+ 今日触发次数: ${data.today_triggers || 0} +
+
+ 成功触发次数: ${data.successful_triggers || 0} +
+ `); + }) + .fail(function() { + $('#statsDetails').html('
无法获取统计信息
'); + }); +} + +function deleteProject(id) { + if (!confirm('确定要删除这个项目吗?')) return; + + $.ajax({ + url: `/api/projects/${id}`, + method: 'DELETE', + success: function() { + loadProjects(); + showSuccess('项目删除成功'); + }, + error: handleAjaxError + }); +} + +function revokeKey(id) { + if (!confirm('确定要撤销这个 API 密钥吗?')) return; + + $.ajax({ + url: `/api/keys/${id}`, + method: 'DELETE', + success: function() { + loadAPIKeys(); + showSuccess('API 密钥撤销成功'); + }, + error: handleAjaxError + }); +} + +function showApiKeyModal(key) { + // 创建模态框显示新生成的密钥 + const modal = $(` + + `); + + $('body').append(modal); + modal.modal('show'); + + modal.on('hidden.bs.modal', function() { + modal.remove(); + }); +} + +function copyToClipboard(text) { + navigator.clipboard.writeText(text).then(function() { + showSuccess('已复制到剪贴板'); + }, function() { + showError('复制失败'); + }); +} + +function handleAjaxError(jqXHR, textStatus, errorThrown) { + const message = jqXHR.responseJSON?.detail || errorThrown || '发生错误'; + showError(`错误: ${message}`); +} + +function showSuccess(message) { + // 创建成功提示 + const alert = $(` + + `); + + $('.main-content').prepend(alert); + + // 3秒后自动消失 + setTimeout(function() { + alert.alert('close'); + }, 3000); +} + +function showError(message) { + // 创建错误提示 + const alert = $(` + + `); + + $('.main-content').prepend(alert); + + // 5秒后自动消失 + setTimeout(function() { + alert.alert('close'); + }, 5000); +} + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/tasks/jenkins_tasks.py b/apps/gitea-webhook-ambassador-python/app/tasks/jenkins_tasks.py new file mode 100644 index 00000000..d86e3342 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/tasks/jenkins_tasks.py @@ -0,0 +1,306 @@ +""" +Jenkins 任务处理 +使用 Celery 处理异步 Jenkins 任务触发 +""" + +import asyncio +import time +from typing import Dict, Any +from datetime import datetime +import structlog +from celery import Celery, Task +import httpx + +from app.config import get_settings +from app.services.jenkins_service import JenkinsService + +logger = structlog.get_logger() +settings = get_settings() + +# 创建 Celery 应用 +celery_app = Celery( + "gitea_webhook_ambassador", + broker=settings.redis.url, + backend=settings.redis.url, + include=["app.tasks.jenkins_tasks"] +) + +# Celery 配置 +celery_app.conf.update( + task_serializer="json", + accept_content=["json"], + result_serializer="json", + timezone="UTC", + enable_utc=True, + task_track_started=True, + task_time_limit=300, # 5分钟超时 + task_soft_time_limit=240, # 4分钟软超时 + worker_prefetch_multiplier=1, + worker_max_tasks_per_child=1000, + worker_max_memory_per_child=200000, # 200MB + task_acks_late=True, + task_reject_on_worker_lost=True, + task_always_eager=False, # 生产环境设为 False + result_expires=3600, # 结果缓存1小时 +) + + +class JenkinsTask(Task): + """Jenkins 任务基类""" + + abstract = True + + def __init__(self): + self.jenkins_service = None + + def __call__(self, *args, **kwargs): + if self.jenkins_service is None: + self.jenkins_service = JenkinsService() + return self.run(*args, **kwargs) + + def on_failure(self, exc, task_id, args, kwargs, einfo): + """任务失败回调""" + logger.error("Task failed", + task_id=task_id, + task_name=self.name, + error=str(exc), + args=args, + kwargs=kwargs) + + def on_retry(self, exc, task_id, args, kwargs, einfo): + """任务重试回调""" + logger.warning("Task retrying", + task_id=task_id, + task_name=self.name, + error=str(exc), + retry_count=self.request.retries) + + def on_success(self, retval, task_id, args, kwargs): + """任务成功回调""" + logger.info("Task completed successfully", + task_id=task_id, + task_name=self.name, + result=retval) + + +@celery_app.task( + bind=True, + base=JenkinsTask, + max_retries=3, + default_retry_delay=60, + autoretry_for=(Exception,), + retry_backoff=True, + retry_jitter=True +) +def trigger_jenkins_job( + self, + job_name: str, + jenkins_url: str, + parameters: Dict[str, str], + event_id: str, + repository: str, + branch: str, + commit_hash: str, + priority: int = 1 +) -> Dict[str, Any]: + """ + 触发 Jenkins 任务 + + Args: + job_name: Jenkins 任务名 + jenkins_url: Jenkins URL + parameters: 任务参数 + event_id: 事件 ID + repository: 仓库名 + branch: 分支名 + commit_hash: 提交哈希 + priority: 优先级 + + Returns: + Dict: 任务执行结果 + """ + start_time = time.time() + + try: + logger.info("Starting Jenkins job trigger", + task_id=self.request.id, + job_name=job_name, + jenkins_url=jenkins_url, + repository=repository, + branch=branch, + commit_hash=commit_hash, + priority=priority) + + # 创建 Jenkins 服务实例 + jenkins_service = JenkinsService() + + # 触发 Jenkins 任务 + result = asyncio.run(jenkins_service.trigger_job( + job_name=job_name, + jenkins_url=jenkins_url, + parameters=parameters + )) + + execution_time = time.time() - start_time + + if result["success"]: + logger.info("Jenkins job triggered successfully", + task_id=self.request.id, + job_name=job_name, + build_number=result.get("build_number"), + execution_time=execution_time) + + return { + "success": True, + "task_id": self.request.id, + "job_name": job_name, + "jenkins_url": jenkins_url, + "build_number": result.get("build_number"), + "build_url": result.get("build_url"), + "event_id": event_id, + "repository": repository, + "branch": branch, + "commit_hash": commit_hash, + "execution_time": execution_time, + "timestamp": datetime.utcnow().isoformat() + } + else: + logger.error("Jenkins job trigger failed", + task_id=self.request.id, + job_name=job_name, + error=result.get("error"), + execution_time=execution_time) + + # 重试任务 + raise self.retry( + countdown=settings.queue.retry_delay * (2 ** self.request.retries), + max_retries=settings.queue.max_retries + ) + + except Exception as e: + execution_time = time.time() - start_time + logger.error("Unexpected error in Jenkins task", + task_id=self.request.id, + job_name=job_name, + error=str(e), + execution_time=execution_time) + + # 重试任务 + raise self.retry( + countdown=settings.queue.retry_delay * (2 ** self.request.retries), + max_retries=settings.queue.max_retries + ) + + +@celery_app.task( + bind=True, + base=JenkinsTask, + max_retries=2, + default_retry_delay=30 +) +def check_jenkins_health( + self, + jenkins_url: str +) -> Dict[str, Any]: + """ + 检查 Jenkins 健康状态 + + Args: + jenkins_url: Jenkins URL + + Returns: + Dict: 健康检查结果 + """ + try: + logger.info("Checking Jenkins health", jenkins_url=jenkins_url) + + jenkins_service = JenkinsService() + result = asyncio.run(jenkins_service.check_health(jenkins_url)) + + return { + "success": True, + "jenkins_url": jenkins_url, + "healthy": result.get("healthy", False), + "response_time": result.get("response_time"), + "timestamp": datetime.utcnow().isoformat() + } + + except Exception as e: + logger.error("Jenkins health check failed", + jenkins_url=jenkins_url, + error=str(e)) + + return { + "success": False, + "jenkins_url": jenkins_url, + "error": str(e), + "timestamp": datetime.utcnow().isoformat() + } + + +@celery_app.task( + bind=True, + base=JenkinsTask +) +def cleanup_expired_tasks(self) -> Dict[str, Any]: + """ + 清理过期任务 + + Returns: + Dict: 清理结果 + """ + try: + logger.info("Starting task cleanup") + + # 获取所有任务 + inspect = self.app.control.inspect() + + # 清理过期的结果 + cleaned_count = 0 + current_time = time.time() + + # 这里可以添加更复杂的清理逻辑 + # 比如清理超过一定时间的任务结果 + + logger.info("Task cleanup completed", cleaned_count=cleaned_count) + + return { + "success": True, + "cleaned_count": cleaned_count, + "timestamp": datetime.utcnow().isoformat() + } + + except Exception as e: + logger.error("Task cleanup failed", error=str(e)) + + return { + "success": False, + "error": str(e), + "timestamp": datetime.utcnow().isoformat() + } + + +# 定时任务 +@celery_app.on_after_configure.connect +def setup_periodic_tasks(sender, **kwargs): + """设置定时任务""" + + # 每小时清理过期任务 + sender.add_periodic_task( + 3600.0, # 1小时 + cleanup_expired_tasks.s(), + name="cleanup-expired-tasks" + ) + + # 每5分钟检查 Jenkins 健康状态 + for env_name, env_config in settings.environments.items(): + sender.add_periodic_task( + 300.0, # 5分钟 + check_jenkins_health.s(env_config.jenkins_url), + name=f"check-jenkins-health-{env_name}" + ) + + +def get_celery_app() -> Celery: + """获取 Celery 应用实例""" + return celery_app \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/templates/dashboard.html b/apps/gitea-webhook-ambassador-python/app/templates/dashboard.html new file mode 100644 index 00000000..56a8f1d4 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/templates/dashboard.html @@ -0,0 +1,326 @@ + + + + + + 仪表板 - Gitea Webhook Ambassador + + + + + + + +
+
+ + +
+
+ +
+
+

项目管理

+ +
+
+ + + + + + + + + + +
项目名称Jenkins 任务Gitea 仓库操作
+
+
+ + +
+
+

API 密钥管理

+ +
+
+ + + + + + + + + + +
描述密钥创建时间操作
+
+
+ + +
+
+

日志查看

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+

健康状态

+
+
+
+
+
+
服务状态
+
+
+
+
+
+
+
+
+
+
统计信息
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/templates/login.html b/apps/gitea-webhook-ambassador-python/app/templates/login.html new file mode 100644 index 00000000..588eec42 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/app/templates/login.html @@ -0,0 +1,174 @@ + + + + + + 登录 - Gitea Webhook Ambassador + + + + + + + + + + + \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/check_version.sh b/apps/gitea-webhook-ambassador-python/check_version.sh new file mode 100755 index 00000000..c2479a12 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/check_version.sh @@ -0,0 +1,133 @@ +#!/bin/bash + +# Gitea Webhook Ambassador - 版本检查脚本 +# 用于区分 Go 版本和 Python 版本 + +echo "🔍 检查 Gitea Webhook Ambassador 版本..." + +# 检查端口 8000 (Python 版本默认端口) +echo "📡 检查端口 8000 (Python 版本)..." +if lsof -i :8000 > /dev/null 2>&1; then + PID=$(lsof -ti :8000) + PROCESS=$(ps -p $PID -o comm= 2>/dev/null) + echo "✅ 端口 8000 被占用 (PID: $PID, 进程: $PROCESS)" + + # 检查是否是 Python 进程 + if echo "$PROCESS" | grep -q "python\|uvicorn"; then + echo "🐍 检测到 Python 版本正在运行" + + # 尝试访问 Python 版本的 API + if curl -s http://localhost:8000/api/health > /dev/null 2>&1; then + echo "✅ Python 版本 API 响应正常" + echo "🌐 访问地址: http://localhost:8000" + echo "📊 仪表板: http://localhost:8000/dashboard" + else + echo "⚠️ Python 版本进程存在但 API 无响应" + fi + else + echo "⚠️ 端口 8000 被其他进程占用" + fi +else + echo "❌ 端口 8000 未被占用 (Python 版本未运行)" +fi + +echo "" + +# 检查端口 8080 (Go 版本默认端口) +echo "📡 检查端口 8080 (Go 版本)..." +if lsof -i :8080 > /dev/null 2>&1; then + PID=$(lsof -ti :8080) + PROCESS=$(ps -p $PID -o comm= 2>/dev/null) + echo "✅ 端口 8080 被占用 (PID: $PID, 进程: $PROCESS)" + + # 检查是否是 Go 进程 + if echo "$PROCESS" | grep -q "gitea-webhook-ambassador"; then + echo "🚀 检测到 Go 版本正在运行" + + # 尝试访问 Go 版本的 API + if curl -s http://localhost:8080/health > /dev/null 2>&1; then + echo "✅ Go 版本 API 响应正常" + echo "🌐 访问地址: http://localhost:8080" + else + echo "⚠️ Go 版本进程存在但 API 无响应" + fi + else + echo "⚠️ 端口 8080 被其他进程占用" + fi +else + echo "❌ 端口 8080 未被占用 (Go 版本未运行)" +fi + +echo "" + +# 检查 PID 文件 +echo "📁 检查 PID 文件..." + +# Python 版本 PID 文件 +PYTHON_PID_FILE="/home/nicolas/freeleaps-ops/apps/gitea-webhook-ambassador-python/service.pid" +if [ -f "$PYTHON_PID_FILE" ]; then + PYTHON_PID=$(cat "$PYTHON_PID_FILE") + if ps -p $PYTHON_PID > /dev/null 2>&1; then + echo "✅ Python 版本 PID 文件存在 (PID: $PYTHON_PID)" + else + echo "⚠️ Python 版本 PID 文件存在但进程不存在" + fi +else + echo "❌ Python 版本 PID 文件不存在" +fi + +# Go 版本 PID 文件 (如果存在) +GO_PID_FILE="/home/nicolas/freeleaps-ops/apps/gitea-webhook-ambassador/service.pid" +if [ -f "$GO_PID_FILE" ]; then + GO_PID=$(cat "$GO_PID_FILE") + if ps -p $GO_PID > /dev/null 2>&1; then + echo "✅ Go 版本 PID 文件存在 (PID: $GO_PID)" + else + echo "⚠️ Go 版本 PID 文件存在但进程不存在" + fi +else + echo "❌ Go 版本 PID 文件不存在" +fi + +echo "" + +# 总结 +echo "📊 总结:" +echo "----------------------------------------" + +PYTHON_RUNNING=false +GO_RUNNING=false + +# 检查 Python 版本 +if lsof -i :8000 > /dev/null 2>&1; then + PID=$(lsof -ti :8000) + PROCESS=$(ps -p $PID -o comm= 2>/dev/null) + if echo "$PROCESS" | grep -q "python\|uvicorn"; then + PYTHON_RUNNING=true + fi +fi + +# 检查 Go 版本 +if lsof -i :8080 > /dev/null 2>&1; then + PID=$(lsof -ti :8080) + PROCESS=$(ps -p $PID -o comm= 2>/dev/null) + if echo "$PROCESS" | grep -q "gitea-webhook-ambassador"; then + GO_RUNNING=true + fi +fi + +if [ "$PYTHON_RUNNING" = true ] && [ "$GO_RUNNING" = true ]; then + echo "⚠️ 两个版本都在运行!" + echo "🐍 Python 版本: http://localhost:8000" + echo "🚀 Go 版本: http://localhost:8080" +elif [ "$PYTHON_RUNNING" = true ]; then + echo "✅ 当前运行: Python 版本" + echo "🌐 访问地址: http://localhost:8000" +elif [ "$GO_RUNNING" = true ]; then + echo "✅ 当前运行: Go 版本" + echo "🌐 访问地址: http://localhost:8080" +else + echo "❌ 没有检测到任何版本在运行" +fi + +echo "----------------------------------------" \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/config/environments.yaml b/apps/gitea-webhook-ambassador-python/config/environments.yaml new file mode 100644 index 00000000..284af05f --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/config/environments.yaml @@ -0,0 +1,39 @@ +# 环境分发配置 +environments: + dev: + branches: ["dev", "develop", "development", "feature/*"] + jenkins_job: "alpha-build" + jenkins_url: "https://jenkins-alpha.freeleaps.com" + priority: 2 + + prod: + branches: ["prod", "production", "main", "master", "release/*"] + jenkins_job: "production-build" + jenkins_url: "https://jenkins-prod.freeleaps.com" + priority: 1 + + staging: + branches: ["staging", "stage", "pre-prod"] + jenkins_job: "staging-build" + jenkins_url: "https://jenkins-staging.freeleaps.com" + priority: 3 + + default: + branches: ["*"] + jenkins_job: "default-build" + jenkins_url: "https://jenkins-default.freeleaps.com" + priority: 4 + +# 防抖配置 +deduplication: + enabled: true + window_seconds: 300 # 5分钟防抖窗口 + strategy: "commit_branch" # commit_hash + branch + cache_ttl: 3600 # 缓存1小时 + +# 队列配置 +queue: + max_concurrent: 10 + max_retries: 3 + retry_delay: 60 # 秒 + priority_levels: 4 \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/configs/config.yaml b/apps/gitea-webhook-ambassador-python/configs/config.yaml new file mode 100644 index 00000000..f7de33dc --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/configs/config.yaml @@ -0,0 +1,32 @@ +server: + port: 8000 + webhookPath: "/webhook" + secretHeader: "X-Gitea-Signature" + secretKey: "admin-secret-key-change-in-production" + +jenkins: + url: "http://jenkins.example.com" + username: "jenkins-user" + token: "jenkins-api-token" + timeout: 30 + +admin: + token: "admin-api-token" # Token for admin API access + +database: + path: "data/gitea-webhook-ambassador.db" # Path to SQLite database file + +logging: + level: "info" # debug, info, warn, error + format: "text" # text, json + file: "logs/service.log" # stdout if empty, or path to log file + +worker: + poolSize: 10 # Number of concurrent workers + queueSize: 100 # Size of job queue + maxRetries: 3 # Maximum number of retry attempts + retryBackoff: 1 # Initial retry backoff in seconds (exponential) + +eventCleanup: + interval: 3600 # Cleanup interval in seconds + expireAfter: 7200 # Event expiration time in seconds \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/devbox b/apps/gitea-webhook-ambassador-python/devbox new file mode 100755 index 00000000..586f00e5 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/devbox @@ -0,0 +1,220 @@ +#!/bin/bash + +# Gitea Webhook Ambassador (Python) - Devbox Script +# This script mimics the Go version's devbox functionality + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_NAME="gitea-webhook-ambassador" +PID_FILE="$SCRIPT_DIR/service.pid" +LOG_FILE="$SCRIPT_DIR/logs/service.log" + +# Create logs directory +mkdir -p "$SCRIPT_DIR/logs" + +# Function to show usage +show_usage() { + echo "Usage: $0 {start|stop|restart|status|logs|follow|init|install|help}" + echo "" + echo "Commands:" + echo " start - Start the service in background" + echo " stop - Stop the service" + echo " restart - Restart the service" + echo " status - Show service status" + echo " logs - Show latest logs" + echo " follow - Follow logs in real-time" + echo " init - Initialize database" + echo " install - Install dependencies" + echo " help - Show this help message" + echo "" + echo "Examples:" + echo " $0 start # Start service" + echo " $0 status # Check status" + echo " $0 logs # View logs" +} + +# Function to check if virtual environment exists +check_venv() { + if [ ! -d "$SCRIPT_DIR/venv" ]; then + echo "❌ Virtual environment not found. Run '$0 install' first." + exit 1 + fi +} + +# Function to activate virtual environment +activate_venv() { + source "$SCRIPT_DIR/venv/bin/activate" +} + +# Function to start service +start_service() { + echo "🚀 Starting $APP_NAME (Python Version)..." + echo "🐍 Version: Python Enhanced with Web UI" + + check_venv + + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p $PID > /dev/null 2>&1; then + echo "❌ Service is already running (PID: $PID)" + return 1 + else + echo "⚠️ Found stale PID file, cleaning up..." + rm -f "$PID_FILE" + fi + fi + + # Activate virtual environment and start service + cd "$SCRIPT_DIR" + activate_venv + + # Start the service in background + nohup python -m uvicorn app.main_enhanced:app --host 0.0.0.0 --port 8000 > "$LOG_FILE" 2>&1 & + PID=$! + echo $PID > "$PID_FILE" + + # Wait a moment for service to start + sleep 3 + + if ps -p $PID > /dev/null 2>&1; then + echo "✅ Python Service started successfully (PID: $PID)" + echo "📝 Log file: $LOG_FILE" + echo "🌐 Access: http://localhost:8000" + echo "📊 Dashboard: http://localhost:8000/dashboard" + echo "🔑 Admin key: admin-secret-key-change-in-production" + echo "🐍 Python Version Features: Web UI, Database, JWT Auth" + else + echo "❌ Service failed to start" + rm -f "$PID_FILE" + return 1 + fi +} + +# Function to stop service +stop_service() { + echo "🛑 Stopping $APP_NAME..." + + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p $PID > /dev/null 2>&1; then + kill $PID + echo "✅ Service stopped (PID: $PID)" + else + echo "⚠️ Service not running" + fi + rm -f "$PID_FILE" + else + echo "⚠️ No PID file found" + fi +} + +# Function to restart service +restart_service() { + echo "🔄 Restarting $APP_NAME..." + stop_service + sleep 2 + start_service +} + +# Function to show status +show_status() { + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p $PID > /dev/null 2>&1; then + echo "✅ $APP_NAME (Python Version) is running (PID: $PID)" + echo "🐍 Version: Python Enhanced with Web UI" + echo "📝 Log file: $LOG_FILE" + echo "🌐 Access: http://localhost:8000" + echo "📊 Dashboard: http://localhost:8000/dashboard" + else + echo "❌ $APP_NAME is not running (PID file exists but process not found)" + rm -f "$PID_FILE" + fi + else + echo "❌ $APP_NAME is not running" + fi +} + +# Function to show logs +show_logs() { + if [ -f "$LOG_FILE" ]; then + echo "📝 Latest logs (last 50 lines):" + echo "----------------------------------------" + tail -n 50 "$LOG_FILE" + echo "----------------------------------------" + echo "Full log file: $LOG_FILE" + else + echo "❌ No log file found" + fi +} + +# Function to follow logs +follow_logs() { + if [ -f "$LOG_FILE" ]; then + echo "📝 Following logs (Ctrl+C to exit):" + tail -f "$LOG_FILE" + else + echo "❌ No log file found" + fi +} + +# Function to initialize database +init_database() { + echo "🗄️ Initializing database..." + check_venv + cd "$SCRIPT_DIR" + activate_venv + python -c "from app.models.database import create_tables; create_tables(); print('Database initialized successfully')" +} + +# Function to install dependencies +install_dependencies() { + echo "📦 Installing dependencies..." + cd "$SCRIPT_DIR" + + if [ -d "venv" ]; then + echo "⚠️ Virtual environment already exists. Removing..." + rm -rf venv + fi + + python3 -m venv venv + activate_venv + pip install -r requirements.txt + echo "✅ Dependencies installed successfully" +} + +# Main logic +case "$1" in + start) + start_service + ;; + stop) + stop_service + ;; + restart) + restart_service + ;; + status) + show_status + ;; + logs) + show_logs + ;; + follow) + follow_logs + ;; + init) + init_database + ;; + install) + install_dependencies + ;; + help|--help|-h) + show_usage + ;; + *) + echo "❌ Unknown command: $1" + echo "" + show_usage + exit 1 + ;; +esac \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/docker-compose.yml b/apps/gitea-webhook-ambassador-python/docker-compose.yml new file mode 100644 index 00000000..581afa9d --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/docker-compose.yml @@ -0,0 +1,176 @@ +version: '3.8' + +services: + # Redis 服务 + redis: + image: redis:7-alpine + container_name: webhook-ambassador-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + + # PostgreSQL 数据库 (可选,用于生产环境) + postgres: + image: postgres:15-alpine + container_name: webhook-ambassador-postgres + environment: + POSTGRES_DB: webhook_ambassador + POSTGRES_USER: webhook_user + POSTGRES_PASSWORD: webhook_password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U webhook_user -d webhook_ambassador"] + interval: 30s + timeout: 10s + retries: 3 + + # Webhook Ambassador API 服务 + api: + build: + context: . + dockerfile: Dockerfile + container_name: webhook-ambassador-api + ports: + - "8000:8000" + environment: + - REDIS_URL=redis://redis:6379/0 + - DATABASE_URL=postgresql://webhook_user:webhook_password@postgres:5432/webhook_ambassador + - JENKINS_USERNAME=${JENKINS_USERNAME} + - JENKINS_TOKEN=${JENKINS_TOKEN} + - SECURITY_SECRET_KEY=${SECURITY_SECRET_KEY} + - LOGGING_LEVEL=INFO + volumes: + - ./config:/app/config + - ./logs:/app/logs + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + restart: unless-stopped + + # Celery Worker + worker: + build: + context: . + dockerfile: Dockerfile + container_name: webhook-ambassador-worker + command: celery -A app.tasks.jenkins_tasks worker --loglevel=info --concurrency=4 + environment: + - REDIS_URL=redis://redis:6379/0 + - DATABASE_URL=postgresql://webhook_user:webhook_password@postgres:5432/webhook_ambassador + - JENKINS_USERNAME=${JENKINS_USERNAME} + - JENKINS_TOKEN=${JENKINS_TOKEN} + - SECURITY_SECRET_KEY=${SECURITY_SECRET_KEY} + - LOGGING_LEVEL=INFO + volumes: + - ./config:/app/config + - ./logs:/app/logs + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + restart: unless-stopped + + # Celery Beat (定时任务调度器) + beat: + build: + context: . + dockerfile: Dockerfile + container_name: webhook-ambassador-beat + command: celery -A app.tasks.jenkins_tasks beat --loglevel=info + environment: + - REDIS_URL=redis://redis:6379/0 + - DATABASE_URL=postgresql://webhook_user:webhook_password@postgres:5432/webhook_ambassador + - JENKINS_USERNAME=${JENKINS_USERNAME} + - JENKINS_TOKEN=${JENKINS_TOKEN} + - SECURITY_SECRET_KEY=${SECURITY_SECRET_KEY} + - LOGGING_LEVEL=INFO + volumes: + - ./config:/app/config + - ./logs:/app/logs + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + restart: unless-stopped + + # Flower (Celery 监控) + flower: + build: + context: . + dockerfile: Dockerfile + container_name: webhook-ambassador-flower + command: celery -A app.tasks.jenkins_tasks flower --port=5555 + ports: + - "5555:5555" + environment: + - REDIS_URL=redis://redis:6379/0 + - DATABASE_URL=postgresql://webhook_user:webhook_password@postgres:5432/webhook_ambassador + - JENKINS_USERNAME=${JENKINS_USERNAME} + - JENKINS_TOKEN=${JENKINS_TOKEN} + - SECURITY_SECRET_KEY=${SECURITY_SECRET_KEY} + - LOGGING_LEVEL=INFO + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + restart: unless-stopped + + # Prometheus (监控) + prometheus: + image: prom/prometheus:latest + container_name: webhook-ambassador-prometheus + ports: + - "9090:9090" + volumes: + - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + - '--storage.tsdb.retention.time=200h' + - '--web.enable-lifecycle' + restart: unless-stopped + + # Grafana (监控面板) + grafana: + image: grafana/grafana:latest + container_name: webhook-ambassador-grafana + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - grafana_data:/var/lib/grafana + - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards + - ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources + depends_on: + - prometheus + restart: unless-stopped + +volumes: + redis_data: + postgres_data: + prometheus_data: + grafana_data: \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/env.example b/apps/gitea-webhook-ambassador-python/env.example new file mode 100644 index 00000000..62589fd6 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/env.example @@ -0,0 +1,42 @@ +# 应用配置 +APP_NAME=Gitea Webhook Ambassador +DEBUG=false +HOST=0.0.0.0 +PORT=8000 + +# 数据库配置 +DATABASE_URL=sqlite:///./webhook_ambassador.db +# 生产环境使用 PostgreSQL: +# DATABASE_URL=postgresql://webhook_user:webhook_password@localhost:5432/webhook_ambassador + +# Redis 配置 +REDIS_URL=redis://localhost:6379/0 +REDIS_PASSWORD= +REDIS_DB=0 + +# Jenkins 配置 +JENKINS_USERNAME=your_jenkins_username +JENKINS_TOKEN=115127e693f1bc6b7194f58ff6d6283bd0 +JENKINS_TIMEOUT=30 + +# 安全配置 +SECURITY_SECRET_KEY=r6Y@QTb*7BQN@hDGsN +SECURITY_WEBHOOK_SECRET_HEADER=X-Gitea-Signature +SECURITY_RATE_LIMIT_PER_MINUTE=100 + +# 日志配置 +LOGGING_LEVEL=INFO +LOGGING_FORMAT=json +LOGGING_FILE= + +# 队列配置 +QUEUE_MAX_CONCURRENT=10 +QUEUE_MAX_RETRIES=3 +QUEUE_RETRY_DELAY=60 +QUEUE_PRIORITY_LEVELS=3 + +# 防抖配置 +DEDUPLICATION_ENABLED=true +DEDUPLICATION_WINDOW_SECONDS=300 +DEDUPLICATION_STRATEGY=commit_branch +DEDUPLICATION_CACHE_TTL=3600 \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/fix_pid.sh b/apps/gitea-webhook-ambassador-python/fix_pid.sh new file mode 100755 index 00000000..37043c16 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/fix_pid.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# 修复 PID 文件问题 +echo "🔧 修复 PID 文件问题..." + +# 查找 Python 服务进程 +PID=$(lsof -ti :8000 2>/dev/null) + +if [ -n "$PID" ]; then + echo "✅ 找到运行中的 Python 服务 (PID: $PID)" + + # 检查进程是否是我们的服务 + PROCESS=$(ps -p $PID -o comm= 2>/dev/null) + if echo "$PROCESS" | grep -q "python"; then + echo "🐍 确认是 Python 版本的 Gitea Webhook Ambassador" + + # 创建 PID 文件 + echo $PID > service.pid + echo "✅ 已创建 PID 文件: service.pid" + + # 验证 PID 文件 + if [ -f "service.pid" ]; then + STORED_PID=$(cat service.pid) + echo "📝 PID 文件内容: $STORED_PID" + + if [ "$STORED_PID" = "$PID" ]; then + echo "✅ PID 文件修复成功" + echo "💡 现在可以使用 './devbox stop' 来停止服务" + else + echo "❌ PID 文件内容不匹配" + fi + else + echo "❌ 无法创建 PID 文件" + fi + else + echo "⚠️ 端口 8000 被其他进程占用" + fi +else + echo "❌ 没有找到运行中的 Python 服务" + echo "💡 请先启动服务: './devbox start'" +fi \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/gitea-webhook-ambassador.service b/apps/gitea-webhook-ambassador-python/gitea-webhook-ambassador.service new file mode 100644 index 00000000..c542612c --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/gitea-webhook-ambassador.service @@ -0,0 +1,15 @@ +[Unit] +Description=Gitea Webhook Ambassador Python Service +After=network.target + +[Service] +Type=simple +User=nicolas +WorkingDirectory=/home/nicolas/freeleaps-ops/apps/gitea-webhook-ambassador-python +Environment=PATH=/home/nicolas/freeleaps-ops/apps/gitea-webhook-ambassador-python/venv/bin +ExecStart=/home/nicolas/freeleaps-ops/apps/gitea-webhook-ambassador-python/venv/bin/python -m uvicorn app.main_demo:app --host 0.0.0.0 --port 8000 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/quick_check.sh b/apps/gitea-webhook-ambassador-python/quick_check.sh new file mode 100755 index 00000000..96a63fc6 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/quick_check.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# 快速检查当前运行的版本 + +echo "🔍 快速检查 Gitea Webhook Ambassador 版本..." + +# 检查 Python 版本 (端口 8000) +if lsof -i :8000 > /dev/null 2>&1; then + PID=$(lsof -ti :8000) + PROCESS=$(ps -p $PID -o comm= 2>/dev/null) + if echo "$PROCESS" | grep -q "python\|uvicorn"; then + echo "🐍 Python 版本正在运行 (PID: $PID)" + echo "🌐 http://localhost:8000" + echo "📊 http://localhost:8000/dashboard" + exit 0 + fi +fi + +# 检查 Go 版本 (端口 8080) +if lsof -i :8080 > /dev/null 2>&1; then + PID=$(lsof -ti :8080) + PROCESS=$(ps -p $PID -o comm= 2>/dev/null) + if echo "$PROCESS" | grep -q "gitea-webhook-ambassador"; then + echo "🚀 Go 版本正在运行 (PID: $PID)" + echo "🌐 http://localhost:8080" + exit 0 + fi +fi + +echo "❌ 没有检测到任何版本在运行" +echo "💡 使用 './devbox start' 启动 Python 版本" \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/requirements.txt b/apps/gitea-webhook-ambassador-python/requirements.txt new file mode 100644 index 00000000..299918dd --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/requirements.txt @@ -0,0 +1,15 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 +pydantic-settings==2.1.0 +structlog==23.2.0 +httpx==0.25.2 +celery==5.3.4 +redis==5.0.1 +sqlalchemy==2.0.23 +jinja2==3.1.2 +python-multipart==0.0.6 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +psutil==5.9.6 +aiofiles==23.2.1 \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/scripts/setup.sh b/apps/gitea-webhook-ambassador-python/scripts/setup.sh new file mode 100755 index 00000000..a2082108 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/scripts/setup.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Gitea Webhook Ambassador 快速设置脚本 + +set -e + +echo "🚀 开始设置 Gitea Webhook Ambassador..." + +# 检查 Python 版本 +python_version=$(python3 --version 2>&1 | grep -oE '[0-9]+\.[0-9]+') +required_version="3.8" + +if [ "$(printf '%s\n' "$required_version" "$python_version" | sort -V | head -n1)" != "$required_version" ]; then + echo "❌ 需要 Python 3.8 或更高版本,当前版本: $python_version" + exit 1 +fi + +echo "✅ Python 版本检查通过: $python_version" + +# 创建虚拟环境 +if [ ! -d "venv" ]; then + echo "📦 创建虚拟环境..." + python3 -m venv venv +fi + +# 激活虚拟环境 +echo "🔧 激活虚拟环境..." +source venv/bin/activate + +# 升级 pip +echo "⬆️ 升级 pip..." +pip install --upgrade pip + +# 安装依赖 +echo "📚 安装依赖..." +pip install -r requirements.txt + +# 创建配置文件 +if [ ! -f ".env" ]; then + echo "⚙️ 创建环境配置文件..." + cp env.example .env + echo "📝 请编辑 .env 文件,配置您的 Jenkins 凭据和其他设置" +fi + +# 创建日志目录 +mkdir -p logs + +# 创建数据库目录 +mkdir -p data + +echo "✅ 设置完成!" +echo "" +echo "📋 下一步操作:" +echo "1. 编辑 .env 文件,配置 Jenkins 凭据" +echo "2. 运行: source venv/bin/activate" +echo "3. 启动 Redis: docker run -d -p 6379:6379 redis:alpine" +echo "4. 启动服务: python -m uvicorn app.main:app --reload" +echo "5. 启动 Celery worker: celery -A app.tasks.jenkins_tasks worker --loglevel=info" +echo "" +echo "🌐 访问地址: http://localhost:8000" +echo "📊 监控面板: http://localhost:8000/health" \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/scripts/start.sh b/apps/gitea-webhook-ambassador-python/scripts/start.sh new file mode 100755 index 00000000..19ede1c4 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/scripts/start.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Gitea Webhook Ambassador 启动脚本 + +set -e + +# 检查虚拟环境 +if [ ! -d "venv" ]; then + echo "❌ 虚拟环境不存在,请先运行 ./scripts/setup.sh" + exit 1 +fi + +# 激活虚拟环境 +source venv/bin/activate + +# 检查环境文件 +if [ ! -f ".env" ]; then + echo "❌ .env 文件不存在,请先运行 ./scripts/setup.sh" + exit 1 +fi + +# 检查 Redis 是否运行 +if ! docker ps | grep -q redis; then + echo "🐳 启动 Redis..." + docker run -d --name webhook-redis -p 6379:6379 redis:alpine + sleep 3 +fi + +echo "🚀 启动 Gitea Webhook Ambassador..." + +# 启动 API 服务 +echo "🌐 启动 API 服务..." +python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload & +API_PID=$! + +# 等待 API 服务启动 +sleep 5 + +# 启动 Celery worker +echo "⚙️ 启动 Celery worker..." +celery -A app.tasks.jenkins_tasks worker --loglevel=info --concurrency=4 & +WORKER_PID=$! + +# 启动 Celery beat (定时任务) +echo "⏰ 启动定时任务调度器..." +celery -A app.tasks.jenkins_tasks beat --loglevel=info & +BEAT_PID=$! + +echo "✅ 所有服务已启动!" +echo "" +echo "📊 服务状态:" +echo "- API 服务: http://localhost:8000 (PID: $API_PID)" +echo "- 健康检查: http://localhost:8000/health" +echo "- 监控指标: http://localhost:8000/metrics" +echo "- Celery Worker: PID $WORKER_PID" +echo "- Celery Beat: PID $BEAT_PID" +echo "" +echo "🛑 按 Ctrl+C 停止所有服务" + +# 等待中断信号 +trap 'echo "🛑 正在停止服务..."; kill $API_PID $WORKER_PID $BEAT_PID 2>/dev/null; exit 0' INT + +# 等待所有后台进程 +wait \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/start.sh b/apps/gitea-webhook-ambassador-python/start.sh new file mode 100755 index 00000000..3fc9259f --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/start.sh @@ -0,0 +1,154 @@ +#!/bin/bash + +# Gitea Webhook Ambassador Python 启动脚本 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SERVICE_NAME="gitea-webhook-ambassador-python" +LOG_FILE="$SCRIPT_DIR/logs/service.log" +PID_FILE="$SCRIPT_DIR/service.pid" + +# 创建日志目录 +mkdir -p "$SCRIPT_DIR/logs" + +# 激活虚拟环境 +source "$SCRIPT_DIR/venv/bin/activate" + +# 函数:启动服务 +start_service() { + echo "🚀 启动 $SERVICE_NAME..." + + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p $PID > /dev/null 2>&1; then + echo "❌ 服务已在运行 (PID: $PID)" + return 1 + else + echo "⚠️ 发现过期的 PID 文件,正在清理..." + rm -f "$PID_FILE" + fi + fi + + # 后台启动服务 + nohup python -m uvicorn app.main_demo:app --host 0.0.0.0 --port 8000 > "$LOG_FILE" 2>&1 & + PID=$! + echo $PID > "$PID_FILE" + + # 等待服务启动 + sleep 3 + + if ps -p $PID > /dev/null 2>&1; then + echo "✅ 服务启动成功 (PID: $PID)" + echo "📝 日志文件: $LOG_FILE" + echo "🌐 访问地址: http://localhost:8000" + echo "🔑 演示密钥: demo_admin_key, demo_user_key" + else + echo "❌ 服务启动失败" + rm -f "$PID_FILE" + return 1 + fi +} + +# 函数:停止服务 +stop_service() { + echo "🛑 停止 $SERVICE_NAME..." + + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p $PID > /dev/null 2>&1; then + kill $PID + echo "✅ 服务已停止 (PID: $PID)" + else + echo "⚠️ 服务未运行" + fi + rm -f "$PID_FILE" + else + echo "⚠️ PID 文件不存在" + fi +} + +# 函数:重启服务 +restart_service() { + echo "🔄 重启 $SERVICE_NAME..." + stop_service + sleep 2 + start_service +} + +# 函数:查看状态 +status_service() { + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p $PID > /dev/null 2>&1; then + echo "✅ $SERVICE_NAME 正在运行 (PID: $PID)" + echo "📝 日志文件: $LOG_FILE" + echo "🌐 访问地址: http://localhost:8000" + else + echo "❌ $SERVICE_NAME 未运行 (PID 文件存在但进程不存在)" + rm -f "$PID_FILE" + fi + else + echo "❌ $SERVICE_NAME 未运行" + fi +} + +# 函数:查看日志 +show_logs() { + if [ -f "$LOG_FILE" ]; then + echo "📝 显示最新日志 (最后 50 行):" + echo "----------------------------------------" + tail -n 50 "$LOG_FILE" + echo "----------------------------------------" + echo "完整日志文件: $LOG_FILE" + else + echo "❌ 日志文件不存在" + fi +} + +# 函数:实时日志 +follow_logs() { + if [ -f "$LOG_FILE" ]; then + echo "📝 实时日志 (按 Ctrl+C 退出):" + tail -f "$LOG_FILE" + else + echo "❌ 日志文件不存在" + fi +} + +# 主逻辑 +case "$1" in + start) + start_service + ;; + stop) + stop_service + ;; + restart) + restart_service + ;; + status) + status_service + ;; + logs) + show_logs + ;; + follow) + follow_logs + ;; + *) + echo "用法: $0 {start|stop|restart|status|logs|follow}" + echo "" + echo "命令说明:" + echo " start - 启动服务" + echo " stop - 停止服务" + echo " restart - 重启服务" + echo " status - 查看服务状态" + echo " logs - 查看最新日志" + echo " follow - 实时查看日志" + echo "" + echo "示例:" + echo " $0 start # 启动服务" + echo " $0 status # 查看状态" + echo " $0 logs # 查看日志" + exit 1 + ;; +esac \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/test_auth.py b/apps/gitea-webhook-ambassador-python/test_auth.py new file mode 100644 index 00000000..1d05c9e5 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/test_auth.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +认证功能测试脚本 +演示如何正确使用 JWT 和 API 密钥认证 +""" + +import asyncio +import aiohttp +import json +from datetime import datetime + +BASE_URL = "http://localhost:8000" + +async def test_jwt_authentication(): + """测试 JWT 认证""" + print("🔐 测试 JWT 认证") + print("-" * 50) + + # 注意:在实际应用中,JWT 令牌应该通过登录端点获取 + # 这里我们使用一个示例令牌(在实际环境中需要从登录端点获取) + + # 模拟 JWT 令牌(实际应用中应该从登录端点获取) + jwt_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTczMjAwMDAwMH0.test" + + async with aiohttp.ClientSession() as session: + # 使用 JWT 令牌访问管理端点 + headers = {"Authorization": f"Bearer {jwt_token}"} + + # 测试访问日志端点 + async with session.get(f"{BASE_URL}/api/logs", headers=headers) as response: + if response.status == 200: + logs = await response.json() + print("✅ JWT 认证成功 - 日志访问") + print(f" 获取到 {len(logs)} 条日志") + else: + print(f"❌ JWT 认证失败 - 日志访问: {response.status}") + if response.status == 401: + print(" 原因: JWT 令牌无效或已过期") + + print() + +async def test_api_key_authentication(): + """测试 API 密钥认证""" + print("🔑 测试 API 密钥认证") + print("-" * 50) + + async with aiohttp.ClientSession() as session: + # 首先创建一个 API 密钥(需要管理员权限) + # 注意:这里我们使用一个临时的认证方式 + + # 方法1:直接使用内存中的 API 密钥(仅用于演示) + # 在实际应用中,API 密钥应该通过管理界面创建 + + # 模拟一个有效的 API 密钥 + api_key = "test_api_key_12345" + + headers = {"Authorization": f"Bearer {api_key}"} + + # 测试访问日志端点 + async with session.get(f"{BASE_URL}/api/logs", headers=headers) as response: + if response.status == 200: + logs = await response.json() + print("✅ API 密钥认证成功 - 日志访问") + print(f" 获取到 {len(logs)} 条日志") + else: + print(f"❌ API 密钥认证失败 - 日志访问: {response.status}") + if response.status == 401: + print(" 原因: API 密钥无效或已撤销") + + print() + +async def test_public_endpoints(): + """测试公开端点(无需认证)""" + print("🌐 测试公开端点") + print("-" * 50) + + async with aiohttp.ClientSession() as session: + # 健康检查端点(无需认证) + async with session.get(f"{BASE_URL}/health") as response: + if response.status == 200: + data = await response.json() + print("✅ 健康检查端点访问成功") + print(f" 状态: {data['status']}") + print(f" Jenkins: {data['jenkins']['status']}") + else: + print(f"❌ 健康检查端点访问失败: {response.status}") + + # Webhook 端点(无需认证) + webhook_data = {"test": "webhook_data"} + async with session.post(f"{BASE_URL}/webhook/gitea", json=webhook_data) as response: + if response.status == 200: + data = await response.json() + print("✅ Webhook 端点访问成功") + print(f" 响应: {data['message']}") + else: + print(f"❌ Webhook 端点访问失败: {response.status}") + + print() + +async def test_authentication_flow(): + """测试完整的认证流程""" + print("🔄 测试完整认证流程") + print("-" * 50) + + print("📋 认证流程说明:") + print("1. 公开端点: /health, /webhook/gitea - 无需认证") + print("2. 管理端点: /api/admin/* - 需要 JWT 或 API 密钥") + print("3. 日志端点: /api/logs/* - 需要 JWT 或 API 密钥") + print() + + print("🔧 如何获取认证令牌:") + print("1. JWT 令牌: 通过登录端点获取(需要实现登录功能)") + print("2. API 密钥: 通过管理界面创建(需要管理员权限)") + print() + + print("⚠️ 当前演示限制:") + print("- 使用模拟的认证令牌") + print("- 实际应用中需要实现完整的登录和密钥管理") + print("- 建议在生产环境中使用真实的认证系统") + + print() + +async def create_demo_api_key(): + """创建演示用的 API 密钥""" + print("🔧 创建演示 API 密钥") + print("-" * 50) + + # 注意:这是一个简化的演示 + # 在实际应用中,API 密钥应该通过安全的方式创建和存储 + + demo_api_key = "demo_api_key_" + str(int(datetime.now().timestamp())) + + print(f"✅ 演示 API 密钥已创建: {demo_api_key}") + print("📝 使用方法:") + print(f" curl -H 'Authorization: Bearer {demo_api_key}' {BASE_URL}/api/logs") + print() + + return demo_api_key + +async def main(): + """主测试函数""" + print("🚀 开始认证功能测试") + print("=" * 60) + print() + + try: + # 等待服务启动 + await asyncio.sleep(2) + + await test_public_endpoints() + await test_jwt_authentication() + await test_api_key_authentication() + await test_authentication_flow() + + # 创建演示 API 密钥 + demo_key = await create_demo_api_key() + + print("=" * 60) + print("🎉 认证功能测试完成!") + print() + print("📚 下一步建议:") + print("1. 实现完整的登录系统") + print("2. 添加用户管理功能") + print("3. 实现 API 密钥的安全存储") + print("4. 添加权限控制机制") + print("5. 实现会话管理") + + except Exception as e: + print(f"❌ 测试过程中出现错误: {str(e)}") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/test_enhanced.py b/apps/gitea-webhook-ambassador-python/test_enhanced.py new file mode 100644 index 00000000..ad3cdb74 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/test_enhanced.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +""" +增强版 Gitea Webhook Ambassador 功能测试脚本 +演示所有新增的监测和管理功能 +""" + +import asyncio +import aiohttp +import json +from datetime import datetime + +BASE_URL = "http://localhost:8000" + +async def test_health_check(): + """测试增强的健康检查""" + print("🧪 测试增强的健康检查") + print("-" * 50) + + async with aiohttp.ClientSession() as session: + async with session.get(f"{BASE_URL}/health") as response: + if response.status == 200: + data = await response.json() + print("✅ 健康检查通过") + print(f" 状态: {data['status']}") + print(f" 服务: {data['service']}") + print(f" Jenkins: {data['jenkins']['status']}") + print(f" 工作池: {data['worker_pool']['active_workers']} 活跃工作线程") + print(f" 队列大小: {data['worker_pool']['queue_size']}") + print(f" 已处理: {data['worker_pool']['total_processed']}") + print(f" 失败: {data['worker_pool']['total_failed']}") + else: + print(f"❌ 健康检查失败: {response.status}") + + print() + +async def test_webhook(): + """测试 Webhook 功能""" + print("🧪 测试 Webhook 功能") + print("-" * 50) + + webhook_data = { + "ref": "refs/heads/dev", + "before": "abc123", + "after": "def456", + "repository": { + "full_name": "freeleaps/test-project", + "clone_url": "https://gitea.freeleaps.com/freeleaps/test-project.git" + }, + "pusher": { + "login": "developer", + "email": "dev@freeleaps.com" + } + } + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{BASE_URL}/webhook/gitea", + json=webhook_data + ) as response: + if response.status == 200: + data = await response.json() + print("✅ Webhook 处理成功") + print(f" 响应: {data['message']}") + print(f" 数据大小: {data['data']['body_size']} bytes") + else: + print(f"❌ Webhook 处理失败: {response.status}") + + print() + +async def test_api_key_management(): + """测试 API 密钥管理""" + print("🧪 测试 API 密钥管理") + print("-" * 50) + + # 创建 API 密钥 + async with aiohttp.ClientSession() as session: + # 创建密钥 + create_data = {"name": "test-api-key"} + async with session.post( + f"{BASE_URL}/api/admin/api-keys", + json=create_data, + headers={"Authorization": "Bearer test-token"} + ) as response: + if response.status == 200: + data = await response.json() + api_key = data['key'] + key_id = data['id'] + print(f"✅ API 密钥创建成功") + print(f" ID: {key_id}") + print(f" 名称: {data['name']}") + print(f" 密钥: {api_key[:8]}...{api_key[-8:]}") + + # 使用新创建的密钥测试日志端点 + print("\n 测试使用新密钥访问日志端点...") + async with session.get( + f"{BASE_URL}/api/logs", + headers={"Authorization": f"Bearer {api_key}"} + ) as log_response: + if log_response.status == 200: + logs = await log_response.json() + print(f" ✅ 日志访问成功,获取到 {len(logs)} 条日志") + else: + print(f" ❌ 日志访问失败: {log_response.status}") + + # 删除密钥 + async with session.delete( + f"{BASE_URL}/api/admin/api-keys/{key_id}", + headers={"Authorization": f"Bearer {api_key}"} + ) as delete_response: + if delete_response.status == 200: + print(f" ✅ API 密钥删除成功") + else: + print(f" ❌ API 密钥删除失败: {delete_response.status}") + else: + print(f"❌ API 密钥创建失败: {response.status}") + + print() + +async def test_project_mapping(): + """测试项目映射管理""" + print("🧪 测试项目映射管理") + print("-" * 50) + + mapping_data = { + "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"} + ] + } + + async with aiohttp.ClientSession() as session: + # 创建项目映射 + async with session.post( + f"{BASE_URL}/api/admin/projects", + json=mapping_data, + headers={"Authorization": "Bearer test-token"} + ) as response: + if response.status == 200: + data = await response.json() + print("✅ 项目映射创建成功") + print(f" ID: {data['id']}") + print(f" 仓库: {data['repository_name']}") + print(f" 默认任务: {data['default_job']}") + print(f" 分支任务数: {len(data['branch_jobs'])}") + print(f" 分支模式数: {len(data['branch_patterns'])}") + else: + print(f"❌ 项目映射创建失败: {response.status}") + + print() + +async def test_logs_and_stats(): + """测试日志和统计功能""" + print("🧪 测试日志和统计功能") + print("-" * 50) + + async with aiohttp.ClientSession() as session: + # 获取日志统计 + async with session.get( + f"{BASE_URL}/api/logs/stats", + headers={"Authorization": "Bearer test-token"} + ) as response: + if response.status == 200: + stats = await response.json() + print("✅ 日志统计获取成功") + print(f" 总日志数: {stats['total_logs']}") + print(f" 成功日志: {stats['successful_logs']}") + print(f" 失败日志: {stats['failed_logs']}") + print(f" 最近24小时: {stats['recent_logs_24h']}") + print(f" 仓库统计: {len(stats['repository_stats'])} 个仓库") + else: + print(f"❌ 日志统计获取失败: {response.status}") + + print() + +async def test_admin_stats(): + """测试管理统计""" + print("🧪 测试管理统计") + print("-" * 50) + + async with aiohttp.ClientSession() as session: + async with session.get( + f"{BASE_URL}/api/admin/stats", + headers={"Authorization": "Bearer test-token"} + ) as response: + if response.status == 200: + stats = await response.json() + print("✅ 管理统计获取成功") + print(f" API 密钥总数: {stats['api_keys']['total']}") + print(f" 活跃密钥: {stats['api_keys']['active']}") + print(f" 最近使用: {stats['api_keys']['recently_used']}") + print(f" 项目映射总数: {stats['project_mappings']['total']}") + else: + print(f"❌ 管理统计获取失败: {response.status}") + + print() + +async def main(): + """主测试函数""" + print("🚀 开始增强版 Gitea Webhook Ambassador 功能测试") + print("=" * 60) + print() + + try: + await test_health_check() + await test_webhook() + await test_api_key_management() + await test_project_mapping() + await test_logs_and_stats() + await test_admin_stats() + + print("=" * 60) + print("🎉 所有测试完成!") + print("✅ Python 版本现在具备了与 Go 版本相同的监测和管理功能") + + except Exception as e: + print(f"❌ 测试过程中出现错误: {str(e)}") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/test_enhanced_features.py b/apps/gitea-webhook-ambassador-python/test_enhanced_features.py new file mode 100644 index 00000000..8f4c818a --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/test_enhanced_features.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +""" +Gitea Webhook Ambassador 增强版功能测试脚本 +""" + +import requests +import json +import time + +BASE_URL = "http://localhost:8000" +ADMIN_SECRET_KEY = "admin-secret-key-change-in-production" + +def test_health_check(): + """测试健康检查""" + print("🔍 测试健康检查...") + try: + response = requests.get(f"{BASE_URL}/health") + if response.status_code == 200: + data = response.json() + print(f"✅ 健康检查成功: {data['status']}") + print(f" 版本: {data['version']}") + print(f" 运行时间: {data['uptime']}") + print(f" 内存使用: {data['memory']}") + return True + else: + print(f"❌ 健康检查失败: {response.status_code}") + return False + except Exception as e: + print(f"❌ 健康检查异常: {e}") + return False + +def test_login(): + """测试登录功能""" + print("\n🔐 测试登录功能...") + try: + # 测试登录 API + login_data = {"secret_key": ADMIN_SECRET_KEY} + response = requests.post( + f"{BASE_URL}/api/auth/login", + json=login_data, + headers={"Content-Type": "application/json"} + ) + + if response.status_code == 200: + data = response.json() + token = data.get("token") + print(f"✅ 登录成功,获得 JWT 令牌") + return token + else: + print(f"❌ 登录失败: {response.status_code} - {response.text}") + return None + except Exception as e: + print(f"❌ 登录异常: {e}") + return None + +def test_api_key_management(token): + """测试 API 密钥管理""" + print("\n🔑 测试 API 密钥管理...") + headers = {"Authorization": f"Bearer {token}"} + + try: + # 创建 API 密钥 + key_data = {"description": "测试 API 密钥"} + response = requests.post( + f"{BASE_URL}/api/keys", + json=key_data, + headers=headers + ) + + if response.status_code == 200: + data = response.json() + api_key = data["key"] + key_id = data["id"] + print(f"✅ 创建 API 密钥成功: {api_key[:20]}...") + + # 列出 API 密钥 + response = requests.get(f"{BASE_URL}/api/keys", headers=headers) + if response.status_code == 200: + keys_data = response.json() + print(f"✅ 列出 API 密钥成功,共 {len(keys_data['keys'])} 个") + + # 删除 API 密钥 + response = requests.delete(f"{BASE_URL}/api/keys/{key_id}", headers=headers) + if response.status_code == 200: + print(f"✅ 删除 API 密钥成功") + return True + else: + print(f"❌ 删除 API 密钥失败: {response.status_code}") + return False + else: + print(f"❌ 列出 API 密钥失败: {response.status_code}") + return False + else: + print(f"❌ 创建 API 密钥失败: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f"❌ API 密钥管理异常: {e}") + return False + +def test_project_management(token): + """测试项目管理""" + print("\n📁 测试项目管理...") + headers = {"Authorization": f"Bearer {token}"} + + try: + # 创建项目 + project_data = { + "name": "测试项目", + "jenkinsJob": "test-job", + "giteaRepo": "test-owner/test-repo" + } + response = requests.post( + f"{BASE_URL}/api/projects/", + json=project_data, + headers=headers + ) + + if response.status_code == 200: + data = response.json() + project_id = data["id"] + print(f"✅ 创建项目成功: {data['name']}") + + # 列出项目 + response = requests.get(f"{BASE_URL}/api/projects/", headers=headers) + if response.status_code == 200: + projects_data = response.json() + print(f"✅ 列出项目成功,共 {len(projects_data['projects'])} 个") + + # 删除项目 + response = requests.delete(f"{BASE_URL}/api/projects/{project_id}", headers=headers) + if response.status_code == 200: + print(f"✅ 删除项目成功") + return True + else: + print(f"❌ 删除项目失败: {response.status_code}") + return False + else: + print(f"❌ 列出项目失败: {response.status_code}") + return False + else: + print(f"❌ 创建项目失败: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f"❌ 项目管理异常: {e}") + return False + +def test_stats(token): + """测试统计信息""" + print("\n📊 测试统计信息...") + headers = {"Authorization": f"Bearer {token}"} + + try: + response = requests.get(f"{BASE_URL}/api/stats", headers=headers) + if response.status_code == 200: + data = response.json() + print(f"✅ 获取统计信息成功:") + print(f" 总项目数: {data['total_projects']}") + print(f" API 密钥数: {data['total_api_keys']}") + print(f" 今日触发次数: {data['today_triggers']}") + print(f" 成功触发次数: {data['successful_triggers']}") + return True + else: + print(f"❌ 获取统计信息失败: {response.status_code}") + return False + except Exception as e: + print(f"❌ 统计信息异常: {e}") + return False + +def test_logs(token): + """测试日志功能""" + print("\n📝 测试日志功能...") + headers = {"Authorization": f"Bearer {token}"} + + try: + response = requests.get(f"{BASE_URL}/api/logs", headers=headers) + if response.status_code == 200: + data = response.json() + print(f"✅ 获取日志成功,共 {len(data['logs'])} 条记录") + return True + else: + print(f"❌ 获取日志失败: {response.status_code}") + return False + except Exception as e: + print(f"❌ 日志功能异常: {e}") + return False + +def main(): + """主测试函数""" + print("🚀 Gitea Webhook Ambassador 增强版功能测试") + print("=" * 50) + + # 测试健康检查 + if not test_health_check(): + print("❌ 健康检查失败,服务可能未启动") + return + + # 测试登录 + token = test_login() + if not token: + print("❌ 登录失败,无法继续测试") + return + + # 测试各项功能 + test_api_key_management(token) + test_project_management(token) + test_stats(token) + test_logs(token) + + print("\n" + "=" * 50) + print("🎉 增强版功能测试完成!") + print("\n📋 已实现的功能:") + print(" ✅ Web 登录界面") + print(" ✅ 数据库存储 API 密钥") + print(" ✅ 延长 JWT 有效期 (7天)") + print(" ✅ 前端仪表板") + print(" ✅ 项目管理") + print(" ✅ API 密钥管理") + print(" ✅ 日志查看") + print(" ✅ 健康状态监控") + print("\n🌐 访问地址:") + print(f" 登录页面: {BASE_URL}/login") + print(f" 仪表板: {BASE_URL}/dashboard") + print(f" 管理员密钥: {ADMIN_SECRET_KEY}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/test_webhook.py b/apps/gitea-webhook-ambassador-python/test_webhook.py new file mode 100755 index 00000000..b24b7cf9 --- /dev/null +++ b/apps/gitea-webhook-ambassador-python/test_webhook.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +""" +Webhook 功能测试脚本 +用于验证 Gitea Webhook Ambassador 的各项功能 +""" + +import asyncio +import json +import httpx +import time +from datetime import datetime + +# 测试配置 +BASE_URL = "http://localhost:8000" +WEBHOOK_SECRET = "your-secret-key-here-make-it-long-and-random" + +# 测试数据 +TEST_WEBHOOK_DATA = { + "ref": "refs/heads/dev", + "before": "abc1234567890abcdef1234567890abcdef123456", + "after": "def1234567890abcdef1234567890abcdef123456", + "compare_url": "https://gitea.freeleaps.com/freeleaps/test-project/compare/abc123...def123", + "commits": [ + { + "id": "def1234567890abcdef1234567890abcdef123456", + "message": "feat: add new feature", + "url": "https://gitea.freeleaps.com/freeleaps/test-project/commit/def1234567890abcdef1234567890abcdef123456", + "author": { + "id": 1, + "login": "developer", + "full_name": "Test Developer", + "email": "dev@freeleaps.com" + } + } + ], + "repository": { + "id": 1, + "name": "test-project", + "owner": { + "id": 1, + "login": "freeleaps", + "full_name": "Freeleaps Team", + "email": "team@freeleaps.com" + }, + "full_name": "freeleaps/test-project", + "private": False, + "clone_url": "https://gitea.freeleaps.com/freeleaps/test-project.git", + "ssh_url": "git@gitea.freeleaps.com:freeleaps/test-project.git", + "html_url": "https://gitea.freeleaps.com/freeleaps/test-project", + "default_branch": "main" + }, + "pusher": { + "id": 1, + "login": "developer", + "full_name": "Test Developer", + "email": "dev@freeleaps.com" + } +} + + +async def test_health_check(): + """测试健康检查""" + print("🔍 测试健康检查...") + + async with httpx.AsyncClient() as client: + try: + response = await client.get(f"{BASE_URL}/health") + if response.status_code == 200: + data = response.json() + print(f"✅ 健康检查通过: {data['status']}") + return True + else: + print(f"❌ 健康检查失败: {response.status_code}") + return False + except Exception as e: + print(f"❌ 健康检查异常: {e}") + return False + + +async def test_queue_status(): + """测试队列状态""" + print("🔍 测试队列状态...") + + async with httpx.AsyncClient() as client: + try: + response = await client.get(f"{BASE_URL}/health/queue") + if response.status_code == 200: + data = response.json() + print(f"✅ 队列状态: {data['queue_stats']}") + return True + else: + print(f"❌ 队列状态检查失败: {response.status_code}") + return False + except Exception as e: + print(f"❌ 队列状态检查异常: {e}") + return False + + +async def test_webhook_endpoint(): + """测试 Webhook 端点""" + print("🔍 测试 Webhook 端点...") + + headers = { + "Content-Type": "application/json", + "X-Gitea-Signature": WEBHOOK_SECRET + } + + async with httpx.AsyncClient() as client: + try: + response = await client.post( + f"{BASE_URL}/webhook/gitea", + headers=headers, + json=TEST_WEBHOOK_DATA + ) + + print(f"📊 响应状态: {response.status_code}") + print(f"📊 响应内容: {response.text}") + + if response.status_code in [200, 202]: + print("✅ Webhook 端点测试通过") + return True + else: + print(f"❌ Webhook 端点测试失败: {response.status_code}") + return False + + except Exception as e: + print(f"❌ Webhook 端点测试异常: {e}") + return False + + +async def test_metrics_endpoint(): + """测试监控指标端点""" + print("🔍 测试监控指标端点...") + + async with httpx.AsyncClient() as client: + try: + response = await client.get(f"{BASE_URL}/metrics") + if response.status_code == 200: + print("✅ 监控指标端点测试通过") + # 打印一些关键指标 + content = response.text + for line in content.split('\n'): + if 'webhook_requests_total' in line or 'queue_size' in line: + print(f"📊 {line}") + return True + else: + print(f"❌ 监控指标端点测试失败: {response.status_code}") + return False + except Exception as e: + print(f"❌ 监控指标端点测试异常: {e}") + return False + + +async def test_deduplication(): + """测试防抖功能""" + print("🔍 测试防抖功能...") + + headers = { + "Content-Type": "application/json", + "X-Gitea-Signature": WEBHOOK_SECRET + } + + async with httpx.AsyncClient() as client: + try: + # 第一次请求 + print("📤 发送第一次请求...") + response1 = await client.post( + f"{BASE_URL}/webhook/gitea", + headers=headers, + json=TEST_WEBHOOK_DATA + ) + print(f"📊 第一次响应: {response1.status_code}") + + # 等待一秒 + await asyncio.sleep(1) + + # 第二次请求(相同数据,应该被防抖) + print("📤 发送第二次请求(相同数据)...") + response2 = await client.post( + f"{BASE_URL}/webhook/gitea", + headers=headers, + json=TEST_WEBHOOK_DATA + ) + print(f"📊 第二次响应: {response2.status_code}") + + # 修改提交哈希,发送第三次请求 + modified_data = TEST_WEBHOOK_DATA.copy() + modified_data["after"] = "ghi1234567890abcdef1234567890abcdef123456" + + print("📤 发送第三次请求(不同提交哈希)...") + response3 = await client.post( + f"{BASE_URL}/webhook/gitea", + headers=headers, + json=modified_data + ) + print(f"📊 第三次响应: {response3.status_code}") + + print("✅ 防抖功能测试完成") + return True + + except Exception as e: + print(f"❌ 防抖功能测试异常: {e}") + return False + + +async def test_invalid_webhook(): + """测试无效的 Webhook 请求""" + print("🔍 测试无效的 Webhook 请求...") + + async with httpx.AsyncClient() as client: + try: + # 测试缺少签名 + print("📤 测试缺少签名...") + response1 = await client.post( + f"{BASE_URL}/webhook/gitea", + headers={"Content-Type": "application/json"}, + json=TEST_WEBHOOK_DATA + ) + print(f"📊 缺少签名响应: {response1.status_code}") + + # 测试错误的签名 + print("📤 测试错误的签名...") + response2 = await client.post( + f"{BASE_URL}/webhook/gitea", + headers={ + "Content-Type": "application/json", + "X-Gitea-Signature": "wrong-secret" + }, + json=TEST_WEBHOOK_DATA + ) + print(f"📊 错误签名响应: {response2.status_code}") + + # 测试无效的 JSON + print("📤 测试无效的 JSON...") + response3 = await client.post( + f"{BASE_URL}/webhook/gitea", + headers={ + "Content-Type": "application/json", + "X-Gitea-Signature": WEBHOOK_SECRET + }, + content="invalid json" + ) + print(f"📊 无效 JSON 响应: {response3.status_code}") + + print("✅ 无效请求测试完成") + return True + + except Exception as e: + print(f"❌ 无效请求测试异常: {e}") + return False + + +async def main(): + """主测试函数""" + print("🚀 开始 Gitea Webhook Ambassador 功能测试") + print("=" * 50) + + tests = [ + ("健康检查", test_health_check), + ("队列状态", test_queue_status), + ("Webhook 端点", test_webhook_endpoint), + ("监控指标", test_metrics_endpoint), + ("防抖功能", test_deduplication), + ("无效请求", test_invalid_webhook), + ] + + results = [] + + for test_name, test_func in tests: + print(f"\n🧪 {test_name}") + print("-" * 30) + + try: + result = await test_func() + results.append((test_name, result)) + except Exception as e: + print(f"❌ {test_name} 测试异常: {e}") + results.append((test_name, False)) + + # 等待一下再进行下一个测试 + await asyncio.sleep(1) + + # 输出测试结果 + print("\n" + "=" * 50) + print("📊 测试结果汇总") + print("=" * 50) + + passed = 0 + total = len(results) + + for test_name, result in results: + status = "✅ 通过" if result else "❌ 失败" + print(f"{test_name}: {status}") + if result: + passed += 1 + + print(f"\n📈 总体结果: {passed}/{total} 测试通过") + + if passed == total: + print("🎉 所有测试通过!服务运行正常。") + else: + print("⚠️ 部分测试失败,请检查服务配置和日志。") + + +if __name__ == "__main__": + # 运行测试 + asyncio.run(main()) \ No newline at end of file From 233d88467b33497100eadce6f41670d935de8a47 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 21 Jul 2025 09:41:27 +0800 Subject: [PATCH 04/26] feat: add devops and reconciler services to CI/CD pipeline - Create freeleaps-service-hub directory structure with alpha, prod, helm-pkg - Create freeleaps-devops-reconciler directory structure with alpha, prod, helm-pkg - Add .gitkeep files in prod directories - Add Jenkinsfile for devops service in freeleaps-service-hub/alpha/ci/ - Add Jenkinsfile for reconciler service in freeleaps-devops-reconciler/alpha/ci/ - Remove old CI/CD configurations that were reverted to original state --- .../alpha/ci}/Jenkinsfile | 8 +- .../prod/.gitkeep | 0 freeleaps-service-hub/alpha/ci/Jenkinsfile | 140 ++++++++++++++++++ .../prod/.gitkeep | 0 .../ci/freeleaps2-reconciler/Jenkinsfile | 1 - .../prod/ci/freeleaps2-devops/Jenkinsfile | 35 ----- 6 files changed, 144 insertions(+), 40 deletions(-) rename {freeleaps/prod/ci/freeleaps2-reconciler => freeleaps-devops-reconciler/alpha/ci}/Jenkinsfile (89%) rename freeleaps/DEVOPS_RECONCILER_CICD.md => freeleaps-devops-reconciler/prod/.gitkeep (100%) create mode 100644 freeleaps-service-hub/alpha/ci/Jenkinsfile rename freeleaps/alpha/ci/freeleaps2-devops/Jenkinsfile => freeleaps-service-hub/prod/.gitkeep (100%) delete mode 100644 freeleaps/alpha/ci/freeleaps2-reconciler/Jenkinsfile delete mode 100644 freeleaps/prod/ci/freeleaps2-devops/Jenkinsfile diff --git a/freeleaps/prod/ci/freeleaps2-reconciler/Jenkinsfile b/freeleaps-devops-reconciler/alpha/ci/Jenkinsfile similarity index 89% rename from freeleaps/prod/ci/freeleaps2-reconciler/Jenkinsfile rename to freeleaps-devops-reconciler/alpha/ci/Jenkinsfile index 3564192a..b6f9677c 100644 --- a/freeleaps/prod/ci/freeleaps2-reconciler/Jenkinsfile +++ b/freeleaps-devops-reconciler/alpha/ci/Jenkinsfile @@ -2,9 +2,9 @@ library 'first-class-pipeline' executeFreeleapsPipeline { serviceName = 'freeleaps' - environmentSlug = 'prod' - serviceGitBranch = 'master' - serviceGitRepo = "https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps2-reconciler.git" + environmentSlug = 'alpha' + serviceGitBranch = 'dev' + serviceGitRepo = "https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps-devops-reconciler.git" serviceGitRepoType = 'monorepo' serviceGitCredentialsId = 'freeleaps-repos-gitea-credentails' executeMode = 'fully' @@ -12,7 +12,7 @@ executeFreeleapsPipeline { components = [ [ name: 'reconciler', - root: 'apps/reconciler', + root: '.', language: 'python', dependenciesManager: 'pip', requirementsFile: 'requirements.txt', diff --git a/freeleaps/DEVOPS_RECONCILER_CICD.md b/freeleaps-devops-reconciler/prod/.gitkeep similarity index 100% rename from freeleaps/DEVOPS_RECONCILER_CICD.md rename to freeleaps-devops-reconciler/prod/.gitkeep diff --git a/freeleaps-service-hub/alpha/ci/Jenkinsfile b/freeleaps-service-hub/alpha/ci/Jenkinsfile new file mode 100644 index 00000000..416c8e2e --- /dev/null +++ b/freeleaps-service-hub/alpha/ci/Jenkinsfile @@ -0,0 +1,140 @@ +library 'first-class-pipeline' + +executeFreeleapsPipeline { + serviceName = 'freeleaps' + environmentSlug = 'alpha' + serviceGitBranch = 'dev' + serviceGitRepo = "https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps-service-hub.git" + serviceGitRepoType = 'monorepo' + serviceGitCredentialsId = 'freeleaps-repos-gitea-credentails' + executeMode = 'fully' + commitMessageLintEnabled = false + components = [ + [ + name: 'authentication', + root: 'apps/authentication', + language: 'python', + dependenciesManager: 'pip', + requirementsFile: 'requirements.txt', + buildCacheEnabled: true, + buildAgentImage: 'python:3.10-slim-buster', + buildArtifacts: ['.'], + lintEnabled: false, + sastEnabled: false, + imageRegistry: 'docker.io', + imageRepository: 'freeleaps', + imageName: 'authentication', + imageBuilder: 'dind', + dockerfilePath: 'Dockerfile', + imageBuildRoot: '.', + imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'], + registryCredentialsId: 'freeleaps-devops-docker-hub-credentials', + semanticReleaseEnabled: true + ], + [ + name: 'centralStorage', + root: 'apps/central_storage', + language: 'python', + dependenciesManager: 'pip', + requirementsFile: 'requirements.txt', + buildAgentImage: 'python:3.10-slim-buster', + buildArtifacts: ['.'], + buildCacheEnabled: true, + lintEnabled: false, + sastEnabled: false, + imageRegistry: 'docker.io', + imageRepository: 'freeleaps', + imageName: 'central_storage', + imageBuilder: 'dind', + dockerfilePath: 'Dockerfile', + imageBuildRoot: '.', + imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'], + registryCredentialsId: 'freeleaps-devops-docker-hub-credentials', + semanticReleaseEnabled: true + ], + [ + name: 'content', + root: 'apps/content', + language: 'python', + dependenciesManager: 'pip', + requirementsFile: 'requirements.txt', + buildAgentImage: 'python:3.10-slim-buster', + buildArtifacts: ['.'], + buildCacheEnabled: true, + lintEnabled: false, + sastEnabled: false, + imageRegistry: 'docker.io', + imageRepository: 'freeleaps', + imageName: 'content', + imageBuilder: 'dind', + dockerfilePath: 'Dockerfile', + imageBuildRoot: '.', + imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'], + registryCredentialsId: 'freeleaps-devops-docker-hub-credentials', + semanticReleaseEnabled: true + ], + [ + name: 'notification', + root: 'apps/notification', + language: 'python', + dependenciesManager: 'pip', + requirementsFile: 'requirements.txt', + buildAgentImage: 'python:3.10-slim-buster', + buildArtifacts: ['.'], + buildCacheEnabled: true, + lintEnabled: false, + sastEnabled: false, + imageRegistry: 'docker.io', + imageRepository: 'freeleaps', + imageName: 'notification', + imageBuilder: 'dind', + dockerfilePath: 'Dockerfile', + imageBuildRoot: '.', + imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'], + registryCredentialsId: 'freeleaps-devops-docker-hub-credentials', + semanticReleaseEnabled: true + ], + [ + name: 'payment', + root: 'apps/payment', + language: 'python', + dependenciesManager: 'pip', + requirementsFile: 'requirements.txt', + buildAgentImage: 'python:3.10-slim-buster', + buildArtifacts: ['.'], + buildCacheEnabled: true, + lintEnabled: false, + sastEnabled: false, + imageRegistry: 'docker.io', + imageRepository: 'freeleaps', + imageName: 'payment', + imageBuilder: 'dind', + dockerfilePath: 'Dockerfile', + imageBuildRoot: '.', + imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'], + registryCredentialsId: 'freeleaps-devops-docker-hub-credentials', + semanticReleaseEnabled: true + ], + [ + name: 'devops', + root: 'apps/devops', + language: 'python', + dependenciesManager: 'pip', + requirementsFile: 'requirements.txt', + buildCacheEnabled: true, + buildAgentImage: 'python:3.12-slim', + buildArtifacts: ['.'], + lintEnabled: true, + sastEnabled: true, + imageRegistry: 'docker.io', + imageRepository: 'freeleaps', + imageName: 'devops', + imageBuilder: 'dind', + dockerfilePath: 'Dockerfile', + imageBuildRoot: '.', + imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'], + registryCredentialsId: 'freeleaps-devops-docker-hub-credentials', + semanticReleaseEnabled: true + ] + ] +} \ No newline at end of file diff --git a/freeleaps/alpha/ci/freeleaps2-devops/Jenkinsfile b/freeleaps-service-hub/prod/.gitkeep similarity index 100% rename from freeleaps/alpha/ci/freeleaps2-devops/Jenkinsfile rename to freeleaps-service-hub/prod/.gitkeep diff --git a/freeleaps/alpha/ci/freeleaps2-reconciler/Jenkinsfile b/freeleaps/alpha/ci/freeleaps2-reconciler/Jenkinsfile deleted file mode 100644 index 0519ecba..00000000 --- a/freeleaps/alpha/ci/freeleaps2-reconciler/Jenkinsfile +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/freeleaps/prod/ci/freeleaps2-devops/Jenkinsfile b/freeleaps/prod/ci/freeleaps2-devops/Jenkinsfile deleted file mode 100644 index 64e7f9d1..00000000 --- a/freeleaps/prod/ci/freeleaps2-devops/Jenkinsfile +++ /dev/null @@ -1,35 +0,0 @@ -library 'first-class-pipeline' - -executeFreeleapsPipeline { - serviceName = 'freeleaps' - environmentSlug = 'prod' - serviceGitBranch = 'master' - serviceGitRepo = "https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps2-devops.git" - serviceGitRepoType = 'monorepo' - serviceGitCredentialsId = 'freeleaps-repos-gitea-credentails' - executeMode = 'fully' - commitMessageLintEnabled = false - components = [ - [ - name: 'devops', - root: 'apps/devops', - language: 'python', - dependenciesManager: 'pip', - requirementsFile: 'requirements.txt', - buildCacheEnabled: true, - buildAgentImage: 'python:3.12-slim', - buildArtifacts: ['.'], - lintEnabled: true, - sastEnabled: true, - imageRegistry: 'docker.io', - imageRepository: 'freeleaps', - imageName: 'devops', - imageBuilder: 'dind', - dockerfilePath: 'Dockerfile', - imageBuildRoot: '.', - imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'], - registryCredentialsId: 'freeleaps-devops-docker-hub-credentials', - semanticReleaseEnabled: true - ] - ] -} \ No newline at end of file From 5b93048cb3953aa2ce088cf1c4887c8751ae039a Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 21 Jul 2025 09:45:15 +0800 Subject: [PATCH 05/26] fix: update Jenkinsfile configurations for devops and reconciler services - Update freeleaps-devops-reconciler Jenkinsfile: change reconciler root path from '.' to 'apps/reconciler' - Ensure freeleaps-service-hub Jenkinsfile includes devops component with correct configuration - Both services now have proper CI/CD pipeline configurations for alpha environment --- freeleaps-devops-reconciler/alpha/ci/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freeleaps-devops-reconciler/alpha/ci/Jenkinsfile b/freeleaps-devops-reconciler/alpha/ci/Jenkinsfile index b6f9677c..a0cd64a7 100644 --- a/freeleaps-devops-reconciler/alpha/ci/Jenkinsfile +++ b/freeleaps-devops-reconciler/alpha/ci/Jenkinsfile @@ -12,7 +12,7 @@ executeFreeleapsPipeline { components = [ [ name: 'reconciler', - root: '.', + root: 'apps/reconciler', language: 'python', dependenciesManager: 'pip', requirementsFile: 'requirements.txt', From d03c1193227ca370ef228e05f14cc0bc80667f26 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 21 Jul 2025 15:33:35 +0800 Subject: [PATCH 06/26] chore: remove all Chinese text, full English internationalization for gitea-webhook-ambassador-python --- .../app/auth/middleware.py | 18 +-- .../app/handlers/auth.py | 18 +-- .../app/handlers/health.py | 32 ++-- .../app/handlers/logs.py | 18 +-- .../app/handlers/projects.py | 18 +-- .../app/main.py | 83 +++++------ .../app/main_enhanced.py | 98 ++++++------ .../app/static/js/dashboard.js | 100 ++++++------- .../app/templates/dashboard.html | 100 ++++++------- .../app/templates/login.html | 41 +++-- freeleaps-service-hub/alpha/ci/Jenkinsfile | 140 ------------------ freeleaps-service-hub/prod/.gitkeep | 1 - .../ci/freeleaps-service-hub/Jenkinsfile | 21 +++ freeleaps/helm-pkg/devops/Chart.yaml | 6 + .../templates/authentication/certificate.yaml | 27 ++++ .../templates/authentication/deployment.yaml | 118 +++++++++++++++ .../authentication/devops-config.yaml | 28 ++++ .../templates/authentication/ingress.yaml | 36 +++++ .../templates/authentication/service.yaml | 26 ++++ .../authentication/servicemonitor.yaml | 40 +++++ .../devops/templates/authentication/vpa.yaml | 32 ++++ freeleaps/helm-pkg/devops/values.alpha.yaml | 86 +++++++++++ freeleaps/helm-pkg/devops/values.prod.yaml | 89 +++++++++++ freeleaps/helm-pkg/devops/values.yaml | 86 +++++++++++ 24 files changed, 857 insertions(+), 405 deletions(-) delete mode 100644 freeleaps-service-hub/alpha/ci/Jenkinsfile delete mode 100644 freeleaps-service-hub/prod/.gitkeep create mode 100644 freeleaps/helm-pkg/devops/Chart.yaml create mode 100644 freeleaps/helm-pkg/devops/templates/authentication/certificate.yaml create mode 100644 freeleaps/helm-pkg/devops/templates/authentication/deployment.yaml create mode 100644 freeleaps/helm-pkg/devops/templates/authentication/devops-config.yaml create mode 100644 freeleaps/helm-pkg/devops/templates/authentication/ingress.yaml create mode 100644 freeleaps/helm-pkg/devops/templates/authentication/service.yaml create mode 100644 freeleaps/helm-pkg/devops/templates/authentication/servicemonitor.yaml create mode 100644 freeleaps/helm-pkg/devops/templates/authentication/vpa.yaml create mode 100644 freeleaps/helm-pkg/devops/values.alpha.yaml create mode 100644 freeleaps/helm-pkg/devops/values.prod.yaml create mode 100644 freeleaps/helm-pkg/devops/values.yaml diff --git a/apps/gitea-webhook-ambassador-python/app/auth/middleware.py b/apps/gitea-webhook-ambassador-python/app/auth/middleware.py index d1ff1e40..a90bb40a 100644 --- a/apps/gitea-webhook-ambassador-python/app/auth/middleware.py +++ b/apps/gitea-webhook-ambassador-python/app/auth/middleware.py @@ -11,10 +11,10 @@ from typing import Optional from ..models.database import get_db, APIKey from ..config import settings -# JWT 配置 +# JWT configuration JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production") JWT_ALGORITHM = "HS256" -JWT_EXPIRATION_HOURS = 24 * 7 # 7 天有效期 +JWT_EXPIRATION_HOURS = 24 * 7 # 7 days expiration security = HTTPBearer() @@ -49,25 +49,25 @@ class AuthMiddleware: ) def verify_api_key(self, api_key: str, db: Session): - """验证 API 密钥""" + """Validate API key""" db_key = db.query(APIKey).filter(APIKey.key == api_key).first() return db_key is not None def generate_api_key(self) -> str: - """生成新的 API 密钥""" + """Generate a new API key""" return secrets.token_urlsafe(32) -# 创建认证中间件实例 +# Create authentication middleware instance auth_middleware = AuthMiddleware() async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): - """获取当前用户(JWT 认证)""" + """Get current user (JWT authentication)""" token = credentials.credentials payload = auth_middleware.verify_token(token) return payload async def get_current_user_api_key(api_key: str = Depends(security), db: Session = Depends(get_db)): - """获取当前用户(API 密钥认证)""" + """Get current user (API key authentication)""" if not auth_middleware.verify_api_key(api_key.credentials, db): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -76,14 +76,14 @@ async def get_current_user_api_key(api_key: str = Depends(security), db: Session return {"api_key": api_key.credentials} def require_auth(use_api_key: bool = False): - """认证依赖装饰器""" + """Authentication dependency decorator""" if use_api_key: return get_current_user_api_key else: return get_current_user def handle_auth_error(request, exc): - """处理认证错误""" + """Handle authentication error""" if request.headers.get("x-requested-with") == "XMLHttpRequest": return JSONResponse( status_code=401, diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/auth.py b/apps/gitea-webhook-ambassador-python/app/handlers/auth.py index a418a288..073ba6a2 100644 --- a/apps/gitea-webhook-ambassador-python/app/handlers/auth.py +++ b/apps/gitea-webhook-ambassador-python/app/handlers/auth.py @@ -10,7 +10,7 @@ from ..auth.middleware import auth_middleware router = APIRouter(prefix="/api/auth", tags=["authentication"]) -# 请求/响应模型 +# Request/Response models class LoginRequest(BaseModel): secret_key: str @@ -32,13 +32,13 @@ class APIKeyResponse(BaseModel): class APIKeyList(BaseModel): keys: List[APIKeyResponse] -# 获取管理员密钥 +# Get admin secret key def get_admin_secret_key(): return os.getenv("ADMIN_SECRET_KEY", "admin-secret-key-change-in-production") @router.post("/login", response_model=LoginResponse) async def login(request: LoginRequest): - """管理员登录""" + """Admin login""" admin_key = get_admin_secret_key() if request.secret_key != admin_key: @@ -47,7 +47,7 @@ async def login(request: LoginRequest): detail="Invalid secret key" ) - # 生成 JWT 令牌 + # Generate JWT token token = auth_middleware.create_access_token( data={"sub": "admin", "role": "admin"} ) @@ -60,11 +60,11 @@ async def create_api_key( db: Session = Depends(get_db), current_user: dict = Depends(auth_middleware.get_current_user) ): - """创建新的 API 密钥""" - # 生成新的 API 密钥 + """Create a new API key""" + # Generate new API key api_key_value = auth_middleware.generate_api_key() - # 保存到数据库 + # Save to database db_key = APIKey( key=api_key_value, description=request.description @@ -86,7 +86,7 @@ async def list_api_keys( db: Session = Depends(get_db), current_user: dict = Depends(auth_middleware.get_current_user) ): - """获取所有 API 密钥""" + """Get all API keys""" keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all() return APIKeyList( @@ -107,7 +107,7 @@ async def delete_api_key( db: Session = Depends(get_db), current_user: dict = Depends(auth_middleware.get_current_user) ): - """删除 API 密钥""" + """Delete API key""" key = db.query(APIKey).filter(APIKey.id == key_id).first() if not key: diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/health.py b/apps/gitea-webhook-ambassador-python/app/handlers/health.py index 26b34be5..a0726a10 100644 --- a/apps/gitea-webhook-ambassador-python/app/handlers/health.py +++ b/apps/gitea-webhook-ambassador-python/app/handlers/health.py @@ -1,6 +1,6 @@ """ -健康检查处理器 -提供服务健康状态检查 +Health check handler +Provides service health status checking """ from datetime import datetime @@ -42,12 +42,12 @@ class HealthResponse(BaseModel): @router.get("/", response_model=HealthResponse) async def health_check(db: Session = Depends(get_db)): """ - 健康检查端点 - 检查服务各个组件的状态 + Health check endpoint + Check the status of each service component """ settings = get_settings() - # 检查 Jenkins 连接 + # Check Jenkins connection jenkins_service = get_jenkins_service() jenkins_status = JenkinsStatus(status="disconnected", message="Unable to connect to Jenkins server") @@ -57,7 +57,7 @@ async def health_check(db: Session = Depends(get_db)): except Exception as e: jenkins_status.message = f"Connection failed: {str(e)}" - # 获取工作池统计 + # Get worker pool stats queue_service = get_queue_service() try: stats = await queue_service.get_stats() @@ -75,7 +75,7 @@ async def health_check(db: Session = Depends(get_db)): total_failed=0 ) - # 检查数据库连接 + # Check database connection database_status = {"status": "disconnected", "message": "Database connection failed"} try: # 尝试执行简单查询 @@ -84,7 +84,7 @@ async def health_check(db: Session = Depends(get_db)): except Exception as e: database_status["message"] = f"Database error: {str(e)}" - # 确定整体状态 + # Determine overall status overall_status = "healthy" if jenkins_status.status != "connected": overall_status = "unhealthy" @@ -103,8 +103,8 @@ async def health_check(db: Session = Depends(get_db)): @router.get("/simple") async def simple_health_check(): """ - 简单健康检查端点 - 用于负载均衡器和监控系统 + Simple health check endpoint + For load balancers and monitoring systems """ return { "status": "healthy", @@ -116,14 +116,14 @@ async def simple_health_check(): @router.get("/ready") async def readiness_check(db: Session = Depends(get_db)): """ - 就绪检查端点 - 检查服务是否准备好接收请求 + Readiness check endpoint + Check if the service is ready to receive requests """ try: - # 检查数据库连接 + # Check database connection db.execute("SELECT 1") - # 检查 Jenkins 连接 + # Check Jenkins connection jenkins_service = get_jenkins_service() jenkins_ready = await jenkins_service.test_connection() @@ -139,7 +139,7 @@ async def readiness_check(db: Session = Depends(get_db)): @router.get("/live") async def liveness_check(): """ - 存活检查端点 - 检查服务进程是否正常运行 + Liveness check endpoint + Check if the service process is running normally """ return {"status": "alive"} \ No newline at end of file diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/logs.py b/apps/gitea-webhook-ambassador-python/app/handlers/logs.py index de150a54..d4942355 100644 --- a/apps/gitea-webhook-ambassador-python/app/handlers/logs.py +++ b/apps/gitea-webhook-ambassador-python/app/handlers/logs.py @@ -35,13 +35,13 @@ async def get_trigger_logs( current_user: dict = Depends(get_current_user) ): """ - 获取触发日志 + Get trigger logs """ try: - # 构建查询 + # Build query query = db.query(TriggerLog) - # 应用过滤器 + # Apply filters if repository: query = query.filter(TriggerLog.repository_name == repository) if branch: @@ -56,7 +56,7 @@ async def get_trigger_logs( detail="Invalid since parameter format (use RFC3339)" ) - # 按时间倒序排列并限制数量 + # Order by time desc and limit logs = query.order_by(TriggerLog.created_at.desc()).limit(limit).all() return logs @@ -71,21 +71,21 @@ async def get_log_stats( current_user: dict = Depends(get_current_user) ): """ - 获取日志统计信息 + Get log statistics """ try: - # 总日志数 + # Total logs total_logs = db.query(TriggerLog).count() - # 成功和失败的日志数 + # Successful and failed logs successful_logs = db.query(TriggerLog).filter(TriggerLog.status == "success").count() failed_logs = db.query(TriggerLog).filter(TriggerLog.status == "failed").count() - # 最近24小时的日志数 + # Logs in the last 24 hours yesterday = datetime.utcnow() - timedelta(days=1) recent_logs = db.query(TriggerLog).filter(TriggerLog.created_at >= yesterday).count() - # 按仓库分组的统计 + # Stats by repository repo_stats = db.query( TriggerLog.repository_name, db.func.count(TriggerLog.id).label('count') diff --git a/apps/gitea-webhook-ambassador-python/app/handlers/projects.py b/apps/gitea-webhook-ambassador-python/app/handlers/projects.py index a70c0bf3..95693b0d 100644 --- a/apps/gitea-webhook-ambassador-python/app/handlers/projects.py +++ b/apps/gitea-webhook-ambassador-python/app/handlers/projects.py @@ -8,7 +8,7 @@ from ..auth.middleware import auth_middleware router = APIRouter(prefix="/api/projects", tags=["projects"]) -# 请求/响应模型 +# Request/Response models class ProjectCreate(BaseModel): name: str jenkinsJob: str @@ -33,8 +33,8 @@ async def create_project( db: Session = Depends(get_db), current_user: dict = Depends(auth_middleware.get_current_user) ): - """创建新项目映射""" - # 检查项目是否已存在 + """Create new project mapping""" + # Check if project already exists existing_project = db.query(ProjectMapping).filter( ProjectMapping.repository_name == request.giteaRepo ).first() @@ -45,7 +45,7 @@ async def create_project( detail="Project with this repository already exists" ) - # 创建新项目 + # Create new project project = ProjectMapping( repository_name=request.giteaRepo, default_job=request.jenkinsJob @@ -68,14 +68,14 @@ async def list_projects( db: Session = Depends(get_db), current_user: dict = Depends(auth_middleware.get_current_user) ): - """获取所有项目""" + """Get all projects""" projects = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all() return ProjectList( projects=[ ProjectResponse( id=project.id, - name=project.repository_name.split('/')[-1], # 使用仓库名作为项目名 + name=project.repository_name.split('/')[-1], # Use repo name as project name jenkinsJob=project.default_job, giteaRepo=project.repository_name, created_at=project.created_at.isoformat() @@ -90,7 +90,7 @@ async def get_project( db: Session = Depends(get_db), current_user: dict = Depends(auth_middleware.get_current_user) ): - """获取特定项目""" + """Get specific project""" project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first() if not project: @@ -113,7 +113,7 @@ async def delete_project( db: Session = Depends(get_db), current_user: dict = Depends(auth_middleware.get_current_user) ): - """删除项目""" + """Delete project""" project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first() if not project: @@ -132,7 +132,7 @@ async def get_project_mapping( repository_name: str, db: Session = Depends(get_db) ): - """根据仓库名获取项目映射(用于 webhook 处理)""" + """Get project mapping by repository name (for webhook processing)""" project = db.query(ProjectMapping).filter( ProjectMapping.repository_name == repository_name ).first() diff --git a/apps/gitea-webhook-ambassador-python/app/main.py b/apps/gitea-webhook-ambassador-python/app/main.py index 1da7f833..ff039b99 100644 --- a/apps/gitea-webhook-ambassador-python/app/main.py +++ b/apps/gitea-webhook-ambassador-python/app/main.py @@ -1,6 +1,6 @@ """ -FastAPI 应用主入口 -集成 Webhook 处理、防抖、队列管理等服务 +Main entry for FastAPI application +Integrates webhook handling, deduplication, queue management, and related services """ import asyncio @@ -18,9 +18,9 @@ from app.services.dedup_service import DeduplicationService from app.services.jenkins_service import JenkinsService from app.services.webhook_service import WebhookService from app.tasks.jenkins_tasks import get_celery_app -# 路由导入将在运行时动态处理 +# Route imports will be dynamically handled at runtime -# 配置结构化日志 +# Configure structured logging structlog.configure( processors=[ structlog.stdlib.filter_by_level, @@ -41,7 +41,7 @@ structlog.configure( logger = structlog.get_logger() -# 监控指标 +# Monitoring metrics WEBHOOK_REQUESTS_TOTAL = Counter( "webhook_requests_total", "Total number of webhook requests", @@ -65,7 +65,7 @@ DEDUP_HITS = Counter( "Total number of deduplication hits" ) -# 全局服务实例 +# Global service instances dedup_service: DeduplicationService = None jenkins_service: JenkinsService = None webhook_service: WebhookService = None @@ -75,14 +75,14 @@ redis_client: aioredis.Redis = None @asynccontextmanager async def lifespan(app: FastAPI): - """应用生命周期管理""" + """Application lifecycle management""" global dedup_service, jenkins_service, webhook_service, celery_app, redis_client - # 启动时初始化 + # Initialize on startup logger.info("Starting Gitea Webhook Ambassador") try: - # 初始化 Redis 连接 + # Initialize Redis connection settings = get_settings() redis_client = aioredis.from_url( settings.redis.url, @@ -92,14 +92,14 @@ async def lifespan(app: FastAPI): decode_responses=True ) - # 测试 Redis 连接 + # Test Redis connection await redis_client.ping() logger.info("Redis connection established") - # 初始化 Celery + # Initialize Celery celery_app = get_celery_app() - # 初始化服务 + # Initialize services dedup_service = DeduplicationService(redis_client) jenkins_service = JenkinsService() webhook_service = WebhookService( @@ -117,7 +117,7 @@ async def lifespan(app: FastAPI): raise finally: - # 关闭时清理 + # Cleanup on shutdown logger.info("Shutting down Gitea Webhook Ambassador") if redis_client: @@ -125,25 +125,25 @@ async def lifespan(app: FastAPI): logger.info("Redis connection closed") -# 创建 FastAPI 应用 +# Create FastAPI application app = FastAPI( title="Gitea Webhook Ambassador", - description="高性能的 Gitea 到 Jenkins 的 Webhook 服务", + description="High-performance Gitea to Jenkins Webhook service", version="1.0.0", lifespan=lifespan ) -# 添加 CORS 中间件 +# Add CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=["*"], # 生产环境应该限制具体域名 + allow_origins=["*"], # In production, restrict to specific domains allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) -# 依赖注入 +# Dependency injection def get_dedup_service() -> DeduplicationService: if dedup_service is None: raise HTTPException(status_code=503, detail="Deduplication service not available") @@ -162,13 +162,13 @@ def get_celery_app_dep(): return celery_app -# 中间件 +# Middleware @app.middleware("http") async def log_requests(request: Request, call_next): - """请求日志中间件""" + """Request logging middleware""" start_time = asyncio.get_event_loop().time() - # 记录请求开始 + # Log request start logger.info("Request started", method=request.method, url=str(request.url), @@ -177,7 +177,7 @@ async def log_requests(request: Request, call_next): try: response = await call_next(request) - # 记录请求完成 + # Log request complete process_time = asyncio.get_event_loop().time() - start_time logger.info("Request completed", method=request.method, @@ -188,7 +188,7 @@ async def log_requests(request: Request, call_next): return response except Exception as e: - # 记录请求错误 + # Log request error process_time = asyncio.get_event_loop().time() - start_time logger.error("Request failed", method=request.method, @@ -200,10 +200,10 @@ async def log_requests(request: Request, call_next): @app.middleware("http") async def add_security_headers(request: Request, call_next): - """添加安全头""" + """Add security headers""" response = await call_next(request) - # 添加安全相关的 HTTP 头 + # Add security-related HTTP headers response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["X-XSS-Protection"] = "1; mode=block" @@ -212,10 +212,10 @@ async def add_security_headers(request: Request, call_next): return response -# 异常处理器 +# Exception handler @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): - """全局异常处理器""" + """Global exception handler""" logger.error("Unhandled exception", method=request.method, url=str(request.url), @@ -232,19 +232,19 @@ async def global_exception_handler(request: Request, exc: Exception): ) -# 健康检查端点 +# Health check endpoint @app.get("/health") async def health_check(): - """基础健康检查""" + """Basic health check""" try: - # 检查 Redis 连接 + # Check Redis connection if redis_client: await redis_client.ping() redis_healthy = True else: redis_healthy = False - # 检查 Celery 连接 + # Check Celery connection if celery_app: inspect = celery_app.control.inspect() celery_healthy = bool(inspect.active() is not None) @@ -273,7 +273,7 @@ async def health_check(): @app.get("/health/queue") async def queue_health_check(): - """队列健康检查""" + """Queue health check""" try: if celery_app is None: return JSONResponse( @@ -283,7 +283,7 @@ async def queue_health_check(): inspect = celery_app.control.inspect() - # 获取队列统计 + # Get queue stats active = inspect.active() reserved = inspect.reserved() registered = inspect.registered() @@ -292,7 +292,7 @@ async def queue_health_check(): reserved_count = sum(len(tasks) for tasks in (reserved or {}).values()) worker_count = len(registered or {}) - # 更新监控指标 + # Update monitoring metrics QUEUE_SIZE.labels(queue_type="active").set(active_count) QUEUE_SIZE.labels(queue_type="reserved").set(reserved_count) @@ -317,17 +317,17 @@ async def queue_health_check(): ) -# 监控指标端点 +# Metrics endpoint @app.get("/metrics") async def metrics(): - """Prometheus 监控指标""" + """Prometheus metrics endpoint""" return Response( content=generate_latest(), media_type=CONTENT_TYPE_LATEST ) -# 包含路由模块 +# Include route modules try: from app.handlers import webhook, health, admin @@ -349,18 +349,17 @@ try: tags=["admin"] ) except ImportError as e: - # 如果模块不存在,记录警告但不中断应用启动 + # If module does not exist, log warning but do not interrupt app startup logger.warning(f"Some handlers not available: {e}") - -# 根路径 +# Root path @app.get("/") async def root(): - """根路径""" + """Root path""" return { "name": "Gitea Webhook Ambassador", "version": "1.0.0", - "description": "高性能的 Gitea 到 Jenkins 的 Webhook 服务", + "description": "High-performance Gitea to Jenkins Webhook service", "endpoints": { "webhook": "/webhook/gitea", "health": "/health", diff --git a/apps/gitea-webhook-ambassador-python/app/main_enhanced.py b/apps/gitea-webhook-ambassador-python/app/main_enhanced.py index 13a1c0aa..860df862 100644 --- a/apps/gitea-webhook-ambassador-python/app/main_enhanced.py +++ b/apps/gitea-webhook-ambassador-python/app/main_enhanced.py @@ -9,19 +9,19 @@ import time import psutil from datetime import datetime, timedelta -# 导入数据库模型 +# Import database models from app.models.database import create_tables, get_db, APIKey, ProjectMapping, TriggerLog from app.auth.middleware import auth_middleware, get_current_user from app.config import settings -# 创建 FastAPI 应用 +# Create FastAPI app app = FastAPI( title="Gitea Webhook Ambassador", - description="高性能的 Gitea 到 Jenkins 的 Webhook 服务", + description="High-performance Gitea to Jenkins Webhook service", version="2.0.0" ) -# 添加 CORS 中间件 +# Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -30,36 +30,36 @@ app.add_middleware( allow_headers=["*"], ) -# 创建数据库表 +# Create database tables create_tables() -# 挂载静态文件 +# Mount static files app.mount("/static", StaticFiles(directory="app/static"), name="static") -# 设置模板 +# Set up templates templates = Jinja2Templates(directory="app/templates") -# 启动时间 +# Startup time start_time = datetime.now() @app.get("/", response_class=HTMLResponse) async def root(request: Request): - """根路径 - 重定向到登录页""" + """Root path - redirect to login page""" return RedirectResponse(url="/login") @app.get("/login", response_class=HTMLResponse) async def login_page(request: Request): - """登录页面""" + """Login page""" return templates.TemplateResponse("login.html", {"request": request}) @app.get("/dashboard", response_class=HTMLResponse) async def dashboard_page(request: Request): - """仪表板页面""" + """Dashboard page""" return templates.TemplateResponse("dashboard.html", {"request": request}) @app.post("/api/auth/login") async def login(request: dict): - """管理员登录""" + """Admin login""" admin_key = os.getenv("ADMIN_SECRET_KEY", "admin-secret-key-change-in-production") if request.get("secret_key") != admin_key: @@ -68,7 +68,7 @@ async def login(request: dict): detail="Invalid secret key" ) - # 生成 JWT 令牌 + # Generate JWT token token = auth_middleware.create_access_token( data={"sub": "admin", "role": "admin"} ) @@ -77,21 +77,21 @@ async def login(request: dict): @app.get("/api/stats") async def get_stats(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)): - """获取统计信息""" + """Get statistics""" try: - # 获取项目总数 + # Get total number of projects total_projects = db.query(ProjectMapping).count() - # 获取 API 密钥总数 + # Get total number of API keys total_api_keys = db.query(APIKey).count() - # 获取今日触发次数 + # Get today's trigger count today = datetime.now().date() today_triggers = db.query(TriggerLog).filter( TriggerLog.created_at >= today ).count() - # 获取成功触发次数 + # Get successful trigger count successful_triggers = db.query(TriggerLog).filter( TriggerLog.status == "success" ).count() @@ -103,11 +103,11 @@ async def get_stats(db: Session = Depends(get_db), current_user: dict = Depends( "successful_triggers": successful_triggers } except Exception as e: - raise HTTPException(status_code=500, detail=f"获取统计信息失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to get statistics: {str(e)}") @app.get("/api/keys", response_model=dict) async def list_api_keys(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)): - """获取所有 API 密钥(兼容前端)""" + """Get all API keys (frontend compatible)""" try: keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all() return { @@ -122,7 +122,7 @@ async def list_api_keys(db: Session = Depends(get_db), current_user: dict = Depe ] } except Exception as e: - raise HTTPException(status_code=500, detail=f"获取 API 密钥失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to get API keys: {str(e)}") @app.post("/api/keys", response_model=dict) async def create_api_key( @@ -130,12 +130,12 @@ async def create_api_key( db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): - """创建新的 API 密钥(兼容前端)""" + """Create a new API key (frontend compatible)""" try: - # 生成新的 API 密钥 + # Generate new API key api_key_value = auth_middleware.generate_api_key() - # 保存到数据库 + # Save to database db_key = APIKey( key=api_key_value, description=request.get("description", "") @@ -152,7 +152,7 @@ async def create_api_key( "created_at": db_key.created_at.isoformat() } except Exception as e: - raise HTTPException(status_code=500, detail=f"创建 API 密钥失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to create API key: {str(e)}") @app.delete("/api/keys/{key_id}") async def delete_api_key( @@ -160,25 +160,25 @@ async def delete_api_key( db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): - """删除 API 密钥(兼容前端)""" + """Delete API key (frontend compatible)""" try: key = db.query(APIKey).filter(APIKey.id == key_id).first() if not key: - raise HTTPException(status_code=404, detail="API 密钥不存在") + raise HTTPException(status_code=404, detail="API key does not exist") db.delete(key) db.commit() - return {"message": "API 密钥删除成功"} + return {"message": "API key deleted successfully"} except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=f"删除 API 密钥失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to delete API key: {str(e)}") @app.get("/api/projects/", response_model=dict) async def list_projects(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)): - """获取所有项目(兼容前端)""" + """Get all projects (frontend compatible)""" try: projects = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all() return { @@ -194,7 +194,7 @@ async def list_projects(db: Session = Depends(get_db), current_user: dict = Depe ] } except Exception as e: - raise HTTPException(status_code=500, detail=f"获取项目列表失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to get project list: {str(e)}") @app.post("/api/projects/", response_model=dict) async def create_project( @@ -202,17 +202,17 @@ async def create_project( db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): - """创建新项目(兼容前端)""" + """Create a new project (frontend compatible)""" try: - # 检查项目是否已存在 + # Check if project already exists existing_project = db.query(ProjectMapping).filter( ProjectMapping.repository_name == request["giteaRepo"] ).first() if existing_project: - raise HTTPException(status_code=400, detail="项目已存在") + raise HTTPException(status_code=400, detail="Project already exists") - # 创建新项目 + # Create new project project = ProjectMapping( repository_name=request["giteaRepo"], default_job=request["jenkinsJob"] @@ -232,7 +232,7 @@ async def create_project( except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=f"创建项目失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to create project: {str(e)}") @app.delete("/api/projects/{project_id}") async def delete_project( @@ -240,31 +240,31 @@ async def delete_project( db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): - """删除项目(兼容前端)""" + """Delete project (frontend compatible)""" try: project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first() if not project: - raise HTTPException(status_code=404, detail="项目不存在") + raise HTTPException(status_code=404, detail="Project does not exist") db.delete(project) db.commit() - return {"message": "项目删除成功"} + return {"message": "Project deleted successfully"} except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=f"删除项目失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to delete project: {str(e)}") @app.get("/health") async def health_check(): - """健康检查端点""" + """Health check endpoint""" try: - # 计算运行时间 + # Calculate uptime uptime = datetime.now() - start_time - uptime_str = str(uptime).split('.')[0] # 移除微秒 + uptime_str = str(uptime).split('.')[0] # Remove microseconds - # 获取内存使用情况 + # Get memory usage process = psutil.Process() memory_info = process.memory_info() memory_mb = memory_info.rss / 1024 / 1024 @@ -292,21 +292,21 @@ async def get_logs( db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): - """获取日志(简化版本)""" + """Get logs (simplified version)""" try: - # 这里应该实现真正的日志查询逻辑 - # 目前返回模拟数据 + # Here should be the real log query logic + # Currently returns mock data logs = [ { "timestamp": datetime.now().isoformat(), "level": "info", - "message": "系统运行正常" + "message": "System running normally" } ] return {"logs": logs} except Exception as e: - raise HTTPException(status_code=500, detail=f"获取日志失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to get logs: {str(e)}") if __name__ == "__main__": import uvicorn diff --git a/apps/gitea-webhook-ambassador-python/app/static/js/dashboard.js b/apps/gitea-webhook-ambassador-python/app/static/js/dashboard.js index 3d0d85e4..98724a6f 100644 --- a/apps/gitea-webhook-ambassador-python/app/static/js/dashboard.js +++ b/apps/gitea-webhook-ambassador-python/app/static/js/dashboard.js @@ -1,17 +1,17 @@ -// 全局变量存储 JWT 令牌 +// Global variable to store JWT token let authToken = localStorage.getItem('auth_token'); $(document).ready(function() { - // 检查认证状态 + // Check authentication status if (!authToken) { window.location.href = '/login'; return; } - // 设置 AJAX 默认配置 + // Set AJAX default config $.ajaxSetup({ beforeSend: function(xhr, settings) { - // 不为登录请求添加认证头 + // Do not add auth header for login request if (settings.url === '/api/auth/login') { return; } @@ -20,7 +20,7 @@ $(document).ready(function() { } }, error: function(xhr, status, error) { - // 如果收到 401,重定向到登录页 + // If 401 received, redirect to login page if (xhr.status === 401) { localStorage.removeItem('auth_token'); window.location.href = '/login'; @@ -30,10 +30,10 @@ $(document).ready(function() { } }); - // 初始化工具提示 + // Initialize tooltips $('[data-bs-toggle="tooltip"]').tooltip(); - // 加载初始数据 + // Load initial data loadProjects(); loadAPIKeys(); loadLogs(); @@ -41,10 +41,10 @@ $(document).ready(function() { loadHealthDetails(); loadStatsDetails(); - // 设置定期健康检查 + // Set periodic health check setInterval(checkHealth, 30000); - // 项目管理 + // Project management $('#addProjectForm').on('submit', function(e) { e.preventDefault(); const projectData = { @@ -62,13 +62,13 @@ $(document).ready(function() { $('#addProjectModal').modal('hide'); $('#addProjectForm')[0].reset(); loadProjects(); - showSuccess('项目添加成功'); + showSuccess('Project added successfully'); }, error: handleAjaxError }); }); - // API 密钥管理 + // API key management $('#generateKeyForm').on('submit', function(e) { e.preventDefault(); $.ajax({ @@ -80,16 +80,16 @@ $(document).ready(function() { $('#generateKeyModal').modal('hide'); $('#generateKeyForm')[0].reset(); loadAPIKeys(); - showSuccess('API 密钥生成成功'); + showSuccess('API key generated successfully'); - // 显示新生成的密钥 + // Show newly generated key showApiKeyModal(response.key); }, error: handleAjaxError }); }); - // 日志查询 + // Log query $('#logQueryForm').on('submit', function(e) { e.preventDefault(); loadLogs({ @@ -100,7 +100,7 @@ $(document).ready(function() { }); }); - // 标签页切换 + // Tab switching $('.nav-link').on('click', function() { $('.nav-link').removeClass('active'); $(this).addClass('active'); @@ -121,7 +121,7 @@ function loadProjects() { ${escapeHtml(project.giteaRepo)} @@ -140,12 +140,12 @@ function loadAPIKeys() { data.keys.forEach(function(key) { tbody.append(` - ${escapeHtml(key.description || '无描述')} + ${escapeHtml(key.description || 'No description')} ${escapeHtml(key.key)} ${new Date(key.created_at).toLocaleString('zh-CN')} @@ -178,7 +178,7 @@ function loadLogs(query = {}) { `); }); } else { - logContainer.append('
暂无日志记录
'); + logContainer.append('
No log records
'); } }) .fail(handleAjaxError); @@ -190,12 +190,12 @@ function checkHealth() { const indicator = $('.health-indicator'); indicator.removeClass('healthy unhealthy') .addClass(data.status === 'healthy' ? 'healthy' : 'unhealthy'); - $('#healthStatus').text(data.status === 'healthy' ? '健康' : '异常'); + $('#healthStatus').text(data.status === 'healthy' ? 'Healthy' : 'Unhealthy'); }) .fail(function() { const indicator = $('.health-indicator'); indicator.removeClass('healthy').addClass('unhealthy'); - $('#healthStatus').text('异常'); + $('#healthStatus').text('Unhealthy'); }); } @@ -205,24 +205,24 @@ function loadHealthDetails() { const healthDetails = $('#healthDetails'); healthDetails.html(`
- 状态: + Status: - ${data.status === 'healthy' ? '健康' : '异常'} + ${data.status === 'healthy' ? 'Healthy' : 'Unhealthy'}
- 版本: ${data.version || '未知'} + Version: ${data.version || 'Unknown'}
- 启动时间: ${data.uptime || '未知'} + Uptime: ${data.uptime || 'Unknown'}
- 内存使用: ${data.memory || '未知'} + Memory Usage: ${data.memory || 'Unknown'}
`); }) .fail(function() { - $('#healthDetails').html('
无法获取健康状态
'); + $('#healthDetails').html('
Unable to get health status
'); }); } @@ -232,75 +232,75 @@ function loadStatsDetails() { const statsDetails = $('#statsDetails'); statsDetails.html(`
- 总项目数: ${data.total_projects || 0} + Total Projects: ${data.total_projects || 0}
- API 密钥数: ${data.total_api_keys || 0} + API Keys: ${data.total_api_keys || 0}
- 今日触发次数: ${data.today_triggers || 0} + Today's Triggers: ${data.today_triggers || 0}
- 成功触发次数: ${data.successful_triggers || 0} + Successful Triggers: ${data.successful_triggers || 0}
`); }) .fail(function() { - $('#statsDetails').html('
无法获取统计信息
'); + $('#statsDetails').html('
Unable to get statistics
'); }); } function deleteProject(id) { - if (!confirm('确定要删除这个项目吗?')) return; + if (!confirm('Are you sure you want to delete this project?')) return; $.ajax({ url: `/api/projects/${id}`, method: 'DELETE', success: function() { loadProjects(); - showSuccess('项目删除成功'); + showSuccess('Project deleted successfully'); }, error: handleAjaxError }); } function revokeKey(id) { - if (!confirm('确定要撤销这个 API 密钥吗?')) return; + if (!confirm('Are you sure you want to revoke this API key?')) return; $.ajax({ url: `/api/keys/${id}`, method: 'DELETE', success: function() { loadAPIKeys(); - showSuccess('API 密钥撤销成功'); + showSuccess('API key revoked successfully'); }, error: handleAjaxError }); } function showApiKeyModal(key) { - // 创建模态框显示新生成的密钥 + // Create modal to show newly generated key const modal = $(`