fix(build): close remaining mutable image inputs

This commit is contained in:
2026-07-26 20:41:29 +00:00
committed by didericis
parent 33b7bcd082
commit 73c566f3ff
16 changed files with 184 additions and 44 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ on:
- "bot_bottle/contrib/*/Dockerfile" - "bot_bottle/contrib/*/Dockerfile"
- "bot_bottle/contrib/*/package.json" - "bot_bottle/contrib/*/package.json"
- "bot_bottle/contrib/*/package-lock.json" - "bot_bottle/contrib/*/package-lock.json"
- "bot_bottle/contrib/codex/install.sh.sha256" - "bot_bottle/contrib/codex/codex-package_SHA256SUMS"
- "requirements.gateway.*" - "requirements.gateway.*"
- "image-build-args.json" - "image-build-args.json"
- ".pylintrc" - ".pylintrc"
+4 -8
View File
@@ -76,19 +76,15 @@ jobs:
bot_bottle/contrib/claude/package-lock.json \ bot_bottle/contrib/claude/package-lock.json \
bot_bottle/contrib/pi/package-lock.json bot_bottle/contrib/pi/package-lock.json
- name: Refresh pinned Codex installer checksum - name: Refresh pinned Codex archive checksums
run: | run: |
CODEX_VERSION=$( CODEX_VERSION=$(
sed -n 's/^ARG CODEX_VERSION=//p' bot_bottle/contrib/codex/Dockerfile sed -n 's/^ARG CODEX_VERSION=//p' bot_bottle/contrib/codex/Dockerfile
) )
test -n "$CODEX_VERSION" test -n "$CODEX_VERSION"
curl -fsSL \ curl -fsSL \
"https://raw.githubusercontent.com/openai/codex/rust-v${CODEX_VERSION}/scripts/install/install.sh" \ "https://github.com/openai/codex/releases/download/rust-v${CODEX_VERSION}/codex-package_SHA256SUMS" \
-o bot_bottle/contrib/codex/install.sh -o bot_bottle/contrib/codex/codex-package_SHA256SUMS
(
cd bot_bottle/contrib/codex
sha256sum install.sh > install.sh.sha256
)
- name: Upload refreshed inputs - name: Upload refreshed inputs
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
@@ -98,4 +94,4 @@ jobs:
requirements.gateway.lock requirements.gateway.lock
bot_bottle/contrib/claude/package-lock.json bot_bottle/contrib/claude/package-lock.json
bot_bottle/contrib/pi/package-lock.json bot_bottle/contrib/pi/package-lock.json
bot_bottle/contrib/codex/install.sh.sha256 bot_bottle/contrib/codex/codex-package_SHA256SUMS
+5 -1
View File
@@ -116,7 +116,11 @@ def _layer_nested_containers(
) )
info(f"using cached nested-container image {derived!r}") info(f"using cached nested-container image {derived!r}")
return derived return derived
return nested_containers_mod.build_image(agent_image, container_mod.build_image) return nested_containers_mod.build_image(
agent_image,
container_mod.build_image,
container_mod.pinned_local_image_ref,
)
@contextmanager @contextmanager
@@ -57,6 +57,7 @@ _GUEST_DEVICES = ("/dev/fuse", "/dev/net/tun")
def build_image( def build_image(
base_image: str, base_image: str,
build: Callable[..., None], build: Callable[..., None],
pin_local_base: Callable[[str], str],
) -> str: ) -> str:
"""Layer the nested-container tooling onto an already-built agent image. """Layer the nested-container tooling onto an already-built agent image.
@@ -67,13 +68,15 @@ def build_image(
# abstraction once that infrastructure exists. # abstraction once that infrastructure exists.
""" """
image = f"{base_image}{IMAGE_SUFFIX}" image = f"{base_image}{IMAGE_SUFFIX}"
pinned_base = pin_local_base(base_image)
init_script = Path(__file__).with_name("nested-containers-init.sh") init_script = Path(__file__).with_name("nested-containers-init.sh")
with tempfile.TemporaryDirectory(prefix="bot-bottle-nested-containers.") as tmp: with tempfile.TemporaryDirectory(prefix="bot-bottle-nested-containers.") as tmp:
context = Path(tmp) context = Path(tmp)
shutil.copy2(init_script, context / "nested-containers-init.sh") shutil.copy2(init_script, context / "nested-containers-init.sh")
(context / "Dockerfile").write_text( (context / "Dockerfile").write_text(
"FROM docker:28-cli AS docker_cli\n" "ARG DOCKER_CLI_BASE_IMAGE\n"
f"FROM {base_image}\n" "FROM ${DOCKER_CLI_BASE_IMAGE} AS docker_cli\n"
f"FROM {pinned_base}\n"
"USER root\n" "USER root\n"
"COPY --from=docker_cli /usr/local/bin/docker /usr/local/bin/docker\n" "COPY --from=docker_cli /usr/local/bin/docker /usr/local/bin/docker\n"
"COPY --from=docker_cli /usr/local/libexec/docker/cli-plugins/" "COPY --from=docker_cli /usr/local/libexec/docker/cli-plugins/"
@@ -682,6 +682,40 @@ def image_id(ref: str) -> str:
raise AssertionError("unreachable") raise AssertionError("unreachable")
def pinned_local_image_ref(ref: str) -> str:
"""Tag a local image with its complete content ID for a stable ``FROM``.
Agent images are immediately used as bases for the optional
nested-containers layer. A content-derived tag prevents another concurrent
build from moving the provider's ordinary ``:latest`` tag between those
two builds.
"""
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 = subprocess.run(
[_CONTAINER, "image", "tag", image, pinned_ref],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
die(
f"could not tag exact local image {image}: "
f"{(result.stderr or result.stdout or '').strip() 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 image_created_at(ref: str) -> datetime | None: def image_created_at(ref: str) -> datetime | None:
"""Return the image creation timestamp as an aware UTC datetime, or None """Return the image creation timestamp as an aware UTC datetime, or None
when the field is absent or unparseable (e.g. FROM-scratch images, images when the field is absent or unparseable (e.g. FROM-scratch images, images
+25 -15
View File
@@ -6,9 +6,8 @@
ARG NODE_BASE_IMAGE ARG NODE_BASE_IMAGE
FROM ${NODE_BASE_IMAGE} FROM ${NODE_BASE_IMAGE}
# The standalone installer is used below because remote-control requires its # Remote-control requires the standalone package layout. Keep this exact release
# managed package layout. Keep this exact release in sync with the verified # in sync with the committed upstream archive checksums.
# installer source and checksum.
ARG CODEX_VERSION=0.145.0 ARG CODEX_VERSION=0.145.0
ARG DEBIAN_SNAPSHOT=20260724T000000Z ARG DEBIAN_SNAPSHOT=20260724T000000Z
@@ -44,18 +43,29 @@ WORKDIR /home/node
ENV PATH="/home/node/.local/bin:${PATH}" ENV PATH="/home/node/.local/bin:${PATH}"
# Remote-control support requires the standalone Codex install layout under # Install the exact standalone release archive selected by the target
# ~/.codex/packages/standalone/current. Fetch the installer from the same # architecture. The checksum list is copied from the immutable upstream release
# immutable release tag as the requested CLI, verify the committed checksum # and committed so a rebuild cannot silently accept changed release bytes.
# before executing it, and let the official installer verify the selected COPY --chown=node:node bot_bottle/contrib/codex/codex-package_SHA256SUMS /tmp/codex-package_SHA256SUMS
# release archive against upstream release metadata. RUN case "$(dpkg --print-architecture)" in \
COPY --chown=node:node bot_bottle/contrib/codex/install.sh.sha256 /tmp/install.sh.sha256 amd64) codex_target=x86_64-unknown-linux-musl ;; \
RUN curl -fsSL \ arm64) codex_target=aarch64-unknown-linux-musl ;; \
"https://raw.githubusercontent.com/openai/codex/rust-v${CODEX_VERSION}/scripts/install/install.sh" \ *) echo "unsupported Codex architecture: $(dpkg --print-architecture)" >&2; exit 1 ;; \
-o /tmp/install.sh \ esac \
&& cd /tmp \ && codex_asset="codex-package-${codex_target}.tar.gz" \
&& sha256sum -c install.sh.sha256 \ && codex_sha256="$(awk -v asset="${codex_asset}" '$2 == asset { print $1 }' /tmp/codex-package_SHA256SUMS)" \
&& CODEX_NON_INTERACTIVE=1 sh /tmp/install.sh --release "${CODEX_VERSION}" \ && test -n "${codex_sha256}" \
&& curl -fsSL \
"https://github.com/openai/codex/releases/download/rust-v${CODEX_VERSION}/${codex_asset}" \
-o "/tmp/${codex_asset}" \
&& echo "${codex_sha256} /tmp/${codex_asset}" | sha256sum -c - \
&& codex_release="/home/node/.codex/packages/standalone/releases/${CODEX_VERSION}-${codex_target}" \
&& mkdir -p "${codex_release}" /home/node/.local/bin \
&& tar -xzf "/tmp/${codex_asset}" -C "${codex_release}" \
&& ln -s bin/codex "${codex_release}/codex" \
&& ln -s "${codex_release}" /home/node/.codex/packages/standalone/current \
&& ln -s /home/node/.codex/packages/standalone/current/bin/codex /home/node/.local/bin/codex \
&& rm "/tmp/${codex_asset}" \
&& test "$(codex --version)" = "codex-cli ${CODEX_VERSION}" && test "$(codex --version)" = "codex-cli ${CODEX_VERSION}"
CMD ["codex"] CMD ["codex"]
@@ -0,0 +1,2 @@
54f79a05aba6f9abf8ef988abcae8bf2fcefba20beb549b4ff2b3acdb2cb6f54 codex-package-aarch64-unknown-linux-musl.tar.gz
71a28d362c96ac9829bf8203a2c71be451aeb726adb843167fdaf0eae8fe7dd9 codex-package-x86_64-unknown-linux-musl.tar.gz
@@ -1 +0,0 @@
1154e9daf713aacd1534efca8042bfd6665ad24bc1d1dfd86b8f439fe60a7a5d install.sh
+16 -2
View File
@@ -52,7 +52,11 @@ BUNDLED_RESOURCES: tuple[str, ...] = (
# tell for which layout we're in. # tell for which layout we're in.
_CHECKOUT_MARKER = "Dockerfile.gateway" _CHECKOUT_MARKER = "Dockerfile.gateway"
_IMAGE_BUILD_ARGS_FILE = "image-build-args.json" _IMAGE_BUILD_ARGS_FILE = "image-build-args.json"
_CENTRAL_BUILD_ARG_NAMES = frozenset({"NODE_BASE_IMAGE", "PYTHON_BASE_IMAGE"}) _CENTRAL_BUILD_ARG_NAMES = frozenset({
"DOCKER_CLI_BASE_IMAGE",
"NODE_BASE_IMAGE",
"PYTHON_BASE_IMAGE",
})
class ResourceError(RuntimeError): class ResourceError(RuntimeError):
@@ -112,7 +116,17 @@ def image_build_args(
wanted = declared & _CENTRAL_BUILD_ARG_NAMES wanted = declared & _CENTRAL_BUILD_ARG_NAMES
if not wanted: if not wanted:
return {} return {}
root = Path(context) if context is not None else build_root() # Generated Dockerfiles (notably the macOS nested-container layer) live in
# temporary build contexts. Resolve their declaration there, but take the
# centralized values from that context only when it carries its own input
# file; otherwise use bot-bottle's staged build root.
context_root = Path(context) if context is not None else None
root = (
context_root
if context_root is not None
and (context_root / _IMAGE_BUILD_ARGS_FILE).is_file()
else build_root()
)
inputs_path = root / _IMAGE_BUILD_ARGS_FILE inputs_path = root / _IMAGE_BUILD_ARGS_FILE
try: try:
inputs = json.loads(inputs_path.read_text(encoding="utf-8")) inputs = json.loads(inputs_path.read_text(encoding="utf-8"))
+5 -4
View File
@@ -4,7 +4,8 @@ Bot-bottle's supported images are intended to rebuild from the same declared
inputs on Linux amd64 and arm64. The repository enforces four layers of inputs on Linux amd64 and arm64. The repository enforces four layers of
immutability: immutability:
- Python and Node base images are required, defaultless Docker build arguments. - Python, Node, and nested-container Docker CLI base images are required,
defaultless Docker build arguments.
Their version-qualified tags and multi-platform OCI index digests live Their version-qualified tags and multi-platform OCI index digests live
together in `image-build-args.json`; every supported builder reads that together in `image-build-args.json`; every supported builder reads that
file and passes only the arguments its Dockerfile declares. file and passes only the arguments its Dockerfile declares.
@@ -16,9 +17,9 @@ immutability:
- 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.
- The Codex standalone installer is fetched from the selected Codex release - The Codex standalone archive is fetched from the exact `CODEX_VERSION`
tag, checked against `install.sh.sha256`, invoked with an exact `--release`, release and checked against the committed upstream
and then verifies the selected release archive against upstream metadata. `codex-package_SHA256SUMS` before extraction.
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
+1
View File
@@ -1,4 +1,5 @@
{ {
"DOCKER_CLI_BASE_IMAGE": "docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d",
"NODE_BASE_IMAGE": "node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba", "NODE_BASE_IMAGE": "node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba",
"PYTHON_BASE_IMAGE": "python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de" "PYTHON_BASE_IMAGE": "python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de"
} }
+1 -1
View File
@@ -41,7 +41,7 @@ bot_bottle = [
"contrib/claude/package.json", "contrib/claude/package.json",
"contrib/claude/package-lock.json", "contrib/claude/package-lock.json",
"contrib/codex/Dockerfile", "contrib/codex/Dockerfile",
"contrib/codex/install.sh.sha256", "contrib/codex/codex-package_SHA256SUMS",
"contrib/pi/Dockerfile", "contrib/pi/Dockerfile",
"contrib/pi/package.json", "contrib/pi/package.json",
"contrib/pi/package-lock.json", "contrib/pi/package-lock.json",
+27 -3
View File
@@ -22,7 +22,11 @@ NPM_MANIFESTS = (
Path("bot_bottle/contrib/pi/package.json"), Path("bot_bottle/contrib/pi/package.json"),
) )
IMAGE_BUILD_ARGS = Path("image-build-args.json") IMAGE_BUILD_ARGS = Path("image-build-args.json")
REQUIRED_BASE_ARGS = frozenset({"NODE_BASE_IMAGE", "PYTHON_BASE_IMAGE"}) REQUIRED_BASE_ARGS = frozenset({
"DOCKER_CLI_BASE_IMAGE",
"NODE_BASE_IMAGE",
"PYTHON_BASE_IMAGE",
})
_DIGEST = re.compile(r"^[0-9a-f]{64}$") _DIGEST = re.compile(r"^[0-9a-f]{64}$")
_EXACT_NPM_VERSION = re.compile( _EXACT_NPM_VERSION = re.compile(
@@ -127,9 +131,9 @@ def check_dockerfile(
problems.append(f"{path}: gateway Python lock is not consumed") problems.append(f"{path}: gateway Python lock is not consumed")
if path == Path("bot_bottle/contrib/codex/Dockerfile"): if path == Path("bot_bottle/contrib/codex/Dockerfile"):
required = ( required = (
"install.sh.sha256", "codex-package_SHA256SUMS",
"sha256sum -c", "sha256sum -c",
'--release "${CODEX_VERSION}"', "codex-package-${codex_target}.tar.gz",
) )
for marker in required: for marker in required:
if marker not in instructions: if marker not in instructions:
@@ -256,6 +260,26 @@ def check_repo(root: Path = REPO_ROOT) -> list[str]:
problems.extend(check_dockerfile(path, text, image_build_args)) problems.extend(check_dockerfile(path, text, image_build_args))
for manifest in NPM_MANIFESTS: for manifest in NPM_MANIFESTS:
problems.extend(check_npm_manifest(root, manifest)) problems.extend(check_npm_manifest(root, manifest))
nested_path = Path(
"bot_bottle/backend/macos_container/nested_containers.py",
)
try:
nested_source = (root / nested_path).read_text(encoding="utf-8")
except OSError as exc:
problems.append(f"{nested_path}: cannot read generated image source: {exc}")
else:
if "FROM docker:" in nested_source:
problems.append(
"nested-containers image uses a mutable Docker CLI base",
)
if '"ARG DOCKER_CLI_BASE_IMAGE\\n"' not in nested_source:
problems.append(
"nested-containers image does not consume DOCKER_CLI_BASE_IMAGE",
)
if "pin_local_base(base_image)" not in nested_source:
problems.append(
"nested-containers image does not pin its local agent base",
)
problems.extend(check_python_lock(root)) problems.extend(check_python_lock(root))
return problems return problems
+23
View File
@@ -146,6 +146,29 @@ resolver #2
], ],
) )
def test_pinned_local_image_ref_tags_and_verifies_exact_id(self):
image = "sha256:" + "a" * 64
completed = util.subprocess.CompletedProcess(
args=[], returncode=0, stdout="", stderr="",
)
with patch.object(util, "image_id", return_value=image) as inspect, \
patch.object(util.subprocess, "run", return_value=completed) as run:
pinned = util.pinned_local_image_ref("registry:5000/agent:latest")
self.assertEqual(
f"registry:5000/agent:sha256-{'a' * 64}",
pinned,
)
run.assert_called_once_with(
["container", "image", "tag", image, pinned],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(
[("registry:5000/agent:latest",), (pinned,)],
[call.args for call in inspect.call_args_list],
)
def test_commit_container_execs_tar_and_builds_image(self): def test_commit_container_execs_tar_and_builds_image(self):
# stderr is bytes because subprocess.run uses stderr=PIPE without text=True # stderr is bytes because subprocess.run uses stderr=PIPE without text=True
completed = util.subprocess.CompletedProcess( completed = util.subprocess.CompletedProcess(
+20 -6
View File
@@ -107,12 +107,16 @@ class TestNestedContainersImage(unittest.TestCase):
def build(image: str, context: str, *, dockerfile: str) -> None: def build(image: str, context: str, *, dockerfile: str) -> None:
calls.append((image, context, dockerfile)) calls.append((image, context, dockerfile))
text = Path(dockerfile).read_text(encoding="utf-8") text = Path(dockerfile).read_text(encoding="utf-8")
self.assertIn("FROM agent:base", text) self.assertIn("ARG DOCKER_CLI_BASE_IMAGE", text)
self.assertIn("FROM ${DOCKER_CLI_BASE_IMAGE} AS docker_cli", text)
self.assertIn("FROM agent:sha256-deadbeef", text)
self.assertIn("aardvark-dns fuse-overlayfs netavark nftables passt podman", text) self.assertIn("aardvark-dns fuse-overlayfs netavark nftables passt podman", text)
self.assertIn("USER node", text) self.assertIn("USER node", text)
self.assertTrue((Path(context) / "nested-containers-init.sh").is_file()) self.assertTrue((Path(context) / "nested-containers-init.sh").is_file())
image = nested_containers.build_image("agent:base", build) image = nested_containers.build_image(
"agent:base", build, lambda _ref: "agent:sha256-deadbeef",
)
self.assertEqual("agent:base-nested-containers", image) self.assertEqual("agent:base-nested-containers", image)
self.assertEqual("agent:base-nested-containers", calls[0][0]) self.assertEqual("agent:base-nested-containers", calls[0][0])
@@ -127,7 +131,9 @@ class TestNestedContainersImage(unittest.TestCase):
def build(_image: str, _context: str, *, dockerfile: str) -> None: def build(_image: str, _context: str, *, dockerfile: str) -> None:
seen.append(Path(dockerfile).read_text(encoding="utf-8")) seen.append(Path(dockerfile).read_text(encoding="utf-8"))
nested_containers.build_image("agent:base", build) nested_containers.build_image(
"agent:base", build, lambda _ref: "agent:sha256-deadbeef",
)
for package in ("podman", "passt", "nftables", "aardvark-dns"): for package in ("podman", "passt", "nftables", "aardvark-dns"):
self.assertIn(package, seen[0]) self.assertIn(package, seen[0])
@@ -143,7 +149,9 @@ class TestNestedContainersImage(unittest.TestCase):
def build(_image: str, _context: str, *, dockerfile: str) -> None: def build(_image: str, _context: str, *, dockerfile: str) -> None:
seen.append(Path(dockerfile).read_text(encoding="utf-8")) seen.append(Path(dockerfile).read_text(encoding="utf-8"))
nested_containers.build_image("agent:base", build) nested_containers.build_image(
"agent:base", build, lambda _ref: "agent:sha256-deadbeef",
)
text = seen[0] text = seen[0]
self.assertIn("sed -i '/^node:/d' /etc/subuid /etc/subgid", text) self.assertIn("sed -i '/^node:/d' /etc/subuid /etc/subgid", text)
self.assertNotIn("subuid", text.replace( self.assertNotIn("subuid", text.replace(
@@ -286,7 +294,9 @@ class TestBuildOrLoadImages(unittest.TestCase):
"agent:base", str(launch_mod.resources.build_root()), "agent:base", str(launch_mod.resources.build_root()),
dockerfile="/repo/Dockerfile", dockerfile="/repo/Dockerfile",
) )
derived.assert_called_once_with("agent:base", build) derived.assert_called_once_with(
"agent:base", build, launch_mod.container_mod.pinned_local_image_ref,
)
self.assertEqual("agent:base-nested-containers", images.agent) self.assertEqual("agent:base-nested-containers", images.agent)
def test_derived_image_layers_onto_a_committed_image(self) -> None: def test_derived_image_layers_onto_a_committed_image(self) -> None:
@@ -307,7 +317,11 @@ class TestBuildOrLoadImages(unittest.TestCase):
images = launch_mod.build_or_load_images(plan) images = launch_mod.build_or_load_images(plan)
build.assert_not_called() build.assert_not_called()
derived.assert_called_once_with("agent:committed", build) derived.assert_called_once_with(
"agent:committed",
build,
launch_mod.container_mod.pinned_local_image_ref,
)
self.assertEqual("agent:committed-nested-containers", images.agent) self.assertEqual("agent:committed-nested-containers", images.agent)
def test_cached_policy_refuses_to_build_the_derived_image(self) -> None: def test_cached_policy_refuses_to_build_the_derived_image(self) -> None:
+15
View File
@@ -50,6 +50,21 @@ class TestCheckoutMode(unittest.TestCase):
r"^python:\d+\.\d+\.\d+-.+@sha256:[0-9a-f]{64}$", r"^python:\d+\.\d+\.\d+-.+@sha256:[0-9a-f]{64}$",
) )
def test_generated_dockerfile_uses_central_input_from_build_root(self):
with tempfile.TemporaryDirectory() as tmp:
dockerfile = Path(tmp) / "Dockerfile"
dockerfile.write_text(
"ARG DOCKER_CLI_BASE_IMAGE\n"
"FROM ${DOCKER_CLI_BASE_IMAGE}\n",
encoding="utf-8",
)
args = resources.image_build_args(dockerfile, context=tmp)
self.assertEqual({"DOCKER_CLI_BASE_IMAGE"}, set(args))
self.assertRegex(
args["DOCKER_CLI_BASE_IMAGE"],
r"^docker:\d+-cli@sha256:[0-9a-f]{64}$",
)
def test_bundled_resources_all_exist_at_root(self): def test_bundled_resources_all_exist_at_root(self):
# Drift guard: every path setup.py bundles must exist in the checkout. # Drift guard: every path setup.py bundles must exist in the checkout.
root = resources.build_root() root = resources.build_root()