build: make pinned image inputs portable
prd-number-check / require-numbered-prds (pull_request) Successful in 8s
refresh-image-locks / refresh (push) Successful in 28s
lint / lint (push) Successful in 1m3s
test / unit (pull_request) Successful in 53s
test / integration-docker (pull_request) Successful in 2m50s
test / coverage (pull_request) Failing after 22s
test / image-input-builds (pull_request) Successful in 3m35s
tracker-policy-pr / check-pr (pull_request) Failing after 11m8s
prd-number-check / require-numbered-prds (pull_request) Successful in 8s
refresh-image-locks / refresh (push) Successful in 28s
lint / lint (push) Successful in 1m3s
test / unit (pull_request) Successful in 53s
test / integration-docker (pull_request) Successful in 2m50s
test / coverage (pull_request) Failing after 22s
test / image-input-builds (pull_request) Successful in 3m35s
tracker-policy-pr / check-pr (pull_request) Failing after 11m8s
This commit is contained in:
@@ -183,9 +183,14 @@ jobs:
|
||||
docker build -t "$orchestrator" -f Dockerfile.orchestrator .
|
||||
orchestrator_id=$(docker image inspect --format '{{.Id}}' "$orchestrator")
|
||||
case "$orchestrator_id" in sha256:*) ;; *) exit 1 ;; esac
|
||||
orchestrator_base="bot-bottle-orchestrator-inputs:sha256-${orchestrator_id#sha256:}"
|
||||
docker image tag "$orchestrator_id" "$orchestrator_base"
|
||||
test "$(
|
||||
docker image inspect --format '{{.Id}}' "$orchestrator_base"
|
||||
)" = "$orchestrator_id"
|
||||
docker build -t "$gateway" -f Dockerfile.gateway .
|
||||
docker build \
|
||||
--build-arg "ORCHESTRATOR_BASE_IMAGE=$orchestrator_id" \
|
||||
--build-arg "ORCHESTRATOR_BASE_IMAGE=$orchestrator_base" \
|
||||
-t "$orchestrator_fc" -f Dockerfile.orchestrator.fc .
|
||||
docker build -t "$claude" -f bot_bottle/contrib/claude/Dockerfile .
|
||||
docker build -t "$codex" -f bot_bottle/contrib/codex/Dockerfile .
|
||||
|
||||
+4
-4
@@ -47,10 +47,10 @@ FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee56815788280
|
||||
# snapshot, the same Dockerfile resolves different package versions over time.
|
||||
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||
RUN sed -i \
|
||||
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian-security|http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian-security|http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian|http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian|http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
/etc/apt/sources.list.d/debian.sources
|
||||
|
||||
# Runtime system deps:
|
||||
|
||||
@@ -20,10 +20,10 @@ FROM ${ORCHESTRATOR_BASE_IMAGE}
|
||||
|
||||
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||
RUN sed -i \
|
||||
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian-security|http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian-security|http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian|http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian|http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
/etc/apt/sources.list.d/debian.sources
|
||||
|
||||
RUN apt-get -o Acquire::Check-Valid-Until=false update \
|
||||
|
||||
@@ -165,6 +165,36 @@ def image_id(ref: str) -> str:
|
||||
return image
|
||||
|
||||
|
||||
def pinned_local_image_ref(ref: str) -> str:
|
||||
"""Give a local image a content-derived tag and verify the tag resolves
|
||||
back to the same image ID.
|
||||
|
||||
BuildKit treats a bare ``sha256:...`` ID in ``FROM`` as a registry
|
||||
repository name. A tag whose complete suffix is the local image ID remains
|
||||
resolvable by BuildKit, while the post-tag inspection keeps the handoff
|
||||
fail-closed.
|
||||
"""
|
||||
image = image_id(ref)
|
||||
digest = image.removeprefix("sha256:")
|
||||
if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
|
||||
die(f"could not derive a local base tag from invalid image ID {image!r}")
|
||||
|
||||
repository = ref.split("@", 1)[0]
|
||||
last_slash = repository.rfind("/")
|
||||
last_colon = repository.rfind(":")
|
||||
if last_colon > last_slash:
|
||||
repository = repository[:last_colon]
|
||||
pinned_ref = f"{repository}:sha256-{digest}"
|
||||
|
||||
result = run_docker(["docker", "image", "tag", image, pinned_ref])
|
||||
if result.returncode != 0:
|
||||
detail = (result.stderr or result.stdout or "").strip()
|
||||
die(f"could not tag exact local image {image}: {detail or '<no detail>'}")
|
||||
if image_id(pinned_ref) != image:
|
||||
die(f"content-derived local tag {pinned_ref!r} did not resolve to {image}")
|
||||
return pinned_ref
|
||||
|
||||
|
||||
def verify_agent_image(image: str, argv: tuple[str, ...]) -> None:
|
||||
"""Run `argv` inside a throwaway container of a freshly built agent
|
||||
image and die loudly if it fails, instead of shipping an image
|
||||
|
||||
@@ -133,14 +133,14 @@ def build_infra_images_with_docker() -> None:
|
||||
root = str(resources.build_root())
|
||||
docker_mod.build_image(
|
||||
_ORCHESTRATOR_IMAGE, root, dockerfile="Dockerfile.orchestrator")
|
||||
orchestrator_id = docker_mod.image_id(_ORCHESTRATOR_IMAGE)
|
||||
orchestrator_base = docker_mod.pinned_local_image_ref(_ORCHESTRATOR_IMAGE)
|
||||
docker_mod.build_image(
|
||||
_GATEWAY_IMAGE, root, dockerfile="Dockerfile.gateway")
|
||||
docker_mod.build_image(
|
||||
_ORCHESTRATOR_FC_IMAGE,
|
||||
root,
|
||||
dockerfile="Dockerfile.orchestrator.fc",
|
||||
build_args={"ORCHESTRATOR_BASE_IMAGE": orchestrator_id},
|
||||
build_args={"ORCHESTRATOR_BASE_IMAGE": orchestrator_base},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e8
|
||||
|
||||
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||
RUN sed -i \
|
||||
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian-security|http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian-security|http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian|http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian|http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
/etc/apt/sources.list.d/debian.sources
|
||||
|
||||
# Install runtime system deps. claude-code shells out to git for several
|
||||
|
||||
@@ -12,10 +12,10 @@ ARG CODEX_VERSION=0.145.0
|
||||
|
||||
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||
RUN sed -i \
|
||||
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian-security|http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian-security|http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian|http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian|http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
/etc/apt/sources.list.d/debian.sources
|
||||
|
||||
RUN apt-get -o Acquire::Check-Valid-Until=false update \
|
||||
|
||||
@@ -6,10 +6,10 @@ FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e8
|
||||
|
||||
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||
RUN sed -i \
|
||||
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian-security|http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian-security|http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|http://deb.debian.org/debian|http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
-e "s|https://deb.debian.org/debian|http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
|
||||
/etc/apt/sources.list.d/debian.sources
|
||||
|
||||
RUN apt-get -o Acquire::Check-Valid-Until=false update \
|
||||
|
||||
@@ -7,7 +7,10 @@ immutability:
|
||||
- Python and Node base images use version-qualified tags plus multi-platform
|
||||
OCI index digests.
|
||||
- Debian packages resolve from the same dated `snapshot.debian.org` archive in
|
||||
every image that runs `apt-get install`.
|
||||
every image that runs `apt-get install`. The snapshot endpoint uses HTTP so a
|
||||
fresh image does not need a host-specific TLS interception CA; APT still
|
||||
authenticates the repository metadata and package hashes with Debian's
|
||||
signed Release files.
|
||||
- Gateway Python dependencies install from `requirements.gateway.lock` with
|
||||
`pip --require-hashes`; provider npm packages install with `npm ci` from
|
||||
committed lockfiles.
|
||||
@@ -17,9 +20,11 @@ immutability:
|
||||
The standalone layout is retained because Codex remote control requires it.
|
||||
|
||||
`Dockerfile.orchestrator.fc` is the one intentionally dynamic `FROM`. It has no
|
||||
default value: the Firecracker build coordinator builds the orchestrator first,
|
||||
resolves its local `sha256:` image ID, and supplies that exact ID as
|
||||
`ORCHESTRATOR_BASE_IMAGE`.
|
||||
default value: the Firecracker build coordinator resolves the freshly built
|
||||
orchestrator's local `sha256:` image ID, creates a local tag containing the
|
||||
complete ID, verifies that tag resolves to the same ID, and supplies the
|
||||
content-derived reference as `ORCHESTRATOR_BASE_IMAGE`. This accommodates
|
||||
BuildKit, which treats a bare image ID in `FROM` as a registry repository.
|
||||
|
||||
## Refreshing inputs
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
import subprocess
|
||||
import unittest
|
||||
from datetime import timezone
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from bot_bottle.backend.docker import util as docker_mod
|
||||
|
||||
@@ -170,5 +170,59 @@ class TestImageId(unittest.TestCase):
|
||||
die.assert_called_once()
|
||||
|
||||
|
||||
class TestPinnedLocalImageRef(unittest.TestCase):
|
||||
def test_tags_and_verifies_content_derived_reference(self):
|
||||
image = "sha256:" + "c" * 64
|
||||
expected = "registry.example:5000/team/base:sha256-" + "c" * 64
|
||||
with patch.object(
|
||||
docker_mod,
|
||||
"image_id",
|
||||
side_effect=[image, image],
|
||||
) as image_id, patch.object(
|
||||
docker_mod,
|
||||
"run_docker",
|
||||
return_value=_ok(),
|
||||
) as run:
|
||||
self.assertEqual(
|
||||
expected,
|
||||
docker_mod.pinned_local_image_ref(
|
||||
"registry.example:5000/team/base:mutable"
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
["docker", "image", "tag", image, expected],
|
||||
run.call_args.args[0],
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
call("registry.example:5000/team/base:mutable"),
|
||||
call(expected),
|
||||
],
|
||||
image_id.call_args_list,
|
||||
)
|
||||
|
||||
def test_rejects_invalid_id_or_failed_tag(self):
|
||||
cases = [
|
||||
("sha256:short", _ok()),
|
||||
("sha256:" + "d" * 64, _fail("tag failed")),
|
||||
]
|
||||
for image, result in cases:
|
||||
with self.subTest(image=image), patch.object(
|
||||
docker_mod,
|
||||
"image_id",
|
||||
return_value=image,
|
||||
), patch.object(
|
||||
docker_mod,
|
||||
"run_docker",
|
||||
return_value=result,
|
||||
), patch.object(
|
||||
docker_mod,
|
||||
"die",
|
||||
side_effect=SystemExit("die"),
|
||||
):
|
||||
with self.assertRaises(SystemExit):
|
||||
docker_mod.pinned_local_image_ref("base:latest")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -110,9 +110,9 @@ class TestEnsureBuilt(unittest.TestCase):
|
||||
patch.object(infra_vm.docker_mod, "build_image") as build, \
|
||||
patch.object(
|
||||
infra_vm.docker_mod,
|
||||
"image_id",
|
||||
return_value="sha256:" + "a" * 64,
|
||||
) as image_id:
|
||||
"pinned_local_image_ref",
|
||||
return_value="bot-bottle-orchestrator:sha256-" + "a" * 64,
|
||||
) as pinned_ref:
|
||||
infra_vm.ensure_built()
|
||||
tags = [c.args[0] for c in build.call_args_list]
|
||||
# orchestrator-fc is FROM orchestrator, so the base is built first.
|
||||
@@ -121,9 +121,12 @@ class TestEnsureBuilt(unittest.TestCase):
|
||||
self.assertEqual(infra_vm._ORCHESTRATOR_FC_IMAGE, tags[-1])
|
||||
self.assertLess(tags.index(infra_vm._ORCHESTRATOR_IMAGE),
|
||||
tags.index(infra_vm._ORCHESTRATOR_FC_IMAGE))
|
||||
image_id.assert_called_once_with(infra_vm._ORCHESTRATOR_IMAGE)
|
||||
pinned_ref.assert_called_once_with(infra_vm._ORCHESTRATOR_IMAGE)
|
||||
self.assertEqual(
|
||||
{"ORCHESTRATOR_BASE_IMAGE": "sha256:" + "a" * 64},
|
||||
{
|
||||
"ORCHESTRATOR_BASE_IMAGE":
|
||||
"bot-bottle-orchestrator:sha256-" + "a" * 64,
|
||||
},
|
||||
build.call_args_list[-1].kwargs["build_args"],
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user