diff --git a/bot_bottle/backend/smolmachines/launch.py b/bot_bottle/backend/smolmachines/launch.py index ff41e0f..9102d4c 100644 --- a/bot_bottle/backend/smolmachines/launch.py +++ b/bot_bottle/backend/smolmachines/launch.py @@ -142,6 +142,7 @@ def launch( # daemons the agent needs to reach from the smolvm guest. bundle_spec = _bundle_launch_spec(plan, network, loopback_ip) token_env = _resolve_token_env(plan, os.environ) + _bundle.ensure_bundle_image(bundle_spec.image) _bundle.start_bundle(bundle_spec, env={**os.environ, **token_env}) stack.callback(_bundle.stop_bundle, plan.slug) diff --git a/bot_bottle/backend/smolmachines/sidecar_bundle.py b/bot_bottle/backend/smolmachines/sidecar_bundle.py index 9aca944..553a972 100644 --- a/bot_bottle/backend/smolmachines/sidecar_bundle.py +++ b/bot_bottle/backend/smolmachines/sidecar_bundle.py @@ -29,7 +29,14 @@ from pathlib import Path from typing import Sequence from ...log import die, warn -from ..docker.sidecar_bundle import SIDECAR_BUNDLE_IMAGE +from ..docker import util as docker_mod +from ..docker.sidecar_bundle import ( + SIDECAR_BUNDLE_DOCKERFILE, + SIDECAR_BUNDLE_IMAGE, +) + + +_REPO_DIR = str(Path(__file__).resolve().parent.parent.parent.parent) def bundle_network_name(slug: str) -> str: @@ -85,6 +92,21 @@ class BundleLaunchSpec: publish_host_ip: str = "127.0.0.1" +def ensure_bundle_image(image: str = SIDECAR_BUNDLE_IMAGE) -> None: + """Build the sidecar bundle image before `docker run`. + + The Docker backend gets this for free from compose's `build:` + stanza. smolmachines starts the bundle with plain `docker run`, + so without an explicit build a first launch tries to pull the + local-only `bot-bottle-sidecars:latest` tag from a registry. + """ + docker_mod.build_image( + image, + _REPO_DIR, + dockerfile=SIDECAR_BUNDLE_DOCKERFILE, + ) + + def create_bundle_network(network_name: str, subnet: str, gateway: str) -> None: """`docker network create` with an explicit subnet + gateway so the bundle's `--ip` lands on the address the Smolfile's diff --git a/tests/unit/test_smolmachines_sidecar_bundle.py b/tests/unit/test_smolmachines_sidecar_bundle.py index f913d23..84f181f 100644 --- a/tests/unit/test_smolmachines_sidecar_bundle.py +++ b/tests/unit/test_smolmachines_sidecar_bundle.py @@ -9,6 +9,7 @@ from __future__ import annotations import subprocess import unittest +from pathlib import Path from unittest.mock import patch from bot_bottle.backend.smolmachines.sidecar_bundle import ( @@ -16,6 +17,7 @@ from bot_bottle.backend.smolmachines.sidecar_bundle import ( bundle_container_name, bundle_network_name, create_bundle_network, + ensure_bundle_image, remove_bundle_network, start_bundle, stop_bundle, @@ -182,6 +184,21 @@ class TestStartBundle(unittest.TestCase): self.assertEqual({"FOO": "bar"}, m.call_args.kwargs["env"]) +class TestEnsureBundleImage(unittest.TestCase): + def test_builds_sidecar_dockerfile_before_plain_docker_run(self): + with patch( + "bot_bottle.backend.smolmachines.sidecar_bundle.docker_mod.build_image", + ) as build: + ensure_bundle_image() + + build.assert_called_once() + args = build.call_args.args + kwargs = build.call_args.kwargs + self.assertEqual("bot-bottle-sidecars:latest", args[0]) + self.assertTrue((Path(args[1]) / "Dockerfile.sidecars").is_file()) + self.assertEqual("Dockerfile.sidecars", kwargs["dockerfile"]) + + class TestStopBundle(unittest.TestCase): def _patch_run(self, **kwargs): return patch(