Files
pulp-console/CLAUDE.md
Hyemin Lee b24d76ad08 chore: containerize for Coolify + docs (stage 5)
- Dockerfile (python:3.13-slim, uvicorn, 비루트, PORT, /healthz HEALTHCHECK)
- .dockerignore
- README: 로컬/컨테이너/Coolify 배포 + 폐쇄망 wheelhouse 반입 절차
- CLAUDE.md 갱신(현재 구현 상태/라우트/명령/배포), MANUAL.md 추가
- 정적자산은 이미 로컬 동봉(런타임 CDN 0) → 최종 폐쇄망 반입은 의존성 wheelhouse만

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

159 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A thin, self-hosted **web admin console for Pulp 3**, a Linux patch-repository
server. Pulp has a full REST API (`/pulp/api/v3/`) but no usable official web UI,
so this project builds a focused operator console on top of that API.
Context: it runs in an **air-gapped (closed) network** at the Bank of Korea IT
Strategy Dept. cloud team. The console lets operators mirror, snapshot, verify,
and deploy RPM/DEB patch repositories by clicking instead of running CLI commands.
The full implementation spec is **`pulp-console-spec.md`** (Korean). `PLAN.md`
tracks staged progress (checkboxes); `ref/design-ref.md` is the visual design
standard; `MANUAL.md` is the Coder/Gitea/Coolify dev+deploy environment guide.
Status: stages 04 are implemented and tested (dashboard, sync+polling, version
list, gated deploy+audit). Stage 5 = containerization/packaging. Code lives under
`app/` (`config.py`, `pulp_client.py`, `deps.py`, `views.py`, `audit.py`,
`task_store.py`, `routes/`, `templates/`, `static/`); tests under `tests/`.
## Stack (decided — do not substitute)
- **Backend / BFF:** FastAPI (Python), using `httpx` to call Pulp.
- **Frontend:** HTMX + Jinja2 templates. **No build step.** Buttons trigger
partial-HTML swaps; progress polling is done with `hx-trigger="every 3s"`.
- **Styling:** Pico.css + a custom layer (`app/static/app.css`) applying the
`ref/design-ref.md` tokens (Toss-style). Pretendard font is vendored locally.
No decorative emoji in chrome — status uses colored chips/dots.
## Hard constraints (these are the point of the project)
- **The browser never calls the Pulp API directly.** All traffic goes through
the FastAPI BFF, which holds Pulp Basic Auth credentials and proxies requests.
This protects credentials and avoids CORS exposure.
- **No heavy frontend tooling** (React, Vite, npm build chains). HTML + HTMX only.
- **Air-gap first:** minimize CDN dependence. Bundle CSS / HTMX JS under
`static/` so the app runs with no internet access.
- **View routes return HTML** (full pages or fragments), **not JSON** — HTMX
consumes HTML.
- **Self-signed CA support from day one:** the `httpx` client must accept a
`verify=PULP_CA_FILE` option for the internal TLS CA (boknet CA family).
## Architecture
```
Browser (HTMX + Jinja) ──form/button──► FastAPI BFF ──Basic Auth──► Pulp REST API (/pulp/api/v3/)
◄──── partial HTML ──── ◄──────── JSON ────────
```
The BFF receives Pulp JSON, renders it into Jinja partial templates, and returns
HTML fragments that HTMX swaps into the page.
### Pulp object model (essential to understand the flow)
`Remote (source def) → Repository (container) → RepositoryVersion (snapshot) →
Publication (deployable form) → Distribution (public URL)`
The URL operators point `yum/dnf` `baseurl` at is the **Distribution**.
Two operations matter:
1. **Sync** (`POST .../repositories/rpm/rpm/{uuid}/sync/`): pulls from an external
mirror and creates a new RepositoryVersion snapshot. Relatively safe — does
not change what production servers receive. Returns an async **task**.
2. **Deploy** (create Publication → `PATCH` the Distribution's `publication`):
**dangerous** — changes the version production actually fetches.
Pulp objects reference each other by `pulp_href` (a path string); **use href, not
name, in code.** Sync and deploy return async **tasks** — capture `task_href` and
poll `GET {task_href}` (`state`: waiting/running/completed/failed) for completion.
RPM paths use `rpm/rpm`; the DEB equivalent swaps that segment for `deb/apt`.
## Safety model (deploy is gated) — implemented in `routes/deploy.py`
- **Verification gate** (re-checked server-side): only `repo_config` GPG-passing
repos are deployable; the button is disabled otherwise. Note verification is
**repo-level config**, so it indicates "verification is configured", not a
per-snapshot cryptographic proof (real package checks happen at sync time).
- **Confirmation modal** with a **preview** (current → target version, net package
delta) and **type-to-confirm** (operator must type the repo name).
- **Audit log → Postgres** (`audit.py`, table `deploy_audit`): who / when / repo /
from-version → to-version (rollback trail). DB write failure does not 500 a
completed deploy — it surfaces an `audit_ok=False` warning (TODO: hard-fail if
audit becomes mandatory policy).
- **Two-step clarity**: deploy = create publication → PATCH distribution; on
failure the UI states clearly whether production was changed.
- **Auth is a TODO** (`operator` is a placeholder; deploy-time password re-entry planned).
## Route surface (BFF)
```
GET / dashboard (full page)
GET /repos repo list fragment
POST /repos/{uuid}/sync start sync, return progress-bar fragment
GET /repos/{uuid}/progress progress-bar fragment (polled every 3s)
GET /repos/{uuid}/versions version list page
GET /repos/{uuid}/deploy/confirm deploy confirmation modal (preview + type-to-confirm)
POST /repos/{uuid}/deploy gated deploy (publication + distribution) + audit
GET /healthz app liveness — always {"ok": true} (container health)
GET /pulp-status Pulp connectivity badge (dashboard polls this)
```
Pulp access is centralized in `pulp_client.py` (`get`/`post`/`patch` with Basic
Auth, plus `status`, `list_repos`, `get_repo`, `get_version`, `list_versions`,
`sync_repo`, `get_task`, `list_distributions`, `get_publication`,
`create_publication`, `update_distribution`, `wait_for_task`). HTML polling is the
*page's* job; the client only single-fetches — except `wait_for_task`, used only
for the compound deploy (publication→distribution) where server-side waiting is
needed. In-flight syncs are tracked in `task_store.py` (in-memory; single
instance only). View-model shaping (pure, httpx-free) lives in `views.py`.
## Configuration (env vars)
`PULP_BASE_URL`, `PULP_USERNAME`, `PULP_PASSWORD` (secret), `PULP_VERIFY_TLS`,
`PULP_CA_FILE` (internal CA pem), `DATABASE_URL` (Postgres for audit log),
`PORT` (default 8000), `PULP_DEMO` (fake data for screen preview, default off).
## Build order (from the spec — build in working-screen increments)
1. Skeleton: FastAPI + Jinja2 + httpx, `pulp_client.py` stub, `/healthz` hitting Pulp status.
2. Dashboard (read-only repo list/cards).
3. Sync: `POST` sync → task_href → polling progress fragment.
4. Version list + verification badges.
5. Deploy: confirm modal → create publication → PATCH distribution → poll → audit log.
6. Styling, bundle static assets for air-gap, containerize.
Each step should produce a working screen. Do not build everything at once.
## Commands
```bash
.venv/Scripts/python.exe -m uvicorn app.main:app --reload # run (http://127.0.0.1:8000)
.venv/Scripts/python.exe -m pytest -q # tests
.venv/Scripts/python.exe -m ruff check app tests # lint
.venv/Scripts/python.exe -m ruff format app tests # format
```
Tests mock the Pulp client via FastAPI `dependency_overrides` (route tests) or
`respx` (client tests); `views.py` is unit-tested as pure functions. Audit DB
writes are intercepted by monkeypatching `app.audit._write`.
## Deployment
Now (demo): **Coolify** builds the repo `Dockerfile` (Build Pack: Dockerfile,
Port 8000) and injects env vars — see `README.md` / `MANUAL.md`. `/healthz` is
liveness only so Pulp being down doesn't fail the container.
Final target is the **air-gapped** network: static assets are already vendored
(no runtime CDN); only Python deps need offline reintroduction via a wheelhouse
(`pip download` → swap the Dockerfile install step). Procedure in `README.md`.
## Conventions
- Python 3.14 local venv; deps pinned in `requirements.txt` (container uses
`python:3.13-slim`). Reference Pulp objects by `pulp_href`, not name.
- Each stage should produce a working screen and stay green (tests + ruff) before commit.