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.
94 lines
3.4 KiB
Python
94 lines
3.4 KiB
Python
"""Experimental rootless Docker bootstrap for Apple-container bottles.
|
|
|
|
The daemon and every child remain inside the existing per-bottle VM. This
|
|
module deliberately refuses to compensate for missing prerequisites with
|
|
outer capabilities, a privileged container, or a host Docker socket.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shlex
|
|
import shutil
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
from ...log import die, info
|
|
|
|
_INIT = "/usr/local/libexec/bot-bottle/rootless-docker-init"
|
|
_RUNTIME_DIR = "/tmp/bot-bottle-docker-run"
|
|
_SOCKET = f"{_RUNTIME_DIR}/docker.sock"
|
|
READY_RETRIES = 30
|
|
|
|
|
|
def build_image(
|
|
base_image: str,
|
|
build: Callable[..., None],
|
|
) -> str:
|
|
"""Layer spike-only tooling on an already-built provider image."""
|
|
image = f"{base_image}-rootless-docker"
|
|
init_script = Path(__file__).with_name("rootless-docker-init.sh")
|
|
with tempfile.TemporaryDirectory(prefix="bot-bottle-rootless-docker.") as tmp:
|
|
context = Path(tmp)
|
|
shutil.copy2(init_script, context / "rootless-docker-init.sh")
|
|
(context / "Dockerfile").write_text(
|
|
"FROM docker:28-cli AS docker_cli\n"
|
|
f"FROM {base_image}\n"
|
|
"USER root\n"
|
|
"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"
|
|
" && apt-get install -y --no-install-recommends docker.io uidmap "
|
|
"rootlesskit slirp4netns fuse-overlayfs \\\n"
|
|
" && rm -rf /var/lib/apt/lists/* \\\n"
|
|
" && echo 'node:100000:65536' >> /etc/subuid \\\n"
|
|
" && echo 'node:100000:65536' >> /etc/subgid \\\n"
|
|
" && ln -s /usr/share/docker.io/contrib/dockerd-rootless.sh "
|
|
"/usr/local/bin/dockerd-rootless.sh\n"
|
|
"COPY rootless-docker-init.sh "
|
|
"/usr/local/libexec/bot-bottle/rootless-docker-init\n"
|
|
"RUN chmod 0755 /usr/local/libexec/bot-bottle/rootless-docker-init\n"
|
|
"USER node\n",
|
|
encoding="utf-8",
|
|
)
|
|
build(image, str(context), dockerfile=str(context / "Dockerfile"))
|
|
return image
|
|
|
|
|
|
def guest_env(enabled: bool) -> dict[str, str]:
|
|
"""Environment consumed by the Docker CLI inside an enabled bottle."""
|
|
if not enabled:
|
|
return {}
|
|
return {
|
|
"DOCKER_HOST": f"unix://{_SOCKET}",
|
|
"XDG_RUNTIME_DIR": _RUNTIME_DIR,
|
|
}
|
|
|
|
|
|
def start(bottle: object) -> None:
|
|
"""Start and verify the unprivileged daemon through the bottle exec API."""
|
|
info("starting experimental rootless Docker daemon")
|
|
result = bottle.exec(shlex.quote(_INIT)) # type: ignore[attr-defined]
|
|
if result.returncode != 0:
|
|
detail = (result.stderr or result.stdout or "").strip()
|
|
die(f"rootless Docker bootstrap failed: {detail or '<no output>'}")
|
|
|
|
for _ in range(READY_RETRIES):
|
|
result = bottle.exec("docker info >/dev/null 2>&1") # type: ignore[attr-defined]
|
|
if result.returncode == 0:
|
|
info("rootless Docker daemon is ready")
|
|
return
|
|
time.sleep(0.2)
|
|
|
|
logs = bottle.exec( # type: ignore[attr-defined]
|
|
"tail -n 80 /tmp/bot-bottle-rootless-docker.log 2>/dev/null || true"
|
|
)
|
|
die(
|
|
"rootless Docker did not become ready without additional outer "
|
|
f"privileges:\n{(logs.stdout or logs.stderr or '<no log>').strip()}"
|
|
)
|
|
|
|
|
|
__all__ = ["build_image", "guest_env", "start"]
|