47eb56bd10
The previous fix (`host.docker.internal:<port>` for daemon-side push) still failed: Get "https://host.docker.internal:53958/v2/": http: server gave HTTP response to HTTPS client `host.docker.internal` is reachable from Docker Desktop's daemon VM but isn't in the daemon's default insecure-registries CIDRs (only `::1/128` and `127.0.0.0/8` are), so docker push tries HTTPS, hits a plain-HTTP registry, and refuses. The daemon.json fix (`"insecure-registries": ["host.docker.internal"]`) works but is a one-time manual step in Docker Desktop's UI — not something we can do for the user. Sidestep the daemon push entirely: 1. docker build (as before) — local layer cache makes no-change rebuilds cheap. 2. docker save the image to a per-digest tarball alongside the cached `.smolmachine`. 3. Start an ephemeral registry container on a per-session docker network, with `-p :5000` so the host can also reach it for the pack step. 4. docker run a one-shot crane container on the SAME network, mount the tarball, `crane push --insecure /img.tar <registry-container>:5000/...`. Container DNS resolves the registry on the network; `--insecure` forces plain HTTP. 5. `smolvm pack create --image localhost:<host port>/...` from the host. smolvm's bundled crane auto-falls-back to HTTP for localhost addresses, so no insecure-registries config is needed on that side. 6. Tear down everything; reap the tarball (registries hold the same bytes, no need to keep both around). Net effect: the docker daemon never does an HTTP/HTTPS-policy decision on our behalf. `docker push` is gone from the prepare path; `docker save`, `docker network create`, `docker run` (for registry + crane) replace it. Tested end-to-end on Docker Desktop / macOS: `_ensure_smolmachine ("claude-bottle:latest")` produces a 204MB `.smolmachine.smolmachine` artifact. Adds: - backend/docker/util.py:save() — thin docker save wrapper. - local_registry.crane_push_tarball() — one-shot crane run on the registry's network. - CRANE_IMAGE constant pinned by digest (gcr.io/go-containerregistry/crane@sha256:0ae17ecb...). Removes: - backend/docker/util.py:tag() / push() — unused without daemon push. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
72 lines
2.2 KiB
Python
72 lines
2.2 KiB
Python
"""Unit: image_id / tag / push helpers in
|
|
claude_bottle.backend.docker.util (PRD 0023 chunk 4c additions).
|
|
|
|
Tests mock `subprocess.run` and assert on argv shape + parsing.
|
|
The actual docker round-trip is covered by the chunk 4c
|
|
integration smoke."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from claude_bottle.backend.docker import util as docker_mod
|
|
|
|
|
|
def _ok(stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess:
|
|
return subprocess.CompletedProcess(
|
|
args=[], returncode=0, stdout=stdout, stderr=stderr,
|
|
)
|
|
|
|
|
|
def _fail(stderr: str = "boom") -> subprocess.CompletedProcess:
|
|
return subprocess.CompletedProcess(
|
|
args=[], returncode=1, stdout="", stderr=stderr,
|
|
)
|
|
|
|
|
|
class TestImageId(unittest.TestCase):
|
|
def test_strips_trailing_newline(self):
|
|
# docker image inspect --format ... emits a trailing newline.
|
|
with patch.object(
|
|
docker_mod.subprocess, "run",
|
|
return_value=_ok(stdout="sha256:abcdef\n"),
|
|
) as run:
|
|
self.assertEqual(
|
|
"sha256:abcdef", docker_mod.image_id("claude-bottle:latest")
|
|
)
|
|
argv = run.call_args.args[0]
|
|
self.assertEqual(
|
|
["docker", "image", "inspect", "--format", "{{.Id}}", "claude-bottle:latest"],
|
|
argv,
|
|
)
|
|
|
|
def test_dies_on_inspect_failure(self):
|
|
with patch.object(
|
|
docker_mod.subprocess, "run", return_value=_fail("No such image"),
|
|
), patch.object(
|
|
docker_mod, "die", side_effect=SystemExit("die"),
|
|
) as die:
|
|
with self.assertRaises(SystemExit):
|
|
docker_mod.image_id("missing:tag")
|
|
die.assert_called_once()
|
|
self.assertIn("missing:tag", die.call_args.args[0])
|
|
|
|
|
|
class TestSave(unittest.TestCase):
|
|
def test_save_runs_docker_save(self):
|
|
with patch.object(
|
|
docker_mod.subprocess, "run", return_value=_ok(),
|
|
) as run:
|
|
docker_mod.save("claude-bottle:latest", "/tmp/img.tar")
|
|
argv = run.call_args.args[0]
|
|
self.assertEqual(
|
|
["docker", "save", "claude-bottle:latest", "-o", "/tmp/img.tar"],
|
|
argv,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|