feat(macos): spike rootless Docker inside bottles
test / integration-docker (pull_request) Successful in 13s
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / unit (pull_request) Successful in 37s
test / stage-firecracker-inputs (pull_request) Successful in 2s
lint / lint (push) Successful in 2m44s
test / build-infra (pull_request) Successful in 3m38s
test / integration-firecracker (pull_request) Successful in 2m7s
test / coverage (pull_request) Failing after 1m59s
test / publish-infra (pull_request) Has been skipped
test / integration-docker (pull_request) Successful in 13s
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / unit (pull_request) Successful in 37s
test / stage-firecracker-inputs (pull_request) Successful in 2s
lint / lint (push) Successful in 2m44s
test / build-infra (pull_request) Successful in 3m38s
test / integration-firecracker (pull_request) Successful in 2m7s
test / coverage (pull_request) Failing after 1m59s
test / publish-infra (pull_request) Has been skipped
Add an opt-in docker_access path that layers rootless Docker tooling onto the selected agent image, starts the daemon only after registration, and retains the existing outer network and capability boundary. Include fail-closed bootstrap checks plus a live-Mac Compose/security acceptance test.\n\nRefs #392.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"""Live-Mac acceptance spike for guest-local rootless Docker (issue #392).
|
||||
|
||||
Run explicitly on an Apple Silicon/macOS 26 host:
|
||||
|
||||
BOT_BOTTLE_ROOTLESS_DOCKER_SPIKE=1 \
|
||||
python3 -m unittest tests.integration.test_macos_rootless_docker_spike -v
|
||||
|
||||
The opt-in is deliberate: ordinary Linux CI cannot execute Apple Container.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from bot_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from bot_bottle.manifest import ManifestIndex
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
platform.system() == "Darwin"
|
||||
and os.environ.get("BOT_BOTTLE_ROOTLESS_DOCKER_SPIKE") == "1",
|
||||
"requires an explicit live-Mac rootless-Docker spike run",
|
||||
)
|
||||
class TestMacosRootlessDockerSpike(unittest.TestCase):
|
||||
def test_compose_stays_inside_registered_bottle(self) -> None:
|
||||
workspace = Path(tempfile.mkdtemp(prefix="rootless-docker-spike."))
|
||||
stage = Path(tempfile.mkdtemp(prefix="rootless-docker-stage."))
|
||||
try:
|
||||
(workspace / "index.html").write_text("bottle-compose-ok\n")
|
||||
(workspace / "compose.yaml").write_text(
|
||||
"services:\n"
|
||||
" web:\n"
|
||||
" image: python:3.12-alpine\n"
|
||||
" working_dir: /workspace\n"
|
||||
" command: python -m http.server 8000\n"
|
||||
" volumes: ['.:/workspace']\n"
|
||||
" ports: ['18080:8000']\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
manifest = ManifestIndex.from_json_obj({
|
||||
"bottles": {"dev": {
|
||||
"docker_access": True,
|
||||
"egress": {"routes": [
|
||||
{"host": "auth.docker.io"},
|
||||
{"host": "registry-1.docker.io"},
|
||||
{"host": "production.cloudflare.docker.com"},
|
||||
]},
|
||||
}},
|
||||
"agents": {"spike": {
|
||||
"bottle": "dev", "skills": [], "prompt": "",
|
||||
}},
|
||||
})
|
||||
spec = BottleSpec(
|
||||
manifest=manifest,
|
||||
agent_name="spike",
|
||||
copy_cwd=True,
|
||||
user_cwd=str(workspace),
|
||||
)
|
||||
backend = get_bottle_backend("macos-container")
|
||||
plan = backend.prepare(spec, stage_dir=stage)
|
||||
with backend.launch(plan) as bottle:
|
||||
workdir = plan.workspace_plan.workdir
|
||||
checks = (
|
||||
"docker info >/dev/null && docker compose version && "
|
||||
f"cd {workdir} && docker compose up -d --wait && "
|
||||
"curl --fail --silent http://127.0.0.1:18080/ | "
|
||||
"grep -q bottle-compose-ok"
|
||||
)
|
||||
result = bottle.exec(checks)
|
||||
self.assertEqual(
|
||||
0, result.returncode,
|
||||
f"stdout={result.stdout!r}\nstderr={result.stderr!r}",
|
||||
)
|
||||
inspect = bottle.exec(
|
||||
"docker info --format '{{json .SecurityOptions}}'"
|
||||
)
|
||||
self.assertIn("rootless", inspect.stdout.lower())
|
||||
self.assertNotEqual(
|
||||
0,
|
||||
bottle.exec("test -S /var/run/docker.sock").returncode,
|
||||
"spike must never expose a host/rootful Docker socket",
|
||||
)
|
||||
direct = bottle.exec(
|
||||
"docker run --rm --env HTTP_PROXY= --env HTTPS_PROXY= "
|
||||
"--env http_proxy= --env https_proxy= python:3.12-alpine "
|
||||
"wget -T 4 -qO- https://evil.example.com/"
|
||||
)
|
||||
self.assertNotEqual(
|
||||
0, direct.returncode,
|
||||
"an inner container obtained direct, unproxied egress",
|
||||
)
|
||||
finally:
|
||||
shutil.rmtree(workspace, ignore_errors=True)
|
||||
shutil.rmtree(stage, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user