Document the compose orchestrator in CLAUDE.md

Covers all five -u/--up steps (compose_orchestrator.{h,cpp}), the
run_container()/run_mounted_container() split, and the newly-exported
commands.cpp functions the orchestrator reuses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-09-07 11:08:26 +00:00
parent c80ebb4e28
commit 778bc9c130
+131
View File
@@ -2584,6 +2584,136 @@ Source layout (all under `src/`):
orchestrator exists, not for this parser's own unit tests, since its orchestrator exists, not for this parser's own unit tests, since its
content is expected to keep changing as more of the orchestrator gets content is expected to keep changing as more of the orchestrator gets
built on top of it. built on top of it.
- `compose_orchestrator.{h,cpp}` — the actual `-u/--up` orchestrator
(`-d/--down`, `commands.cpp`'s `compose_down_command()`, is still the
stub `load_and_validate_compose()`-only implementation described above —
not touched by this file yet), built as five separate steps/commits,
matching the user's own explicit sequencing request:
1. `resolve_compose_images()` — matches each service's own `image:`
reference against `list_oci_images(images_directory)` (`oci_image.h`
— the exact function `-l/--list-images` already uses, reused as-is).
`split_image_reference()` (`.cpp`-local) splits `"image[:tag]"` on
the *last* `':'`, but only when nothing after it contains a `'/'` (so
a registry `host:port` prefix, e.g.
`"myregistry:5000/busybox"`, isn't misread as a tag) — a deliberately
simplified image-reference split, not a full one. Fails the whole
resolution (not just the one missing service) if any service's image
isn't found, since mounting happens for *every* service before
starting *any* of them (next step) specifically so a missing/slow
image for any one service is caught up front.
2. `mount_compose_images()` — mounts every resolved image
(`mount_image()`, exported from `commands.cpp`'s own former
anonymous-namespace pair, see below) *before* starting any service —
mounting can be slow on some devices, so this deliberately
front-loads every mount rather than interleaving mount+start per
service, per the user's own explicit request. On any failure partway
through, unmounts+cleans up every image already mounted in the same
call, so a failed `-u/--up` never leaves a partial mount set behind.
3. `provision_compose_networks_and_volumes()` — ensures every
network/volume the compose file needs actually exists, creating
whatever's managed (`ComposeNetworkMode::managed`, or any declared
top-level volume — `compose_file.h` has no `external` concept for
volumes yet) and not already present, via `create_network_command()`/
`create_volume_command()` (also exported from `commands.cpp`'s own
former anonymous namespace, reused exactly as `-n/--network`/
`-v/--volume` already do — so a managed network/volume this creates
is genuinely no different from one a user created by hand). Every
created/reused name is prefixed with `compose_project_name()` — the
sanitized basename of the compose file's own parent directory
(`sanitize_for_filename()`, `pid_file.h`), matching real Docker
Compose's own default project-naming convention — so two unrelated
compose projects can each declare e.g. a network named `"backend"`
without colliding in slocker-lite's single, flat
`persistent.yaml` networks/volumes namespace. **Finding an
already-existing entry under its project-prefixed name is
deliberately *not* an error** — per the user's own explicit
direction (a `-u/--up` re-run against the same compose file, e.g.
after an interrupted previous one, should reuse rather than fail) —
with no attempt to verify it still matches what the compose file
currently declares. `external: true` networks map to themselves,
unprefixed (real, pre-existing networks by definition, already
confirmed to exist by `validate_compose_external_state()` before this
ever runs). A managed volume's host directory is auto-chosen under
`xdg_state_dir()/"compose-volumes"/<actual-name>` (`pid_file.h`).
Returns a `ComposeProvisionedNames{networks, volumes}` map from each
compose-declared name to the actual slocker-lite name, needed by the
next step to translate a service's own `networks:`/named-volume
`volumes:` references.
4. `start_compose_services()` — starts every service in dependency order
(`topological_service_order()`, `.cpp`-local, Kahn's algorithm — no
cycle-detection needed here, since `load_compose_file()` already
rejects a `depends_on` cycle before a `ComposeFile` is ever
produced), each one daemonized (as if `-D/--daemonize` had been
given) against its own already-mounted image and already-provisioned
networks/volumes. A service whose dependency failed to start (or was
itself skipped for the same reason) is skipped too, logged clearly,
never started against a dependency that isn't actually running. A
service's own session identity (pid-file/log/cgroup naming,
`container_name` throughout the rest of this project) is its
explicit `container_name:` if given, used verbatim (matching real
Compose's own semantics for that field), else
`"<project_name>_<service_name>"`; its *DNS* hostname (what a sibling
service resolves it by, via the per-session DNS resolver,
`network_dns.h`) is instead its explicit `container_name:` or its
bare compose service name, **never** project-prefixed — real Compose
resolves services by their bare service key regardless of project
name, and `test-compose/compose.yaml`'s own skeleton relies on
exactly that (its server reaches the worker by the plain hostname it
declared). Since this loops over multiple services within one
process, each daemonized start follows `daemonize()`'s own documented
contract adapted for a loop rather than a single top-level dispatch:
in the **parent** branch (a real pid, or a hard failure), the loop
just records the outcome and moves on to the next service, never
blocking; in the freshly forked **child** branch, the only way out is
an explicit `_exit()` right after `run_mounted_container()` returns —
it must never fall back into the loop and attempt to start another
service, unlike a single top-level `-r -D` invocation, which just
lets `main()` return naturally once its own one-and-only session
ends. Port-forwarding and an explicit `--user`/`--group` aren't wired
up for compose services yet (`ComposeService::ports` is parsed but
unused here) — every service runs as whatever user its own image
declares, same as `-r/--run`'s own default.
5. `compose_state_file_path()`/`record_compose_services()` — write one
line per started service (`"<service_name> <container_name> <pid>"`)
to `xdg_state_dir()/"compose"/sanitize_for_filename(<absolute compose
path>)`, the same state-file-naming pattern
`port_forward.h`'s/`network_tap_relay.h`'s own crash-orphan records
already use, so a future `-d/--down` implementation can find exactly
which sessions a given `-u/--up` started. Overwrites any previous
run's own record for the same file — a known, expected limitation
until `-d/--down` itself exists to keep the two in sync (there's no
way yet to tell which of an older run's services are still actually
running versus already stopped by hand).
**`commands.cpp` exports needed for all of the above** (each a pure
refactor out of its own former anonymous-namespace scope, no behavior
change, verified via `meson test` plus manual `-r/--run` smoke tests
after each): `MountedImage`/`mount_image()`, `create_volume_command()`,
`create_network_command()`, and a new `run_mounted_container()`
`run_container()` (`-r/--run`) itself now only decides `container_name`,
handles the `-D/--daemonize` fork (which must happen *before* mounting
so the daemon's own log file can be named from its very first line —
unchanged from before), and calls `mount_image()`; everything after that
(volume/env/user resolution, namespace policy, network/port-forward/DNS
setup, running `bwrap`, unmount/cleanup) moved into
`run_mounted_container()`, taking an already-mounted image instead of
mounting its own — reused as-is by `start_compose_services()` above
against an image it mounted itself, rather than a second, drifting copy
of ~200 lines of already-debugged logic.
**Verified manually, end to end, rootless**: a two-service compose file
(`worker`, and `web` with `depends_on: worker`, no networks/volumes so
the whole thing stays rootless-testable) correctly resolves, mounts both
images, starts both daemonized in dependency order with the expected
per-service `--hostname`, confirmed genuinely running via
`--list-processes`/`ps`, writes a correct compose state file, and
`--kill` against the recorded pids leaves no processes behind. A
single-service compose file with a managed named volume correctly
creates it project-prefixed, and a second `-u/--up` run against the same
file correctly reuses it without error. Network provisioning itself
reuses already-proven `create_network_command()`/
`ensure_network_provisioned()` as-is (no new logic there) but wasn't
separately re-verified live, since it needs root.
Errors are logged via `spdlog::error`; every external command is also traced at debug Errors are logged via `spdlog::error`; every external command is also traced at debug
level in `run_process()`/`run_process_foreground()` (`src/process.cpp`) — visible via level in `run_process()`/`run_process_foreground()` (`src/process.cpp`) — visible via
@@ -2644,6 +2774,7 @@ Build directory is `buildDir/` (already configured).
`--user`, `--group`, `--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`, `--user`, `--group`, `--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
`--delete-volume-full`, `-n/--network`, `--extern`, `--intern`, `--subnet`, `--with-ipv6`, `--subnet6`, `--delete-volume-full`, `-n/--network`, `--extern`, `--intern`, `--subnet`, `--with-ipv6`, `--subnet6`,
`--with-veth`, `--list-networks`, `--delete-network`, `-p/--port-forward`, `--no-dns`, `--list-processes`, `--clean-processes`, `--with-veth`, `--list-networks`, `--delete-network`, `-p/--port-forward`, `--no-dns`, `--list-processes`, `--clean-processes`,
`-u/--up`, `-d/--down`,
`-c/--config-file`, `-w/--write-config`, `-t/--test [-- <catch-command-line-options>]`, `--log-level`, `-c/--config-file`, `-w/--write-config`, `-t/--test [-- <catch-command-line-options>]`, `--log-level`,
`-h/--help`, `-V/--version`) `-h/--help`, `-V/--version`)
- Run tests: `meson test -C buildDir` (the `[unit]` + safe `[integration]` categories - Run tests: `meson test -C buildDir` (the `[unit]` + safe `[integration]` categories