72e35a1343
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.
104 lines
4.0 KiB
Python
104 lines
4.0 KiB
Python
"""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()
|