Architecture: Backend Interface¶
CUTIP's execution layer is abstracted behind CutipBackend, an abstract base class in cutip/backends/base.py. Two backends are supported: Docker (default) and Podman. Both are core dependencies. The interface isolates backend-specific code and makes the execution layer testable independently of the runtime.
The ABC¶
class CutipBackend(ABC):
@abstractmethod
def pull_image(self, card: ImageCard) -> None: ...
@abstractmethod
def build_image(
self,
card: ImageCard,
project_root: Path | None = None,
vars: dict | None = None,
) -> None: ...
@abstractmethod
def ensure_network(self, card: NetworkCard) -> None: ...
@abstractmethod
def create_container(self, card: ContainerCard, image_name: str | None = None) -> str: ...
@abstractmethod
def start_container(self, name: str) -> None: ...
@abstractmethod
def stop_container(self, name: str) -> None: ...
@abstractmethod
def remove_container(self, name: str) -> None: ...
@abstractmethod
def container_status(self, name: str) -> str: ...
@abstractmethod
def container_logs(self, name: str) -> str: ...
Idempotency contract¶
All ensure_* and create_* methods must be idempotent — calling them when the resource already exists must be a no-op, not an error. This is how CUTIP achieves safe re-runs:
| Method | Idempotent behaviour |
|---|---|
ensure_network |
Return without error if network exists |
create_container |
Return without error if container exists |
Available backends¶
| Backend | Package | Connection | CLI flag |
|---|---|---|---|
PodmanBackend |
podman>=4.0 (core dep) |
SSH tunnel or local socket | --backend podman |
DockerBackend |
docker>=6.0 (core dep) |
Local daemon via docker.from_env() |
--backend docker (default) |
The get_backend(name, local) factory in cutip/backends/__init__.py routes the CLI --backend flag to the correct class.
Implementing a new backend¶
- Create
cutip/backends/myruntime/package - Subclass
CutipBackendand implement all abstract methods - Add a
connect()classmethod that returns a connected instance and raisesCutipErroron failure - Register the backend in
cutip/backends/__init__.py→get_backend() - Add the optional dependency to
pyproject.tomlunder[project.optional-dependencies]
Minimal skeleton:
from cutip.backends.base import CutipBackend
from cutip.utils.exceptions import CutipError
class MyRuntimeBackend(CutipBackend):
def __init__(self, client) -> None:
self._client = client
@classmethod
def connect(cls) -> "MyRuntimeBackend":
try:
import myruntime
client = myruntime.connect()
except ImportError as exc:
raise CutipError("Install cutip[myruntime] to use this backend") from exc
except Exception as exc:
raise CutipError(f"Connection failed: {exc}") from exc
return cls(client=client)
def pull_image(self, card): ...
def build_image(self, card, project_root=None, vars=None): ...
def ensure_network(self, card): ...
def create_container(self, card, image_name=None): ...
def start_container(self, name): ...
def stop_container(self, name): ...
def remove_container(self, name): ...
def container_status(self, name) -> str: ...
def container_logs(self, name) -> str: ...
Error handling conventions¶
- Raise
CutipError(fromcutip.utils.exceptions) for all expected failure modes (connection refused, resource not found during removal, build failed) - Let unexpected exceptions propagate — they will be caught by the CLI and displayed with a traceback
stop_containerandremove_containershould catch and log "not found" errors rather than raising — containers may already be stopped/removed in cleanup paths