Skip to content

make compose networks actually work: external:, project isolation, down --rmi - #80

Merged
us merged 3 commits into
mainfrom
fix/external-networks-and-ignored-flags
Aug 9, 2026
Merged

make compose networks actually work: external:, project isolation, down --rmi#80
us merged 3 commits into
mainfrom
fix/external-networks-and-ignored-flags

Conversation

@us

@us us commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Follow-up to #77, taking the three items its "Not covered" section listed. The first one
turned out to sit on top of a much larger gap.

Compose networks did nothing at all

networks: was decorative. NetworkManager persisted networks to a JSON directory of
mocker's own that the runtime never saw, and ContainerConfig.network was set and then
never emitted — buildRunArguments had no --network. So every container, compose or
not, landed on the runtime's default network no matter what the file said, and
mocker run --network was ignored too.

  • NetworkManager now goes through the real container network CLI (create / ls /
    delete / inspect) via the injectable ProcessRunning, so what it reports is what the
    runtime actually has.
  • buildRunArguments emits --network, which is what attaches the container.
  • connect / disconnect now say they are unsupported instead of writing a record that
    changed nothing. README no longer advertises them.

Verified end to end: a service on a declared network lands on it (192.168.66.2 on
final-backend, confirmed via container inspect), and mocker run --network does too.

external: for networks (the original ask)

Mirrors the volume design from #77: ComposeNetwork gained external and customName
with runtimeName(projectName:), plus pure networksToCreate / networksToRemove.
up verifies an external network exists before starting anything and fails naming it,
never creates it, and down never removes it. An explicit name: is used verbatim.

A service's networks: mapping form (networks: {backend: {aliases: [db]}}) is now
parsed — it previously read as no networks at all, which silently left the container on
the default network. A service listing several networks joins the first and says so on
stderr: attaching a container to two networks fails inside the guest on this runtime.

compose down --rmi all|local

Implemented, with a pure imagesToRemove: local removes only images compose built
(services with no explicit image:), all includes pulled ones. Removal is best-effort
per image so a shared or missing image cannot abort the teardown, and each removal is
reported. An invalid value is rejected up front instead of being accepted and ignored.

compose rm --volumes

Warns and carries on. Upstream removes the anonymous volumes attached to the removed
containers; mocker never creates one — an anonymous mount is dropped before it reaches the
runtime — so there is nothing to remove. Reporting a cleanup that did not happen is the
pattern #72/#74 were about, but failing the whole command over a flag with nothing to do
would stop rm from removing the containers the user actually asked about.

Also fixed here

  • Services with no networks: now get a project-scoped <project>-default instead of
    landing on the runtime's global network, where unrelated projects could reach each other.
  • A container records the network it was created on, so changing a service's network
    recreates it — and a container from a version that recorded none is recreated too, which
    is what moves an upgraded project onto its own network.
  • A service joining a network the file never declares is an error instead of a container
    started against a network nothing creates.
  • network prune no longer removes the runtime's built-in network, and counts only what
    it actually removed.

Fixes found while reviewing this change

  • compose --timeout was parsed and dropped: ContainerEngine.stop never took one, so
    every teardown used the runtime's own default. It is now passed as container stop -t,
    which also makes down predictable instead of appearing to hang.
  • up no longer swallows a failed network creation. The runtime rejects names it does not
    like, and a service cannot join a network that was never created.
  • down retries network removal briefly (the runtime can still consider a just-removed
    container attached) and warns on stderr if it still could not remove it.
  • network create says so when it is handed a driver or gateway the runtime cannot
    apply, rather than reporting success for a network that does not match the request.

A deadlock this work surfaced

RealProcessRunner waited on waitUntilExit(), which can wedge a thread Swift
concurrency still needs. One shell-out per command got away with it; once networks went
through the CLI, compose down makes several calls in a row and hung indefinitely
(measured: 150s and still going, against 2s after the fix). Exit is now observed through
terminationHandler, matching what ContainerEngine already did, and the pipes are
drained before the process starts. down also skips removal work entirely for a project
that is already down, instead of retrying and then warning about a network that is gone.

Verification

swift build clean, swift test 434 tests / 35 suites green (417 before). Exercised
against the real backend: external network missing → up fails naming it and creates
nothing; present → joined, not created, and left alone by down; a project-owned network
is created, joined, and removed; down --rmi local removed the built image and kept the
pulled base; --rmi all removed both; --rmi wat and rm -v are rejected;
mocker network create/ls/inspect/rm all operate on real networks. Teardown timings
after the deadlock fix: 2-3s across every fixture, against 5s on v0.8.0 for a project
that was not really networked at all.

Not covered

  • A container can only be on one network, so compose's multi-network topologies are not
    reproducible here. The warning names which one is used.
  • network connect / disconnect need runtime support that does not exist yet.
  • Zh-CN docs still describe the old behavior.

ref #77

us added 3 commits August 9, 2026 16:51
…red flags

Compose networks did nothing. NetworkManager persisted them to a JSON directory
of mocker's own that the runtime never saw, and ContainerConfig.network was set
and then never emitted, so every container landed on the runtime's global
network no matter what the file declared — and `mocker run --network` was
ignored too.

- NetworkManager now goes through the real `container network` CLI, so what it
  reports and removes is what the runtime actually has; connect/disconnect say
  they are unsupported instead of writing a record that changed nothing
- buildRunArguments emits --network, which is what attaches a container
- services that name no network get a project-scoped `<project>-default`, so
  unrelated projects no longer share one network
- networks support `external:` and `name:`: up verifies an external network
  exists and fails naming it, never creates it, and down never removes it
- a service's `networks:` mapping form is parsed; it previously read as no
  networks at all
- a container records the network it was created on, so changing a service's
  network recreates it instead of leaving it on the old one
- `down --rmi all|local` removes the services' images, `local` limited to the
  ones compose built; an invalid value is rejected rather than ignored
- `rm --volumes` errors: nothing tracks anonymous volumes, so honoring it would
  report a cleanup that never happened
- `--timeout` reaches `container stop -t` instead of being parsed and dropped
- `network prune` no longer removes the runtime's built-in network, and counts
  only what it actually removed
- down --rmi and compose stop warn on stderr rather than reporting success for
  something that failed

BREAKING CHANGE: containers now really join the networks a compose file
declares, so services that relied on every container sharing the runtime's
global network can no longer reach services in other projects. Networks are no
longer stored under ~/.mocker/networks; `network connect`/`disconnect` report
that the runtime does not support them. MockerKit: NetworkInfo.created is
optional, NetworkManager's methods are async and throwing, and ObservedContainer
gained a network field.
RealProcessRunner waited for a child with `waitUntilExit()`, which can wedge a
thread Swift concurrency still needs. A command that shells out once got away
with it; now that networks go through the CLI, `compose down` makes several
calls in a row and hung indefinitely — 150s and counting, against 2s once exit
is observed through terminationHandler instead. Pipes are drained before the
process starts so a full buffer cannot block the child either.

Also skip network removal for a project that is already down: it retried three
times and then told the user to remove a network that was not there.
… error

`rm --volumes` refused to run at all, so a flag that merely has nothing to do
also stopped the containers from being removed. It now warns and removes them,
matching how the codebase treats a flag the runtime cannot honor.

Network creation failures no longer lose the runtime's own message: a rejected
name or an unavailable backend was being reported as a generic "failed to
create network".
@us
us merged commit 2859bcd into main Aug 9, 2026
1 check passed
@us
us deleted the fix/external-networks-and-ignored-flags branch August 9, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant