Files
pulp-console/app/routes/deploy.py
Hyemin Lee aef4124da0 refactor: audit log to Postgres + split /healthz liveness from Pulp status
- audit.record_deploy 를 JSONL 파일 → Postgres deploy_audit 테이블로 (재배포 휘발 방지)
  - psycopg 지연 import, _write 는 테스트에서 monkeypatch 지점, DATABASE_URL 사용
  - 감사 기록 실패는 배포 완료를 막지 않고 UI 경고(audit_ok)
- /healthz: Pulp 무관 라이브니스 {"ok":true} (컨테이너 health check),
  Pulp 연결 배지는 /pulp-status 로 분리 (대시보드가 폴링)
- config: DATABASE_URL 추가, audit_log_path 제거

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:42:11 +09:00

221 lines
7.4 KiB
Python

"""배포 확정 + 감사 로그 (화면 3 후반, 위험 동작 — 스펙 §5-2).
GET /repos/{uuid}/deploy/confirm → 확인 모달(미리보기 + type-to-confirm)
POST /repos/{uuid}/deploy → publication 생성 → distribution 교체 → 감사 로그
안전장치:
- 검증 게이트: 검증 통과(pass) 저장소만 배포 가능 (서버측에서도 재확인)
- type-to-confirm: 저장소 이름을 정확히 입력해야 실행
- 2단계 실패 명확화: 어느 단계에서 실패하든 "운영망 변경 여부"를 분명히 안내
- 감사 로그: 누가/언제/어떤 repo/어느 버전→어느 버전 (이전 버전 기록 = 롤백 추적)
"""
from __future__ import annotations
import httpx
from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import HTMLResponse
from .. import audit, views
from ..deps import get_pulp_client, templates
from ..pulp_client import PulpClient
from .versions import _deployed_version_number, load_version_context
router = APIRouter()
def _distribution_for_repo(pulp: PulpClient, uuid: str) -> str | None:
"""이 저장소에 연결된 distribution href. repository 직접 참조 또는 현재 publication 으로 매핑."""
needle = f"/repositories/rpm/rpm/{uuid}/"
for dist in pulp.list_distributions():
if needle in (dist.get("repository") or ""):
return dist.get("pulp_href")
pub_href = dist.get("publication")
if pub_href:
try:
pub = pulp.get_publication(pub_href)
except httpx.HTTPError:
continue
if needle in (pub.get("repository_version") or ""):
return dist.get("pulp_href")
return None
def _preview(pulp: PulpClient, uuid: str, version_href: str):
"""확인 모달용 컨텍스트(repo_name, gpg, preview, deployed)."""
repo = pulp.get_repo(uuid)
raw = pulp.list_versions(uuid)
gpg = views.gpg_status(repo)
deployed = _deployed_version_number(pulp, uuid)
vv = sorted(
(views.build_version_view(v, deployed, gpg) for v in raw),
key=lambda v: v["number"] or 0,
reverse=True,
)
target = views.version_number(version_href)
return {
"uuid": uuid,
"repo_name": repo.get("name", uuid),
"version_href": version_href,
"gpg": gpg,
"deployed": deployed,
"preview": views.deploy_preview(vv, target, deployed),
"target_number": target,
}
def _modal(request: Request, ctx: dict, error: str | None = None, status: int = 200):
return templates.TemplateResponse(
request,
"partials/deploy_modal.html",
{**ctx, "error": error},
status_code=status,
)
@router.get("/repos/{uuid}/deploy/confirm", response_class=HTMLResponse)
def deploy_confirm(
request: Request,
uuid: str,
version_href: str,
pulp: PulpClient = Depends(get_pulp_client),
) -> HTMLResponse:
try:
ctx = _preview(pulp, uuid, version_href)
except httpx.HTTPError as exc:
return templates.TemplateResponse(
request,
"partials/deploy_modal.html",
{
"uuid": uuid,
"repo_name": uuid,
"version_href": version_href,
"preview": None,
"error": f"조회 실패: {exc}",
},
status_code=502,
)
return _modal(request, ctx)
@router.post("/repos/{uuid}/deploy", response_class=HTMLResponse)
def deploy(
request: Request,
uuid: str,
version_href: str = Form(...),
confirm_name: str = Form(""),
pulp: PulpClient = Depends(get_pulp_client),
) -> HTMLResponse:
try:
ctx = _preview(pulp, uuid, version_href)
except httpx.HTTPError as exc:
return _modal(
request,
{
"uuid": uuid,
"repo_name": uuid,
"version_href": version_href,
"preview": None,
},
error=f"조회 실패: {exc}",
status=502,
)
repo_name = ctx["repo_name"]
# 1) 검증 게이트 (서버측 재확인 — 클라이언트만 믿지 않는다)
if ctx["gpg"]["level"] != "pass":
return _modal(
request, ctx, "검증을 통과하지 못한 저장소는 배포할 수 없습니다.", 403
)
# 2) type-to-confirm
if confirm_name != repo_name:
return _modal(request, ctx, "저장소 이름이 일치하지 않습니다.", 400)
# 3) 대상 distribution 확인
dist_href = _distribution_for_repo(pulp, uuid)
if not dist_href:
return _modal(
request, ctx, "이 저장소에 연결된 distribution 을 찾을 수 없습니다.", 404
)
# 4) publication 생성 (실패 시 운영망은 절대 변경되지 않음)
try:
pub_task = pulp.wait_for_task(pulp.create_publication(version_href))
except httpx.HTTPError as exc:
return _modal(
request,
ctx,
f"배포 실패(publication 생성). 운영망은 변경되지 않았습니다: {exc}",
502,
)
if pub_task.get("state") != "completed":
return _modal(
request,
ctx,
"배포 실패(publication 생성 단계). 운영망은 변경되지 않았습니다.",
502,
)
pub_href = next(
(h for h in pub_task.get("created_resources") or [] if "/publications/" in h),
None,
)
if not pub_href:
return _modal(
request,
ctx,
"publication 결과를 찾을 수 없습니다. 운영망은 변경되지 않았습니다.",
502,
)
# 5) distribution 교체 = 실제 운영 배포
try:
dist_task = pulp.wait_for_task(pulp.update_distribution(dist_href, pub_href))
except httpx.HTTPError as exc:
return _modal(
request,
ctx,
f"배포 실패(distribution 교체). 운영망 변경에 실패했습니다: {exc}",
502,
)
if dist_task.get("state") != "completed":
return _modal(
request,
ctx,
"배포 실패(distribution 교체 단계). 운영망 변경에 실패했습니다.",
502,
)
# 6) 감사 로그 (이전 버전 기록 = 롤백 추적). DB 기록 실패해도 배포는 이미 완료됐으므로
# 500 으로 죽이지 않고 audit_ok=False 로 UI 에 경고한다.
# TODO(운영): 감사 필수 정책이면 기록 실패 시 배포를 거부(hard-fail)하도록 강화.
audit_ok = True
try:
audit.record_deploy(
repo_uuid=uuid,
repo_name=repo_name,
from_version=ctx["deployed"],
to_version=ctx["target_number"],
publication_href=pub_href,
)
except Exception: # noqa: BLE001 - DB/드라이버 오류 등 기록 실패는 배포 완료를 막지 않음
audit_ok = False
# 7) 성공: 버전 목록 OOB 갱신 + 모달 성공 표시
try:
new_ctx = load_version_context(pulp, uuid)
except httpx.HTTPError:
new_ctx = {"versions": [], "uuid": uuid}
return templates.TemplateResponse(
request,
"partials/deploy_success.html",
{
**new_ctx,
"uuid": uuid,
"from_version": ctx["deployed"],
"to_version": ctx["target_number"],
"audit_ok": audit_ok,
},
)