chore: remove all Chinese text, full English internationalization for gitea-webhook-ambassador-python

This commit is contained in:
Nicolas 2025-07-21 15:33:35 +08:00
parent 5b93048cb3
commit d03c119322
24 changed files with 857 additions and 405 deletions

View File

@ -11,10 +11,10 @@ from typing import Optional
from ..models.database import get_db, APIKey from ..models.database import get_db, APIKey
from ..config import settings from ..config import settings
# JWT 配置 # JWT configuration
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production") JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
JWT_ALGORITHM = "HS256" JWT_ALGORITHM = "HS256"
JWT_EXPIRATION_HOURS = 24 * 7 # 7 天有效期 JWT_EXPIRATION_HOURS = 24 * 7 # 7 days expiration
security = HTTPBearer() security = HTTPBearer()
@ -49,25 +49,25 @@ class AuthMiddleware:
) )
def verify_api_key(self, api_key: str, db: Session): 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() db_key = db.query(APIKey).filter(APIKey.key == api_key).first()
return db_key is not None return db_key is not None
def generate_api_key(self) -> str: def generate_api_key(self) -> str:
"""生成新的 API 密钥""" """Generate a new API key"""
return secrets.token_urlsafe(32) return secrets.token_urlsafe(32)
# 创建认证中间件实例 # Create authentication middleware instance
auth_middleware = AuthMiddleware() auth_middleware = AuthMiddleware()
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""获取当前用户JWT 认证)""" """Get current user (JWT authentication)"""
token = credentials.credentials token = credentials.credentials
payload = auth_middleware.verify_token(token) payload = auth_middleware.verify_token(token)
return payload return payload
async def get_current_user_api_key(api_key: str = Depends(security), db: Session = Depends(get_db)): 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): if not auth_middleware.verify_api_key(api_key.credentials, db):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, 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} return {"api_key": api_key.credentials}
def require_auth(use_api_key: bool = False): def require_auth(use_api_key: bool = False):
"""认证依赖装饰器""" """Authentication dependency decorator"""
if use_api_key: if use_api_key:
return get_current_user_api_key return get_current_user_api_key
else: else:
return get_current_user return get_current_user
def handle_auth_error(request, exc): def handle_auth_error(request, exc):
"""处理认证错误""" """Handle authentication error"""
if request.headers.get("x-requested-with") == "XMLHttpRequest": if request.headers.get("x-requested-with") == "XMLHttpRequest":
return JSONResponse( return JSONResponse(
status_code=401, status_code=401,

View File

@ -10,7 +10,7 @@ from ..auth.middleware import auth_middleware
router = APIRouter(prefix="/api/auth", tags=["authentication"]) router = APIRouter(prefix="/api/auth", tags=["authentication"])
# 请求/响应模型 # Request/Response models
class LoginRequest(BaseModel): class LoginRequest(BaseModel):
secret_key: str secret_key: str
@ -32,13 +32,13 @@ class APIKeyResponse(BaseModel):
class APIKeyList(BaseModel): class APIKeyList(BaseModel):
keys: List[APIKeyResponse] keys: List[APIKeyResponse]
# 获取管理员密钥 # Get admin secret key
def get_admin_secret_key(): def get_admin_secret_key():
return os.getenv("ADMIN_SECRET_KEY", "admin-secret-key-change-in-production") return os.getenv("ADMIN_SECRET_KEY", "admin-secret-key-change-in-production")
@router.post("/login", response_model=LoginResponse) @router.post("/login", response_model=LoginResponse)
async def login(request: LoginRequest): async def login(request: LoginRequest):
"""管理员登录""" """Admin login"""
admin_key = get_admin_secret_key() admin_key = get_admin_secret_key()
if request.secret_key != admin_key: if request.secret_key != admin_key:
@ -47,7 +47,7 @@ async def login(request: LoginRequest):
detail="Invalid secret key" detail="Invalid secret key"
) )
# 生成 JWT 令牌 # Generate JWT token
token = auth_middleware.create_access_token( token = auth_middleware.create_access_token(
data={"sub": "admin", "role": "admin"} data={"sub": "admin", "role": "admin"}
) )
@ -60,11 +60,11 @@ async def create_api_key(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(auth_middleware.get_current_user) current_user: dict = Depends(auth_middleware.get_current_user)
): ):
"""创建新的 API 密钥""" """Create a new API key"""
# 生成新的 API 密钥 # Generate new API key
api_key_value = auth_middleware.generate_api_key() api_key_value = auth_middleware.generate_api_key()
# 保存到数据库 # Save to database
db_key = APIKey( db_key = APIKey(
key=api_key_value, key=api_key_value,
description=request.description description=request.description
@ -86,7 +86,7 @@ async def list_api_keys(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(auth_middleware.get_current_user) 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() keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all()
return APIKeyList( return APIKeyList(
@ -107,7 +107,7 @@ async def delete_api_key(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(auth_middleware.get_current_user) current_user: dict = Depends(auth_middleware.get_current_user)
): ):
"""删除 API 密钥""" """Delete API key"""
key = db.query(APIKey).filter(APIKey.id == key_id).first() key = db.query(APIKey).filter(APIKey.id == key_id).first()
if not key: if not key:

View File

@ -1,6 +1,6 @@
""" """
健康检查处理器 Health check handler
提供服务健康状态检查 Provides service health status checking
""" """
from datetime import datetime from datetime import datetime
@ -42,12 +42,12 @@ class HealthResponse(BaseModel):
@router.get("/", response_model=HealthResponse) @router.get("/", response_model=HealthResponse)
async def health_check(db: Session = Depends(get_db)): async def health_check(db: Session = Depends(get_db)):
""" """
健康检查端点 Health check endpoint
检查服务各个组件的状态 Check the status of each service component
""" """
settings = get_settings() settings = get_settings()
# 检查 Jenkins 连接 # Check Jenkins connection
jenkins_service = get_jenkins_service() jenkins_service = get_jenkins_service()
jenkins_status = JenkinsStatus(status="disconnected", message="Unable to connect to Jenkins server") 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: except Exception as e:
jenkins_status.message = f"Connection failed: {str(e)}" jenkins_status.message = f"Connection failed: {str(e)}"
# 获取工作池统计 # Get worker pool stats
queue_service = get_queue_service() queue_service = get_queue_service()
try: try:
stats = await queue_service.get_stats() stats = await queue_service.get_stats()
@ -75,7 +75,7 @@ async def health_check(db: Session = Depends(get_db)):
total_failed=0 total_failed=0
) )
# 检查数据库连接 # Check database connection
database_status = {"status": "disconnected", "message": "Database connection failed"} database_status = {"status": "disconnected", "message": "Database connection failed"}
try: try:
# 尝试执行简单查询 # 尝试执行简单查询
@ -84,7 +84,7 @@ async def health_check(db: Session = Depends(get_db)):
except Exception as e: except Exception as e:
database_status["message"] = f"Database error: {str(e)}" database_status["message"] = f"Database error: {str(e)}"
# 确定整体状态 # Determine overall status
overall_status = "healthy" overall_status = "healthy"
if jenkins_status.status != "connected": if jenkins_status.status != "connected":
overall_status = "unhealthy" overall_status = "unhealthy"
@ -103,8 +103,8 @@ async def health_check(db: Session = Depends(get_db)):
@router.get("/simple") @router.get("/simple")
async def simple_health_check(): async def simple_health_check():
""" """
简单健康检查端点 Simple health check endpoint
用于负载均衡器和监控系统 For load balancers and monitoring systems
""" """
return { return {
"status": "healthy", "status": "healthy",
@ -116,14 +116,14 @@ async def simple_health_check():
@router.get("/ready") @router.get("/ready")
async def readiness_check(db: Session = Depends(get_db)): async def readiness_check(db: Session = Depends(get_db)):
""" """
就绪检查端点 Readiness check endpoint
检查服务是否准备好接收请求 Check if the service is ready to receive requests
""" """
try: try:
# 检查数据库连接 # Check database connection
db.execute("SELECT 1") db.execute("SELECT 1")
# 检查 Jenkins 连接 # Check Jenkins connection
jenkins_service = get_jenkins_service() jenkins_service = get_jenkins_service()
jenkins_ready = await jenkins_service.test_connection() jenkins_ready = await jenkins_service.test_connection()
@ -139,7 +139,7 @@ async def readiness_check(db: Session = Depends(get_db)):
@router.get("/live") @router.get("/live")
async def liveness_check(): async def liveness_check():
""" """
存活检查端点 Liveness check endpoint
检查服务进程是否正常运行 Check if the service process is running normally
""" """
return {"status": "alive"} return {"status": "alive"}

View File

@ -35,13 +35,13 @@ async def get_trigger_logs(
current_user: dict = Depends(get_current_user) current_user: dict = Depends(get_current_user)
): ):
""" """
获取触发日志 Get trigger logs
""" """
try: try:
# 构建查询 # Build query
query = db.query(TriggerLog) query = db.query(TriggerLog)
# 应用过滤器 # Apply filters
if repository: if repository:
query = query.filter(TriggerLog.repository_name == repository) query = query.filter(TriggerLog.repository_name == repository)
if branch: if branch:
@ -56,7 +56,7 @@ async def get_trigger_logs(
detail="Invalid since parameter format (use RFC3339)" detail="Invalid since parameter format (use RFC3339)"
) )
# 按时间倒序排列并限制数量 # Order by time desc and limit
logs = query.order_by(TriggerLog.created_at.desc()).limit(limit).all() logs = query.order_by(TriggerLog.created_at.desc()).limit(limit).all()
return logs return logs
@ -71,21 +71,21 @@ async def get_log_stats(
current_user: dict = Depends(get_current_user) current_user: dict = Depends(get_current_user)
): ):
""" """
获取日志统计信息 Get log statistics
""" """
try: try:
# 总日志数 # Total logs
total_logs = db.query(TriggerLog).count() total_logs = db.query(TriggerLog).count()
# 成功和失败的日志数 # Successful and failed logs
successful_logs = db.query(TriggerLog).filter(TriggerLog.status == "success").count() successful_logs = db.query(TriggerLog).filter(TriggerLog.status == "success").count()
failed_logs = db.query(TriggerLog).filter(TriggerLog.status == "failed").count() failed_logs = db.query(TriggerLog).filter(TriggerLog.status == "failed").count()
# 最近24小时的日志数 # Logs in the last 24 hours
yesterday = datetime.utcnow() - timedelta(days=1) yesterday = datetime.utcnow() - timedelta(days=1)
recent_logs = db.query(TriggerLog).filter(TriggerLog.created_at >= yesterday).count() recent_logs = db.query(TriggerLog).filter(TriggerLog.created_at >= yesterday).count()
# 按仓库分组的统计 # Stats by repository
repo_stats = db.query( repo_stats = db.query(
TriggerLog.repository_name, TriggerLog.repository_name,
db.func.count(TriggerLog.id).label('count') db.func.count(TriggerLog.id).label('count')

View File

@ -8,7 +8,7 @@ from ..auth.middleware import auth_middleware
router = APIRouter(prefix="/api/projects", tags=["projects"]) router = APIRouter(prefix="/api/projects", tags=["projects"])
# 请求/响应模型 # Request/Response models
class ProjectCreate(BaseModel): class ProjectCreate(BaseModel):
name: str name: str
jenkinsJob: str jenkinsJob: str
@ -33,8 +33,8 @@ async def create_project(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(auth_middleware.get_current_user) current_user: dict = Depends(auth_middleware.get_current_user)
): ):
"""创建新项目映射""" """Create new project mapping"""
# 检查项目是否已存在 # Check if project already exists
existing_project = db.query(ProjectMapping).filter( existing_project = db.query(ProjectMapping).filter(
ProjectMapping.repository_name == request.giteaRepo ProjectMapping.repository_name == request.giteaRepo
).first() ).first()
@ -45,7 +45,7 @@ async def create_project(
detail="Project with this repository already exists" detail="Project with this repository already exists"
) )
# 创建新项目 # Create new project
project = ProjectMapping( project = ProjectMapping(
repository_name=request.giteaRepo, repository_name=request.giteaRepo,
default_job=request.jenkinsJob default_job=request.jenkinsJob
@ -68,14 +68,14 @@ async def list_projects(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(auth_middleware.get_current_user) current_user: dict = Depends(auth_middleware.get_current_user)
): ):
"""获取所有项目""" """Get all projects"""
projects = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all() projects = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all()
return ProjectList( return ProjectList(
projects=[ projects=[
ProjectResponse( ProjectResponse(
id=project.id, 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, jenkinsJob=project.default_job,
giteaRepo=project.repository_name, giteaRepo=project.repository_name,
created_at=project.created_at.isoformat() created_at=project.created_at.isoformat()
@ -90,7 +90,7 @@ async def get_project(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(auth_middleware.get_current_user) current_user: dict = Depends(auth_middleware.get_current_user)
): ):
"""获取特定项目""" """Get specific project"""
project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first() project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first()
if not project: if not project:
@ -113,7 +113,7 @@ async def delete_project(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(auth_middleware.get_current_user) current_user: dict = Depends(auth_middleware.get_current_user)
): ):
"""删除项目""" """Delete project"""
project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first() project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first()
if not project: if not project:
@ -132,7 +132,7 @@ async def get_project_mapping(
repository_name: str, repository_name: str,
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""根据仓库名获取项目映射(用于 webhook 处理)""" """Get project mapping by repository name (for webhook processing)"""
project = db.query(ProjectMapping).filter( project = db.query(ProjectMapping).filter(
ProjectMapping.repository_name == repository_name ProjectMapping.repository_name == repository_name
).first() ).first()

