# PLAN.md — Pulp 패치 관리 콘솔 구현 계획 `pulp-console-spec.md`를 근거로 한 단계별 작업 계획. 각 단계는 **"동작하는 화면/엔드포인트"가 나오는 단위**로 끊는다. 한 번에 다 만들지 않는다. 각 단계 끝에 검증 방법과 커밋 포인트를 둔다. 원칙(스펙 §2, §5 재확인): - 브라우저는 Pulp API를 직접 호출하지 않는다. 모든 호출은 FastAPI(BFF) 경유. - 프론트 빌드 도구(React/Vite) 금지. HTMX + Jinja2만. - 정적 자산(Pico.css, htmx.min.js)은 `static/`에 동봉 — CDN 의존 금지(폐쇄망). - 화면용 라우트는 JSON이 아니라 **HTML(전체 페이지 또는 조각)** 을 반환. - 위험 동작(배포 확정)은 확인 + 검증 게이트 + 감사 로그. --- ## [x] 단계 0 — 프로젝트 골격 + 설정 + 헬스체크 **목표:** 서버가 뜨고 `/healthz`가 Pulp `status`를 프록시해 "연결됨/끊김"을 보여준다. 작업: 1. 의존성 고정: `requirements.txt` (`fastapi`, `uvicorn[standard]`, `httpx`, `jinja2`, `python-multipart`, `pydantic-settings`). 폐쇄망 반입 위해 버전 핀. 2. 디렉터리 구조 생성: ``` app/ main.py # FastAPI 인스턴스, 라우터 등록, 정적/템플릿 마운트 config.py # pydantic-settings로 환경변수 로드 (§8) pulp_client.py # Pulp 호출 래퍼 (스텁) routes/ __init__.py dashboard.py templates/ base.html partials/ static/ pico.min.css htmx.min.js tests/ .env.example ``` 3. `config.py`: `PULP_BASE_URL`, `PULP_USERNAME`, `PULP_PASSWORD`, `PULP_VERIFY_TLS`, `PULP_CA_FILE` 로드. `httpx` 클라이언트 생성 시 `verify=` 에 `PULP_CA_FILE`(있으면 경로, 없고 VERIFY_TLS=false면 False) 적용 — 사내 self-signed CA 대응을 **처음부터** 넣는다. 4. `pulp_client.py` 골격: `get/post/patch` (Basic Auth 포함, base_url 결합), `status()`. 5. `GET /healthz`: `pulp_client.status()` 호출 결과를 JSON 또는 작은 HTML 배지로 반환. 6. `.env.example` 작성. 실제 `.env`는 커밋 금지(`.gitignore`). **검증:** `uvicorn app.main:app --reload` → `/healthz`가 Pulp 연결 상태 반영. (Pulp 미연결 환경이면 httpx 예외를 잡아 "연결 실패" 표시까지 확인.) --- ## [x] 단계 1 — 대시보드 (화면 1, 읽기 전용) **목표:** 저장소 목록을 표/카드로 렌더. 조작 버튼은 아직 동작 안 해도 됨(자리만). 작업: 1. `pulp_client.list_repos()` — `GET /pulp/api/v3/repositories/rpm/rpm/` 결과 파싱. 2. 각 저장소에 대해 표시용 모델로 가공: 이름, 현재 운영 배포 버전(vN), 마지막 동기화 시각, 패키지 수, GPG 검증 배지 근거(`repo_config`의 `gpgcheck`/`repo-gpgcheck`). - 현재 배포 버전은 Distribution → publication → repository_version 역추적이 필요할 수 있음. 이 단계에선 가능한 범위까지만 표시하고 TODO 주석으로 남긴다. 3. `GET /` → `base.html` + 대시보드 본문(전체 페이지). 4. `GET /repos` → 저장소 목록 **조각**(새로고침/폴링용 partial). `/`는 이 조각을 include. 5. 상단 요약 카드: 전체 저장소 수 / 동기화 중 수 / GPG 통과 수 / 총 패키지 수. 6. 검증 배지 컴포넌트: ✅ 통과 / ⚠️ 경고 / ⚪ 미설정 3단계(화면 4 로직을 여기서 함수로). **검증:** `/`에서 실제 저장소 목록과 배지가 보인다. `/repos` 조각만 단독 요청해도 렌더된다. --- ## [x] 단계 2 — 동기화 실행 + 진행률 폴링 (화면 2) **목표:** [동기화] 클릭 → sync task 시작 → 3초 폴링으로 진행률 갱신 → 완료/실패 표시. 작업: 1. `pulp_client.sync_repo(uuid, remote_href)` — `POST .../repositories/rpm/rpm/{uuid}/sync/` body `{"remote": remote_href}` → 반환된 `task_href` 보관. - remote_href는 저장소에 연결된 remote를 조회해 결정(또는 repo 객체의 `remote` 필드). 2. `POST /repos/{uuid}/sync` → sync 시작 후 **진행률 바 조각** 반환. 이 조각은 `hx-get="/repos/{uuid}/progress" hx-trigger="every 3s"` 를 포함. - task_href를 클라이언트로 어떻게 넘길지: 진행률 조각/폴링 URL에 task_href를 쿼리로 싣거나, 서버측에 `{uuid: task_href}` 임시 매핑(메모리 dict)으로 보관. 폐쇄망 단일 인스턴스이므로 메모리 매핑으로 시작하고, 다중 워커 시 한계는 주석으로 명시. 3. `pulp_client.get_task(task_href)` — `state`(waiting/running/completed/failed), `progress_reports[]`(done/total) 파싱. 4. `GET /repos/{uuid}/progress` → 진행률 바 조각: - running: `n/m 패키지, NN%` + 진행률 바, 계속 폴링. - completed: "동기화 완료, 새 버전 vN 생성됨" 배지로 교체(폴링 중단 — `hx-trigger` 제거). - failed: 빨간 에러 + task의 `error` 필드 사유. 5. 동기화는 비교적 안전(스펙 §5-1) → 확인 모달 없이 바로 실행 허용. **검증:** 실제 sync 실행 시 진행률이 3초마다 갱신되고, 완료 시 새 버전 안내가 뜬다. 폴링 종료(완료/실패 후 더 이상 요청 안 감) 확인. --- ## [x] 단계 3 — 버전(스냅샷) 목록 + 검증 배지 (화면 3 전반) **목표:** 저장소별 RepositoryVersion 목록을 최신순으로, 검증/배포 상태와 함께 표시. 작업: 1. `pulp_client.list_versions(uuid)` — `GET .../repositories/rpm/rpm/{uuid}/versions/`. 2. `GET /repos/{uuid}/versions` → 버전 목록 조각/페이지: - 각 버전: vN, 생성일시, 패키지 수(가능하면 증감), 검증 상태, **현재 운영 배포 여부 배지**. - 현재 배포 버전 판별: Distribution의 publication → repository_version 과 비교. 3. 검증 상태(화면 4): GPG 서명, checksum 타입(sha256 등), `repo_config.gpgcheck` 값을 ✅/⚠️/⚪ 로. 단계 1의 배지 함수를 재사용. 4. 배포 버튼 자리만 둔다(다음 단계에서 활성화 로직 연결). **검증:** 한 저장소의 버전들이 최신순으로, 검증/배포 배지와 함께 보인다. --- ## [x] 단계 4 — 배포 확정 + 감사 로그 (화면 3 후반, 위험 동작) **목표:** 검증 완료 + 미배포 버전만 배포 가능. 확인 모달 → publication 생성 → distribution PATCH → task 폴링 → 배지 갱신 → 감사 로그 기록. 작업: 1. 배포 버튼 활성화 게이트(스펙 §5-2): - "검증 완료(GPG/checksum 통과)"가 아닌 버전 → 버튼 **비활성화**. - 이미 현재 배포 중인 버전 → 버튼 숨김/비활성. 2. 확인 단계: `hx-confirm` 또는 별도 확인 모달 조각("이 버전을 운영망에 배포합니다"). 3. `POST /repos/{uuid}/deploy` body: 선택한 `version_href`: - `pulp_client.create_publication(version_href)` → `POST /publications/rpm/rpm/` body `{"repository_version": version_href}` → task → 폴링으로 publication_href 확보. - `pulp_client.update_distribution(dist_href, publication_href)` → `PATCH {dist_href}` body `{"publication": publication_href}` → task 폴링. - 대상 distribution_href는 저장소에 매핑되는 distribution을 조회해 결정. 4. 완료 시: 결과 조각 반환, 해당 버전에 "현재 운영 배포" 배지 갱신. 5. **감사 로그**(스펙 §5-2): 누가 / 언제 / 어떤 repo / 어떤 버전 → 으로 배포. - 최소 구현: append-only 파일(JSONL) 또는 SQLite. 시각은 서버 시각. - "누가"는 인증 도입 전까지 placeholder(예: 단일 운영자) — §5-3 권한 분리 시 확장. **검증:** 미검증 버전은 배포 버튼이 막혀 있다. 검증된 버전을 확인 모달 거쳐 배포하면 운영 배포 배지가 그 버전으로 옮겨가고, 감사 로그에 1줄이 남는다. --- ## [x] 단계 5 — 스타일 마감 + 폐쇄망 패키징 (배포: Coolify, 빌드 검증은 워크스페이스에서) **목표:** 운영툴로서 보기 좋고, 인터넷 없이 그대로 구동/반입 가능. 작업: 1. Pico.css(권장) 또는 Tailwind standalone으로 표·버튼·배지·진행률 바 정돈. 모든 CSS/JS는 `static/`에 동봉, 템플릿은 로컬 경로만 참조(CDN 링크 0개 확인). 2. 컨테이너화: `Dockerfile`(또는 단순 venv + 실행 스크립트). 의존성은 사전 다운로드한 wheel/오프라인 인덱스로 설치 가능하게(litellm 반입 방식 참고, 스펙 §9). 3. 실행 문서화: `README` 또는 CLAUDE.md "Conventions"에 기동 명령/환경변수 정리. 4. (선택, §5-3) 읽기전용 / 배포권한 계정 분리 여지를 남긴 인증 훅 위치 표시. 5. 디자인 가이드는 ref 하위의 markdown 파일들을 참고. **검증:** 네트워크 차단 상태에서 컨테이너/venv만으로 앱이 뜨고 모든 정적 자산이 로드된다. --- ## 교차 관심사 (전 단계 공통) - **에러 처리:** httpx 타임아웃/연결 실패/4xx·5xx를 잡아 화면에 사람이 읽을 수 있는 메시지로. Pulp task `failed` 시 `error` 필드 노출. - **href 기준:** Pulp 객체는 이름이 아니라 `pulp_href`로 참조(스펙 §4). 코드 전반 href 우선. - **비동기 task:** sync/deploy는 task 반환 → 반드시 폴링. 폴링 주체는 **화면(HTMX)**, `pulp_client`는 단발 조회만(스펙 §7). - **DEB 지원:** RPM 경로 `rpm/rpm` 자리를 `deb/apt`로. 1차 MVP는 RPM에 집중하되, `pulp_client`에서 content-type을 파라미터화해 확장 여지를 둔다. - **테스트:** `pulp_client`는 httpx mock(`respx` 등)으로 단위 테스트. 라우트는 `TestClient`로 조각 HTML에 기대 요소(배지/진행률/버튼 비활성)가 있는지 검사. ## 권장 진행 순서 요약 0 골격·헬스체크 → 1 대시보드(읽기) → 2 동기화·폴링 → 3 버전목록·검증배지 → 4 배포확정·감사로그 → 5 스타일·폐쇄망 패키징. 각 단계 완료 시 동작 확인 후 커밋. 단계 4 전까지는 운영망에 영향 없는 안전 구간.