From 3c92e797755094be34bc65709a6a4c6594c458e0 Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 21 Jul 2026 19:28:42 -0400 Subject: [PATCH] fix(macos): install podman 5's full networking stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit netavark shells out to `nft` for the bridge network every compose file expects, and aardvark-dns serves name resolution inside nested containers. Neither arrives with the base image's `--no-install-recommends` podman, and each fails at a different and misleading layer: without nft containers are created but never start; without aardvark-dns DNS inside a container fails while the engine, the pulls, and the bridge all look healthy. That last one is the likeliest explanation for the "DNS inside nested containers fails entirely" note in #392 — a missing package, not a design gap. Verified on the macOS host: with passt alone, `docker run` reached netavark and died on "unable to execute nft". Co-Authored-By: Claude Opus 4.8 --- .../macos_container/nested-containers-init.sh | 10 ++++++---- .../macos_container/nested_containers.py | 19 ++++++++++++++----- tests/unit/test_macos_nested_containers.py | 16 +++++++++------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/bot_bottle/backend/macos_container/nested-containers-init.sh b/bot_bottle/backend/macos_container/nested-containers-init.sh index 8fcc363..56daaf4 100755 --- a/bot_bottle/backend/macos_container/nested-containers-init.sh +++ b/bot_bottle/backend/macos_container/nested-containers-init.sh @@ -7,10 +7,12 @@ if [ "$uid" -eq 0 ]; then exit 1 fi -# pasta is podman 5's default rootless network helper. Checked here so a -# missing package fails the bootstrap with a clear message rather than -# letting every later `docker run` die with "could not find pasta". -for command in podman docker fuse-overlayfs pasta slirp4netns; do +# Every piece of podman 5's networking stack is checked here, because each +# one fails at a different and misleading layer if it is absent: no pasta and +# nothing starts at all; no nft and netavark cannot build the bridge every +# compose file expects; no aardvark-dns and DNS inside nested containers fails +# while everything else looks healthy. +for command in podman docker fuse-overlayfs pasta nft slirp4netns; do command -v "$command" >/dev/null 2>&1 || { echo "missing nested-container prerequisite: $command" >&2 exit 1 diff --git a/bot_bottle/backend/macos_container/nested_containers.py b/bot_bottle/backend/macos_container/nested_containers.py index 5edb4fc..57104d4 100644 --- a/bot_bottle/backend/macos_container/nested_containers.py +++ b/bot_bottle/backend/macos_container/nested_containers.py @@ -82,12 +82,21 @@ def build_image( "COPY --from=docker_cli /usr/local/libexec/docker/cli-plugins/" "docker-compose /usr/local/libexec/docker/cli-plugins/docker-compose\n" "RUN apt-get update \\\n" - # passt provides `pasta`, which podman 5 uses by default for - # rootless networking (podman 4 defaulted to slirp4netns). Without - # it every container fails to start with "could not find pasta". - # slirp4netns stays as the documented fallback. + # podman 5's networking stack, installed explicitly because the + # base image's `--no-install-recommends` podman does not pull it + # in and each piece fails at a different, misleading layer: + # passt -> `pasta`, the default rootless netns helper + # (podman 4 used slirp4netns); without it + # nothing starts: "could not find pasta" + # nftables -> `nft`, which netavark shells out to for the + # bridge network every compose file expects + # aardvark-dns -> name resolution *inside* nested containers; + # without it DNS fails while everything else + # looks healthy + # slirp4netns stays as the documented fallback for pasta. " && apt-get install -y --no-install-recommends " - "fuse-overlayfs passt slirp4netns uidmap \\\n" + "aardvark-dns fuse-overlayfs netavark nftables passt " + "slirp4netns uidmap \\\n" " && rm -rf /var/lib/apt/lists/* \\\n" # Deliberate: an empty subordinate range keeps podman on the # single-UID mapping that needs no CAP_SYS_ADMIN. Adding ranges diff --git a/tests/unit/test_macos_nested_containers.py b/tests/unit/test_macos_nested_containers.py index b064db5..01e117c 100644 --- a/tests/unit/test_macos_nested_containers.py +++ b/tests/unit/test_macos_nested_containers.py @@ -108,7 +108,7 @@ class TestNestedContainersImage(unittest.TestCase): calls.append((image, context, dockerfile)) text = Path(dockerfile).read_text(encoding="utf-8") self.assertIn("FROM agent:base", text) - self.assertIn("fuse-overlayfs passt slirp4netns uidmap", text) + self.assertIn("aardvark-dns fuse-overlayfs netavark nftables passt", text) self.assertIn("USER node", text) self.assertTrue((Path(context) / "nested-containers-init.sh").is_file()) @@ -116,18 +116,20 @@ class TestNestedContainersImage(unittest.TestCase): self.assertEqual("agent:base-nested-containers", image) self.assertEqual("agent:base-nested-containers", calls[0][0]) - def test_installs_pasta_for_podman_5_rootless_networking(self) -> None: - """podman 5 defaults to pasta where podman 4 used slirp4netns. Without - the passt package every nested container fails to start with "could - not find pasta" — pulls still work, which makes it look like a - compat-API bug rather than a missing dependency.""" + def test_installs_the_whole_podman_5_networking_stack(self) -> None: + """Each missing piece fails at a different, misleading layer: no pasta + and nothing starts; no nft and netavark cannot build the bridge that + compose expects; no aardvark-dns and DNS inside nested containers + fails while everything else looks healthy. Image pulls keep working + throughout, which is what made these read as compat-API bugs.""" seen: list[str] = [] def build(_image: str, _context: str, *, dockerfile: str) -> None: seen.append(Path(dockerfile).read_text(encoding="utf-8")) nested_containers.build_image("agent:base", build) - self.assertIn("passt", seen[0]) + for package in ("passt", "nftables", "aardvark-dns"): + self.assertIn(package, seen[0]) def test_strips_subordinate_ranges_so_podman_avoids_newuidmap(self) -> None: """The single-UID fallback is the entire reason podman works here.