freeleaps-ops/apps/gitea-webhook-ambassador-python/app/services/database_service.py

380 lines
14 KiB
Python

"""
Database service
Implements project mapping, branch pattern matching, and related features
"""
import asyncio
from typing import Optional, List, Dict, Any
from datetime import datetime
import structlog
import re
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.sql import text
from app.config import get_settings
logger = structlog.get_logger()
Base = declarative_base()
# Database models
class APIKey(Base):
"""API key model"""
__tablename__ = "api_keys"
id = Column(Integer, primary_key=True, autoincrement=True)
key = Column(String(255), unique=True, nullable=False)
description = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class ProjectMapping(Base):
"""Project mapping model"""
__tablename__ = "project_mappings"
id = Column(Integer, primary_key=True, autoincrement=True)
repository_name = Column(String(255), unique=True, nullable=False)
default_job = Column(String(255))
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
branch_jobs = relationship("BranchJob", back_populates="project", cascade="all, delete-orphan")
branch_patterns = relationship("BranchPattern", back_populates="project", cascade="all, delete-orphan")
class BranchJob(Base):
"""Branch job mapping model"""
__tablename__ = "branch_jobs"
id = Column(Integer, primary_key=True, autoincrement=True)
project_id = Column(Integer, ForeignKey("project_mappings.id"), nullable=False)
branch_name = Column(String(255), nullable=False)
job_name = Column(String(255), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationship
project = relationship("ProjectMapping", back_populates="branch_jobs")
class BranchPattern(Base):
"""Branch pattern mapping model"""
__tablename__ = "branch_patterns"
id = Column(Integer, primary_key=True, autoincrement=True)
project_id = Column(Integer, ForeignKey("project_mappings.id"), nullable=False)
pattern = Column(String(255), nullable=False)
job_name = Column(String(255), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationship
project = relationship("ProjectMapping", back_populates="branch_patterns")
class TriggerLog(Base):
"""Trigger log model"""
__tablename__ = "trigger_logs"
id = Column(Integer, primary_key=True, autoincrement=True)
repository_name = Column(String(255), nullable=False)
branch_name = Column(String(255), nullable=False)
commit_sha = Column(String(255), nullable=False)
job_name = Column(String(255), nullable=False)
status = Column(String(50), nullable=False)
error_message = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
class DatabaseService:
"""Database service"""
def __init__(self):
self.settings = get_settings()
self.engine = None
self.SessionLocal = None
self._init_database()
def _init_database(self):
"""Initialize database"""
try:
self.engine = create_engine(
self.settings.database.url,
echo=self.settings.database.echo,
pool_size=self.settings.database.pool_size,
max_overflow=self.settings.database.max_overflow
)
# Create tables
Base.metadata.create_all(bind=self.engine)
# Create session factory
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
logger.info("Database initialized successfully")
except Exception as e:
logger.error("Failed to initialize database", error=str(e))
raise
def get_session(self):
"""Get database session"""
return self.SessionLocal()
async def get_project_mapping(self, repository_name: str) -> Optional[Dict[str, Any]]:
"""
Get project mapping
Args:
repository_name: repository name
Returns:
Dict: project mapping info
"""
try:
def _get_mapping():
session = self.get_session()
try:
project = session.query(ProjectMapping).filter(
ProjectMapping.repository_name == repository_name
).first()
if not project:
return None
# Build return data
result = {
"id": project.id,
"repository_name": project.repository_name,
"default_job": project.default_job,
"branch_jobs": [],
"branch_patterns": []
}
# Add branch job mappings
for branch_job in project.branch_jobs:
result["branch_jobs"].append({
"id": branch_job.id,
"branch_name": branch_job.branch_name,
"job_name": branch_job.job_name
})
# Add branch pattern mappings
for pattern in project.branch_patterns:
result["branch_patterns"].append({
"id": pattern.id,
"pattern": pattern.pattern,
"job_name": pattern.job_name
})
return result
finally:
session.close()
# Run DB operation in thread pool
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _get_mapping)
except Exception as e:
logger.error("Failed to get project mapping",
repository_name=repository_name, error=str(e))
return None
async def determine_job_name(self, repository_name: str, branch_name: str) -> Optional[str]:
"""
Determine job name by branch
Args:
repository_name: repository name
branch_name: branch name
Returns:
str: job name
"""
try:
project = await self.get_project_mapping(repository_name)
if not project:
return None
# 1. Check exact branch match
for branch_job in project["branch_jobs"]:
if branch_job["branch_name"] == branch_name:
logger.debug("Found exact branch match",
branch=branch_name, job=branch_job["job_name"])
return branch_job["job_name"]
# 2. Check pattern match
for pattern in project["branch_patterns"]:
try:
if re.match(pattern["pattern"], branch_name):
logger.debug("Branch matched pattern",
branch=branch_name, pattern=pattern["pattern"],
job=pattern["job_name"])
return pattern["job_name"]
except re.error as e:
logger.error("Invalid regex pattern",
pattern=pattern["pattern"], error=str(e))
continue
# 3. Use default job
if project["default_job"]:
logger.debug("Using default job",
branch=branch_name, job=project["default_job"])
return project["default_job"]
return None
except Exception as e:
logger.error("Failed to determine job name",
repository_name=repository_name, branch_name=branch_name,
error=str(e))
return None
async def log_trigger(self, log_data: Dict[str, Any]) -> bool:
"""
Log trigger event
Args:
log_data: log data
Returns:
bool: success or not
"""
try:
def _log_trigger():
session = self.get_session()
try:
log = TriggerLog(
repository_name=log_data["repository_name"],
branch_name=log_data["branch_name"],
commit_sha=log_data["commit_sha"],
job_name=log_data["job_name"],
status=log_data["status"],
error_message=log_data.get("error_message")
)
session.add(log)
session.commit()
return True
except Exception as e:
session.rollback()
logger.error("Failed to log trigger", error=str(e))
return False
finally:
session.close()
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _log_trigger)
except Exception as e:
logger.error("Failed to log trigger", error=str(e))
return False
async def get_trigger_logs(self, repository_name: str = None,
branch_name: str = None, limit: int = 100) -> List[Dict[str, Any]]:
"""
Get trigger logs
Args:
repository_name: repository name (optional)
branch_name: branch name (optional)
limit: limit number
Returns:
List: log list
"""
try:
def _get_logs():
session = self.get_session()
try:
query = session.query(TriggerLog)
if repository_name:
query = query.filter(TriggerLog.repository_name == repository_name)
if branch_name:
query = query.filter(TriggerLog.branch_name == branch_name)
logs = query.order_by(TriggerLog.created_at.desc()).limit(limit).all()
return [
{
"id": log.id,
"repository_name": log.repository_name,
"branch_name": log.branch_name,
"commit_sha": log.commit_sha,
"job_name": log.job_name,
"status": log.status,
"error_message": log.error_message,
"created_at": log.created_at.isoformat()
}
for log in logs
]
finally:
session.close()
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _get_logs)
except Exception as e:
logger.error("Failed to get trigger logs", error=str(e))
return []
async def create_project_mapping(self, mapping_data: Dict[str, Any]) -> bool:
"""
Create project mapping
Args:
mapping_data: mapping data
Returns:
bool: success or not
"""
try:
def _create_mapping():
session = self.get_session()
try:
# Create project mapping
project = ProjectMapping(
repository_name=mapping_data["repository_name"],
default_job=mapping_data.get("default_job")
)
session.add(project)
session.flush() # Get ID
# Add branch job mappings
for branch_job in mapping_data.get("branch_jobs", []):
job = BranchJob(
project_id=project.id,
branch_name=branch_job["branch_name"],
job_name=branch_job["job_name"]
)
session.add(job)
# Add branch pattern mappings
for pattern in mapping_data.get("branch_patterns", []):
pattern_obj = BranchPattern(
project_id=project.id,
pattern=pattern["pattern"],
job_name=pattern["job_name"]
)
session.add(pattern_obj)
session.commit()
return True
except Exception as e:
session.rollback()
logger.error("Failed to create project mapping", error=str(e))
return False
finally:
session.close()
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _create_mapping)
except Exception as e:
logger.error("Failed to create project mapping", error=str(e))
return False
# Global database service instance
_database_service: Optional[DatabaseService] = None
def get_database_service() -> DatabaseService:
"""Get database service instance"""
global _database_service
if _database_service is None:
_database_service = DatabaseService()
return _database_service