docker.compose

Docker Compose orchestration: bring stacks up/down, register containers as live hosts in the active Lab, and idempotently re-enter when the same stack is already running.

The public surface (re-exported from otto.docker) is:

otto.docker.compose.get_user_compose_project(repo_name: str, suffix: str | None = None) str

Return a compose project name unique enough to coexist with other runs.

Format: otto-<repo>-<suffix>. suffix defaults to the OS username, or OTTO_COMPOSE_SUFFIX if set in the environment. Lowercase only — compose project names must be lowercase.

async otto.docker.compose.compose_up(repo: Repo, lab: Lab, *, on: str | None = None, project_name: str | None = None, build: bool = True) dict[str, DockerContainerHost]

Bring up repo’s compose stack on a parent host.

Idempotent at the project-name level: if a stack with the same project_name is already running on parent, this becomes a lookup instead of a fresh up. Either way, returns a dict mapping each declared service to its DockerContainerHost, with the hosts also registered in lab.hosts so --list-hosts and otto host <id> see them.

Parameters:

build – When True (the default) and the repo declares [[docker.images]], run build_images() first so locally- built images exist on the parent before compose tries to pull them. The build is idempotent via the context-hash skip, so this is cheap when nothing changed. Pass build=False if the compose file references only published images (or if you already built explicitly).

async otto.docker.compose.compose_down(repo: Repo, lab: Lab, *, on: str | None = None, project_name: str | None = None, stop_timeout: int = 1) Status

Tear down repo’s compose stack and unregister its container hosts.

stop_timeout is the per-container graceful-shutdown grace period in seconds passed to docker compose down --timeout. Defaults to 1s rather than docker’s default of 10s — otto’s typical workload is integration tests with disposable stacks where waiting 10s on every teardown adds up fast (4 tests × 10s = 40s of wall time on the serialized docker_e2e group). Pass a larger value for stacks where graceful shutdown matters.

otto.docker.compose.composed(repo: Repo, lab: Lab, *, on: str | None = None, project_name: str | None = None, own: bool = False, build: bool = True) AsyncIterator[dict[str, DockerContainerHost]]

Context manager wrapping compose_up / compose_down.

By default the stack is not torn down on exit if it was already running on entry — this lets a suite-level fixture hold the stack while inner instructions also call composed without yanking it from each other. Pass own=True to force teardown.

build is forwarded to compose_up().

async otto.docker.compose.compose_ps(parent: Host) list[dict[str, Any]]

Return a list of dicts describing running containers on parent.

Uses docker ps --format '{{json .}}' so the output is structured.

otto.docker.compose.register_declared_container_hosts(lab: Lab, repos: list[Repo]) int

Pre-register placeholder container hosts in lab for every declared <parent>.<project>.<service>.

The placeholders carry an empty container_id so that any operation against a not-yet-up container fails with a clear “run otto docker up” message rather than a confusing not-found error. Once compose_up() runs, it overwrites the placeholder with a real entry containing the resolved container id.

Returns the number of placeholders registered.

otto.docker.compose.get_container_host(host_id: str) DockerContainerHost

Look up a registered container host by id. Raises if not present.