This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
pulp-service is a Django REST Framework plugin for Pulpcore that extends the Pulp content management platform with Red Hat cloud-specific features: multi-tenant authentication (X-RH-IDENTITY), S3 storage (via pulpcore's built-in backend with CloudFront patches), domain-based org isolation, content guards, vulnerability reporting, and OpenTelemetry observability.
The plugin is registered via the pulpcore.plugin entry point in pulp_service/setup.py.
pulp_service/— The Python package (all source code lives here)pulp_service/app/— Core Django app: models, viewsets, serializers, middleware, auth, taskspulp_service/tests/functional/— Functional tests (pytest + pytest-django)setup.py,requirements.txt— Package definition and dependencies
images/— Container build assets (startup scripts for pulp-api, pulp-content, pulp-worker; WSGI middleware)deploy/— OpenShift ClowdApp deployment manifestsdocs/ARCHITECTURE.md— Comprehensive architecture reference (read this for deep context)CHANGES/— Towncrier changelog fragments
# Install in development mode
cd pulp_service && pip install -e .
# Format code
make format
# Lint (report only)
make lint
# Lint (auto-fix safe violations)
make lint-fix
# Lint only new violations (changed lines vs main)
make lint-diff
# Install git pre-commit hook (optional)
make install-hook
# Run all functional tests
pytest pulp_service/pulp_service/tests/functional/
# Run a single test file
pytest pulp_service/pulp_service/tests/functional/test_authentication.py
# Run a single test
pytest pulp_service/pulp_service/tests/functional/test_authentication.py::TestClass::test_methodTest dependencies: pytest, pytest-django (see unittest_requirements.txt / functest_requirements.txt).
Three-service model:
- pulp-api — Gunicorn WSGI serving Django REST API (port 24817 local, 8000 prod)
- pulp-content — Gunicorn + aiohttp async content delivery (port 24816 local, 8000 prod)
- pulp-worker — Celery workers for background tasks (Redis broker)
Request flow:
WSGI middleware (images/assets/log_middleware.py) → Django middleware stack (app/middleware.py) → DRF ViewSets (app/viewsets.py)
Key patterns:
- Authentication:
X-RH-IDENTITYheader (base64-encoded JSON) → custom auth classes inapp/authentication.py - Multi-tenancy:
DomainOrgmodel maps org_id → Pulp domain; domain-based routing for content APIs - Context variables:
ContextVarinstances inapp/middleware.pycarry request-scoped data (org_id, user_id, request_path) across layers - Storage: S3 via pulpcore's built-in
S3Boto3Storagewith CloudFront patches; domain creation clones settings fromtemplate-domain-s3 - Tasks: Background work in
app/tasks/(package scanning, domain metrics, RDS testing)
Upstream plugins this extends: pulpcore, pulp-python, pulp-container, pulp-rpm, pulp-npm, pulp-maven, pulp-hugging-face.
Uses towncrier. For any non-trivial change, create a file in CHANGES/ named {issue_number}.{category} where category is one of: feature, bugfix, doc, removal, deprecation, misc.
- Ruff formatter and linter, line length 120, targeting py311
- 23 lint rule categories as "harness sensors" for agentic development (including security via flake8-bandit)
- Complexity thresholds: max-complexity=10, max-branches=10, max-args=6, max-statements=40
- Per-file exceptions: viewsets.py and rds_connection_tests.py (complexity rules), tests (argument count, print, unused args, security), migrations (excluded entirely)
// emptyraisesStopIteration— never use// emptyin jq filters passed to.first(). An empty stream raisesStopIteration, notNone. Use direct null comparisons:if .foo == "bar" then ... else null end.- String interpolation produces
"null"not null —"\(.foo)"where.foois null produces the string"null". The parentJSONHeaderRemoteAuthenticationaccepts any non-null string as valid. Always add explicit null checks before interpolation.
@mergeappends, doesn't prepend — Pulp's@mergedirective forREST_FRAMEWORK__DEFAULT_AUTHENTICATION_CLASSESappends custom classes after pulpcore's defaults. Use an explicit full list when auth class order matters.
pulp_label_selectnegation causes 500s — the!labelandlabel!=valueoperators can cause server errors. Fall back to client-side filtering when excluding by labels.- CloudWatch domain listing needs
fieldsparameter — domain objects include largestorage_settingsblobs. Use?fields=name,pulp_href,pulp_labels&limit=50to avoid 500 errors from oversized responses.
- PyArrow
S3FileSystemregion issues — passingregion=Noneoverrides auto-detection (different from omitting the parameter). Use**kwargsand only includeregionwhen explicitly set.copy_files()uses multipart upload which fails with PermanentRedirect; useopen_output_stream()or boto3 instead. UV_INDEXvsUV_DEFAULT_INDEX—UV_DEFAULT_INDEXreplaces PyPI entirely (dependencies won't resolve). UseUV_INDEXto add a private repo alongside PyPI.
A self-contained development container with PostgreSQL, Redis, and all Pulp services running locally. Built automatically on every push to any branch.
ghcr.io/pulp/hosted-pulp-dev-env:<branch-name>
Branch names are sanitized for Docker tags: / is replaced with - (e.g., branch feature/foo produces tag feature-foo).
Mount the shared workspace volume at /workspace per alcove conventions:
docker run -d \
-v workspace:/workspace \
-p 24817:24817 \
-p 24816:24816 \
--name pulp-dev \
ghcr.io/pulp/hosted-pulp-dev-env:mainIf the container finds a pulp-service checkout at /workspace/pulp-service, it installs it in development mode (pip install -e) at startup. Because PULP_GUNICORN_RELOAD is enabled by default, Gunicorn watches for Python file changes and automatically reloads -- you do not need to run pulp-restart after editing source files in most cases.
| Service | Port | Description |
|---|---|---|
| pulp-api | 24817 | Django REST API (Gunicorn WSGI) |
| pulp-content | 24816 | Async content delivery (Gunicorn aiohttp) |
| pulp-worker | -- | Celery background task worker |
| PostgreSQL | 5432 | Database (local, trust auth) |
| Redis | 6379 | Cache and Celery broker |
All five services are managed by supervisord. The pulp-restart command only restarts Pulp services (api, content, worker), not PostgreSQL or Redis.
The dev container has domain support enabled (DOMAIN_ENABLED=True) and token authentication disabled (TOKEN_AUTH_DISABLED=True).
Override these at docker run time with -e:
| Variable | Default | Description |
|---|---|---|
PULP_DEFAULT_ADMIN_PASSWORD |
password |
Admin user password set during initialization |
PULP_API_WORKERS |
2 |
Number of Gunicorn workers for the API |
PULP_CONTENT_WORKERS |
2 |
Number of Gunicorn workers for the content app |
PULP_WORKERS |
2 |
Number of Celery worker processes |
PULP_GUNICORN_TIMEOUT |
90 |
Gunicorn request timeout in seconds |
PULP_GUNICORN_RELOAD |
true |
Auto-reload Gunicorn on Python file changes |
Patch Management:
pulp-add-patch /path/to/my-fix.patch # Apply a patch to pulpcore site-packages
pulp-remove-patch /path/to/my-fix.patch # Reverse a patch
pulp-restart # Restart after patching (required)Service Management:
pulp-restart # Restart all Pulp services
pulp-restart api # Restart pulp-api only
pulp-restart content # Restart pulp-content only
pulp-restart worker # Restart pulp-worker only
supervisorctl status # Check all service statusYou need pulp-restart after applying/removing patches, changing /etc/pulp/settings.py, or modifying Celery task definitions. Regular Python source edits auto-reload via Gunicorn.
Running Tests:
pulp-test # Run all functional tests
pulp-test path/to/test_file.py # Run specific test file
pulp-test path/to/test_file.py::TestClass # Run specific test classDatabase Access:
runuser -u pulp -- psql -d pulp # PostgreSQL shell
runuser -u pulp -- pulpcore-manager showmigrations # Check migrations
runuser -u pulp -- pulpcore-manager shell # Django shellAdmin Credentials: admin / password (override with PULP_DEFAULT_ADMIN_PASSWORD)
When used with alcove, the /workspace volume is automatically shared between the Skiff container (running Claude Code) and this dev container. Repositories are cloned into /workspace/<name>/. The dev container image is declared in the alcove agent definition via dev_container.image.
The repository includes an automated post-merge release pipeline using Alcove workflows that triggers on pushes to the main branch. The pipeline consists of three agents:
- Konflux Release Monitor (
.alcove/agents/konflux-release-monitor.yml) — Monitors GitHub check runs until the Konflux release pipeline completes, extracts the built image tag - Stage Health Checker (
.alcove/agents/stage-health-checker.yml) — Verifies pulp-stage pod health on OpenShift after deployment usingoccommands - CLAUDE.md Updater (
.alcove/agents/claude-md-updater.yml) — Reviews recent commits and updates this file if new patterns, commands, or architecture changes are introduced
The main workflow (.alcove/workflows/post-merge-release-pipeline.yml) coordinates these agents:
- await-konflux-release — Polls commit check runs until Konflux build/release pipelines complete successfully
- check-stage-health — Verifies deployed pods are healthy and running the new image
- update-claude-md — Updates documentation to reflect any architectural changes in the merged commit
This ensures that every merge to main triggers a complete release verification cycle and keeps documentation current.