diff --git a/PLAN.md b/PLAN.md index a3a2623..5617481 100644 --- a/PLAN.md +++ b/PLAN.md @@ -94,7 +94,7 @@ --- -## [ ] 단계 3 — 버전(스냅샷) 목록 + 검증 배지 (화면 3 전반) +## [x] 단계 3 — 버전(스냅샷) 목록 + 검증 배지 (화면 3 전반) **목표:** 저장소별 RepositoryVersion 목록을 최신순으로, 검증/배포 상태와 함께 표시. diff --git a/app/demo.py b/app/demo.py index a180f90..66605e1 100644 --- a/app/demo.py +++ b/app/demo.py @@ -69,6 +69,9 @@ class DemoPulpClient: return _VERSIONS.get(version_href, {}) def get_repo(self, uuid: str) -> dict[str, Any]: + for r in _REPOS: + if r["pulp_href"].rstrip("/").endswith(uuid): + return {**r, "remote": "/pulp/api/v3/remotes/rpm/rpm/demo/"} return {"remote": "/pulp/api/v3/remotes/rpm/rpm/demo/"} def sync_repo(self, uuid: str, remote_href: str) -> str: @@ -81,3 +84,52 @@ class DemoPulpClient: "progress_reports": [{"done": 120, "total": 120}], "created_resources": ["/pulp/api/v3/repositories/rpm/rpm/demo/versions/9/"], } + + def list_versions(self, uuid: str) -> list[dict[str, Any]]: + base = f"/pulp/api/v3/repositories/rpm/rpm/{uuid}/versions/" + return [ + { + "pulp_href": base + "1/", + "pulp_created": "2026-05-02T01:00:00Z", + "content_summary": { + "present": {"rpm.package": {"count": 4500}}, + "added": {"rpm.package": {"count": 4500}}, + "removed": {}, + }, + }, + { + "pulp_href": base + "2/", + "pulp_created": "2026-05-28T04:30:00Z", + "content_summary": { + "present": {"rpm.package": {"count": 4700}}, + "added": {"rpm.package": {"count": 250}}, + "removed": {"rpm.package": {"count": 50}}, + }, + }, + { + "pulp_href": base + "3/", + "pulp_created": "2026-06-15T02:11:00Z", + "content_summary": { + "present": {"rpm.package": {"count": 4821}}, + "added": {"rpm.package": {"count": 130}}, + "removed": {"rpm.package": {"count": 9}}, + }, + }, + ] + + def list_distributions(self) -> list[dict[str, Any]]: + # 저장소별 현재 배포 버전 (publication href 에 uuid-버전 인코딩) + deployed = {"0001": 2, "0002": 1, "0003": 3, "0004": 1} + return [ + {"publication": f"/pulp/api/v3/publications/rpm/rpm/{u}-{v}/"} + for u, v in deployed.items() + ] + + def get_publication(self, publication_href: str) -> dict[str, Any]: + tail = publication_href.rstrip("/").split("/")[-1] # "0001-2" + uuid, number = tail.split("-") + return { + "repository_version": ( + f"/pulp/api/v3/repositories/rpm/rpm/{uuid}/versions/{number}/" + ) + } diff --git a/app/main.py b/app/main.py index 566d690..dbc7b74 100644 --- a/app/main.py +++ b/app/main.py @@ -15,7 +15,7 @@ from fastapi.staticfiles import StaticFiles # 테스트 호환을 위해 re-export (tests 가 app.main.get_pulp_client 를 override 함) from .deps import get_pulp_client, templates from .pulp_client import PulpClient -from .routes import dashboard, sync +from .routes import dashboard, sync, versions BASE_DIR = Path(__file__).resolve().parent @@ -23,6 +23,7 @@ app = FastAPI(title="Pulp 패치 관리 콘솔") app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static") app.include_router(dashboard.router) app.include_router(sync.router) +app.include_router(versions.router) __all__ = ["app", "get_pulp_client"] diff --git a/app/pulp_client.py b/app/pulp_client.py index 8ad0fd7..1918e30 100644 --- a/app/pulp_client.py +++ b/app/pulp_client.py @@ -69,6 +69,23 @@ class PulpClient: """RepositoryVersion 단건 조회 (href 기준). content_summary 등 포함.""" return self.get(version_href) + def list_versions(self, uuid: str) -> list[dict[str, Any]]: + """저장소의 RepositoryVersion 목록: GET .../{uuid}/versions/ → results.""" + data = self.get(f"{API_PREFIX}/repositories/rpm/rpm/{uuid}/versions/") + return data.get("results", []) + + def list_distributions(self) -> list[dict[str, Any]]: + """Distribution 목록: GET /pulp/api/v3/distributions/rpm/rpm/ → results. + + 운영자가 yum/dnf baseurl 로 바라보는 공개 URL들. publication 으로 배포 버전 추적. + """ + data = self.get(f"{API_PREFIX}/distributions/rpm/rpm/") + return data.get("results", []) + + def get_publication(self, publication_href: str) -> dict[str, Any]: + """Publication 단건 조회 (repository_version 역추적용).""" + return self.get(publication_href) + def get_repo(self, uuid: str) -> dict[str, Any]: """저장소 단건 조회 (remote href 등 확인용).""" return self.get(f"{API_PREFIX}/repositories/rpm/rpm/{uuid}/") diff --git a/app/routes/versions.py b/app/routes/versions.py new file mode 100644 index 0000000..f299710 --- /dev/null +++ b/app/routes/versions.py @@ -0,0 +1,78 @@ +"""버전(스냅샷) 목록 + 검증 배지 (화면 3 전반). + +GET /repos/{uuid}/versions → 해당 저장소의 RepositoryVersion 목록 페이지(최신순). + +현재 운영 배포 버전은 Distribution → publication → repository_version 역추적으로 판별한다 +(스펙 §4). 배포 버튼 활성화/확인은 단계 4에서 연결. +""" + +from __future__ import annotations + +import httpx +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse + +from .. import views +from ..deps import get_pulp_client, templates +from ..pulp_client import PulpClient + +router = APIRouter() + + +def _deployed_version_number(pulp: PulpClient, uuid: str) -> int | None: + """이 저장소가 현재 운영 배포 중인 버전 번호. 없거나 조회 실패 시 None. + + Distribution.publication → Publication.repository_version 이 이 저장소의 + 버전을 가리키면 그 번호를 반환. + """ + needle = f"/repositories/rpm/rpm/{uuid}/versions/" + try: + distributions = pulp.list_distributions() + except httpx.HTTPError: + return None + + for dist in distributions: + pub_href = dist.get("publication") + if not pub_href: + continue # repository 직접 배포(최신 추종)는 단계4 TODO + try: + pub = pulp.get_publication(pub_href) + except httpx.HTTPError: + continue + rv = pub.get("repository_version") or "" + if needle in rv: + return views.version_number(rv) + return None + + +@router.get("/repos/{uuid}/versions", response_class=HTMLResponse) +def versions( + request: Request, uuid: str, pulp: PulpClient = Depends(get_pulp_client) +) -> HTMLResponse: + try: + repo = pulp.get_repo(uuid) + raw_versions = pulp.list_versions(uuid) + except httpx.HTTPError as exc: + return templates.TemplateResponse( + request, + "versions.html", + {"repo_name": uuid, "uuid": uuid, "error": str(exc)}, + ) + + gpg = views.gpg_status(repo) + deployed = _deployed_version_number(pulp, uuid) + version_views = [views.build_version_view(v, deployed, gpg) for v in raw_versions] + version_views.sort(key=lambda v: v["number"] or 0, reverse=True) # 최신순 + + return templates.TemplateResponse( + request, + "versions.html", + { + "repo_name": repo.get("name", uuid), + "uuid": uuid, + "versions": version_views, + "verification": views.verification_detail(repo), + "deployed": deployed, + "error": None, + }, + ) diff --git a/app/static/app.css b/app/static/app.css index 94eeae2..941b865 100644 --- a/app/static/app.css +++ b/app/static/app.css @@ -205,6 +205,29 @@ th.num { color: var(--text-muted); background: var(--surface); } +.badge-deployed { + color: var(--accent-hover); + background: var(--accent-soft); +} + +/* 버전 검증 상세 */ +.verify-detail { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} +.verify-detail small { + color: var(--text-muted); +} + +/* 패키지 증감 */ +.delta-add { + color: var(--success-text); +} +.delta-del { + color: var(--danger); +} /* 보조 버튼: 틴티드 블루 (design-ref §4 two-step blue) */ button.secondary, diff --git a/app/templates/partials/repo_list.html b/app/templates/partials/repo_list.html index 3b5d22a..57834e3 100644 --- a/app/templates/partials/repo_list.html +++ b/app/templates/partials/repo_list.html @@ -25,7 +25,7 @@
{% for repo in repos %}| 버전 | +생성일시 | +패키지 | +증감 | +검증 | +배포 상태 | ++ |
|---|---|---|---|---|---|---|
| v{{ v.number }} | +{{ v.created or "—" }} | +{{ v.package_count }} | ++ {%- if v.added %}+{{ v.added }}{% endif -%} + {%- if v.removed %} −{{ v.removed }}{% endif -%} + {%- if not v.added and not v.removed %}—{% endif -%} + | +{% if v.gpg %}{{ v.gpg.label }}{% endif %} | +{% if v.is_deployed %}운영 배포 중{% endif %} | ++ {% if not v.is_deployed %} + + + {% endif %} + | +
버전이 없습니다. 먼저 동기화를 실행하세요.
+ {% endif %} +버전 목록을 불러오지 못했습니다: {{ error }}
+ {% else %} +