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
8 changes: 8 additions & 0 deletions src/providers/docker/images.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import hashlib
import io
import re
import tarfile

from core.settings import get_settings
Expand All @@ -13,10 +14,15 @@ def derive_image_name(
*,
base_image: str,
setup_script: str,
label: str,
repository: str = "drukbox-template",
) -> str:
identity = base_image.encode("utf-8") + b"\0" + setup_script.encode("utf-8")
digest = hashlib.sha256(identity).hexdigest()[:12]
# A tag holds at most 128 characters, and the digest needs 13 of them.
purpose = re.sub(r"[^a-z0-9]+", "-", label.lower())[:115].strip("-")
if purpose:
return f"{repository}:{purpose}-{digest}"
return f"{repository}:{digest}"


Expand All @@ -42,6 +48,7 @@ async def build_derived_image(
*,
base_image: str,
setup_script: str,
label: str,
) -> str:
settings = get_settings()
repository = "drukbox-template"
Expand All @@ -50,6 +57,7 @@ async def build_derived_image(
image = derive_image_name(
base_image=base_image,
setup_script=setup_script,
label=label,
repository=repository,
)
context_tar = create_build_context(base_image=base_image, setup_script=setup_script)
Expand Down
1 change: 1 addition & 0 deletions src/providers/docker/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ async def build_template_image(
self.api,
base_image=base_image,
setup_script=setup_script,
label=label,
)

async def delete_template_image(self, image: str) -> None:
Expand Down
29 changes: 24 additions & 5 deletions src/providers/docker/tests/test_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,35 @@ def test_create_build_context_contains_the_base_and_verbatim_script() -> None:


def test_derive_image_name_is_deterministic_and_base_specific() -> None:
first = derive_image_name(base_image="sandbox:base", setup_script="apt-get update")
repeated = derive_image_name(base_image="sandbox:base", setup_script="apt-get update")
different_base = derive_image_name(base_image="sandbox:other", setup_script="apt-get update")
first = derive_image_name(base_image="sandbox:base", setup_script="apt-get update", label="")
repeated = derive_image_name(base_image="sandbox:base", setup_script="apt-get update", label="")
different_base = derive_image_name(
base_image="sandbox:other", setup_script="apt-get update", label=""
)

assert first == repeated
assert first.startswith("drukbox-template:")
assert len(first.removeprefix("drukbox-template:")) == 12
assert different_base != first


@pytest.mark.parametrize(
("label", "purpose"),
[
("site-builder-build", "site-builder-build"),
("Site_Builder / Build.sh", "site-builder-build-sh"),
("x" * 200, "x" * 115),
],
)
def test_derive_image_name_leads_the_tag_with_the_label(label: str, purpose: str) -> None:
image = derive_image_name(base_image="sandbox:base", setup_script="apt-get update", label=label)

tag = image.removeprefix("drukbox-template:")
assert tag.startswith(f"{purpose}-")
assert len(tag) == len(purpose) + 13
assert len(tag) <= 128


async def test_build_derived_image_publishes_to_the_template_repository(
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand All @@ -52,9 +71,9 @@ async def test_build_derived_image_publishes_to_the_template_repository(
docker = MagicMock(build_image=AsyncMock(), push_image=AsyncMock())

image = await build_derived_image(
docker, base_image="sandbox:base", setup_script="apt-get update"
docker, base_image="sandbox:base", setup_script="apt-get update", label="node-tools"
)

assert image.startswith("ghcr.io/acme/templates:")
assert image.startswith("ghcr.io/acme/templates:node-tools-")
assert docker.build_image.await_args.args[0] == image
docker.push_image.assert_awaited_once_with(image, username="bot", password="secret")
3 changes: 1 addition & 2 deletions src/providers/docker/tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,7 @@ async def test_build_template_image_builds_and_returns_the_derived_tag():
label="Node tools",
)

assert image.startswith("drukbox-template:")
assert len(image.removeprefix("drukbox-template:")) == 12
assert image.startswith("drukbox-template:node-tools-")
assert api.build_image.await_args.args[0] == image


Expand Down
1 change: 1 addition & 0 deletions src/providers/docker_sbx/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ async def build_template_image(
self.docker,
base_image=base_image,
setup_script=setup_script,
label=label,
)
# sbx keeps its own image store. The Docker image only carries the build.
try:
Expand Down
1 change: 1 addition & 0 deletions src/providers/exe/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ async def build_template_image(
self.docker,
base_image=base_image,
setup_script=setup_script,
label=label,
)

async def delete_template_image(self, image: str) -> None:
Expand Down
3 changes: 1 addition & 2 deletions src/providers/exe/tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,7 @@ async def test_build_template_image_builds_logs_in_and_pushes(
label="Node tools",
)

assert image.startswith("ghcr.io/acme/drukbox-templates:")
assert len(image.rpartition(":")[2]) == 12
assert image.startswith("ghcr.io/acme/drukbox-templates:node-tools-")
assert docker.build_image.await_args.args[0] == image
docker.push_image.assert_awaited_once_with(image, username="bot", password="secret")

Expand Down
2 changes: 1 addition & 1 deletion src/templates/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async def build_template_image(
self.built.append((base_image, setup_script, label))
if self.build_error:
raise self.build_error
return derive_image_name(base_image=base_image, setup_script=setup_script)
return derive_image_name(base_image=base_image, setup_script=setup_script, label=label)

async def delete_template_image(self, image: str) -> None:
self.deleted.append(image)
Expand Down
1 change: 1 addition & 0 deletions src/templates/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ async def test_create_template_returns_building_then_becomes_available(client, t
assert polled.json()["image"] == derive_image_name(
base_image=template_provider.base_image_ref,
setup_script=SETUP_SCRIPT,
label="Node tools",
)
assert "setup_script" not in polled.json()
assert template_provider.built == [
Expand Down
Loading