Skip to navigation

nemo_gym.sandbox.adapters.docker_compose

View as Markdown

Run Compose services through the sandbox API.

Input YAML is resolved upstream; unsupported runtime semantics are rejected before provisioning services. Service fields get their meaning from nemo_gym.sandbox.compose_config.check_compose_fields: deploy.resources sizes each service’s sandbox, restart and pull_policy are recorded (and the pull policy applied where the provider can), networks scope which services see each other by name, extra_hosts add hostnames, post_start hooks run once the service is up, and tmpfs, ulimits, sysctls and security_opt need a provider that implements SupportsSandboxServiceOptions. A service may run on its own provider (service_providers), the way a GPU sidecar lands on a GPU deployment, and then resolves its external volumes through that provider’s own mapping (service_volume_sources).

Module Contents

Classes

NameDescription
AsyncSandboxComposeStart services from a Compose YAML file and own their sandbox lifecycle.

Functions

NameDescription
_merge_provider_optionsProvider options from several sources: lists (run_args, volumes) concatenate, mappings
_provider_name-
_seconds-

Data

LOGGER

PLACEMENT_GROUP_KEY

_HIDDEN_MARK

API

class nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose(
provider,
compose_file: str | pathlib.Path | None,
service_specs: collections.abc.Mapping[str, nemo_gym.sandbox.providers.SandboxSpec] | None = None,
timeout_s: float = 1200,
poll_interval_s: float = 0.5,
volume_init_image: str = 'alpine:3.22',
volume_sources: collections.abc.Mapping[str, str] | None = None,
service_providers: collections.abc.Mapping[str, typing.Any] | None = None,
platform: str = 'linux/amd64',
upload_root: str | pathlib.Path | None = None,
service_volume_sources: collections.abc.Mapping[str, collections.abc.Mapping[str, str]] | None = None,
service_setup: collections.abc.Mapping[str, collections.abc.Callable[[AsyncSandbox], collections.abc.Awaitable[None]]] | None = None,
placement_group: str | None = None
)

Start services from a Compose YAML file and own their sandbox lifecycle.

Deployment settings stay in the provider and optional service_specs. Compose fields override the corresponding spec fields, except environment: a spec’s env is the caller’s host-resolved override for that service and wins over the service’s Compose environment (see _prepare). service_providers names another provider (instance or single-key config) for particular services; every provider must reach the others’ sandboxes on the network. platform is the one the deployment runs, which a service’s platform must match. restart_policies and pull_policies record what each service declared.

x-sandbox.uploads entries (files or folders copied into a service once it starts) are read in this process, so every source must lie under upload_root, the one folder the caller vouches for (the task folder the planner validated against); a document with uploads and no root is refused. x-sandbox.logs entries name the paths of Harbor’s /logs tree a service bind-mounted: each source is created, world-writable, before the service’s command, hooks and healthcheck, and a target elsewhere is linked to it, the way Docker creates both bind targets at container create.

volume_sources maps the external volume names and foreign bind paths of the collection’s provider to its shared storage; service_volume_sources gives a service on its own provider that provider’s mapping, which its volumes resolve through instead. service_setup names, per service, a coroutine function the group awaits with that service’s sandbox once it is created and its runtime is configured, before its log links, uploads, command, hooks and healthcheck. Named/external volumes are already mounted when the sandbox is created, so setup (including Harbor’s overlay Dockerfile RUN lines) can write to shared storage. For a service with setup, its working directory is applied afterwards: setup may create that directory under a different user. An exception fails the start and stops the group.

placement_group keeps related collections and external helpers on the same router cell. It defaults to this collection’s unique project; sharing placement does not share managed volumes or cleanup ownership.

_plans
dict[str, dict[str, Any]] = {}
_processes
dict[str, Task] = {}
_runtime_metadata
dict[str, dict[str, str]] = {}
_seeds
list[AsyncSandbox] = []
_service_options
dict[str, dict[str, Any]] = {}
_stop_task
Task | None = None
_volume_helper
AsyncSandbox | None

The shared-storage init helper on the collection’s provider (one per provider that mounts volumes).

_volume_helpers
dict[int, AsyncSandbox] = {}
compose_file
document
dict[str, Any] = {}
placement_group
project
= 'compose-' + uuid.uuid4().hex
provider
pull_policies
dict[str, str] = {}
restart_policies
dict[str, str] = {}
service_providers
service_setup
= dict(service_setup or {})
service_specs
= dict(service_specs or {})
service_volume_sources
services
dict[str, AsyncSandbox] = {}
upload_root
volume_sources
= dict(volume_sources or {})
nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose.__aenter__()
async
nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose.__aexit__(
exc = ()
)
async
nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._command_log_note(
name: str
) -> str
async

What a failed service’s command log says, for the failure’s message: a bounded tail of the log the run script redirects the command’s stdout and stderr to, a note that it wrote nothing, or why the log could not be read. The read is a second exec after the verdict that names the service, and the service’s sandbox may be gone by then (a provider that ends an OOM-killed pod between the final healthcheck and this read), so anything it raises goes into the message rather than replacing the verdict.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._create_workdir(
name: str
) -> None
async

Create the service’s working directory once its sandbox is up, as Docker creates a missing working_dir (or image WORKDIR) at container create.

Every exec in the service defaults to that directory, this group’s bootstrap ones included, and a provider may refuse a command whose working directory does not exist (OpenSandbox answers 400): a main started from a base image whose Dockerfile only names a WORKDIR has nothing else to create it. Root from /, else the default user; the caller owns it afterwards as it owns a plain sandbox’s.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._exec_privileged(
command: str
)
async

