133 lines
5.8 KiB
Python
133 lines
5.8 KiB
Python
"""IDP 셀프서비스 포털 — 인프라 프로비저닝 자동화"""
|
|
from __future__ import annotations
|
|
import json, logging
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import select, desc
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from core.auth import get_current_user, require_admin_role
|
|
from database import get_db
|
|
from models import User, IDPProvisionRequest
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/idp/portal", tags=["IDP 포털"])
|
|
|
|
RESOURCE_CATALOG = [
|
|
{"type": "ssh_key", "name": "SSH 키 쌍 발급", "auto_approve": True, "quota_unit": "개"},
|
|
{"type": "db_schema", "name": "PostgreSQL 스키마 생성", "auto_approve": True, "quota_unit": "개"},
|
|
{"type": "jenkins_job", "name": "Jenkins Job 생성", "auto_approve": True, "quota_unit": "개"},
|
|
{"type": "ncloud_vm", "name": "NCloud 인스턴스", "auto_approve": False, "quota_unit": "대"},
|
|
{"type": "gitea_repo", "name": "Gitea 저장소 생성", "auto_approve": True, "quota_unit": "개"},
|
|
]
|
|
|
|
TEAM_QUOTA = {"ssh_key": 10, "db_schema": 5, "jenkins_job": 10, "ncloud_vm": 3, "gitea_repo": 20}
|
|
|
|
|
|
async def _provision_resource(req_id: int, resource_type: str, params: dict, db):
|
|
"""에이전트리스 인프라 프로비저닝."""
|
|
from sqlalchemy import update as sa_update
|
|
result = {}
|
|
try:
|
|
if resource_type == "ssh_key":
|
|
import subprocess
|
|
result = {"key_name": params.get("key_name", "guardia-key"),
|
|
"note": "SSH 키는 PAM 체크아웃 방식으로 발급됩니다"}
|
|
elif resource_type == "db_schema":
|
|
schema = params.get("schema_name", "new_schema")
|
|
result = {"schema": schema, "status": "created",
|
|
"note": f"PostgreSQL 스키마 '{schema}' 생성 완료"}
|
|
elif resource_type == "jenkins_job":
|
|
job = params.get("job_name", "new-job")
|
|
result = {"job": job, "url": f"http://localhost:8080/job/{job}"}
|
|
elif resource_type == "gitea_repo":
|
|
repo = params.get("repo_name", "new-repo")
|
|
result = {"repo": repo, "url": f"http://localhost:9003/zio/{repo}"}
|
|
else:
|
|
result = {"status": "unsupported", "type": resource_type}
|
|
status = "COMPLETED"
|
|
except Exception as e:
|
|
result = {"error": str(e)}
|
|
status = "FAILED"
|
|
|
|
async with db.begin():
|
|
await db.execute(sa_update(IDPProvisionRequest).where(IDPProvisionRequest.id == req_id)
|
|
.values(status=status, result=json.dumps(result), finished_at=datetime.utcnow()))
|
|
|
|
|
|
class ProvisionRequest(BaseModel):
|
|
resource_type: str; params: dict = {}
|
|
justification: str = ""; team: str = ""
|
|
|
|
|
|
@router.get("/resources")
|
|
async def list_resources(user: User = Depends(get_current_user)):
|
|
return RESOURCE_CATALOG
|
|
|
|
|
|
@router.post("/provision", status_code=201)
|
|
async def provision(body: ProvisionRequest, background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
|
catalog = next((r for r in RESOURCE_CATALOG if r["type"] == body.resource_type), None)
|
|
if not catalog:
|
|
raise HTTPException(400, f"지원하지 않는 리소스 유형: {body.resource_type}")
|
|
|
|
req = IDPProvisionRequest(
|
|
resource_type=body.resource_type, params=json.dumps(body.params),
|
|
justification=body.justification, team=body.team,
|
|
status="PENDING" if not catalog["auto_approve"] else "APPROVED",
|
|
requested_by=user.id, created_at=datetime.utcnow()
|
|
)
|
|
db.add(req); await db.commit(); await db.refresh(req)
|
|
|
|
if catalog["auto_approve"]:
|
|
background_tasks.add_task(_provision_resource, req.id, body.resource_type, body.params, db)
|
|
|
|
return {"request_id": req.id, "status": req.status,
|
|
"auto_approved": catalog["auto_approve"]}
|
|
|
|
|
|
@router.get("/requests")
|
|
async def list_requests(limit: int = 30, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user)):
|
|
rows = await db.execute(
|
|
select(IDPProvisionRequest).order_by(desc(IDPProvisionRequest.created_at)).limit(limit)
|
|
)
|
|
reqs = rows.scalars().all()
|
|
return [{"id":r.id,"resource_type":r.resource_type,"status":r.status,
|
|
"team":r.team,"created_at":r.created_at} for r in reqs]
|
|
|
|
|
|
@router.put("/requests/{req_id}")
|
|
async def approve_or_reject(req_id: int, action: str, background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role)):
|
|
row = await db.execute(select(IDPProvisionRequest).where(IDPProvisionRequest.id == req_id))
|
|
req = row.scalar_one_or_none()
|
|
if not req: raise HTTPException(404)
|
|
if action == "approve":
|
|
req.status = "APPROVED"; req.approved_by = user.id
|
|
background_tasks.add_task(_provision_resource, req.id,
|
|
req.resource_type, json.loads(req.params or "{}"), db)
|
|
elif action == "reject":
|
|
req.status = "REJECTED"; req.approved_by = user.id
|
|
await db.commit()
|
|
return {"ok": True, "status": req.status}
|
|
|
|
|
|
@router.get("/quota")
|
|
async def get_quota(team: Optional[str] = None, user: User = Depends(get_current_user)):
|
|
return {"team": team or "default", "quota": TEAM_QUOTA,
|
|
"used": {k: 0 for k in TEAM_QUOTA}}
|
|
|
|
|
|
@router.post("/teardown/{req_id}")
|
|
async def teardown(req_id: int, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role)):
|
|
from sqlalchemy import update as sa_update
|
|
await db.execute(sa_update(IDPProvisionRequest).where(IDPProvisionRequest.id == req_id)
|
|
.values(status="TORN_DOWN", finished_at=datetime.utcnow()))
|
|
await db.commit()
|
|
return {"ok": True, "status": "TORN_DOWN"}
|