View File

@ -1,6 +1,6 @@
""" """
FastAPI 应用主入口 Main entry for FastAPI application
集成 Webhook 处理防抖队列管理等服务 Integrates webhook handling, deduplication, queue management, and related services
""" """
import asyncio import asyncio
@ -18,9 +18,9 @@ from app.services.dedup_service import DeduplicationService
from app.services.jenkins_service import JenkinsService from app.services.jenkins_service import JenkinsService
from app.services.webhook_service import WebhookService from app.services.webhook_service import WebhookService
from app.tasks.jenkins_tasks import get_celery_app from app.tasks.jenkins_tasks import get_celery_app
# 路由导入将在运行时动态处理 # Route imports will be dynamically handled at runtime
# 配置结构化日志 # Configure structured logging
structlog.configure( structlog.configure(
processors=[ processors=[
structlog.stdlib.filter_by_level, structlog.stdlib.filter_by_level,
@ -41,7 +41,7 @@ structlog.configure(
logger = structlog.get_logger() logger = structlog.get_logger()
# 监控指标 # Monitoring metrics
WEBHOOK_REQUESTS_TOTAL = Counter( WEBHOOK_REQUESTS_TOTAL = Counter(
"webhook_requests_total", "webhook_requests_total",
"Total number of webhook requests", "Total number of webhook requests",
@ -65,7 +65,7 @@ DEDUP_HITS = Counter(
"Total number of deduplication hits" "Total number of deduplication hits"
) )
# 全局服务实例 # Global service instances
dedup_service: DeduplicationService = None dedup_service: DeduplicationService = None
jenkins_service: JenkinsService = None jenkins_service: JenkinsService = None
webhook_service: WebhookService = None webhook_service: WebhookService = None
@ -75,14 +75,14 @@ redis_client: aioredis.Redis = None
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""应用生命周期管理""" """Application lifecycle management"""
global dedup_service, jenkins_service, webhook_service, celery_app, redis_client global dedup_service, jenkins_service, webhook_service, celery_app, redis_client
# 启动时初始化 # Initialize on startup
logger.info("Starting Gitea Webhook Ambassador") logger.info("Starting Gitea Webhook Ambassador")
try: try:
# 初始化 Redis 连接 # Initialize Redis connection
settings = get_settings() settings = get_settings()
redis_client = aioredis.from_url( redis_client = aioredis.from_url(
settings.redis.url, settings.redis.url,
@ -92,14 +92,14 @@ async def lifespan(app: FastAPI):
decode_responses=True decode_responses=True
) )
# 测试 Redis 连接 # Test Redis connection
await redis_client.ping() await redis_client.ping()
logger.info("Redis connection established") logger.info("Redis connection established")
# 初始化 Celery # Initialize Celery
celery_app = get_celery_app() celery_app = get_celery_app()
# 初始化服务 # Initialize services
dedup_service = DeduplicationService(redis_client) dedup_service = DeduplicationService(redis_client)
jenkins_service = JenkinsService() jenkins_service = JenkinsService()
webhook_service = WebhookService( webhook_service = WebhookService(
@ -117,7 +117,7 @@ async def lifespan(app: FastAPI):
raise raise
finally: finally:
# 关闭时清理 # Cleanup on shutdown
logger.info("Shutting down Gitea Webhook Ambassador") logger.info("Shutting down Gitea Webhook Ambassador")
if redis_client: if redis_client:
@ -125,25 +125,25 @@ async def lifespan(app: FastAPI):
logger.info("Redis connection closed") logger.info("Redis connection closed")
# 创建 FastAPI 应用 # Create FastAPI application
app = FastAPI( app = FastAPI(
title="Gitea Webhook Ambassador", title="Gitea Webhook Ambassador",
description="高性能的 Gitea 到 Jenkins 的 Webhook 服务", description="High-performance Gitea to Jenkins Webhook service",
version="1.0.0", version="1.0.0",
lifespan=lifespan lifespan=lifespan
) )
# 添加 CORS 中间件 # Add CORS middleware
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], # 生产环境应该限制具体域名 allow_origins=["*"], # In production, restrict to specific domains
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
# 依赖注入 # Dependency injection
def get_dedup_service() -> DeduplicationService: def get_dedup_service() -> DeduplicationService:
if dedup_service is None: if dedup_service is None:
raise HTTPException(status_code=503, detail="Deduplication service not available") raise HTTPException(status_code=503, detail="Deduplication service not available")
@ -162,13 +162,13 @@ def get_celery_app_dep():
return celery_app return celery_app
# 中间件 # Middleware
@app.middleware("http") @app.middleware("http")
async def log_requests(request: Request, call_next): async def log_requests(request: Request, call_next):
"""请求日志中间件""" """Request logging middleware"""
start_time = asyncio.get_event_loop().time() start_time = asyncio.get_event_loop().time()
# 记录请求开始 # Log request start
logger.info("Request started", logger.info("Request started",
method=request.method, method=request.method,
url=str(request.url), url=str(request.url),
@ -177,7 +177,7 @@ async def log_requests(request: Request, call_next):
try: try:
response = await call_next(request) response = await call_next(request)
# 记录请求完成 # Log request complete
process_time = asyncio.get_event_loop().time() - start_time process_time = asyncio.get_event_loop().time() - start_time
logger.info("Request completed", logger.info("Request completed",
method=request.method, method=request.method,
@ -188,7 +188,7 @@ async def log_requests(request: Request, call_next):
return response return response
except Exception as e: except Exception as e:
# 记录请求错误 # Log request error
process_time = asyncio.get_event_loop().time() - start_time process_time = asyncio.get_event_loop().time() - start_time
logger.error("Request failed", logger.error("Request failed",
method=request.method, method=request.method,
@ -200,10 +200,10 @@ async def log_requests(request: Request, call_next):
@app.middleware("http") @app.middleware("http")
async def add_security_headers(request: Request, call_next): async def add_security_headers(request: Request, call_next):
"""添加安全头""" """Add security headers"""
response = await call_next(request) response = await call_next(request)
# 添加安全相关的 HTTP 头 # Add security-related HTTP headers
response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY" response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block" response.headers["X-XSS-Protection"] = "1; mode=block"
@ -212,10 +212,10 @@ async def add_security_headers(request: Request, call_next):
return response return response
# 异常处理器 # Exception handler
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception): async def global_exception_handler(request: Request, exc: Exception):
"""全局异常处理器""" """Global exception handler"""
logger.error("Unhandled exception", logger.error("Unhandled exception",
method=request.method, method=request.method,
url=str(request.url), url=str(request.url),
@ -232,19 +232,19 @@ async def global_exception_handler(request: Request, exc: Exception):
) )
# 健康检查端点 # Health check endpoint
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
"""基础健康检查""" """Basic health check"""
try: try:
# 检查 Redis 连接 # Check Redis connection
if redis_client: if redis_client:
await redis_client.ping() await redis_client.ping()
redis_healthy = True redis_healthy = True
else: else:
redis_healthy = False redis_healthy = False
# 检查 Celery 连接 # Check Celery connection
if celery_app: if celery_app:
inspect = celery_app.control.inspect() inspect = celery_app.control.inspect()
celery_healthy = bool(inspect.active() is not None) celery_healthy = bool(inspect.active() is not None)
@ -273,7 +273,7 @@ async def health_check():
@app.get("/health/queue") @app.get("/health/queue")
async def queue_health_check(): async def queue_health_check():
"""队列健康检查""" """Queue health check"""
try: try:
if celery_app is None: if celery_app is None:
return JSONResponse( return JSONResponse(
@ -283,7 +283,7 @@ async def queue_health_check():
inspect = celery_app.control.inspect() inspect = celery_app.control.inspect()
# 获取队列统计 # Get queue stats
active = inspect.active() active = inspect.active()
reserved = inspect.reserved() reserved = inspect.reserved()
registered = inspect.registered() registered = inspect.registered()
@ -292,7 +292,7 @@ async def queue_health_check():
reserved_count = sum(len(tasks) for tasks in (reserved or {}).values()) reserved_count = sum(len(tasks) for tasks in (reserved or {}).values())
worker_count = len(registered or {}) worker_count = len(registered or {})
# 更新监控指标 # Update monitoring metrics
QUEUE_SIZE.labels(queue_type="active").set(active_count) QUEUE_SIZE.labels(queue_type="active").set(active_count)
QUEUE_SIZE.labels(queue_type="reserved").set(reserved_count) QUEUE_SIZE.labels(queue_type="reserved").set(reserved_count)
@ -317,17 +317,17 @@ async def queue_health_check():
) )
# 监控指标端点 # Metrics endpoint
@app.get("/metrics") @app.get("/metrics")
async def metrics(): async def metrics():
"""Prometheus 监控指标""" """Prometheus metrics endpoint"""
return Response( return Response(
content=generate_latest(), content=generate_latest(),
media_type=CONTENT_TYPE_LATEST media_type=CONTENT_TYPE_LATEST
) )
# 包含路由模块 # Include route modules
try: try:
from app.handlers import webhook, health, admin from app.handlers import webhook, health, admin
@ -349,18 +349,17 @@ try:
tags=["admin"] tags=["admin"]
) )
except ImportError as e: 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}") logger.warning(f"Some handlers not available: {e}")
# Root path
# 根路径
@app.get("/") @app.get("/")
async def root(): async def root():
"""根路径""" """Root path"""
return { return {
"name": "Gitea Webhook Ambassador", "name": "Gitea Webhook Ambassador",
"version": "1.0.0", "version": "1.0.0",
"description": "高性能的 Gitea 到 Jenkins 的 Webhook 服务", "description": "High-performance Gitea to Jenkins Webhook service",
"endpoints": { "endpoints": {
"webhook": "/webhook/gitea", "webhook": "/webhook/gitea",
"health": "/health", "health": "/health",

View File

@ -9,19 +9,19 @@ import time
import psutil import psutil
from datetime import datetime, timedelta from datetime import datetime, timedelta
# 导入数据库模型 # Import database models
from app.models.database import create_tables, get_db, APIKey, ProjectMapping, TriggerLog from app.models.database import create_tables, get_db, APIKey, ProjectMapping, TriggerLog
from app.auth.middleware import auth_middleware, get_current_user from app.auth.middleware import auth_middleware, get_current_user
from app.config import settings from app.config import settings
# 创建 FastAPI 应用 # Create FastAPI app
app = FastAPI( app = FastAPI(
title="Gitea Webhook Ambassador", title="Gitea Webhook Ambassador",
description="高性能的 Gitea 到 Jenkins 的 Webhook 服务", description="High-performance Gitea to Jenkins Webhook service",
version="2.0.0" version="2.0.0"
) )
# 添加 CORS 中间件 # Add CORS middleware
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
@ -30,36 +30,36 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# 创建数据库表 # Create database tables
create_tables() create_tables()
# 挂载静态文件 # Mount static files
app.mount("/static", StaticFiles(directory="app/static"), name="static") app.mount("/static", StaticFiles(directory="app/static"), name="static")
# 设置模板 # Set up templates
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
# 启动时间 # Startup time
start_time = datetime.now() start_time = datetime.now()
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def root(request: Request): async def root(request: Request):
"""根路径 - 重定向到登录页""" """Root path - redirect to login page"""
return RedirectResponse(url="/login") return RedirectResponse(url="/login")
@app.get("/login", response_class=HTMLResponse) @app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request): async def login_page(request: Request):
"""登录页面""" """Login page"""
return templates.TemplateResponse("login.html", {"request": request}) return templates.TemplateResponse("login.html", {"request": request})
@app.get("/dashboard", response_class=HTMLResponse) @app.get("/dashboard", response_class=HTMLResponse)
async def dashboard_page(request: Request): async def dashboard_page(request: Request):
"""仪表板页面""" """Dashboard page"""
return templates.TemplateResponse("dashboard.html", {"request": request}) return templates.TemplateResponse("dashboard.html", {"request": request})
@app.post("/api/auth/login") @app.post("/api/auth/login")
async def login(request: dict): async def login(request: dict):
"""管理员登录""" """Admin login"""
admin_key = os.getenv("ADMIN_SECRET_KEY", "admin-secret-key-change-in-production") admin_key = os.getenv("ADMIN_SECRET_KEY", "admin-secret-key-change-in-production")
if request.get("secret_key") != admin_key: if request.get("secret_key") != admin_key:
@ -68,7 +68,7 @@ async def login(request: dict):
detail="Invalid secret key" detail="Invalid secret key"
) )
# 生成 JWT 令牌 # Generate JWT token
token = auth_middleware.create_access_token( token = auth_middleware.create_access_token(
data={"sub": "admin", "role": "admin"} data={"sub": "admin", "role": "admin"}
) )
@ -77,21 +77,21 @@ async def login(request: dict):
@app.get("/api/stats") @app.get("/api/stats")
async def get_stats(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)): async def get_stats(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)):
"""获取统计信息""" """Get statistics"""
try: try:
# 获取项目总数 # Get total number of projects
total_projects = db.query(ProjectMapping).count() total_projects = db.query(ProjectMapping).count()
# 获取 API 密钥总数 # Get total number of API keys
total_api_keys = db.query(APIKey).count() total_api_keys = db.query(APIKey).count()
# 获取今日触发次数 # Get today's trigger count
today = datetime.now().date() today = datetime.now().date()
today_triggers = db.query(TriggerLog).filter( today_triggers = db.query(TriggerLog).filter(
TriggerLog.created_at >= today TriggerLog.created_at >= today
).count() ).count()
# 获取成功触发次数 # Get successful trigger count
successful_triggers = db.query(TriggerLog).filter( successful_triggers = db.query(TriggerLog).filter(
TriggerLog.status == "success" TriggerLog.status == "success"
).count() ).count()
@ -103,11 +103,11 @@ async def get_stats(db: Session = Depends(get_db), current_user: dict = Depends(
"successful_triggers": successful_triggers "successful_triggers": successful_triggers
} }
except Exception as e: 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) @app.get("/api/keys", response_model=dict)
async def list_api_keys(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)): 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: try:
keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all() keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all()
return { return {
@ -122,7 +122,7 @@ async def list_api_keys(db: Session = Depends(get_db), current_user: dict = Depe
] ]
} }
except Exception as e: 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) @app.post("/api/keys", response_model=dict)
async def create_api_key( async def create_api_key(
@ -130,12 +130,12 @@ async def create_api_key(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user) current_user: dict = Depends(get_current_user)
): ):
"""创建新的 API 密钥(兼容前端)""" """Create a new API key (frontend compatible)"""
try: try:
# 生成新的 API 密钥 # Generate new API key
api_key_value = auth_middleware.generate_api_key() api_key_value = auth_middleware.generate_api_key()
# 保存到数据库 # Save to database
db_key = APIKey( db_key = APIKey(
key=api_key_value, key=api_key_value,
description=request.get("description", "") description=request.get("description", "")
@ -152,7 +152,7 @@ async def create_api_key(
"created_at": db_key.created_at.isoformat() "created_at": db_key.created_at.isoformat()
} }
except Exception as e: 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}") @app.delete("/api/keys/{key_id}")
async def delete_api_key( async def delete_api_key(
@ -160,25 +160,25 @@ async def delete_api_key(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user) current_user: dict = Depends(get_current_user)
): ):
"""删除 API 密钥(兼容前端)""" """Delete API key (frontend compatible)"""
try: try:
key = db.query(APIKey).filter(APIKey.id == key_id).first() key = db.query(APIKey).filter(APIKey.id == key_id).first()
if not key: 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.delete(key)
db.commit() db.commit()
return {"message": "API 密钥删除成功"} return {"message": "API key deleted successfully"}
except HTTPException: except HTTPException:
raise raise
except Exception as e: 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) @app.get("/api/projects/", response_model=dict)
async def list_projects(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)): async def list_projects(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)):
"""获取所有项目(兼容前端)""" """Get all projects (frontend compatible)"""
try: try:
projects = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all() projects = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all()
return { return {
@ -194,7 +194,7 @@ async def list_projects(db: Session = Depends(get_db), current_user: dict = Depe
] ]
} }
except Exception as e: 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) @app.post("/api/projects/", response_model=dict)
async def create_project( async def create_project(
@ -202,17 +202,17 @@ async def create_project(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user) current_user: dict = Depends(get_current_user)
): ):
"""创建新项目(兼容前端)""" """Create a new project (frontend compatible)"""
try: try:
# 检查项目是否已存在 # Check if project already exists
existing_project = db.query(ProjectMapping).filter( existing_project = db.query(ProjectMapping).filter(
ProjectMapping.repository_name == request["giteaRepo"] ProjectMapping.repository_name == request["giteaRepo"]
).first() ).first()
if existing_project: if existing_project:
raise HTTPException(status_code=400, detail="项目已存在") raise HTTPException(status_code=400, detail="Project already exists")
# 创建新项目 # Create new project
project = ProjectMapping( project = ProjectMapping(
repository_name=request["giteaRepo"], repository_name=request["giteaRepo"],
default_job=request["jenkinsJob"] default_job=request["jenkinsJob"]
@ -232,7 +232,7 @@ async def create_project(
except HTTPException: except HTTPException:
raise raise
except Exception as e: 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}") @app.delete("/api/projects/{project_id}")
async def delete_project( async def delete_project(
@ -240,31 +240,31 @@ async def delete_project(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user) current_user: dict = Depends(get_current_user)
): ):
"""删除项目(兼容前端)""" """Delete project (frontend compatible)"""
try: try:
project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first() project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first()
if not project: if not project:
raise HTTPException(status_code=404, detail="项目不存在") raise HTTPException(status_code=404, detail="Project does not exist")
db.delete(project) db.delete(project)
db.commit() db.commit()
return {"message": "项目删除成功"} return {"message": "Project deleted successfully"}
except HTTPException: except HTTPException:
raise raise
except Exception as e: 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") @app.get("/health")
async def health_check(): async def health_check():
"""健康检查端点""" """Health check endpoint"""
try: try:
# 计算运行时间 # Calculate uptime
uptime = datetime.now() - start_time 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() process = psutil.Process()
memory_info = process.memory_info() memory_info = process.memory_info()
memory_mb = memory_info.rss / 1024 / 1024 memory_mb = memory_info.rss / 1024 / 1024
@ -292,21 +292,21 @@ async def get_logs(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user) current_user: dict = Depends(get_current_user)
): ):
"""获取日志(简化版本)""" """Get logs (simplified version)"""
try: try:
# 这里应该实现真正的日志查询逻辑 # Here should be the real log query logic
# 目前返回模拟数据 # Currently returns mock data
logs = [ logs = [
{ {
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"level": "info", "level": "info",
"message": "系统运行正常" "message": "System running normally"
} }
] ]
return {"logs": logs} return {"logs": logs}
except Exception as e: 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__": if __name__ == "__main__":
import uvicorn import uvicorn

View File

@ -1,17 +1,17 @@
// 全局变量存储 JWT 令牌 // Global variable to store JWT token
let authToken = localStorage.getItem('auth_token'); let authToken = localStorage.getItem('auth_token');
$(document).ready(function() { $(document).ready(function() {
// 检查认证状态 // Check authentication status
if (!authToken) { if (!authToken) {
window.location.href = '/login'; window.location.href = '/login';
return; return;
} }
// 设置 AJAX 默认配置 // Set AJAX default config
$.ajaxSetup({ $.ajaxSetup({
beforeSend: function(xhr, settings) { beforeSend: function(xhr, settings) {
// 不为登录请求添加认证头 // Do not add auth header for login request
if (settings.url === '/api/auth/login') { if (settings.url === '/api/auth/login') {
return; return;
} }
@ -20,7 +20,7 @@ $(document).ready(function() {
} }
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
// 如果收到 401重定向到登录页 // If 401 received, redirect to login page
if (xhr.status === 401) { if (xhr.status === 401) {
localStorage.removeItem('auth_token'); localStorage.removeItem('auth_token');
window.location.href = '/login'; window.location.href = '/login';
@ -30,10 +30,10 @@ $(document).ready(function() {
} }
}); });
// 初始化工具提示 // Initialize tooltips
$('[data-bs-toggle="tooltip"]').tooltip(); $('[data-bs-toggle="tooltip"]').tooltip();
// 加载初始数据 // Load initial data
loadProjects(); loadProjects();
loadAPIKeys(); loadAPIKeys();
loadLogs(); loadLogs();
@ -41,10 +41,10 @@ $(document).ready(function() {
loadHealthDetails(); loadHealthDetails();
loadStatsDetails(); loadStatsDetails();
// 设置定期健康检查 // Set periodic health check
setInterval(checkHealth, 30000); setInterval(checkHealth, 30000);
// 项目管理 // Project management
$('#addProjectForm').on('submit', function(e) { $('#addProjectForm').on('submit', function(e) {
e.preventDefault(); e.preventDefault();
const projectData = { const projectData = {
@ -62,13 +62,13 @@ $(document).ready(function() {
$('#addProjectModal').modal('hide'); $('#addProjectModal').modal('hide');
$('#addProjectForm')[0].reset(); $('#addProjectForm')[0].reset();
loadProjects(); loadProjects();
showSuccess('项目添加成功'); showSuccess('Project added successfully');
}, },
error: handleAjaxError error: handleAjaxError
}); });
}); });
// API 密钥管理 // API key management
$('#generateKeyForm').on('submit', function(e) { $('#generateKeyForm').on('submit', function(e) {
e.preventDefault(); e.preventDefault();
$.ajax({ $.ajax({
@ -80,16 +80,16 @@ $(document).ready(function() {
$('#generateKeyModal').modal('hide'); $('#generateKeyModal').modal('hide');
$('#generateKeyForm')[0].reset(); $('#generateKeyForm')[0].reset();
loadAPIKeys(); loadAPIKeys();
showSuccess('API 密钥生成成功'); showSuccess('API key generated successfully');
// 显示新生成的密钥 // Show newly generated key
showApiKeyModal(response.key); showApiKeyModal(response.key);
}, },
error: handleAjaxError error: handleAjaxError
}); });
}); });
// 日志查询 // Log query
$('#logQueryForm').on('submit', function(e) { $('#logQueryForm').on('submit', function(e) {
e.preventDefault(); e.preventDefault();
loadLogs({ loadLogs({
@ -100,7 +100,7 @@ $(document).ready(function() {
}); });
}); });
// 标签页切换 // Tab switching
$('.nav-link').on('click', function() { $('.nav-link').on('click', function() {
$('.nav-link').removeClass('active'); $('.nav-link').removeClass('active');
$(this).addClass('active'); $(this).addClass('active');
@ -121,7 +121,7 @@ function loadProjects() {
<td>${escapeHtml(project.giteaRepo)}</td> <td>${escapeHtml(project.giteaRepo)}</td>
<td> <td>
<button class="btn btn-sm btn-danger" onclick="deleteProject(${project.id})"> <button class="btn btn-sm btn-danger" onclick="deleteProject(${project.id})">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i> Delete
</button> </button>
</td> </td>
</tr> </tr>
@ -140,12 +140,12 @@ function loadAPIKeys() {
data.keys.forEach(function(key) { data.keys.forEach(function(key) {
tbody.append(` tbody.append(`
<tr> <tr>
<td>${escapeHtml(key.description || '无描述')}</td> <td>${escapeHtml(key.description || 'No description')}</td>
<td><code class="api-key">${escapeHtml(key.key)}</code></td> <td><code class="api-key">${escapeHtml(key.key)}</code></td>
<td>${new Date(key.created_at).toLocaleString('zh-CN')}</td> <td>${new Date(key.created_at).toLocaleString('zh-CN')}</td>
<td> <td>
<button class="btn btn-sm btn-danger" onclick="revokeKey(${key.id})"> <button class="btn btn-sm btn-danger" onclick="revokeKey(${key.id})">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i> Revoke
</button> </button>
</td> </td>
</tr> </tr>
@ -178,7 +178,7 @@ function loadLogs(query = {}) {
`); `);
}); });
} else { } else {
logContainer.append('<div class="text-muted">暂无日志记录</div>'); logContainer.append('<div class="text-muted">No log records</div>');
} }
}) })
.fail(handleAjaxError); .fail(handleAjaxError);
@ -190,12 +190,12 @@ function checkHealth() {
const indicator = $('.health-indicator'); const indicator = $('.health-indicator');
indicator.removeClass('healthy unhealthy') indicator.removeClass('healthy unhealthy')
.addClass(data.status === 'healthy' ? 'healthy' : 'unhealthy'); .addClass(data.status === 'healthy' ? 'healthy' : 'unhealthy');
$('#healthStatus').text(data.status === 'healthy' ? '健康' : '异常'); $('#healthStatus').text(data.status === 'healthy' ? 'Healthy' : 'Unhealthy');
}) })
.fail(function() { .fail(function() {
const indicator = $('.health-indicator'); const indicator = $('.health-indicator');
indicator.removeClass('healthy').addClass('unhealthy'); indicator.removeClass('healthy').addClass('unhealthy');
$('#healthStatus').text('异常'); $('#healthStatus').text('Unhealthy');
}); });
} }
@ -205,24 +205,24 @@ function loadHealthDetails() {
const healthDetails = $('#healthDetails'); const healthDetails = $('#healthDetails');
healthDetails.html(` healthDetails.html(`
<div class="mb-3"> <div class="mb-3">
<strong>状态:</strong> <strong>Status:</strong>
<span class="badge ${data.status === 'healthy' ? 'bg-success' : 'bg-danger'}"> <span class="badge ${data.status === 'healthy' ? 'bg-success' : 'bg-danger'}">
${data.status === 'healthy' ? '健康' : '异常'} ${data.status === 'healthy' ? 'Healthy' : 'Unhealthy'}
</span> </span>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<strong>版本:</strong> ${data.version || ''} <strong>Version:</strong> ${data.version || 'Unknown'}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<strong>启动时间:</strong> ${data.uptime || ''} <strong>Uptime:</strong> ${data.uptime || 'Unknown'}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<strong>内存使用:</strong> ${data.memory || ''} <strong>Memory Usage:</strong> ${data.memory || 'Unknown'}
</div> </div>
`); `);
}) })
.fail(function() { .fail(function() {
$('#healthDetails').html('<div class="text-danger">无法获取健康状态</div>'); $('#healthDetails').html('<div class="text-danger">Unable to get health status</div>');
}); });
} }
@ -232,75 +232,75 @@ function loadStatsDetails() {
const statsDetails = $('#statsDetails'); const statsDetails = $('#statsDetails');
statsDetails.html(` statsDetails.html(`
<div class="mb-3"> <div class="mb-3">
<strong>总项目数:</strong> ${data.total_projects || 0} <strong>Total Projects:</strong> ${data.total_projects || 0}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<strong>API 密钥数:</strong> ${data.total_api_keys || 0} <strong>API Keys:</strong> ${data.total_api_keys || 0}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<strong>今日触发次数:</strong> ${data.today_triggers || 0} <strong>Today's Triggers:</strong> ${data.today_triggers || 0}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<strong>成功触发次数:</strong> ${data.successful_triggers || 0} <strong>Successful Triggers:</strong> ${data.successful_triggers || 0}
</div> </div>
`); `);
}) })
.fail(function() { .fail(function() {
$('#statsDetails').html('<div class="text-danger">无法获取统计信息</div>'); $('#statsDetails').html('<div class="text-danger">Unable to get statistics</div>');
}); });
} }
function deleteProject(id) { function deleteProject(id) {
if (!confirm('确定要删除这个项目吗?')) return; if (!confirm('Are you sure you want to delete this project?')) return;
$.ajax({ $.ajax({
url: `/api/projects/${id}`, url: `/api/projects/${id}`,
method: 'DELETE', method: 'DELETE',
success: function() { success: function() {
loadProjects(); loadProjects();
showSuccess('项目删除成功'); showSuccess('Project deleted successfully');
}, },
error: handleAjaxError error: handleAjaxError
}); });
} }
function revokeKey(id) { function revokeKey(id) {
if (!confirm('确定要撤销这个 API 密钥吗?')) return; if (!confirm('Are you sure you want to revoke this API key?')) return;
$.ajax({ $.ajax({
url: `/api/keys/${id}`, url: `/api/keys/${id}`,
method: 'DELETE', method: 'DELETE',
success: function() { success: function() {
loadAPIKeys(); loadAPIKeys();
showSuccess('API 密钥撤销成功'); showSuccess('API key revoked successfully');
}, },
error: handleAjaxError error: handleAjaxError
}); });
} }
function showApiKeyModal(key) { function showApiKeyModal(key) {
// 创建模态框显示新生成的密钥 // Create modal to show newly generated key
const modal = $(` const modal = $(`
<div class="modal fade" id="newApiKeyModal" tabindex="-1"> <div class="modal fade" id="newApiKeyModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">API 密钥</h5> <h5 class="modal-title">New API Key</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="alert alert-warning"> <div class="alert alert-warning">
<strong>重要提示:</strong> <strong>Important:</strong> Please save this key, as it will only be shown once!
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">API 密钥:</label> <label class="form-label">API Key:</label>
<input type="text" class="form-control" value="${key}" readonly> <input type="text" class="form-control" value="${key}" readonly>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="copyToClipboard('${key}')"> <button type="button" class="btn btn-primary" onclick="copyToClipboard('${key}')">
复制到剪贴板 Copy to Clipboard
</button> </button>
</div> </div>
</div> </div>
@ -318,19 +318,19 @@ function showApiKeyModal(key) {
function copyToClipboard(text) { function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() { navigator.clipboard.writeText(text).then(function() {
showSuccess('已复制到剪贴板'); showSuccess('Copied to clipboard');
}, function() { }, function() {
showError('复制失败'); showError('Copy failed');
}); });
} }
function handleAjaxError(jqXHR, textStatus, errorThrown) { function handleAjaxError(jqXHR, textStatus, errorThrown) {
const message = jqXHR.responseJSON?.detail || errorThrown || '发生错误'; const message = jqXHR.responseJSON?.detail || errorThrown || 'An error occurred';
showError(`错误: ${message}`); showError(`Error: ${message}`);
} }
function showSuccess(message) { function showSuccess(message) {
// 创建成功提示 // Create success alert
const alert = $(` const alert = $(`
<div class="alert alert-success alert-dismissible fade show" role="alert"> <div class="alert alert-success alert-dismissible fade show" role="alert">
${message} ${message}
@ -340,14 +340,14 @@ function showSuccess(message) {
$('.main-content').prepend(alert); $('.main-content').prepend(alert);
// 3秒后自动消失 // Auto dismiss after 3 seconds
setTimeout(function() { setTimeout(function() {
alert.alert('close'); alert.alert('close');
}, 3000); }, 3000);
} }
function showError(message) { function showError(message) {
// 创建错误提示 // Create error alert
const alert = $(` const alert = $(`
<div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="alert alert-danger alert-dismissible fade show" role="alert">
${message} ${message}
@ -357,7 +357,7 @@ function showError(message) {
$('.main-content').prepend(alert); $('.main-content').prepend(alert);
// 5秒后自动消失 // Auto dismiss after 5 seconds
setTimeout(function() { setTimeout(function() {
alert.alert('close'); alert.alert('close');
}, 5000); }, 5000);

View File

@ -1,9 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>仪表板 - Gitea Webhook Ambassador</title> <title>Dashboard - Gitea Webhook Ambassador</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
@ -111,7 +111,7 @@
<div class="nav-item text-nowrap"> <div class="nav-item text-nowrap">
<span class="px-3 text-white"> <span class="px-3 text-white">
<span class="health-indicator"></span> <span class="health-indicator"></span>
<span id="healthStatus">检查中...</span> <span id="healthStatus">Checking...</span>
</span> </span>
</div> </div>
</div> </div>
@ -124,22 +124,22 @@
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" href="#projects" data-bs-toggle="tab"> <a class="nav-link active" href="#projects" data-bs-toggle="tab">
<i class="bi bi-folder"></i> 项目管理 <i class="bi bi-folder"></i> Project Management
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#api-keys" data-bs-toggle="tab"> <a class="nav-link" href="#api-keys" data-bs-toggle="tab">
<i class="bi bi-key"></i> API 密钥 <i class="bi bi-key"></i> API Keys
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#logs" data-bs-toggle="tab"> <a class="nav-link" href="#logs" data-bs-toggle="tab">
<i class="bi bi-journal-text"></i> 日志查看 <i class="bi bi-journal-text"></i> Logs
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#health" data-bs-toggle="tab"> <a class="nav-link" href="#health" data-bs-toggle="tab">
<i class="bi bi-heart-pulse"></i> 健康状态 <i class="bi bi-heart-pulse"></i> Health Status
</a> </a>
</li> </li>
</ul> </ul>
@ -148,22 +148,22 @@
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content"> <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
<div class="tab-content" id="myTabContent"> <div class="tab-content" id="myTabContent">
<!-- 项目管理 Tab --> <!-- Project Management Tab -->
<div class="tab-pane fade show active" id="projects"> <div class="tab-pane fade show active" id="projects">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">项目管理</h1> <h1 class="h2">Project Management</h1>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProjectModal"> <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProjectModal">
<i class="bi bi-plus"></i> 添加项目 <i class="bi bi-plus"></i> Add Project
</button> </button>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped" id="projectsTable"> <table class="table table-striped" id="projectsTable">
<thead> <thead>
<tr> <tr>
<th>项目名称</th> <th>Project Name</th>
<th>Jenkins 任务</th> <th>Jenkins Job</th>
<th>Gitea 仓库</th> <th>Gitea Repo</th>
<th>操作</th> <th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@ -171,22 +171,22 @@
</div> </div>
</div> </div>
<!-- API 密钥 Tab --> <!-- API Keys Tab -->
<div class="tab-pane fade" id="api-keys"> <div class="tab-pane fade" id="api-keys">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">API 密钥管理</h1> <h1 class="h2">API Key Management</h1>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#generateKeyModal"> <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#generateKeyModal">
<i class="bi bi-plus"></i> 生成新密钥 <i class="bi bi-plus"></i> Generate New Key
</button> </button>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped" id="apiKeysTable"> <table class="table table-striped" id="apiKeysTable">
<thead> <thead>
<tr> <tr>
<th>描述</th> <th>Description</th>
<th>密钥</th> <th>Key</th>
<th>创建时间</th> <th>Created At</th>
<th>操作</th> <th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@ -194,52 +194,52 @@
</div> </div>
</div> </div>
<!-- 日志查看 Tab --> <!-- Logs Tab -->
<div class="tab-pane fade" id="logs"> <div class="tab-pane fade" id="logs">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">日志查看</h1> <h1 class="h2">Logs</h1>
</div> </div>
<form id="logQueryForm" class="row g-3 mb-3"> <form id="logQueryForm" class="row g-3 mb-3">
<div class="col-md-3"> <div class="col-md-3">
<label for="startTime" class="form-label">开始时间</label> <label for="startTime" class="form-label">Start Time</label>
<input type="datetime-local" class="form-control" id="startTime"> <input type="datetime-local" class="form-control" id="startTime">
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label for="endTime" class="form-label">结束时间</label> <label for="endTime" class="form-label">End Time</label>
<input type="datetime-local" class="form-control" id="endTime"> <input type="datetime-local" class="form-control" id="endTime">
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label for="logLevel" class="form-label">日志级别</label> <label for="logLevel" class="form-label">Log Level</label>
<select class="form-select" id="logLevel"> <select class="form-select" id="logLevel">
<option value="">全部</option> <option value="">All</option>
<option value="error">错误</option> <option value="error">Error</option>
<option value="warn">警告</option> <option value="warn">Warning</option>
<option value="info">信息</option> <option value="info">Info</option>
<option value="debug">调试</option> <option value="debug">Debug</option>
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label for="logQuery" class="form-label">搜索关键词</label> <label for="logQuery" class="form-label">Search Keyword</label>
<input type="text" class="form-control" id="logQuery" placeholder="搜索日志..."> <input type="text" class="form-control" id="logQuery" placeholder="Search logs...">
</div> </div>
<div class="col-md-1"> <div class="col-md-1">
<label class="form-label">&nbsp;</label> <label class="form-label">&nbsp;</label>
<button type="submit" class="btn btn-primary w-100">搜索</button> <button type="submit" class="btn btn-primary w-100">Search</button>
</div> </div>
</form> </form>
<div id="logEntries" class="border rounded p-3 bg-light" style="max-height: 500px; overflow-y: auto;"></div> <div id="logEntries" class="border rounded p-3 bg-light" style="max-height: 500px; overflow-y: auto;"></div>
</div> </div>
<!-- 健康状态 Tab --> <!-- Health Status Tab -->
<div class="tab-pane fade" id="health"> <div class="tab-pane fade" id="health">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">健康状态</h1> <h1 class="h2">Health Status</h1>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">服务状态</h5> <h5 class="card-title mb-0">Service Status</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="healthDetails"></div> <div id="healthDetails"></div>
@ -249,7 +249,7 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">统计信息</h5> <h5 class="card-title mb-0">Statistics</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="statsDetails"></div> <div id="statsDetails"></div>
@ -263,56 +263,56 @@
</div> </div>
</div> </div>
<!-- 添加项目模态框 --> <!-- Add Project Modal -->
<div class="modal fade" id="addProjectModal" tabindex="-1"> <div class="modal fade" id="addProjectModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">添加新项目</h5> <h5 class="modal-title">Add New Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<form id="addProjectForm"> <form id="addProjectForm">
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label for="projectName" class="form-label">项目名称</label> <label for="projectName" class="form-label">Project Name</label>
<input type="text" class="form-control" id="projectName" required> <input type="text" class="form-control" id="projectName" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="jenkinsJob" class="form-label">Jenkins 任务</label> <label for="jenkinsJob" class="form-label">Jenkins Job</label>
<input type="text" class="form-control" id="jenkinsJob" required> <input type="text" class="form-control" id="jenkinsJob" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="giteaRepo" class="form-label">Gitea 仓库</label> <label for="giteaRepo" class="form-label">Gitea Repo</label>
<input type="text" class="form-control" id="giteaRepo" placeholder="owner/repo" required> <input type="text" class="form-control" id="giteaRepo" placeholder="owner/repo" required>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">添加项目</button> <button type="submit" class="btn btn-primary">Add Project</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<!-- 生成 API 密钥模态框 --> <!-- Generate API Key Modal -->
<div class="modal fade" id="generateKeyModal" tabindex="-1"> <div class="modal fade" id="generateKeyModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">生成新 API 密钥</h5> <h5 class="modal-title">Generate New API Key</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<form id="generateKeyForm"> <form id="generateKeyForm">
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label for="keyDescription" class="form-label">密钥描述</label> <label for="keyDescription" class="form-label">Key Description</label>
<input type="text" class="form-control" id="keyDescription" required> <input type="text" class="form-control" id="keyDescription" required>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">生成密钥</button> <button type="submit" class="btn btn-primary">Generate Key</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,9 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - Gitea Webhook Ambassador</title> <title>Login - Gitea Webhook Ambassador</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style> <style>
.login-container { .login-container {
@ -61,7 +61,7 @@
<div class="login-form"> <div class="login-form">
<div class="login-header"> <div class="login-header">
<h1>🔗 Gitea Webhook Ambassador</h1> <h1>🔗 Gitea Webhook Ambassador</h1>
<p>高性能的 Gitea 到 Jenkins 的 Webhook 服务</p> <p>High-performance Gitea to Jenkins Webhook Service</p>
</div> </div>
<form id="loginForm"> <form id="loginForm">
@ -69,20 +69,19 @@
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input type="password" class="form-control" id="secret_key" name="secret_key" <input type="password" class="form-control" id="secret_key" name="secret_key" placeholder="Admin Secret Key" required>
placeholder="管理员密钥" required> <label for="secret_key">Admin Secret Key</label>
<label for="secret_key">管理员密钥</label>
</div> </div>
<button class="btn btn-primary btn-login" type="submit"> <button class="btn btn-primary btn-login" type="submit">
<span id="loginBtnText">登录</span> <span id="loginBtnText">Login</span>
<span id="loginBtnSpinner" class="spinner-border spinner-border-sm" style="display: none;"></span> <span id="loginBtnSpinner" class="spinner-border spinner-border-sm" style="display: none;"></span>
</button> </button>
</form> </form>
<div class="text-center mt-3"> <div class="text-center mt-3">
<small class="text-muted"> <small class="text-muted">
使用管理员密钥进行身份验证 Use the admin secret key for authentication
</small> </small>
</div> </div>
</div> </div>
@ -92,39 +91,39 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
// 检查是否已登录 // Check if already logged in
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
if (token) { if (token) {
window.location.href = '/dashboard'; window.location.href = '/dashboard';
return; return;
} }
// 检查 URL 参数中的 secret_key // Check secret_key in URL params
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const secretKeyFromUrl = urlParams.get('secret_key'); const secretKeyFromUrl = urlParams.get('secret_key');
if (secretKeyFromUrl) { if (secretKeyFromUrl) {
$('#secret_key').val(secretKeyFromUrl); $('#secret_key').val(secretKeyFromUrl);
// 自动提交登录 // Auto-submit login
$('#loginForm').submit(); $('#loginForm').submit();
return; return;
} }
// 处理登录表单提交 // Handle login form submit
$('#loginForm').on('submit', function(e) { $('#loginForm').on('submit', function(e) {
e.preventDefault(); e.preventDefault();
const secretKey = $('#secret_key').val(); const secretKey = $('#secret_key').val();
if (!secretKey) { if (!secretKey) {
showError('请输入管理员密钥'); showError('Please enter the admin secret key');
return; return;
} }
// 显示加载状态 // Show loading state
$('#loginBtnText').hide(); $('#loginBtnText').hide();
$('#loginBtnSpinner').show(); $('#loginBtnSpinner').show();
$('#loginError').hide(); $('#loginError').hide();
// 发送登录请求 // Send login request
$.ajax({ $.ajax({
url: '/api/auth/login', url: '/api/auth/login',
method: 'POST', method: 'POST',
@ -132,16 +131,16 @@
data: JSON.stringify({ secret_key: secretKey }), data: JSON.stringify({ secret_key: secretKey }),
success: function(response) { success: function(response) {
if (response && response.token) { if (response && response.token) {
// 保存令牌并跳转 // Save token and redirect
localStorage.setItem('auth_token', response.token); localStorage.setItem('auth_token', response.token);
window.location.href = '/dashboard'; window.location.href = '/dashboard';
} else { } else {
showError('服务器响应无效'); showError('Invalid server response');
} }
}, },
error: function(xhr) { error: function(xhr) {
console.error('登录错误:', xhr); console.error('Login error:', xhr);
let errorMsg = '登录失败,请重试'; let errorMsg = 'Login failed, please try again';
if (xhr.responseJSON && xhr.responseJSON.detail) { if (xhr.responseJSON && xhr.responseJSON.detail) {
errorMsg = xhr.responseJSON.detail; errorMsg = xhr.responseJSON.detail;
@ -151,7 +150,7 @@
$('#secret_key').val('').focus(); $('#secret_key').val('').focus();
}, },
complete: function() { complete: function() {
// 恢复按钮状态 // Restore button state
$('#loginBtnText').show(); $('#loginBtnText').show();
$('#loginBtnSpinner').hide(); $('#loginBtnSpinner').hide();
} }
@ -162,7 +161,7 @@
$('#loginError').text(message).show(); $('#loginError').text(message).show();
} }
// 回车键提交 // Enter key submit
$('#secret_key').on('keypress', function(e) { $('#secret_key').on('keypress', function(e) {
if (e.which === 13) { if (e.which === 13) {
$('#loginForm').submit(); $('#loginForm').submit();

View File

@ -1,140 +0,0 @@
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
]
]
}

View File

@ -1 +0,0 @@

View File

@ -114,6 +114,27 @@ executeFreeleapsPipeline {
imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'], imageReleaseArchitectures: ['linux/amd64', 'linux/arm64/v8'],
registryCredentialsId: 'freeleaps-devops-docker-hub-credentials', registryCredentialsId: 'freeleaps-devops-docker-hub-credentials',
semanticReleaseEnabled: true semanticReleaseEnabled: true
],
[
name: 'devops',
root: 'apps/devops',
language: 'python',
dependenciesManager: 'pip',
requirementsFile: 'requirements.txt',
buildCacheEnabled: true,
buildAgentImage: 'python:3.12-slim',
buildArtifacts: ['.'],
lintEnabled: false,
sastEnabled: false,
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
] ]
] ]
} }

View File

@ -0,0 +1,6 @@
apiVersion: v2
name: devops
description: A Helm Chart of devops, which part of Freeleaps Platform, powered by Freeleaps.
type: application
version: 0.0.1
appVersion: "0.0.1"

View File

@ -0,0 +1,27 @@
{{ $namespace := .Release.Namespace }}
{{ $appVersion := .Chart.AppVersion | quote }}
{{ $releaseCertificate := .Release.Service }}
{{ $releaseName := .Release.Name }}
{{- range $ingress := .Values.authentication.ingresses }}
{{- if not $ingress.tls.exists }}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ $ingress.name }}
namespace: {{ $namespace }}
labels:
app.kubernetes.io/version: {{ $appVersion }}
app.kubernetes.io/name: {{ $ingress.name | quote }}
app.kubernetes.io/managed-by: {{ $releaseCertificate }}
app.kubernetes.io/instance: {{ $releaseName }}
spec:
commonName: {{ $ingress.host }}
dnsNames:
- {{ $ingress.host }}
issuerRef:
name: {{ $ingress.tls.issuerRef.name }}
kind: {{ $ingress.tls.issuerRef.kind }}
secretName: {{ $ingress.tls.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,118 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/name: "devops"
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
# {{- if .Values.logIngest.enabled }}
# annotations:
# opentelemetry.io/config-checksum: {{ include (print $.Template.BasePath "/authentication/opentelemetry.yaml") . | sha256sum }}
# {{- end }}
name: "devops"
namespace: {{ .Release.Namespace | quote }}
spec:
selector:
matchLabels:
app.kubernetes.io/name: "devops"
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
replicas: {{ .Values.devops.replicas }}
template:
metadata:
labels:
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/name: "devops"
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
app.kubernetes.io/config-checksum: {{ include (print $.Template.BasePath "/devops/devops-config.yaml") . | sha256sum }}
{{- if .Values.logIngest.enabled }}
opentelemetry.io/config-checksum: {{ include (print $.Template.BasePath "/devops/opentelemetry.yaml") . | sha256sum }}
sidecar.opentelemetry.io/inject: "{{ .Release.Namespace}}/{{ .Release.Name }}-opentelemetry-collector"
{{- end }}
spec:
# {{- if .Values.logIngest.enabled }}
# serviceAccountName: "{{ .Release.Name }}-otel-collector"
# {{- end }}
containers:
- name: "devops"
image: "{{ coalesce .Values.devops.image.registry .Values.global.registry "docker.io"}}/{{ coalesce .Values.devops.image.repository .Values.global.repository }}/{{ .Values.devops.image.name }}:{{ .Values.devops.image.tag | default "latest" }}"
imagePullPolicy: {{ .Values.devops.image.imagePullPolicy | default "IfNotPresent" }}
ports:
{{- range $port := .Values.devops.ports }}
- containerPort: {{ $port.containerPort }}
name: {{ $port.name }}
protocol: {{ $port.protocol }}
{{- end }}
{{- if .Values.devops.resources }}
resources:
{{- toYaml .Values.devops.resources | nindent 12 }}
{{- end }}
{{- if .Values.devops.probes }}
{{- if and (.Values.devops.probes.liveness) (eq .Values.devops.probes.liveness.type "httpGet") }}
livenessProbe:
httpGet:
path: {{ .Values.devops.probes.liveness.config.path }}
port: {{ .Values.devops.probes.liveness.config.port }}
{{- if .Values.devops.probes.liveness.config.initialDelaySeconds }}
initialDelaySeconds: {{ .Values.devops.probes.liveness.config.initialDelaySeconds }}
{{- end }}
{{- if .Values.devops.probes.liveness.config.periodSeconds }}
periodSeconds: {{ .Values.devops.probes.liveness.config.periodSeconds }}
{{- end }}
{{- if .Values.devops.probes.liveness.config.timeoutSeconds }}
timeoutSeconds: {{ .Values.devops.probes.liveness.config.timeoutSeconds }}
{{- end }}
{{- if .Values.devops.probes.liveness.config.successThreshold }}
successThreshold: {{ .Values.devops.probes.liveness.config.successThreshold }}
{{- end }}
{{- if .Values.devops.probes.liveness.config.failureThreshold }}
failureThreshold: {{ .Values.devops.probes.liveness.config.failureThreshold }}
{{- end }}
{{- if .Values.devops.probes.liveness.config.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.devops.probes.liveness.config.terminationGracePeriodSeconds }}
{{- end }}
{{- end }}
{{- if and (.Values.devops.probes.readiness) (eq .Values.devops.probes.readiness.type "httpGet") }}
readinessProbe:
httpGet:
path: {{ .Values.devops.probes.readiness.config.path }}
port: {{ .Values.devops.probes.readiness.config.port }}
{{- if .Values.devops.probes.readiness.config.initialDelaySeconds }}
initialDelaySeconds: {{ .Values.devops.probes.readiness.config.initialDelaySeconds }}
{{- end }}
{{- if .Values.devops.probes.readiness.config.periodSeconds }}
periodSeconds: {{ .Values.devops.probes.readiness.config.periodSeconds }}
{{- end }}
{{- if .Values.devops.probes.readiness.config.timeoutSeconds }}
timeoutSeconds: {{ .Values.devops.probes.readiness.config.timeoutSeconds }}
{{- end }}
{{- if .Values.devops.probes.readiness.config.successThreshold }}
successThreshold: {{ .Values.devops.probes.readiness.config.successThreshold }}
{{- end }}
{{- if .Values.devops.probes.readiness.config.failureThreshold }}
failureThreshold: {{ .Values.devops.probes.readiness.config.failureThreshold }}
{{- end }}
{{- end }}
{{- end}}
env:
{{- range $key, $value := .Values.devops.configs }}
- name: {{ $key | snakecase | upper }}
valueFrom:
secretKeyRef:
name: devops-config
key: {{ $key | snakecase | upper }}
{{- end }}
# {{- if .Values.logIngest.enabled }}
# volumeMounts:
# - name: app-logs
# mountPath: {{ .Values.logIngest.logPath }}
# {{- end }}
# {{- if .Values.logIngest.enabled }}
# volumes:
# - name: app-logs
# emptyDir: {}
# {{- end }}

View File

@ -0,0 +1,28 @@
apiVersion: v1
kind: Secret
metadata:
name: devops-config
namespace: {{ .Release.Namespace }}
type: Opaque
data:
TZ: {{ .Values.devops.configs.tz | b64enc | quote }}
APP_NAME: {{ .Values.devops.configs.appName | b64enc | quote }}
JWT_SECRET_KEY: {{ .Values.devops.configs.jwtSecretKey | b64enc | quote }}
JWT_ALGORITHM: {{ .Values.devops.configs.jwtAlgorithm | b64enc | quote }}
ACCESS_TOKEN_EXPIRE_MINUTES: {{ .Values.devops.configs.accessTokenExpireMinutes | toString | b64enc | quote }}
REFRESH_TOKEN_EXPIRE_DAYS: {{ .Values.devops.configs.refreshTokenExpireDays | toString | b64enc | quote }}
MONGODB_NAME: {{ .Values.devops.configs.mongodbName | b64enc | quote }}
MONGODB_PORT: {{ .Values.devops.configs.mongodbPort | toString | b64enc | quote }}
MONGODB_URI: {{ .Values.devops.configs.mongodbUri | b64enc | quote }}
METRICS_ENABLED: {{ .Values.devops.configs.metricsEnabled | toString | b64enc | quote }}
PROBES_ENABLED: {{ .Values.devops.configs.probesEnabled | toString | b64enc | quote }}
BASE_GITEA_URL: {{ .Values.devops.configs.baseGiteaUrl | b64enc | quote }}
BASE_RECONCILE_URL: {{ .Values.devops.configs.baseReconcileUrl | b64enc | quote }}
BASE_LOKI_URL: {{ .Values.devops.configs.baseLokiUrl | b64enc | quote }}
LOG_BASE_PATH: {{ .Values.devops.configs.logBasePath | b64enc | quote }}
LOG_RETENTION: {{ .Values.devops.configs.logRetention | b64enc | quote }}
LOG_ROTATION: {{ .Values.devops.configs.logRotation | b64enc | quote }}
LOG_BACKUP_FILES: {{ .Values.devops.configs.logBackupFiles | toString | b64enc | quote }}
LOG_ROTATION_BYTES: {{ .Values.devops.configs.logRotationBytes | toString | b64enc | quote }}
MOCK_MODE: {{ .Values.devops.configs.mockMode | toString | b64enc | quote }}
MOCK_RESPONSE_DELAY: {{ .Values.devops.configs.mockResponseDelay | toString | b64enc | quote }}

View File

@ -0,0 +1,36 @@
{{ $namespace := .Release.Namespace }}
{{ $appVersion := .Chart.AppVersion | quote }}
{{ $releaseIngress := .Release.Service }}
{{ $releaseName := .Release.Name }}
{{- range $ingress := .Values.authentication.ingresses }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $ingress.name }}
namespace: {{ $namespace }}
labels:
app.kubernetes.io/version: {{ $appVersion }}
app.kubernetes.io/name: {{ $ingress.name | quote }}
app.kubernetes.io/managed-by: {{ $releaseIngress }}
app.kubernetes.io/instance: {{ $releaseName }}
spec:
{{- if $ingress.class }}
ingressClassName: {{ $ingress.class }}
{{- end }}
{{- if $ingress.tls }}
tls:
- hosts:
- {{ $ingress.host }}
{{- if $ingress.tls.exists }}
secretName: {{ $ingress.tls.secretRef.name }}
{{- else }}
secretName: {{ $ingress.tls.name }}
{{- end }}
{{- end }}
rules:
- host: {{ $ingress.host }}
http:
paths:
{{- toYaml $ingress.rules | nindent 10 }}
{{- end }}

View File

@ -0,0 +1,26 @@
{{ $namespace := .Release.Namespace }}
{{ $appVersion := .Chart.AppVersion | quote }}
{{ $releaseService := .Release.Service }}
{{ $releaseName := .Release.Name }}
{{- range $service := .Values.authentication.services }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $service.name }}
namespace: {{ $namespace }}
labels:
app.kubernetes.io/version: {{ $appVersion }}
app.kubernetes.io/name: {{ $service.name | quote }}
app.kubernetes.io/managed-by: {{ $releaseService }}
app.kubernetes.io/instance: {{ $releaseName }}
spec:
ports:
- port: {{ $service.port }}
targetPort: {{ $service.targetPort }}
selector:
app.kubernetes.io/version: {{ $appVersion }}
app.kubernetes.io/name: "authentication"
app.kubernetes.io/managed-by: {{ $releaseService }}
app.kubernetes.io/instance: {{ $releaseName }}
{{- end }}

View File

@ -0,0 +1,40 @@
{{ $namespace := .Release.Namespace }}
{{ $appVersion := .Chart.AppVersion | quote }}
{{ $releaseService := .Release.Service }}
{{ $releaseName := .Release.Name }}
{{- range $service := .Values.authentication.services }}
{{- if $service.serviceMonitor.enabled }}
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ $service.name }}-monitor
namespace: {{ $service.serviceMonitor.namespace }}
labels:
app.kubernetes.io/version: {{ $appVersion }}
app.kubernetes.io/name: {{ $service.name }}-monitor
app.kubernetes.io/managed-by: {{ $releaseService }}
app.kubernetes.io/instance: {{ $releaseName }}
{{- if $service.serviceMonitor.labels }}
{{- toYaml $service.serviceMonitor.labels | nindent 4 }}
{{- end }}
spec:
endpoints:
- path: /api/_/metrics
targetPort: {{ $service.targetPort }}
{{- if $service.serviceMonitor.interval }}
interval: {{ $service.serviceMonitor.interval }}
{{- end }}
{{- if $service.serviceMonitor.scrapeTimeout }}
scrapeTimeout: {{ $service.serviceMonitor.scrapeTimeout }}
{{- end }}
namespaceSelector:
matchNames:
- {{ $namespace | quote }}
selector:
matchLabels:
app.kubernetes.io/name: {{ $service.name }}
app.kubernetes.io/instance: {{ $releaseName }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,32 @@
{{- if .Values.devops.vpa }}
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: {{ .Release.Name }}-vpa
namespace: {{ .Release.Namespace }}
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: devops
resourcePolicy:
containerPolicies:
- containerName: '*'
{{- if .Values.devops.vpa.minAllowed.enabled }}
minAllowed:
cpu: {{ .Values.devops.vpa.minAllowed.cpu }}
memory: {{ .Values.devops.vpa.minAllowed.memory }}
{{- end }}
{{- if .Values.devops.vpa.maxAllowed.enabled }}
maxAllowed:
cpu: {{ .Values.devops.vpa.maxAllowed.cpu }}
memory: {{ .Values.devops.vpa.maxAllowed.memory }}
{{- end }}
{{- if .Values.devops.vpa.controlledResources }}
controlledResources:
{{- range .Values.devops.vpa.controlledResources }}
- {{ . }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,86 @@
global:
registry: docker.io
repository: freeleaps
nodeSelector: {}
devops:
replicas: 1
image:
registry:
repository: freeleaps
name: devops
tag: 1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8014
protocol: TCP
resources:
requests:
cpu: "0.1"
memory: "64Mi"
limits:
cpu: "0.2"
memory: "128Mi"
# FIXME: Wait until the developers implements the probes APIs
probes: {}
services:
- name: devops-service
type: ClusterIP
port: 8014
targetPort: 8014
serviceMonitor:
enabled: false
labels:
release: kube-prometheus-stack
namespace: freeleaps-monitoring-system
interval: 30s
scrapeTimeout: ""
# Defaults to {}, which means doesn't have any ingress
ingresses: {}
configs:
# 基础配置
tz: "UTC"
appName: "devops"
# JWT 配置
jwtSecretKey: ""
jwtAlgorithm: "HS256"
accessTokenExpireMinutes: "3600"
refreshTokenExpireDays: "1"
# MongoDB 配置
mongodbName: ""
mongodbPort: "27017"
mongodbUri: ""
# 功能开关
metricsEnabled: "false"
probesEnabled: "true"
# 外部服务 URL
baseGiteaUrl: "https://gitea.freeleaps.mathmast.com"
baseReconcileUrl: "https://reconcile.freeleaps.mathmast.com"
baseLokiUrl: "http://loki-gateway.freeleaps-logging-system"
# 日志配置
logBasePath: "/app/log"
logRetention: "30 days"
logRotation: "00:00"
logBackupFiles: "5"
logRotationBytes: "10485760"
# Mock 模式配置
mockMode: "false"
mockResponseDelay: "1000"
vpa:
minAllowed:
enabled: false
cpu: 100m
memory: 64Mi
maxAllowed:
enabled: true
cpu: 100m
memory: 128Mi
controlledResources:
- cpu
- memory

View File

@ -0,0 +1,89 @@
global:
registry: docker.io
repository: freeleaps
nodeSelector: {}
dashboard:
enabled: true
name: freeleaps-prod-authentication-dashboard
title: Authentication Service Dashboard (PROD)
metricsPrefix: freeleaps_authentication
authentication:
replicas: 1
image:
registry: docker.io
repository: null
name: authentication
tag: snapshot-40e0faf
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8004
protocol: TCP
resources:
requests:
cpu: 200m
memory: 64Mi
limits:
cpu: 300m
memory: 128Mi
probes:
readiness:
type: httpGet
config:
path: /api/_/readyz
port: 8004
initialDelaySeconds: 5
periodSeconds: 30
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
liveness:
type: httpGet
config:
path: /api/_/livez
port: 8004
initialDelaySeconds: 5
periodSeconds: 15
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
terminationGracePeriodSeconds: 30
services:
- name: authentication-service
type: ClusterIP
port: 8004
targetPort: 8004
serviceMonitor:
enabled: true
labels:
release: kube-prometheus-stack
namespace: freeleaps-monitoring-system
interval: 30s
scrapeTimeout: ''
ingresses: {}
configs:
tz: UTC
appName: authentication
devsvcWebapiUrlBase: http://devsvc-service.freeleaps-prod.svc.freeleaps.cluster:8007/api/devsvc/
notificationWebapiUrlBase: http://notification-service.freeleaps-prod.svc.freeleaps.cluster:8003/api/notification/
jwtSecretKey: ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0
jwtAlgorithm: HS256
serviceApiAccessHost: 0.0.0.0
serviceApiAccessPort: 8004
mongodbName: freeleaps2
mongodbPort: 27017
mongodbUri: mongodb+srv://freeadmin:0eMV0bt8oyaknA0m@freeleaps2.zmsmpos.mongodb.net/?retryWrites=true&w=majority
metricsEnabled: 'true'
probesEnabled: 'true'
vpa:
minAllowed:
enabled: true
cpu: 50m
memory: 64Mi
maxAllowed:
enabled: true
cpu: 200m
memory: 128Mi
controlledResources:
- cpu
- memory

View File

@ -0,0 +1,86 @@
global:
registry: docker.io
repository: freeleaps
nodeSelector: {}
devops:
replicas: 1
image:
registry:
repository: freeleaps
name: devops
tag: 1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8014
protocol: TCP
resources:
requests:
cpu: "0.1"
memory: "64Mi"
limits:
cpu: "0.2"
memory: "128Mi"
# FIXME: Wait until the developers implements the probes APIs
probes: {}
services:
- name: devops-service
type: ClusterIP
port: 8014
targetPort: 8014
serviceMonitor:
enabled: false
labels:
release: kube-prometheus-stack
namespace: freeleaps-monitoring-system
interval: 30s
scrapeTimeout: ""
# Defaults to {}, which means doesn't have any ingress
ingresses: {}
configs:
# 基础配置
tz: "UTC"
appName: "devops"
# JWT 配置
jwtSecretKey: ""
jwtAlgorithm: "HS256"
accessTokenExpireMinutes: "3600"
refreshTokenExpireDays: "1"
# MongoDB 配置
mongodbName: ""
mongodbPort: "27017"
mongodbUri: ""
# 功能开关
metricsEnabled: "false"
probesEnabled: "true"
# 外部服务 URL
baseGiteaUrl: "https://gitea.freeleaps.mathmast.com"
baseReconcileUrl: "https://reconcile.freeleaps.mathmast.com"
baseLokiUrl: "http://loki-gateway.freeleaps-logging-system"
# 日志配置
logBasePath: "/app/log"
logRetention: "30 days"
logRotation: "00:00"
logBackupFiles: "5"
logRotationBytes: "10485760"
# Mock 模式配置
mockMode: "false"
mockResponseDelay: "1000"
vpa:
minAllowed:
enabled: false
cpu: 100m
memory: 64Mi
maxAllowed:
enabled: true
cpu: 100m
memory: 128Mi
controlledResources:
- cpu
- memory