chore: remove all Chinese text, full English internationalization for gitea-webhook-ambassador-python
This commit is contained in:
parent
5b93048cb3
commit
d03c119322
@ -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,
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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"}
|
||||||
@ -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')
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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"> </label>
|
<label class="form-label"> </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>
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
140
freeleaps-service-hub/alpha/ci/Jenkinsfile
vendored
140
freeleaps-service-hub/alpha/ci/Jenkinsfile
vendored
@ -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
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -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
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
6
freeleaps/helm-pkg/devops/Chart.yaml
Normal file
6
freeleaps/helm-pkg/devops/Chart.yaml
Normal 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"
|
||||||
@ -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 }}
|
||||||
@ -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 }}
|
||||||
@ -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 }}
|
||||||
@ -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 }}
|
||||||
@ -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 }}
|
||||||
@ -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 }}
|
||||||
32
freeleaps/helm-pkg/devops/templates/authentication/vpa.yaml
Normal file
32
freeleaps/helm-pkg/devops/templates/authentication/vpa.yaml
Normal 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 }}
|
||||||
86
freeleaps/helm-pkg/devops/values.alpha.yaml
Normal file
86
freeleaps/helm-pkg/devops/values.alpha.yaml
Normal 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
|
||||||
89
freeleaps/helm-pkg/devops/values.prod.yaml
Normal file
89
freeleaps/helm-pkg/devops/values.prod.yaml
Normal 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
|
||||||
86
freeleaps/helm-pkg/devops/values.yaml
Normal file
86
freeleaps/helm-pkg/devops/values.yaml
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user