"""IDP 소프트웨어 카탈로그 — Backstage-style 서비스 등록·조회""" from __future__ import annotations import json, logging from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy import select, desc from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import User, IDPComponent logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/idp/catalog", tags=["IDP 카탈로그"]) class ComponentCreate(BaseModel): name: str; display_name: str = ""; component_type: str = "service" description: str = ""; language: str = ""; framework: str = "" gitea_repo: str = ""; ci_job: str = ""; docs_url: str = "" owner_team: str = ""; tags: list = []; lifecycle: str = "production" @router.get("") async def list_components( q: Optional[str] = None, type_: Optional[str] = Query(None, alias="type"), limit: int = Query(50, le=200), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): stmt = select(IDPComponent).order_by(IDPComponent.name).limit(limit) if q: stmt = stmt.where(IDPComponent.name.contains(q) | IDPComponent.description.contains(q)) if type_: stmt = stmt.where(IDPComponent.component_type == type_) rows = await db.execute(stmt) comps = rows.scalars().all() return [{"id":c.id,"name":c.name,"display_name":c.display_name,"component_type":c.component_type, "language":c.language,"lifecycle":c.lifecycle,"owner_team":c.owner_team} for c in comps] @router.post("", status_code=201) async def register_component(body: ComponentCreate, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): comp = IDPComponent( **{k: v for k, v in body.model_dump().items() if k != "tags"}, tags=json.dumps(body.tags), registered_by=user.id, created_at=datetime.utcnow() ) db.add(comp); await db.commit(); await db.refresh(comp) return {"id": comp.id} @router.get("/{comp_id}") async def get_component(comp_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): row = await db.execute(select(IDPComponent).where(IDPComponent.id == comp_id)) comp = row.scalar_one_or_none() if not comp: raise HTTPException(404) return { "id": comp.id, "name": comp.name, "display_name": comp.display_name, "component_type": comp.component_type, "description": comp.description, "language": comp.language, "framework": comp.framework, "gitea_repo": comp.gitea_repo, "ci_job": comp.ci_job, "docs_url": comp.docs_url, "owner_team": comp.owner_team, "tags": json.loads(comp.tags or "[]"), "lifecycle": comp.lifecycle, "created_at": comp.created_at, } @router.put("/{comp_id}") async def update_component(comp_id: int, body: ComponentCreate, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): row = await db.execute(select(IDPComponent).where(IDPComponent.id == comp_id)) comp = row.scalar_one_or_none() if not comp: raise HTTPException(404) for k, v in body.model_dump().items(): if k == "tags": comp.tags = json.dumps(v) else: setattr(comp, k, v) await db.commit(); return {"ok": True} @router.delete("/{comp_id}") async def delete_component(comp_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): row = await db.execute(select(IDPComponent).where(IDPComponent.id == comp_id)) comp = row.scalar_one_or_none() if not comp: raise HTTPException(404) await db.delete(comp); await db.commit(); return {"ok": True} @router.get("/{comp_id}/deps") async def get_deps(comp_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): row = await db.execute(select(IDPComponent).where(IDPComponent.id == comp_id)) comp = row.scalar_one_or_none() if not comp: raise HTTPException(404) deps = json.loads(comp.dependencies or "[]") return {"component": comp.name, "dependencies": deps} @router.get("/{comp_id}/health") async def get_health(comp_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): row = await db.execute(select(IDPComponent).where(IDPComponent.id == comp_id)) comp = row.scalar_one_or_none() if not comp: raise HTTPException(404) return {"component": comp.name, "status": "UNKNOWN", "last_deploy": comp.created_at, "note": "CI/CD 연동 시 실시간 상태 제공"} @router.get("/search") async def search(q: str = "", db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): stmt = select(IDPComponent).where( IDPComponent.name.contains(q) | IDPComponent.description.contains(q) | IDPComponent.tags.contains(q) ).limit(20) rows = await db.execute(stmt) return [{"id":c.id,"name":c.name,"type":c.component_type,"language":c.language} for c in rows.scalars().all()]