diff --git a/src/aio_fleet/registry.py b/src/aio_fleet/registry.py index e577f18..9feb2a3 100644 --- a/src/aio_fleet/registry.py +++ b/src/aio_fleet/registry.py @@ -73,7 +73,7 @@ def verify_registry_tags( failures: list[str] = [] for tag in tags: failure = ( - _verify_dockerhub_tag(tag) + _verify_dockerhub_tag(docker, tag, env=env) if _is_dockerhub_tag(tag) else _verify_with_docker_imagetools(docker, tag, env=env) ) @@ -104,7 +104,17 @@ def _is_dockerhub_tag(tag: str) -> bool: return first in {"docker.io", "index.docker.io"} or "." not in first -def _verify_dockerhub_tag(tag: str, *, attempts: int = 8) -> str | None: +def _verify_dockerhub_tag( + docker: str, + tag: str, + *, + env: Mapping[str, str] | None = None, + attempts: int = 8, +) -> str | None: + docker_failure = _verify_with_docker_imagetools(docker, tag, env=env) + if docker_failure is None: + return None + parsed = _dockerhub_tag_parts(tag) if parsed is None: return f"{tag}: unsupported Docker Hub tag format" diff --git a/tests/test_registry.py b/tests/test_registry.py index 39041eb..ab85936 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -186,43 +186,40 @@ def test_component_registry_tags_use_configured_component_image( assert "jsonbored/example-aio:latest" not in command # nosec B101 -def test_dockerhub_verification_uses_tag_api(monkeypatch) -> None: - seen_urls: list[str] = [] - - class Response: - status = 200 - - def __enter__(self): - return self - - def __exit__(self, *_args) -> None: - return None +def test_dockerhub_verification_uses_docker_imagetools_first(monkeypatch) -> None: + seen_commands: list[list[str]] = [] + inspect_env = {"DOCKER_CONFIG": "/workspace/aio-fleet-docker"} + seen_envs: list[dict[str, str] | None] = [] + monkeypatch.setattr(registry.shutil, "which", lambda _name: "docker") - def read(self) -> bytes: - return b"{}" + def fake_run(command: list[str], **kwargs): + seen_commands.append(command) + seen_envs.append(kwargs.get("env")) + return SimpleNamespace(returncode=0, stdout="", stderr="") - monkeypatch.setattr(registry.shutil, "which", lambda _name: "docker") + monkeypatch.setattr(registry.subprocess, "run", fake_run) monkeypatch.setattr( - registry.subprocess, - "run", + registry.urllib.request, + "urlopen", lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("Docker Hub tags should not use docker inspect") + AssertionError("Docker Hub tags should use docker inspect before API") ), ) - def fake_urlopen(url: str, timeout: int): - seen_urls.append(url) - assert timeout == 20 # nosec B101 - return Response() - - monkeypatch.setattr(registry.urllib.request, "urlopen", fake_urlopen) - assert ( - registry.verify_registry_tags(["jsonbored/sure-aio:latest"]) == [] + registry.verify_registry_tags(["jsonbored/sure-aio:latest"], env=inspect_env) + == [] ) # nosec B101 - assert seen_urls == [ # nosec B101 - "https://hub.docker.com/v2/repositories/jsonbored/sure-aio/tags/latest" + assert seen_commands == [ # nosec B101 + [ + "docker", + "buildx", + "imagetools", + "inspect", + "jsonbored/sure-aio:latest", + ] ] + assert seen_envs == [inspect_env] # nosec B101 def test_dockerhub_verification_reports_malformed_json(monkeypatch) -> None: @@ -239,6 +236,13 @@ def read(self) -> bytes: return b"not json" monkeypatch.setattr(registry.shutil, "which", lambda _name: "docker") + monkeypatch.setattr( + registry.subprocess, + "run", + lambda *_args, **_kwargs: SimpleNamespace( + returncode=1, stdout="", stderr="not indexed yet" + ), + ) monkeypatch.setattr( registry.urllib.request, "urlopen", lambda *_args, **_kwargs: Response() ) @@ -255,6 +259,13 @@ def read(self) -> bytes: def test_dockerhub_verification_reports_missing_tag(monkeypatch) -> None: monkeypatch.setattr(registry.shutil, "which", lambda _name: "docker") + monkeypatch.setattr( + registry.subprocess, + "run", + lambda *_args, **_kwargs: SimpleNamespace( + returncode=1, stdout="", stderr="not found" + ), + ) monkeypatch.setattr(registry.time, "sleep", lambda _seconds: None) def fake_urlopen(url: str, timeout: int): @@ -293,6 +304,13 @@ def fake_urlopen(url: str, timeout: int): return Response() monkeypatch.setattr(registry.shutil, "which", lambda _name: "docker") + monkeypatch.setattr( + registry.subprocess, + "run", + lambda *_args, **_kwargs: SimpleNamespace( + returncode=1, stdout="", stderr="not indexed yet" + ), + ) monkeypatch.setattr(registry.urllib.request, "urlopen", fake_urlopen) monkeypatch.setattr(registry.time, "sleep", sleeps.append)