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
content is expected to keep changing as more of the orchestrator gets
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
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`,
`--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`,
`-u/--up`, `-d/--down`,
`-c/--config-file`, `-w/--write-config`, `-t/--test [-- <catch-command-line-options>]`, `--log-level`,
`-h/--help`, `-V/--version`)
- Run tests: `meson test -C buildDir` (the `[unit]` + safe `[integration]` categories