Bonfire is a CLI tool and MCP server for deploying and managing ephemeral test environments on OpenShift/Kubernetes clusters for console.redhat.com applications. The codebase has three distinct packages that serve different purposes and have different dependency trees:
| Package | Purpose | Key Dependencies |
|---|---|---|
bonfire |
CLI tool (Click-based) for deploying apps via OpenShift templates | click, ocviapy, gql, sh, tabulate |
bonfire_lib |
Shared library for ephemeral reservation lifecycle | kubernetes, jinja2, pyyaml (no oc binary needed) |
bonfire_mcp |
MCP server exposing reservation tools to AI agents | mcp, bonfire_lib |
# Full CLI with all extras
pip install -e ".[cli,test,mcp]"
# MCP server only (minimal deps)
pip install -e ".[test,mcp]"# All tests (excludes integration tests by default via pytest config)
pytest -sv
# Specific test suites
pytest tests/test_bonfire_mcp/ -sv # MCP server tests
pytest tests/test_bonfire_lib/ -sv # Shared library tests
pytest tests/test_bonfire.py -sv # CLI tests
# Integration tests (require live K8s cluster — never run in CI)
pytest -m integration -svruff check --fix .
ruff format .python -m build -o dist/bonfire CLI (Click commands) bonfire_mcp (MCP server)
│ │
│ uses ocviapy/oc binary │ calls bonfire_lib directly
│ for K8s operations │ (no oc binary needed)
↓ ↓
bonfire.openshift / bonfire.namespaces bonfire_lib.*
│ │
↓ ↓
K8s API via oc CLI K8s API via kubernetes Python client
│ │
↓ ↓
Ephemeral Namespace Operator (ENO) — manages NamespaceReservation / ClusterReservation CRDs
The codebase has two independent paths to the K8s API — this is the most important architectural detail:
-
bonfire(CLI): Usesocviapywhich shells out to theocbinary. Lives inbonfire/openshift.pyandbonfire/namespaces.py. Requiresocto be installed andoc loginto have been run. -
bonfire_lib(shared library): Uses thekubernetesPython client directly viaEphemeralK8sClient. Noocbinary dependency. Used bybonfire_mcp.
These two paths are not interchangeable. The CLI imports from bonfire.*, the MCP server imports from bonfire_lib.*. The CLI has a bridge point in bonfire/namespaces.py where _get_lib_client() creates an EphemeralK8sClient for some operations.
All custom resources use the same API version: cloud.redhat.com/v1alpha1. Key CRD kinds:
NamespaceReservation— reserves an ephemeral namespaceNamespacePool— defines a pool of namespacesClusterReservation— reserves a ROSA HCP cluster (async, 20-40 min provisioning)ClusterPool— defines a pool of clustersClowdApp,ClowdEnvironment,ClowdJobInvocation— Clowder operator CRDsFrontend,FrontendEnvironment— Frontend operator CRDs
Each module in bonfire_lib is a thin, stateless function layer over EphemeralK8sClient:
reservations.py— reserve/release/extend namespace reservationsclusters.py— reserve/release/extend/kubeconfig for cluster reservationspools.py— list namespace and cluster poolsstatus.py— get/list reservations, polling, namespace describecore_resources.py— Jinja2 template rendering for CRsk8s_client.py—EphemeralK8sClientwrappingkubernetesDynamicClientconfig.py—Settingsdataclass (from env vars or explicit construction)utils.py—FatalError, duration parsing, DNS name validation
server.py— MCPServerinstance, tool definitions (TOOLSlist), andcall_tool()dispatcherauth.py—load_k8s_client()with three auth modes (token, in-cluster, kubeconfig) + preflight CRD checkformatters.py— Plain-text formatting functions for MCP responses (no JSON — text is more useful for LLMs)__main__.py— Entry point forpython -m bonfire_mcp
Releasing a reservation sets spec.duration to "0s" via a merge patch. The ENO poller picks this up within ~10 seconds and cascades deletion via OwnerRef. This is not a delete operation — it's a patch.
MCP server tests use pytest-asyncio with strict mode (asyncio_mode = "strict" in pyproject.toml). Every async test must be decorated with @pytest.mark.asyncio:
@pytest.mark.asyncio
async def test_something(self):
result = await call_tool("ephemeral_reserve", {"name": "test"})Tests mock at the K8s client boundary — never hit a real cluster:
- bonfire_lib tests: Mock
EphemeralK8sClientviaMagicMock(spec=EphemeralK8sClient)in conftest - bonfire_mcp tests: Patch
bonfire_mcp.server._get_clientand patch individual modules (bonfire_mcp.server.reservations,bonfire_mcp.server.clusters, etc.) - bonfire CLI tests: Patch
bonfire.namespaces._get_lib_client
All conftest fixtures provide a mock_client with whoami() pre-configured to return a test user.
The MCP server returns CallToolResult(isError=True) for errors, not exceptions. Tests check:
assert isinstance(result, CallToolResult)
assert result.isError is True
assert "Error" in result.content[0].textSuccessful results return list[TextContent], not CallToolResult.
Tests marked @pytest.mark.integration are excluded by default (configured in pyproject.toml addopts). They require a live K8s cluster and are never run in CI.
Namespace reservations are synchronous (the reserve() function polls until a namespace is assigned). Cluster reservations are asynchronous — reserve_cluster() returns immediately with state: "waiting" and the caller must poll get_cluster_status(). This asymmetry flows through to the MCP server's ephemeral_reserve tool, which handles both via the type parameter.
Durations must be between 30 minutes and 14 days. Format is NhNmNs (e.g., "1h30m", "45m", "2h"). The validate_time_string() function in bonfire_lib/utils.py enforces this. The CLI in bonfire/bonfire.py has its own validate_time_string() in bonfire/utils.py — they are separate implementations.
K8s label values can't contain @ or :. The _sanitize_username() function in k8s_client.py replaces @ with _at_ and : with _. All requester values stored as labels go through this. The whoami() method for kubeconfig auth also strips the cluster URL suffix from context user strings (e.g., user/api-cluster:6443 → user).
bonfire_mcp/server.py uses module-level globals _client and _settings with lazy initialization via _get_client() and _get_settings(). Tests must patch _get_client to avoid real K8s connections. The _client is shared across all tool calls.
Cluster CRDs may not exist on all management clusters. list_cluster_pools() and list_cluster_reservations() catch all exceptions and return empty lists if the CRD isn't installed, rather than failing.
Two entry points are defined in pyproject.toml:
bonfire→bonfire.bonfire:main_with_handlerbonfire-mcp→bonfire_mcp.server:main
Uses ruff for both linting (ruff check --fix) and formatting (ruff format). Line length is 100 chars, indent width is 4 spaces.
Tests run on Python 3.10, 3.11, 3.12 across Ubuntu and macOS. CI installs the built wheel with all extras ([cli,test,mcp]) and requires oc 4.16 to be available for CLI tests.
bonfire_lib/core_resources.py renders CRs from Jinja2 templates in bonfire_lib/templates/. These are separate from the OpenShift Template YAML files in bonfire/resources/ — the CLI uses oc process on the latter, while bonfire_lib uses Jinja2 rendering.
These are not the same templates. bonfire/resources/ contains OpenShift Template YAML files processed by oc process. bonfire_lib/templates/ contains Jinja2 templates rendered by Python. Don't confuse them.
Version is managed by setuptools_scm (derived from git tags). There is no hardcoded version string in the source.
Key env vars for MCP server auth:
K8S_SERVER+K8S_TOKEN— explicit token authK8S_CA_DATA— base64-encoded CA cert (optional with token auth)K8S_SKIP_TLS_VERIFY— skip TLS verificationKUBECONFIG— kubeconfig file pathK8S_CONTEXT— kubeconfig context name
Key env vars for bonfire_lib settings:
BONFIRE_DEFAULT_NAMESPACE_POOL— default pool (default:"default")BONFIRE_DEFAULT_DURATION— default reservation duration (default:"1h")BONFIRE_NS_REQUESTER— override requester identityBONFIRE_BOT— set to"true"for automated/CI usage (disables interactive prompts and context switching)