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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user