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

8.1 KiB
Raw Blame History

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

.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.