diff --git a/bot_bottle/backend/macos_container/nested-containers-init.sh b/bot_bottle/backend/macos_container/nested-containers-init.sh index 0c7c65f..8fcc363 100755 --- a/bot_bottle/backend/macos_container/nested-containers-init.sh +++ b/bot_bottle/backend/macos_container/nested-containers-init.sh @@ -7,7 +7,10 @@ if [ "$uid" -eq 0 ]; then exit 1 fi -for command in podman docker fuse-overlayfs slirp4netns; do +# 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 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 5f90965..5edb4fc 100644 --- a/bot_bottle/backend/macos_container/nested_containers.py +++ b/bot_bottle/backend/macos_container/nested_containers.py @@ -41,10 +41,9 @@ _INIT = "/usr/local/libexec/bot-bottle/nested-containers-init" # Deliberately cryptic and short. podman derives conmon's attach socket as # `$XDG_RUNTIME_DIR/libpod/tmp/socket/<64-hex-id>/attach`, and a Unix socket # path may not exceed 108 bytes (`sun_path`). The descriptive -# `/tmp/bot-bottle-podman-run` produced a 116-byte path, so every attached -# `docker run` failed with "unable to upgrade to tcp, received 500" while -# detached runs and image pulls worked fine. Do not lengthen this for -# readability — it buys 8 bytes of headroom over the limit. +# `/tmp/bot-bottle-podman-run` produced a 116-byte path — over the limit, so +# attach would have broken as soon as anything got far enough to attach. Do +# not lengthen this for readability; it buys 8 bytes of headroom. _RUNTIME_DIR = "/tmp/bbp" _SOCKET = f"{_RUNTIME_DIR}/podman.sock" _LOG = "/tmp/bot-bottle-nested-containers.log" @@ -83,8 +82,12 @@ 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. " && apt-get install -y --no-install-recommends " - "fuse-overlayfs slirp4netns uidmap \\\n" + "fuse-overlayfs 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 035a371..b064db5 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 slirp4netns uidmap", text) + self.assertIn("fuse-overlayfs passt slirp4netns uidmap", text) self.assertIn("USER node", text) self.assertTrue((Path(context) / "nested-containers-init.sh").is_file()) @@ -116,6 +116,19 @@ 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.""" + 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]) + def test_strips_subordinate_ranges_so_podman_avoids_newuidmap(self) -> None: """The single-UID fallback is the entire reason podman works here.