- 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>
8.1 KiB
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 0–4 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
httpxto 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 theref/design-ref.mdtokens (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
httpxclient must accept averify=PULP_CA_FILEoption 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:
- 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. - Deploy (create Publication →
PATCHthe Distribution'spublication): 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_configGPG-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, tabledeploy_audit): who / when / repo / from-version → to-version (rollback trail). DB write failure does not 500 a completed deploy — it surfaces anaudit_ok=Falsewarning (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 (
operatoris 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)
- Skeleton: FastAPI + Jinja2 + httpx,
pulp_client.pystub,/healthzhitting Pulp status. - Dashboard (read-only repo list/cards).
- Sync:
POSTsync → task_href → polling progress fragment. - Version list + verification badges.
- Deploy: confirm modal → create publication → PATCH distribution → poll → audit log.
- Styling, bundle static assets for air-gap, containerize.
Each step should produce a working screen. Do not build everything at once.
Commands
.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 usespython:3.13-slim). Reference Pulp objects bypulp_href, not name. - Each stage should produce a working screen and stay green (tests + ruff) before commit.