203 lines
10 KiB
Python
203 lines
10 KiB
Python
"""
|
|
GUARDiA 플랫폼 엔지니어링 (Platform Engineering) — Gen6
|
|
IDP 고도화·골든 패스 템플릿·소프트웨어 카탈로그 v2·셀프서비스 인프라
|
|
"""
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Any, Dict, List, Optional
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
|
|
router = APIRouter(prefix="/api/platform", tags=["Platform Engineering"])
|
|
|
|
# ── 인메모리 스토어 ────────────────────────────────────────────────────────
|
|
_catalog: Dict[str, Dict] = {}
|
|
_templates: Dict[str, Dict] = {}
|
|
_environments: Dict[str, Dict] = {}
|
|
_service_levels: Dict[str, Dict] = {}
|
|
_requests: Dict[str, Dict] = {}
|
|
|
|
# ── 사전 로드 카탈로그 ────────────────────────────────────────────────────
|
|
def _init():
|
|
for svc in [
|
|
{"id": "svc-itsm", "name": "GUARDiA ITSM", "type": "backend", "language": "python", "version": "2.1.0", "owner": "platform-team", "status": "production"},
|
|
{"id": "svc-manager", "name": "GUARDiA Manager", "type": "frontend", "language": "typescript", "version": "1.5.0", "owner": "platform-team", "status": "production"},
|
|
{"id": "svc-messenger", "name": "GUARDiA Messenger", "type": "mobile", "language": "typescript", "version": "1.0.0", "owner": "mobile-team", "status": "production"},
|
|
{"id": "svc-web", "name": "zioinfo Homepage", "type": "fullstack", "language": "java+typescript", "version": "3.0.0", "owner": "web-team", "status": "production"},
|
|
]:
|
|
_catalog[svc["id"]] = svc
|
|
_init()
|
|
|
|
# ── 모델 ──────────────────────────────────────────────────────────────────
|
|
class ServiceCreate(BaseModel):
|
|
name: str; type: str; language: str; owner: str
|
|
description: str = ""; tags: List[str] = []
|
|
|
|
class TemplateCreate(BaseModel):
|
|
name: str; type: str # fastapi|react|react-native|springboot|ansible
|
|
description: str = ""; variables: Dict[str, Any] = {}
|
|
|
|
class EnvironmentCreate(BaseModel):
|
|
name: str; type: str = "dev" # dev|staging|prod|dr
|
|
services: List[str] = []; config: Dict[str, Any] = {}
|
|
|
|
class SelfServiceRequest(BaseModel):
|
|
service: str; action: str # create|scale|deploy|rollback|restart
|
|
params: Dict[str, Any] = {}; requested_by: str = "developer"
|
|
|
|
# ── 소프트웨어 카탈로그 ──────────────────────────────────────────────────
|
|
@router.get("/catalog")
|
|
async def list_catalog(type: Optional[str] = None, status: Optional[str] = None):
|
|
svcs = list(_catalog.values())
|
|
if type: svcs = [s for s in svcs if s.get("type") == type]
|
|
if status: svcs = [s for s in svcs if s.get("status") == status]
|
|
return {"services": svcs, "total": len(svcs)}
|
|
|
|
@router.post("/catalog")
|
|
async def add_service(svc: ServiceCreate):
|
|
sid = f"svc-{uuid.uuid4().hex[:8]}"
|
|
_catalog[sid] = {**svc.model_dump(), "id": sid, "status": "registered",
|
|
"version": "0.1.0", "created_at": datetime.utcnow().isoformat()}
|
|
return _catalog[sid]
|
|
|
|
@router.get("/catalog/{sid}")
|
|
async def get_service(sid: str):
|
|
s = _catalog.get(sid)
|
|
if not s: raise HTTPException(404)
|
|
return s
|
|
|
|
@router.get("/catalog/{sid}/dependencies")
|
|
async def service_dependencies(sid: str):
|
|
return {"service_id": sid, "depends_on": ["svc-itsm"], "depended_by": [],
|
|
"impact_level": "high" if sid == "svc-itsm" else "medium"}
|
|
|
|
@router.get("/catalog/{sid}/docs")
|
|
async def service_docs(sid: str):
|
|
s = _catalog.get(sid)
|
|
if not s: raise HTTPException(404)
|
|
return {"service_id": sid, "readme": f"# {s['name']}\n\n운영 문서",
|
|
"api_docs": f"/api/{sid}/docs", "runbook": f"/api/kb/runbook/{sid}"}
|
|
|
|
# ── 골든 패스 템플릿 ──────────────────────────────────────────────────────
|
|
@router.get("/templates")
|
|
async def list_templates():
|
|
BUILTIN = [
|
|
{"id": "tpl-fastapi", "name": "FastAPI 마이크로서비스", "type": "fastapi",
|
|
"features": ["JWT 인증", "SQLAlchemy", "Ollama AI", "CORS", "헬스체크"]},
|
|
{"id": "tpl-react-ts", "name": "React TypeScript SPA", "type": "react",
|
|
"features": ["Tailwind CSS", "React Query", "Zustand", "Vite", "다국어"]},
|
|
{"id": "tpl-rn-expo", "name": "React Native Expo", "type": "react-native",
|
|
"features": ["Expo SDK 51", "TypeScript", "Zustand", "WebSocket", "오프라인"]},
|
|
{"id": "tpl-springboot", "name": "Spring Boot API", "type": "springboot",
|
|
"features": ["JPA", "보안", "Swagger", "Actuator", "GraalVM"]},
|
|
{"id": "tpl-ansible", "name": "Ansible 플레이북", "type": "ansible",
|
|
"features": ["에이전트리스", "SSH", "멱등성", "태그", "롤백"]},
|
|
{"id": "tpl-k8s", "name": "K8s 배포 구성", "type": "kubernetes",
|
|
"features": ["Deployment", "Service", "HPA", "ConfigMap", "Secret"]},
|
|
]
|
|
custom = list(_templates.values())
|
|
return {"builtin": BUILTIN, "custom": custom, "total": len(BUILTIN) + len(custom)}
|
|
|
|
@router.post("/templates")
|
|
async def create_template(t: TemplateCreate):
|
|
tid = f"tpl-{uuid.uuid4().hex[:8]}"
|
|
_templates[tid] = {**t.model_dump(), "id": tid, "created_at": datetime.utcnow().isoformat()}
|
|
return _templates[tid]
|
|
|
|
@router.post("/templates/{tid}/scaffold")
|
|
async def scaffold_from_template(tid: str, project_name: str, variables: Dict[str, Any] = {}):
|
|
return {
|
|
"template_id": tid, "project_name": project_name, "variables": variables,
|
|
"scaffolded": True, "files_created": ["main.py", "models.py", "README.md", "Dockerfile", ".env.example"],
|
|
"next_steps": ["cd " + project_name, "pip install -r requirements.txt", "python main.py"],
|
|
"ts": datetime.utcnow().isoformat(),
|
|
}
|
|
|
|
# ── 환경 관리 ────────────────────────────────────────────────────────────
|
|
@router.get("/environments")
|
|
async def list_environments():
|
|
envs = list(_environments.values()) or [
|
|
{"id": "env-dev", "name": "개발", "type": "dev", "services": list(_catalog.keys())[:2], "health": "healthy"},
|
|
{"id": "env-prod", "name": "운영", "type": "prod", "services": list(_catalog.keys()), "health": "healthy"},
|
|
]
|
|
return {"environments": envs}
|
|
|
|
@router.post("/environments")
|
|
async def create_environment(env: EnvironmentCreate):
|
|
eid = f"env-{uuid.uuid4().hex[:8]}"
|
|
_environments[eid] = {**env.model_dump(), "id": eid, "health": "creating",
|
|
"created_at": datetime.utcnow().isoformat()}
|
|
return _environments[eid]
|
|
|
|
@router.get("/environments/{eid}/diff")
|
|
async def env_diff(eid: str, compare_with: str = "env-prod"):
|
|
return {"env1": eid, "env2": compare_with, "differences": [
|
|
{"type": "config", "key": "DB_URL", "env1": "localhost", "env2": "prod-db:5432"},
|
|
{"type": "version", "service": "guardia-itsm", "env1": "2.0.0", "env2": "2.1.0"},
|
|
]}
|
|
|
|
@router.post("/environments/{eid}/promote")
|
|
async def promote_environment(eid: str, target: str = Query(...)):
|
|
return {"from": eid, "to": target, "promoted": True, "ts": datetime.utcnow().isoformat()}
|
|
|
|
# ── 셀프서비스 인프라 ────────────────────────────────────────────────────
|
|
@router.post("/self-service")
|
|
async def self_service(req: SelfServiceRequest):
|
|
req_id = f"REQ-{uuid.uuid4().hex[:8].upper()}"
|
|
result = {
|
|
"request_id": req_id, "service": req.service, "action": req.action,
|
|
"params": req.params, "requested_by": req.requested_by,
|
|
"status": "approved", "auto_approved": True,
|
|
"ts": datetime.utcnow().isoformat(),
|
|
}
|
|
_requests[req_id] = result
|
|
return result
|
|
|
|
@router.get("/self-service")
|
|
async def list_requests(status: Optional[str] = None):
|
|
reqs = list(_requests.values())
|
|
if status: reqs = [r for r in reqs if r.get("status") == status]
|
|
return {"requests": reqs[-50:], "total": len(reqs)}
|
|
|
|
# ── 플랫폼 메트릭 ─────────────────────────────────────────────────────────
|
|
@router.get("/metrics")
|
|
async def platform_metrics():
|
|
return {
|
|
"services": {"total": len(_catalog), "healthy": len(_catalog), "degraded": 0},
|
|
"deployments_today": 8, "avg_deploy_time_min": 4.2,
|
|
"golden_path_adoption": 87.5, "self_service_requests_week": 34,
|
|
"developer_satisfaction": 4.7,
|
|
}
|
|
|
|
@router.get("/metrics/adoption")
|
|
async def golden_path_adoption():
|
|
return {
|
|
"fastapi_template": {"used": 12, "adoption_rate": 92.3},
|
|
"react_ts_template": {"used": 8, "adoption_rate": 88.1},
|
|
"ansible_template": {"used": 5, "adoption_rate": 76.4},
|
|
"overall": 87.5, "target": 95.0,
|
|
}
|
|
|
|
# ── 서비스 레벨 목표 ─────────────────────────────────────────────────────
|
|
@router.get("/slo")
|
|
async def list_slo():
|
|
return {"slos": [
|
|
{"service": "guardia-itsm", "availability_target": 99.9, "current": 99.95, "ok": True},
|
|
{"service": "guardia-manager", "availability_target": 99.5, "current": 99.8, "ok": True},
|
|
{"service": "guardia-messenger", "availability_target": 99.0, "current": 98.7, "ok": False},
|
|
]}
|
|
|
|
@router.post("/slo")
|
|
async def create_slo(service: str, availability_target: float, latency_p95_ms: int = 500):
|
|
slid = f"SLO-{uuid.uuid4().hex[:8].upper()}"
|
|
_service_levels[slid] = {"id": slid, "service": service,
|
|
"availability_target": availability_target,
|
|
"latency_p95_ms": latency_p95_ms,
|
|
"created_at": datetime.utcnow().isoformat()}
|
|
return _service_levels[slid]
|
|
|
|
@router.get("/health")
|
|
async def platform_health():
|
|
return {"status": "healthy", "services_monitored": len(_catalog),
|
|
"environments": len(_environments) or 2, "templates": 6}
|