Skip to content

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

  1. Create cutip/backends/myruntime/ package
  2. Subclass CutipBackend and implement all abstract methods
  3. Add a connect() classmethod that returns a connected instance and raises CutipError on failure
  4. Register the backend in cutip/backends/__init__.pyget_backend()
  5. Add the optional dependency to pyproject.toml under [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 (from cutip.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_container and remove_container should catch and log "not found" errors rather than raising — containers may already be stopped/removed in cleanup paths