diff --git a/bot_bottle/backend/docker/util.py b/bot_bottle/backend/docker/util.py index e827c24..43c5c54 100644 --- a/bot_bottle/backend/docker/util.py +++ b/bot_bottle/backend/docker/util.py @@ -89,6 +89,31 @@ def docker_exec_root(container: str, argv: list[str]) -> None: ) +def docker_exec(container: str, argv: list[str], *, user: str = "") -> None: + """Run `docker exec` in the named container, dying with the + command's own stderr on failure. Pass `user=\"0\"` to run as root.""" + cmd = ["docker", "exec"] + if user: + cmd += ["-u", user] + cmd += [container, *argv] + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + if result.returncode != 0: + die( + f"docker exec in {container} failed: " + f"{(result.stderr or '').strip() or ''}" + ) + + +def docker_cp(src: str, dest: str) -> None: + """Run `docker cp`, dying with the command's own stderr on failure.""" + result = subprocess.run( + ["docker", "cp", src, dest], capture_output=True, text=True, check=False, + ) + if result.returncode != 0: + die(f"docker cp {src} -> {dest} failed: " + f"{(result.stderr or '').strip() or ''}") + + _SLUG_RE = re.compile(r"[^a-z0-9]+") diff --git a/bot_bottle/backend/firecracker/launch.py b/bot_bottle/backend/firecracker/launch.py index a0b0486..bd45b5e 100644 --- a/bot_bottle/backend/firecracker/launch.py +++ b/bot_bottle/backend/firecracker/launch.py @@ -42,6 +42,7 @@ from ...git_gate import ( from ...log import die, info, warn from ...supervise import DB_PATH_IN_CONTAINER, SUPERVISE_PORT from ...util import expand_tilde +from ..docker import util as docker_mod from ..docker.egress import ( EGRESS_CA_IN_CONTAINER, EGRESS_PORT, @@ -106,9 +107,9 @@ def launch( plan = _provision_git_gate_keys(plan) sidecar_name = sidecar_container_name(plan.slug) - _force_remove_container(sidecar_name) + docker_mod.force_remove_container(sidecar_name) _start_sidecar_bundle(plan, sidecar_name, slot.host_ip) - stack.callback(_force_remove_container, sidecar_name) + stack.callback(docker_mod.force_remove_container, sidecar_name) _stage_git_gate(plan, sidecar_name) plan = _stamp_agent_urls(plan, slot.host_ip) @@ -170,15 +171,15 @@ def _mint_certs(plan: FirecrackerBottlePlan) -> FirecrackerBottlePlan: def _build_agent_image(plan: FirecrackerBottlePlan) -> FirecrackerBottlePlan: - _docker_build(SIDECAR_BUNDLE_IMAGE, _REPO_DIR, dockerfile=SIDECAR_BUNDLE_DOCKERFILE) + docker_mod.build_image(SIDECAR_BUNDLE_IMAGE, _REPO_DIR, dockerfile=SIDECAR_BUNDLE_DOCKERFILE) committed = read_committed_image(plan.slug) - if committed and _image_exists(committed): + if committed and docker_mod.image_exists(committed): info(f"using committed image {committed!r}") return dataclasses.replace( plan, agent_provision=dataclasses.replace(plan.agent_provision, image=committed), ) - _docker_build(plan.image, _REPO_DIR, dockerfile=plan.dockerfile_path) + docker_mod.build_image(plan.image, _REPO_DIR, dockerfile=plan.dockerfile_path) return plan @@ -291,15 +292,15 @@ def _stage_git_gate(plan: FirecrackerBottlePlan, sidecar_name: str) -> None: gp = plan.git_gate_plan if not gp.upstreams: return - _docker_exec(sidecar_name, [ + docker_mod.docker_exec(sidecar_name, [ "mkdir", "-p", str(Path(GIT_GATE_HOOK_IN_CONTAINER).parent), GIT_GATE_CREDS_DIR_IN_CONTAINER, "/git", str(Path(_GIT_GATE_READY_FILE).parent), ]) for host_path, container_path in _git_gate_files(plan): - _docker_cp(host_path, f"{sidecar_name}:{container_path}") - _docker_exec(sidecar_name, [ + docker_mod.docker_cp(host_path, f"{sidecar_name}:{container_path}") + docker_mod.docker_exec(sidecar_name, [ "sh", "-c", f"chmod 755 {GIT_GATE_ENTRYPOINT_IN_CONTAINER} " f"{GIT_GATE_HOOK_IN_CONTAINER} {GIT_GATE_ACCESS_HOOK_IN_CONTAINER} && " @@ -355,50 +356,3 @@ def _agent_guest_env(plan: FirecrackerBottlePlan, host_ip: str) -> dict[str, str if value is not None: env[name] = value return env - - -# --- docker helpers -------------------------------------------------- - -def _docker_build(ref: str, context: str, *, dockerfile: str = "") -> None: - info(f"docker build {ref}") - args = ["docker", "build", "-t", ref] - if dockerfile: - if not os.path.isabs(dockerfile): - dockerfile = os.path.join(context, dockerfile) - args += ["-f", dockerfile] - args.append(context) - result = subprocess.run(args, check=False) - if result.returncode != 0: - die(f"docker build for {ref!r} failed") - - -def _image_exists(ref: str) -> bool: - return subprocess.run( - ["docker", "image", "inspect", ref], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, - ).returncode == 0 - - -def _force_remove_container(name: str) -> None: - subprocess.run( - ["docker", "rm", "-f", name], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, - ) - - -def _docker_exec(name: str, argv: list[str]) -> None: - result = subprocess.run( - ["docker", "exec", name, *argv], capture_output=True, text=True, check=False, - ) - if result.returncode != 0: - die(f"docker exec in {name} failed: " - f"{(result.stderr or '').strip() or ''}") - - -def _docker_cp(host_path: str, dest: str) -> None: - result = subprocess.run( - ["docker", "cp", host_path, dest], capture_output=True, text=True, check=False, - ) - if result.returncode != 0: - die(f"docker cp {host_path} -> {dest} failed: " - f"{(result.stderr or '').strip() or ''}")