Files
pulp-console/PLAN.md
Hyemin Lee 7ad6d65a11 feat: gated deploy with preview, audit log, rollback trail (stage 4)
- pulp_client: create_publication / update_distribution / wait_for_task
- GET /repos/{uuid}/deploy/confirm: 미리보기(현재→대상, 순 변화) + type-to-confirm 모달
- POST /repos/{uuid}/deploy: 검증 게이트(서버측 재확인) → publication 생성 →
  distribution 교체 → 감사 로그. 2단계 실패 시 '운영망 변경 여부' 명확화
- audit.record_deploy: 누가/언제/repo/이전버전→대상버전 JSONL (롤백 추적)
- 배포 버튼은 검증 통과 + 미배포 버전만 활성, 성공 시 버전목록 OOB 갱신
- 인증은 TODO (operator placeholder, 배포 비밀번호 재확인 예정)

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

175 lines
9.8 KiB
Markdown

# 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줄이 남는다.
---
## [ ] 단계 5 — 스타일 마감 + 폐쇄망 패키징
**목표:** 운영툴로서 보기 좋고, 인터넷 없이 그대로 구동/반입 가능.
작업:
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 전까지는 운영망에 영향 없는 안전 구간.