Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 2 additions & 32 deletions tst/integration/env/gateway/gateway_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@
GET_TIME_TOOL_NAME as GET_TIME,
_SCHEMA as GET_TIME_SCHEMA,
)
from agent_env.env.bootstrap import PUBLISHED_IMAGES
from agent_env.env.envs.service_db import ServiceDBEnv
from agent_env.env.published_images import published_pinned
from agent_env.store.image_store.registry_api import read_manifest
from agent_env.env.envs.playwright_mcp import (
PLAYWRIGHT_MCP_IMAGE_TAG,
PLAYWRIGHT_MCP_ENVIRONMENT_NAME,
Expand All @@ -50,6 +47,7 @@
from agent_env.env.snapshot_store import compute_env_fingerprint
from agent_env.config import get_config
from agent_env.task_step.task_steps.prompt_agent import PromptAgentTaskStep
from tst.util.published_image import pull_published_else_build
from tst.util.capabilities import (
MCP_SERVER_SOURCES,
missing_capability_reason,
Expand Down Expand Up @@ -152,36 +150,8 @@ class DockerImageConfig:
service_name="slack",
)

_PUBLISHED_IMAGES = {(image.dockerfile, image.dockerfile.parent): image for image in PUBLISHED_IMAGES}


def _amd64_image(pinned: str | None) -> str | None:
"""``pinned``'s linux/amd64 image, pinned by its own digest: the containerd image store keeps a pulled index whole,
and ``docker save`` of one it holds only some platforms of fails."""
if pinned is None:
return None
manifest = read_manifest(pinned, None)
if "manifests" not in manifest:
return pinned
digest = next((m["digest"] for m in manifest["manifests"]
if m.get("platform", {}).get("os") == "linux" and m["platform"].get("architecture") == "amd64"), None)
return digest and f"{pinned.partition('@')[0]}@{digest}"


def _pull_published_else_build(img: DockerImageConfig) -> None:
published = _PUBLISHED_IMAGES.get((img.dockerfile, img.context))
pinned = published and _amd64_image(published_pinned(published))
if pinned:
logger.info(f"Pulling {img.name}'s published image {pinned}...")
commands = [["docker", "pull", "--platform", "linux/amd64", pinned], ["docker", "tag", pinned, img.tag]]
else:
logger.info(f"Building {img.name} Docker image...")
commands = [["docker", "build", "--platform", "linux/amd64", "-f", str(img.dockerfile), "-t", img.tag,
str(img.context)]]
for argv in commands:
result = subprocess.run(argv, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"{shlex.join(argv)} failed: {result.stderr}")
pull_published_else_build(img.name, img.dockerfile, img.context, img.tag)


@pytest.fixture(scope="module")
Expand Down
82 changes: 82 additions & 0 deletions tst/unit/test_published_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""The gateway tier's fixtures pull a published image of unchanged sources, and build it when there's none or the pull
fails."""

from __future__ import annotations

import subprocess

import pytest

from agent_env.env.bootstrap import PUBLISHED_IMAGES
from tst.util import published_image

IMAGE = PUBLISHED_IMAGES[0]
INDEX = f"{IMAGE.ref()}@sha256:{'1' * 64}"
AMD64 = f"sha256:{'a' * 64}"


@pytest.fixture
def docker(monkeypatch):
"""The ``docker`` commands run, each failing with ``fails[command]`` when set; the registry serves ``INDEX``, an
index of an amd64 and an arm64 image, unless ``served`` is emptied."""
state = {"calls": [], "fails": {}, "served": INDEX}

def run(argv, **kwargs):
state["calls"].append(argv[1:])
return subprocess.CompletedProcess(argv, 1 if argv[1] in state["fails"] else 0, "", state["fails"].get(argv[1], ""))

monkeypatch.setattr(published_image.subprocess, "run", run)
monkeypatch.setattr(published_image, "published_pinned", lambda image: state["served"])
monkeypatch.setattr(published_image, "read_manifest", lambda ref, auth: {"manifests": [
{"digest": f"sha256:{'b' * 64}", "platform": {"os": "linux", "architecture": "arm64"}},
{"digest": AMD64, "platform": {"os": "linux", "architecture": "amd64"}}]})
return state


def _make(tag="env-gateway"):
published_image.pull_published_else_build("gateway", IMAGE.dockerfile, IMAGE.dockerfile.parent, tag)


def test_a_published_image_of_the_same_sources_is_pulled_by_its_amd64_digest_and_tagged_not_built(docker):
_make()

pinned = f"{IMAGE.ref()}@{AMD64}"
assert docker["calls"] == [["pull", "--platform", "linux/amd64", pinned], ["tag", pinned, "env-gateway"]]


@pytest.mark.parametrize("failing", ["pull", "tag"])
def test_a_failed_pull_or_tag_builds_the_image_instead(docker, failing, caplog):
docker["fails"][failing] = "failed to copy: httpReadSeeker: failed open"

_make()

assert docker["calls"][-1][0] == "build" and "so it's built" in caplog.text


def test_a_registry_lookup_that_fails_builds_the_image(docker, monkeypatch):
def unreachable(image):
raise ValueError("ghcr.io: couldn't read its manifest")

monkeypatch.setattr(published_image, "published_pinned", unreachable)

_make()

assert [call[0] for call in docker["calls"]] == ["build"]


def test_sources_nothing_has_published_and_ones_agent_env_doesnt_publish_are_built(docker, tmp_path):
docker["served"] = None
_make()
published_image.pull_published_else_build("slack", tmp_path / "Dockerfile", tmp_path, "mcp-slack")

assert [call[0] for call in docker["calls"]] == ["build", "build"]
assert docker["calls"][1] == ["build", "--platform", "linux/amd64", "-f", str(tmp_path / "Dockerfile"), "-t",
"mcp-slack", str(tmp_path)]


def test_a_failed_build_raises_with_dockers_output(docker):
docker["served"] = None
docker["fails"]["build"] = "no such file: Dockerfile"

with pytest.raises(RuntimeError, match="docker build .* failed: no such file: Dockerfile"):
_make()
52 changes: 52 additions & 0 deletions tst/util/published_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Tag an image on this machine's Docker the way the gateway tier's fixtures do: pulled when the registry serves the
image agent-env published from the same Dockerfile and build context, else built here."""

from __future__ import annotations

import logging
import shlex
import subprocess
from pathlib import Path

from agent_env.env.bootstrap import PUBLISHED_IMAGES
from agent_env.env.published_images import published_pinned
from agent_env.store.image_store.registry_api import read_manifest

logger = logging.getLogger(__name__)

_PUBLISHED_IMAGES = {(image.dockerfile, image.dockerfile.parent): image for image in PUBLISHED_IMAGES}


def pull_published_else_build(name: str, dockerfile: Path, context: Path, tag: str) -> None:
"""``tag``, pulled for linux/amd64 when the registry serves the published image of these sources, whose tag is a
digest of them, so a change to them is built; also built when the lookup or pull fails. A failed build raises."""
published = _PUBLISHED_IMAGES.get((dockerfile, context))
try:
if pinned := published and _amd64_image(published_pinned(published)):
logger.info(f"Pulling {name}'s published image {pinned}...")
_docker("pull", "--platform", "linux/amd64", pinned)
_docker("tag", pinned, tag)
return
except (RuntimeError, ValueError) as e:
logger.warning(f"Pulling {name}'s published image failed, so it's built: {e}")
logger.info(f"Building {name} Docker image...")
_docker("build", "--platform", "linux/amd64", "-f", str(dockerfile), "-t", tag, str(context))


def _amd64_image(pinned: str | None) -> str | None:
"""``pinned``'s linux/amd64 image, pinned by its own digest: the containerd image store keeps a pulled index whole,
and ``docker save`` of one it holds only some platforms of fails."""
if pinned is None:
return None
manifest = read_manifest(pinned, None)
if "manifests" not in manifest:
return pinned
digest = next((m["digest"] for m in manifest["manifests"]
if m.get("platform", {}).get("os") == "linux" and m["platform"].get("architecture") == "amd64"), None)
return digest and f"{pinned.partition('@')[0]}@{digest}"


def _docker(*args: str) -> None:
result = subprocess.run(["docker", *args], capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"{shlex.join(['docker', *args])} failed: {result.stderr}")
Loading