Run command as root, or as the sandbox’s default user when it cannot switch users.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._forwarding_root(
name: str
) -> str

The service whose network namespace name ends up in: itself when it declares no network_mode, else the end of its network_mode: service:<target> chain. A chain that returns to a service it already passed is a cycle, which raises naming it; _validate runs this before any sandbox exists.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._load()
nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._memberships() -> dict[str, set[str]]

The networks each service is on. A network_mode: service:<target> service shares the target’s network namespace, so it is on the networks of the service at the end of its forwarding chain.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._missing_folders(
folders: set[str]
) -> set[str]
async

The paths mkdir -p would create for folders: each one and its missing ancestors, below /.

One probe lists every ancestor that does not exist yet; only paths from the candidate set are taken from its output, so a name the shell printed oddly is dropped rather than chmod-ed. A probe that fails reports nothing created: the chmod is best effort, the mkdir -p that follows is not.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._prepare()
async
nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._prepare_logs(
name: str
) -> None
async

Create each x-sandbox.logs source (a path of Harbor’s /logs tree the service bind-mounted) in the service, world-writable, and link a target elsewhere to it.

Runs after the caller’s image setup and before the uploads, the service’s command, its post_start hooks and its healthcheck, where Harbor’s bind sits between the image build and docker compose cp: a healthcheck that needs /logs/verifier passes on a clean image, and an upload under the target lands in the source through the link. /logs/artifacts:/app/output under Harbor is a second mount of one host folder that hides whatever the image has at the target, so here the target becomes a link to the source unconditionally (/logs/artifacts is where artifacts are collected from): a folder the image ships there, or an overlay RUN mkdir -p created, is removed first, with a warning when it was not empty, as a plain folder would keep the task’s artifacts out of the collection and show the agent what the mount hides.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._prepare_volumes()
async
nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._provider_for(
name: str
)

The provider a service runs on: its own from service_providers, else the collection’s.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._providers() -> list[typing.Any]

Every distinct provider of the collection, the default first.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._run_hooks(
name: str
) -> None
async

Run the service’s post_start hooks in order, once its command has started (as Compose does: after the container starts, not after it is healthy). A hook that exits non-zero fails the start.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._stop()
async
nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._upload(
name: str
) -> None
async

Copy each x-sandbox.uploads entry (a task file or folder a bind mount named) into the service.

Runs after the sandbox starts and before the service’s command, so the process finds the files where the Compose file mounted them. The receiving folders are created first, and the folders and files the upload touched (every folder created for it, the folders the files landed in and the files; nothing else under the target) are made readable by every user, for an image whose process runs as a non-root user. A folder’s tree is recreated under the target, empty folders included (a program that expects the output or cache folder the task supplies finds it, as under a bind mount), over whatever the image already has there (a merge, where a bind mount would hide the image’s folder). The names the task digest skips (__pycache__, *.pyc, editor leftovers: nemo_gym.data.tasks.harbor.digest.is_ignored) are left out, so what reaches the service is what the digest covers, and a link inside the folder is refused, naming it, rather than read through.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._validate() -> list[str]
nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._visible_hosts(
addresses: collections.abc.Mapping[str, str]
) -> dict[str, dict[str, str]]

Per service, the hostnames it resolves: itself, every service sharing a network with it (by name and by the aliases on those networks) and its own extra_hosts.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._volume_helper_for(
provider,
root: str
async

The init helper on provider’s shared storage, started on first use. A service on its own provider mounts that provider’s storage, so its directories are made and cleaned there.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._volume_sources_for(
name: str
) -> dict[str, str]

The shared-storage mapping a service’s volumes resolve through: its own provider’s from service_volume_sources, else the collection’s volume_sources.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose._wait(
name,
condition
)
async
descriptor: collections.abc.Mapping[str, typing.Any],
provider,
service_providers: collections.abc.Mapping[str, typing.Any] | None = None
asyncclassmethod

Connect to all members without provisioning or restarting services.

Like AsyncSandbox.connect, stop() closes the connected sandboxes using provider semantics. The creator retains ownership of managed-volume cleanup and running service/forwarding tasks and must also call stop(). service_providers names the provider of members that live on another one, as it did at start.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose.serialize(
scope: str | None = None
) -> dict[str, typing.Any]
async

Describe a running collection using provider connection descriptors.

The creating process must keep the collection alive: it owns service and forwarding tasks and managed-volume cleanup. Provider configuration and YAML are not included.

nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose.start()
async
nemo_gym.sandbox.adapters.docker_compose.AsyncSandboxCompose.stop()
async
nemo_gym.sandbox.adapters.docker_compose._merge_provider_options(
layers: collections.abc.Mapping[str, typing.Any] = ()
) -> dict[str, typing.Any]

Provider options from several sources: lists (run_args, volumes) concatenate, mappings (extensions) merge with later layers winning, scalars are replaced.

nemo_gym.sandbox.adapters.docker_compose._provider_name(
provider
) -> str
nemo_gym.sandbox.adapters.docker_compose._seconds(
value: str | int | float
) -> float
nemo_gym.sandbox.adapters.docker_compose.LOGGER = logging.getLogger(__name__)
nemo_gym.sandbox.adapters.docker_compose.PLACEMENT_GROUP_KEY = 'nemo.nvidia.com/placement-group'
nemo_gym.sandbox.adapters.docker_compose._HIDDEN_MARK = 'gym-logs-target-hidden'