guardia-itsm/routers/cicd.py

111 lines
3.8 KiB
Python

"""
Jenkins CI/CD 상태 API (모바일 기능 #99, #100).
GET /api/cicd/builds — 빌드 목록 (최근 20건)
GET /api/cicd/builds/{id} — 빌드 상세
POST /api/cicd/builds/trigger — 빌드 트리거
GET /api/cicd/status — 전체 파이프라인 상태
WS /ws/cicd-status — 실시간 빌드 상태 스트림
"""
from __future__ import annotations
import asyncio
import json
import logging
import os
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
from pydantic import BaseModel
from core.auth import get_current_user
from models import User
logger = logging.getLogger(__name__)
router = APIRouter(tags=["CI/CD"])
JENKINS_URL = os.getenv("JENKINS_URL", "http://localhost:8080")
_MOCK_BUILDS = [
{"id": 1, "project": "guardia-itsm", "branch": "main", "status": "SUCCESS", "started_at": "2026-06-06T10:00:00", "duration_sec": 125, "triggered_by": "admin"},
{"id": 2, "project": "guardia-messenger", "branch": "feature/100feat", "status": "SUCCESS", "started_at": "2026-06-06T09:30:00", "duration_sec": 340, "triggered_by": "ythong"},
{"id": 3, "project": "guardia-manager", "branch": "main", "status": "FAILURE", "started_at": "2026-06-06T08:00:00", "duration_sec": 60, "triggered_by": "admin"},
{"id": 4, "project": "zioinfo-web", "branch": "main", "status": "SUCCESS", "started_at": "2026-06-05T17:00:00", "duration_sec": 90, "triggered_by": "admin"},
{"id": 5, "project": "guardia-itsm", "branch": "develop", "status": "RUNNING", "started_at": "2026-06-06T10:30:00", "duration_sec": None, "triggered_by": "ythong"},
]
class TriggerIn(BaseModel):
project: str
branch: Optional[str] = "main"
@router.get("/api/cicd/builds")
async def list_builds(
limit: int = 20,
current_user: User = Depends(get_current_user),
):
return {"jenkins_url": JENKINS_URL, "items": _MOCK_BUILDS[:limit]}
@router.get("/api/cicd/builds/{build_id}")
async def get_build(
build_id: int,
current_user: User = Depends(get_current_user),
):
b = next((b for b in _MOCK_BUILDS if b["id"] == build_id), None)
if not b:
return {"error": "빌드를 찾을 수 없습니다."}
return b
@router.post("/api/cicd/builds/trigger", status_code=202)
async def trigger_build(
payload: TriggerIn,
current_user: User = Depends(get_current_user),
):
return {
"queued": True,
"project": payload.project,
"branch": payload.branch,
"triggered_by": current_user.username,
"message": f"{payload.project}@{payload.branch} 빌드가 대기열에 추가됐습니다.",
}
@router.get("/api/cicd/status")
async def pipeline_status(current_user: User = Depends(get_current_user)):
running = [b for b in _MOCK_BUILDS if b["status"] == "RUNNING"]
return {
"jenkins_connected": False,
"jenkins_url": JENKINS_URL,
"running_builds": len(running),
"latest_apk_url": "/api/cicd/apk/latest",
"apk_qr_data": "https://zioinfo.co.kr:8443/static/apk/guardia-latest.apk",
"builds": _MOCK_BUILDS[:5],
}
_cicd_clients: set[WebSocket] = set()
@router.websocket("/ws/cicd-status")
async def cicd_ws(websocket: WebSocket):
await websocket.accept()
_cicd_clients.add(websocket)
try:
while True:
await asyncio.sleep(10)
running = [b for b in _MOCK_BUILDS if b["status"] == "RUNNING"]
await websocket.send_text(json.dumps({
"type": "status",
"running": len(running),
"ts": datetime.now().isoformat(),
}))
except WebSocketDisconnect:
_cicd_clients.discard(websocket)
except Exception as e:
logger.warning("cicd ws error: %s", e)
_cicd_clients.discard(websocket)