build: pin and verify image inputs
This commit is contained in:
+9
-5
@@ -36,11 +36,12 @@
|
|||||||
# 9420 git-gate smart HTTP (VM-backend agent-facing transport)
|
# 9420 git-gate smart HTTP (VM-backend agent-facing transport)
|
||||||
# 9100 supervise (MCP HTTP)
|
# 9100 supervise (MCP HTTP)
|
||||||
|
|
||||||
# Based on `python:3.12-slim` (Debian trixie) rather than the
|
# Based on an exact `python:3.12.13-slim-trixie` multi-architecture manifest
|
||||||
|
# rather than the
|
||||||
# `mitmproxy/mitmproxy` image (Debian bookworm), matching the trixie base the
|
# `mitmproxy/mitmproxy` image (Debian bookworm), matching the trixie base the
|
||||||
# orchestrator image needs for buildah (Dockerfile.orchestrator.fc). mitmproxy
|
# orchestrator image needs for buildah (Dockerfile.orchestrator.fc). mitmproxy
|
||||||
# is pip-installed to the same effect as the upstream image.
|
# is pip-installed to the same effect as the upstream image.
|
||||||
FROM python:3.12-slim
|
FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
|
||||||
|
|
||||||
# Runtime system deps:
|
# Runtime system deps:
|
||||||
# git supplies the `git daemon` subcommand (no separate package)
|
# git supplies the `git daemon` subcommand (no separate package)
|
||||||
@@ -54,10 +55,13 @@ RUN apt-get update \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# mitmdump (the egress data plane). The upstream mitmproxy image baked
|
# mitmdump (the egress data plane). The upstream mitmproxy image baked
|
||||||
# this in; on the plain python base we pip-install the same pinned
|
# this in; on the plain python base we install a fully resolved lock whose
|
||||||
# version. Its CA dir is set explicitly via `--set confdir=` in
|
# distributions are all hash-verified. Its CA dir is set explicitly via
|
||||||
|
# `--set confdir=` in
|
||||||
# egress-entrypoint.sh, so it doesn't depend on a `mitmproxy` home user.
|
# egress-entrypoint.sh, so it doesn't depend on a `mitmproxy` home user.
|
||||||
RUN pip install --no-cache-dir mitmproxy==11.1.3
|
COPY requirements.gateway.lock /tmp/requirements.gateway.lock
|
||||||
|
RUN pip install --no-cache-dir --require-hashes \
|
||||||
|
-r /tmp/requirements.gateway.lock
|
||||||
|
|
||||||
# gitleaks (the pre-receive hook's secret scanner). Installed from its
|
# gitleaks (the pre-receive hook's secret scanner). Installed from its
|
||||||
# official release, pinned by version + SHA256 and verified — rather than
|
# official release, pinned by version + SHA256 and verified — rather than
|
||||||
|
|||||||
@@ -16,9 +16,11 @@
|
|||||||
# secret-dense control plane on a minimal dependency surface is the point
|
# secret-dense control plane on a minimal dependency surface is the point
|
||||||
# (PRD 0070's "secret concentration").
|
# (PRD 0070's "secret concentration").
|
||||||
#
|
#
|
||||||
# Shares the trixie `python:3.12-slim` base with the gateway image.
|
# Shares an exact multi-architecture Python/trixie manifest with the gateway
|
||||||
|
# image. The version-qualified tag keeps the human-readable upstream version;
|
||||||
|
# the digest makes the bytes immutable.
|
||||||
|
|
||||||
FROM python:3.12-slim
|
FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,11 @@
|
|||||||
# bare microVM (no fuse-overlayfs / overlay module / subuid maps). The trixie
|
# bare microVM (no fuse-overlayfs / overlay module / subuid maps). The trixie
|
||||||
# base (from Dockerfile.orchestrator's python:3.12-slim) carries buildah 1.39,
|
# base (from Dockerfile.orchestrator's python:3.12-slim) carries buildah 1.39,
|
||||||
# which parses the Dockerfile heredocs agent images use (bookworm's 1.28 can't).
|
# which parses the Dockerfile heredocs agent images use (bookworm's 1.28 can't).
|
||||||
# Matches image_builder.
|
# Matches image_builder. There is deliberately no default: the build coordinator
|
||||||
FROM bot-bottle-orchestrator:latest
|
# passes the exact local image ID returned by `docker image inspect`, so this
|
||||||
|
# stage cannot silently resolve a stale `:latest` tag.
|
||||||
|
ARG ORCHESTRATOR_BASE_IMAGE
|
||||||
|
FROM ${ORCHESTRATOR_BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
|||||||
@@ -4,5 +4,7 @@
|
|||||||
include Dockerfile.gateway
|
include Dockerfile.gateway
|
||||||
include Dockerfile.orchestrator
|
include Dockerfile.orchestrator
|
||||||
include Dockerfile.orchestrator.fc
|
include Dockerfile.orchestrator.fc
|
||||||
|
include requirements.gateway.in
|
||||||
|
include requirements.gateway.lock
|
||||||
include nix/firecracker-netpool.nix
|
include nix/firecracker-netpool.nix
|
||||||
include scripts/firecracker-netpool.sh
|
include scripts/firecracker-netpool.sh
|
||||||
|
|||||||
@@ -119,7 +119,13 @@ def docker_cp(src: str, dest: str) -> None:
|
|||||||
f"{(result.stderr or '').strip() or '<no stderr>'}")
|
f"{(result.stderr or '').strip() or '<no stderr>'}")
|
||||||
|
|
||||||
|
|
||||||
def build_image(ref: str, context: str, *, dockerfile: str = "") -> None:
|
def build_image(
|
||||||
|
ref: str,
|
||||||
|
context: str,
|
||||||
|
*,
|
||||||
|
dockerfile: str = "",
|
||||||
|
build_args: dict[str, str] | None = None,
|
||||||
|
) -> None:
|
||||||
"""Invokes `docker build` every call. Layer cache makes no-change
|
"""Invokes `docker build` every call. Layer cache makes no-change
|
||||||
rebuilds cheap; running every time means Dockerfile edits land
|
rebuilds cheap; running every time means Dockerfile edits land
|
||||||
without manual `docker rmi`.
|
without manual `docker rmi`.
|
||||||
@@ -139,10 +145,26 @@ def build_image(ref: str, context: str, *, dockerfile: str = "") -> None:
|
|||||||
args.append("--no-cache")
|
args.append("--no-cache")
|
||||||
if dockerfile:
|
if dockerfile:
|
||||||
args.extend(["-f", dockerfile])
|
args.extend(["-f", dockerfile])
|
||||||
|
for name, value in (build_args or {}).items():
|
||||||
|
args.extend(["--build-arg", f"{name}={value}"])
|
||||||
args.append(context)
|
args.append(context)
|
||||||
subprocess.run(args, check=True)
|
subprocess.run(args, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def image_id(ref: str) -> str:
|
||||||
|
"""Return the exact content-addressed ID for a local image.
|
||||||
|
|
||||||
|
This is used when one locally built image is another Dockerfile's base:
|
||||||
|
passing the ID prevents a mutable tag from being resolved between builds.
|
||||||
|
"""
|
||||||
|
result = run_docker(["docker", "image", "inspect", "--format", "{{.Id}}", ref])
|
||||||
|
image = result.stdout.strip()
|
||||||
|
if result.returncode != 0 or not image.startswith("sha256:"):
|
||||||
|
detail = (result.stderr or result.stdout or "").strip()
|
||||||
|
die(f"could not resolve exact image ID for {ref!r}: {detail or '<no detail>'}")
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ _ARTIFACT_FORMAT = "1"
|
|||||||
# from its own generic package; the Dockerfiles baked into each differ (only the
|
# from its own generic package; the Dockerfiles baked into each differ (only the
|
||||||
# orchestrator rootfs carries buildah), so the versions are hashed separately.
|
# orchestrator rootfs carries buildah), so the versions are hashed separately.
|
||||||
ROLES = ("orchestrator", "gateway")
|
ROLES = ("orchestrator", "gateway")
|
||||||
_DOCKERFILES = {
|
_BUILD_INPUTS = {
|
||||||
"orchestrator": ("Dockerfile.orchestrator", "Dockerfile.orchestrator.fc"),
|
"orchestrator": ("Dockerfile.orchestrator", "Dockerfile.orchestrator.fc"),
|
||||||
"gateway": ("Dockerfile.gateway",),
|
"gateway": ("Dockerfile.gateway", "requirements.gateway.lock"),
|
||||||
}
|
}
|
||||||
|
|
||||||
_DEFAULT_BASE = "https://gitea.dideric.is"
|
_DEFAULT_BASE = "https://gitea.dideric.is"
|
||||||
@@ -101,7 +101,7 @@ def infra_artifact_version(
|
|||||||
h.update(str(path.relative_to(repo_root)).encode())
|
h.update(str(path.relative_to(repo_root)).encode())
|
||||||
h.update(b"\0")
|
h.update(b"\0")
|
||||||
h.update(path.read_bytes())
|
h.update(path.read_bytes())
|
||||||
for name in _DOCKERFILES[role]:
|
for name in _BUILD_INPUTS[role]:
|
||||||
h.update(name.encode())
|
h.update(name.encode())
|
||||||
h.update(b"\0")
|
h.update(b"\0")
|
||||||
h.update((repo_root / name).read_bytes())
|
h.update((repo_root / name).read_bytes())
|
||||||
|
|||||||
@@ -133,10 +133,15 @@ 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)
|
||||||
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, root, dockerfile="Dockerfile.orchestrator.fc")
|
_ORCHESTRATOR_FC_IMAGE,
|
||||||
|
root,
|
||||||
|
dockerfile="Dockerfile.orchestrator.fc",
|
||||||
|
build_args={"ORCHESTRATOR_BASE_IMAGE": orchestrator_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_rootfs_dir(role: str) -> Path:
|
def build_rootfs_dir(role: str) -> Path:
|
||||||
|
|||||||
@@ -8,9 +8,8 @@
|
|||||||
# Layer ordering is deliberate: the npm install lives in its own layer so
|
# Layer ordering is deliberate: the npm install lives in its own layer so
|
||||||
# changes to the rest of the repo (or to the CMD) don't bust it.
|
# changes to the rest of the repo (or to the CMD) don't bust it.
|
||||||
|
|
||||||
# Current Node LTS; slim variant keeps the image small while still
|
# Version-qualified Node LTS, pinned to its multi-architecture manifest.
|
||||||
# providing apt-get for any future additions.
|
FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba
|
||||||
FROM node:22-trixie-slim
|
|
||||||
|
|
||||||
# Install runtime system deps. claude-code shells out to git for several
|
# Install runtime system deps. claude-code shells out to git for several
|
||||||
# features (status checks, commits, PR creation) — without git in the
|
# features (status checks, commits, PR creation) — without git in the
|
||||||
@@ -39,11 +38,13 @@ RUN apt-get update \
|
|||||||
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
|
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install claude-code globally. Pinned to the version verified in the v1
|
# Install from the committed npm lock. `npm ci` verifies every registry
|
||||||
# build (`claude --version` returns 2.1.126). Bump deliberately when
|
# artifact against its lockfile integrity and refuses dependency drift.
|
||||||
# rolling forward; an unpinned install would mean rebuilds silently pick
|
COPY bot_bottle/contrib/claude/package.json \
|
||||||
# up new behavior.
|
bot_bottle/contrib/claude/package-lock.json /opt/claude/
|
||||||
RUN npm install -g --no-fund --no-audit @anthropic-ai/claude-code@2.1.172 \
|
RUN cd /opt/claude \
|
||||||
|
&& npm ci --omit=dev --no-fund --no-audit \
|
||||||
|
&& ln -s /opt/claude/node_modules/.bin/claude /usr/local/bin/claude \
|
||||||
&& npm cache clean --force
|
&& npm cache clean --force
|
||||||
|
|
||||||
# Git reads both ~/.gitconfig and ~/.config/git/config. Keep its XDG config
|
# Git reads both ~/.gitconfig and ~/.config/git/config. Keep its XDG config
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Mirrors the default Claude image shape: Node LTS, git/network tooling,
|
# Mirrors the default Claude image shape: Node LTS, git/network tooling,
|
||||||
# non-root node user, and the provider CLI installed for that user.
|
# non-root node user, and the provider CLI installed for that user.
|
||||||
|
|
||||||
FROM node:22-trixie-slim
|
FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba
|
||||||
|
|
||||||
# The standalone installer is used below because remote-control requires its
|
# The standalone installer is used below because remote-control requires its
|
||||||
# managed package layout. Keep this exact release in sync with the verified
|
# managed package layout. Keep this exact release in sync with the verified
|
||||||
@@ -35,10 +35,18 @@ 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
|
# Remote-control support requires the standalone Codex install layout under
|
||||||
# under ~/.codex/packages/standalone/current. The npm package can run
|
# ~/.codex/packages/standalone/current. Fetch the installer from the same
|
||||||
# the TUI, but remote-control commands expect this installer-owned path.
|
# immutable release tag as the requested CLI, verify the committed checksum
|
||||||
RUN mkdir -p /home/node/.codex \
|
# before executing it, and let the official installer verify the selected
|
||||||
&& curl -fsSL https://chatgpt.com/codex/install.sh | sh
|
# release archive against upstream release metadata.
|
||||||
|
COPY --chown=node:node bot_bottle/contrib/codex/install.sh.sha256 /tmp/install.sh.sha256
|
||||||
|
RUN curl -fsSL \
|
||||||
|
"https://raw.githubusercontent.com/openai/codex/rust-v${CODEX_VERSION}/scripts/install/install.sh" \
|
||||||
|
-o /tmp/install.sh \
|
||||||
|
&& cd /tmp \
|
||||||
|
&& sha256sum -c install.sh.sha256 \
|
||||||
|
&& CODEX_NON_INTERACTIVE=1 sh /tmp/install.sh --release "${CODEX_VERSION}" \
|
||||||
|
&& test "$(codex --version)" = "codex-cli ${CODEX_VERSION}"
|
||||||
|
|
||||||
CMD ["codex"]
|
CMD ["codex"]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# Node LTS, git/network tooling, and the Pi coding-agent CLI installed globally.
|
# Node LTS, git/network tooling, and the Pi coding-agent CLI installed globally.
|
||||||
|
|
||||||
FROM node:22-trixie-slim
|
FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
@@ -19,9 +19,6 @@ RUN apt-get update \
|
|||||||
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
|
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN npm install -g --ignore-scripts --no-fund --no-audit @earendil-works/pi-coding-agent \
|
|
||||||
&& npm cache clean --force
|
|
||||||
|
|
||||||
RUN install -d -o node -g node -m 755 /home/node/.config /home/node/.config/git \
|
RUN install -d -o node -g node -m 755 /home/node/.config /home/node/.config/git \
|
||||||
&& mkdir -p /home/node/.pi/agent \
|
&& mkdir -p /home/node/.pi/agent \
|
||||||
/home/node/.pi/context-mode/sessions \
|
/home/node/.pi/context-mode/sessions \
|
||||||
@@ -34,10 +31,15 @@ RUN install -d -o node -g node -m 755 /home/node/.config /home/node/.config/git
|
|||||||
USER node
|
USER node
|
||||||
WORKDIR /home/node
|
WORKDIR /home/node
|
||||||
|
|
||||||
RUN pi install npm:@harms-haus/pi-cwd \
|
# Pi discovers npm packages from its agent directory. Installing the CLI and
|
||||||
&& pi install npm:pi-web-access \
|
# extensions together from the committed lock makes all direct and transitive
|
||||||
&& pi install npm:context-mode \
|
# package versions deterministic, with npm integrity verification.
|
||||||
&& pi install npm:pi-subagents \
|
COPY --chown=node:node bot_bottle/contrib/pi/package.json \
|
||||||
&& pi install npm:pi-mcp-adapter
|
bot_bottle/contrib/pi/package-lock.json /home/node/.pi/agent/
|
||||||
|
RUN cd /home/node/.pi/agent \
|
||||||
|
&& npm ci --omit=dev --no-fund --no-audit \
|
||||||
|
&& npm cache clean --force
|
||||||
|
|
||||||
|
ENV PATH="/home/node/.pi/agent/node_modules/.bin:${PATH}"
|
||||||
|
|
||||||
CMD ["pi"]
|
CMD ["pi"]
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "0.81.1",
|
"@earendil-works/pi-coding-agent": "0.81.1",
|
||||||
|
"@earendil-works/pi-agent-core": "0.81.1",
|
||||||
|
"@earendil-works/pi-ai": "0.81.1",
|
||||||
|
"@earendil-works/pi-tui": "0.81.1",
|
||||||
"@harms-haus/pi-cwd": "1.0.0",
|
"@harms-haus/pi-cwd": "1.0.0",
|
||||||
"context-mode": "1.0.169",
|
"context-mode": "1.0.169",
|
||||||
"pi-mcp-adapter": "2.11.0",
|
"pi-mcp-adapter": "2.11.0",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ BUNDLED_RESOURCES: tuple[str, ...] = (
|
|||||||
"Dockerfile.gateway",
|
"Dockerfile.gateway",
|
||||||
"Dockerfile.orchestrator",
|
"Dockerfile.orchestrator",
|
||||||
"Dockerfile.orchestrator.fc",
|
"Dockerfile.orchestrator.fc",
|
||||||
|
"requirements.gateway.lock",
|
||||||
"nix/firecracker-netpool.nix",
|
"nix/firecracker-netpool.nix",
|
||||||
"scripts/firecracker-netpool.sh",
|
"scripts/firecracker-netpool.sh",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,8 +38,13 @@ include = ["bot_bottle*"]
|
|||||||
bot_bottle = [
|
bot_bottle = [
|
||||||
"gateway/egress/entrypoint.sh",
|
"gateway/egress/entrypoint.sh",
|
||||||
"contrib/claude/Dockerfile",
|
"contrib/claude/Dockerfile",
|
||||||
|
"contrib/claude/package.json",
|
||||||
|
"contrib/claude/package-lock.json",
|
||||||
"contrib/codex/Dockerfile",
|
"contrib/codex/Dockerfile",
|
||||||
|
"contrib/codex/install.sh.sha256",
|
||||||
"contrib/pi/Dockerfile",
|
"contrib/pi/Dockerfile",
|
||||||
|
"contrib/pi/package.json",
|
||||||
|
"contrib/pi/package-lock.json",
|
||||||
"backend/firecracker/netpool.defaults.env",
|
"backend/firecracker/netpool.defaults.env",
|
||||||
"backend/macos_container/nested-containers-init.sh",
|
"backend/macos_container/nested-containers-init.sh",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ _BUNDLED_RESOURCES = (
|
|||||||
"Dockerfile.gateway",
|
"Dockerfile.gateway",
|
||||||
"Dockerfile.orchestrator",
|
"Dockerfile.orchestrator",
|
||||||
"Dockerfile.orchestrator.fc",
|
"Dockerfile.orchestrator.fc",
|
||||||
|
"requirements.gateway.lock",
|
||||||
"nix/firecracker-netpool.nix",
|
"nix/firecracker-netpool.nix",
|
||||||
"scripts/firecracker-netpool.sh",
|
"scripts/firecracker-netpool.sh",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -121,5 +121,54 @@ class TestCommitContainer(unittest.TestCase):
|
|||||||
self.assertIn("my-tag:v1", die.call_args.args[0])
|
self.assertIn("my-tag:v1", die.call_args.args[0])
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildImage(unittest.TestCase):
|
||||||
|
def test_passes_build_args_without_mutating_the_value(self):
|
||||||
|
with patch.object(
|
||||||
|
docker_mod.subprocess, "run", return_value=_ok(),
|
||||||
|
) as run, patch.object(docker_mod, "info"):
|
||||||
|
docker_mod.build_image(
|
||||||
|
"derived:test",
|
||||||
|
"/context",
|
||||||
|
dockerfile="Dockerfile.derived",
|
||||||
|
build_args={"BASE_IMAGE": "sha256:" + "a" * 64},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"docker", "build", "-t", "derived:test",
|
||||||
|
"-f", "Dockerfile.derived",
|
||||||
|
"--build-arg", "BASE_IMAGE=sha256:" + "a" * 64,
|
||||||
|
"/context",
|
||||||
|
],
|
||||||
|
run.call_args.args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestImageId(unittest.TestCase):
|
||||||
|
def test_returns_content_addressed_local_id(self):
|
||||||
|
expected = "sha256:" + "b" * 64
|
||||||
|
with patch.object(
|
||||||
|
docker_mod, "run_docker", return_value=_ok(stdout=expected + "\n"),
|
||||||
|
) as run:
|
||||||
|
self.assertEqual(expected, docker_mod.image_id("base:mutable"))
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"docker", "image", "inspect", "--format", "{{.Id}}",
|
||||||
|
"base:mutable",
|
||||||
|
],
|
||||||
|
run.call_args.args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rejects_failed_or_non_content_addressed_inspect(self):
|
||||||
|
for result in (_fail("missing"), _ok(stdout="base:latest\n")):
|
||||||
|
with self.subTest(result=result), patch.object(
|
||||||
|
docker_mod, "run_docker", return_value=result,
|
||||||
|
), patch.object(
|
||||||
|
docker_mod, "die", side_effect=SystemExit("die"),
|
||||||
|
) as die:
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
docker_mod.image_id("base:latest")
|
||||||
|
die.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -107,7 +107,12 @@ class TestEnsureBuilt(unittest.TestCase):
|
|||||||
|
|
||||||
def test_local_mode_builds_orchestrator_before_its_fc_image(self):
|
def test_local_mode_builds_orchestrator_before_its_fc_image(self):
|
||||||
with patch.dict(os.environ, {"BOT_BOTTLE_INFRA_BUILD": "local"}), \
|
with patch.dict(os.environ, {"BOT_BOTTLE_INFRA_BUILD": "local"}), \
|
||||||
patch.object(infra_vm.docker_mod, "build_image") as build:
|
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:
|
||||||
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.
|
||||||
@@ -116,6 +121,11 @@ 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)
|
||||||
|
self.assertEqual(
|
||||||
|
{"ORCHESTRATOR_BASE_IMAGE": "sha256:" + "a" * 64},
|
||||||
|
build.call_args_list[-1].kwargs["build_args"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestStop(unittest.TestCase):
|
class TestStop(unittest.TestCase):
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class TestVersionInputs(unittest.TestCase):
|
|||||||
for name in ("Dockerfile.orchestrator", "Dockerfile.orchestrator.fc",
|
for name in ("Dockerfile.orchestrator", "Dockerfile.orchestrator.fc",
|
||||||
"Dockerfile.gateway"):
|
"Dockerfile.gateway"):
|
||||||
(root / name).write_text(f"FROM scratch # {name}\n")
|
(root / name).write_text(f"FROM scratch # {name}\n")
|
||||||
|
(root / "requirements.gateway.lock").write_text("mitmproxy==11.1.3\n")
|
||||||
(root / "pyproject.toml").write_text("[project]\nname = 'bot-bottle'\n")
|
(root / "pyproject.toml").write_text("[project]\nname = 'bot-bottle'\n")
|
||||||
|
|
||||||
def test_pyproject_toml_change_bumps_version(self) -> None:
|
def test_pyproject_toml_change_bumps_version(self) -> None:
|
||||||
@@ -146,6 +147,21 @@ class TestVersionInputs(unittest.TestCase):
|
|||||||
after = ia.infra_artifact_version("init", _ROLE, repo_root=root)
|
after = ia.infra_artifact_version("init", _ROLE, repo_root=root)
|
||||||
self.assertNotEqual(before, after)
|
self.assertNotEqual(before, after)
|
||||||
|
|
||||||
|
def test_gateway_lock_change_bumps_gateway_version(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
root = Path(d)
|
||||||
|
self._fake_repo(root)
|
||||||
|
before = ia.infra_artifact_version(
|
||||||
|
"init", "gateway", repo_root=root,
|
||||||
|
)
|
||||||
|
(root / "requirements.gateway.lock").write_text(
|
||||||
|
"mitmproxy==11.1.3 --hash=sha256:changed\n",
|
||||||
|
)
|
||||||
|
after = ia.infra_artifact_version(
|
||||||
|
"init", "gateway", repo_root=root,
|
||||||
|
)
|
||||||
|
self.assertNotEqual(before, after)
|
||||||
|
|
||||||
def test_pyc_and_pycache_ignored(self) -> None:
|
def test_pyc_and_pycache_ignored(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
root = Path(d)
|
root = Path(d)
|
||||||
|
|||||||
Reference in New Issue
Block a user