From 778bc9c1300aade2183d3eae57bc5e529607ec02 Mon Sep 17 00:00:00 2001 From: Viorel Munteanu Date: Mon, 7 Sep 2026 11:08:26 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz --- CLAUDE.md | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 7877a6b..1043abf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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"/` (`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 + `"_"`; 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 (`" "`) + to `xdg_state_dir()/"compose"/sanitize_for_filename()`, 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 [-- ]`, `--log-level`, `-h/--help`, `-V/--version`) - Run tests: `meson test -C buildDir` (the `[unit]` + safe `[integration]` categories