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 .
|
docker build -t "$orchestrator" -f Dockerfile.orchestrator .
|
||||||
orchestrator_id=$(docker image inspect --format '{{.Id}}' "$orchestrator")
|
orchestrator_id=$(docker image inspect --format '{{.Id}}' "$orchestrator")
|
||||||
case "$orchestrator_id" in sha256:*) ;; *) exit 1 ;; esac
|
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 -t "$gateway" -f Dockerfile.gateway .
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg "ORCHESTRATOR_BASE_IMAGE=$orchestrator_id" \
|
--build-arg "ORCHESTRATOR_BASE_IMAGE=$orchestrator_base" \
|
||||||
-t "$orchestrator_fc" -f Dockerfile.orchestrator.fc .
|
-t "$orchestrator_fc" -f Dockerfile.orchestrator.fc .
|
||||||
docker build -t "$claude" -f bot_bottle/contrib/claude/Dockerfile .
|
docker build -t "$claude" -f bot_bottle/contrib/claude/Dockerfile .
|
||||||
docker build -t "$codex" -f bot_bottle/contrib/codex/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.
|
# snapshot, the same Dockerfile resolves different package versions over time.
|
||||||
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||||
RUN sed -i \
|
RUN sed -i \
|
||||||
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${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|https://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|https://snapshot.debian.org/archive/debian/${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|https://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
|
/etc/apt/sources.list.d/debian.sources
|
||||||
|
|
||||||
# Runtime system deps:
|
# Runtime system deps:
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ FROM ${ORCHESTRATOR_BASE_IMAGE}
|
|||||||
|
|
||||||
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||||
RUN sed -i \
|
RUN sed -i \
|
||||||
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${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|https://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|https://snapshot.debian.org/archive/debian/${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|https://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
|
/etc/apt/sources.list.d/debian.sources
|
||||||
|
|
||||||
RUN apt-get -o Acquire::Check-Valid-Until=false update \
|
RUN apt-get -o Acquire::Check-Valid-Until=false update \
|
||||||
|
|||||||
@@ -165,6 +165,36 @@ def image_id(ref: str) -> str:
|
|||||||
return image
|
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:
|
def verify_agent_image(image: str, argv: tuple[str, ...]) -> None:
|
||||||
"""Run `argv` inside a throwaway container of a freshly built agent
|
"""Run `argv` inside a throwaway container of a freshly built agent
|
||||||
image and die loudly if it fails, instead of shipping an image
|
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())
|
root = str(resources.build_root())
|
||||||
docker_mod.build_image(
|
docker_mod.build_image(
|
||||||
_ORCHESTRATOR_IMAGE, root, dockerfile="Dockerfile.orchestrator")
|
_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(
|
docker_mod.build_image(
|
||||||
_GATEWAY_IMAGE, root, dockerfile="Dockerfile.gateway")
|
_GATEWAY_IMAGE, root, dockerfile="Dockerfile.gateway")
|
||||||
docker_mod.build_image(
|
docker_mod.build_image(
|
||||||
_ORCHESTRATOR_FC_IMAGE,
|
_ORCHESTRATOR_FC_IMAGE,
|
||||||
root,
|
root,
|
||||||
dockerfile="Dockerfile.orchestrator.fc",
|
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
|
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||||
RUN sed -i \
|
RUN sed -i \
|
||||||
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${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|https://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|https://snapshot.debian.org/archive/debian/${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|https://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
|
/etc/apt/sources.list.d/debian.sources
|
||||||
|
|
||||||
# Install runtime system deps. claude-code shells out to git for several
|
# 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
|
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||||
RUN sed -i \
|
RUN sed -i \
|
||||||
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${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|https://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|https://snapshot.debian.org/archive/debian/${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|https://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
|
/etc/apt/sources.list.d/debian.sources
|
||||||
|
|
||||||
RUN apt-get -o Acquire::Check-Valid-Until=false update \
|
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
|
ARG DEBIAN_SNAPSHOT=20260724T000000Z
|
||||||
RUN sed -i \
|
RUN sed -i \
|
||||||
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${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|https://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|https://snapshot.debian.org/archive/debian/${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|https://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
|
/etc/apt/sources.list.d/debian.sources
|
||||||
|
|
||||||
RUN apt-get -o Acquire::Check-Valid-Until=false update \
|
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
|
- Python and Node base images use version-qualified tags plus multi-platform
|
||||||
OCI index digests.
|
OCI index digests.
|
||||||
- Debian packages resolve from the same dated `snapshot.debian.org` archive in
|
- 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
|
- Gateway Python dependencies install from `requirements.gateway.lock` with
|
||||||
`pip --require-hashes`; provider npm packages install with `npm ci` from
|
`pip --require-hashes`; provider npm packages install with `npm ci` from
|
||||||
committed lockfiles.
|
committed lockfiles.
|
||||||
@@ -17,9 +20,11 @@ immutability:
|
|||||||
The standalone layout is retained because Codex remote control requires it.
|
The standalone layout is retained because Codex remote control requires it.
|
||||||
|
|
||||||
`Dockerfile.orchestrator.fc` is the one intentionally dynamic `FROM`. It has no
|
`Dockerfile.orchestrator.fc` is the one intentionally dynamic `FROM`. It has no
|
||||||
default value: the Firecracker build coordinator builds the orchestrator first,
|
default value: the Firecracker build coordinator resolves the freshly built
|
||||||
resolves its local `sha256:` image ID, and supplies that exact ID as
|
orchestrator's local `sha256:` image ID, creates a local tag containing the
|
||||||
`ORCHESTRATOR_BASE_IMAGE`.
|
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
|
## Refreshing inputs
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|||||||
import subprocess
|
import subprocess
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import timezone
|
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
|
from bot_bottle.backend.docker import util as docker_mod
|
||||||
|
|
||||||
@@ -170,5 +170,59 @@ class TestImageId(unittest.TestCase):
|
|||||||
die.assert_called_once()
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.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, "build_image") as build, \
|
||||||
patch.object(
|
patch.object(
|
||||||
infra_vm.docker_mod,
|
infra_vm.docker_mod,
|
||||||
"image_id",
|
"pinned_local_image_ref",
|
||||||
return_value="sha256:" + "a" * 64,
|
return_value="bot-bottle-orchestrator:sha256-" + "a" * 64,
|
||||||
) as image_id:
|
) as pinned_ref:
|
||||||
infra_vm.ensure_built()
|
infra_vm.ensure_built()
|
||||||
tags = [c.args[0] for c in build.call_args_list]
|
tags = [c.args[0] for c in build.call_args_list]
|
||||||
# orchestrator-fc is FROM orchestrator, so the base is built first.
|
# 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.assertEqual(infra_vm._ORCHESTRATOR_FC_IMAGE, tags[-1])
|
||||||
self.assertLess(tags.index(infra_vm._ORCHESTRATOR_IMAGE),
|
self.assertLess(tags.index(infra_vm._ORCHESTRATOR_IMAGE),
|
||||||
tags.index(infra_vm._ORCHESTRATOR_FC_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(
|
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"],
|
build.call_args_list[-1].kwargs["build_args"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user