Files
computer-playbook/tests/unit/python/utils/docker/test_image_architectures.py
T
kevinveenbirkenbachandClaude Opus 5 0bb437d9ce feat(images): check declared architectures against the registry, and make a pin say why
Nothing held a role's architectures to reality: test_image_reachable proves a tag exists, test_arm64_images proves four named roles publish arm64, and the closure lint only proves the declarations do not intersect to nothing. An amd64-only bump became a scheduling failure on an arm64 row rather than a red test.

A new external test sweeps every pinned image and judges a role by the intersection over its services, because a role runs only where all of its images run; judging per image would demand arm64 from an amd64-only role's nginx and call a narrowing stale because nginx publishes arm64 while the application does not. A role declaring nothing must cover every architecture the matrix rotates; a role that narrows must narrow to exactly its capability, which is what retires its TODO by itself. An unread image downgrades the stale verdict to a warning, since the image a probe could not read may be the one the narrowing exists for. The probe reads the mirror first and caches on disk, because the sweep is ~160 manifests and Docker Hub counts its allowance per IP, shared.

A new lint requires every architectures declaration to carry a nocheck marker and a Reason. bigbluebutton and opentalk keep their pins with measured reasons; decidim, pretix and taiga lose theirs, because each builds its own image and their upstream Dockerfiles are arch-neutral. Their source builds are not converted yet, so until that lands the matrix may hand them an arm64 row that fails at FROM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 22:11:48 +02:00

99 lines
3.6 KiB
Python

"""The reduction from published platforms to where a role can be placed.
The intersection is the whole point: a role pins an application image beside
generic sidecars, and judging per image gets it wrong in both directions. It
would demand arm64 from an amd64-only role's nginx, and it would call a
narrowing stale because nginx publishes arm64 while the application does not.
"""
from __future__ import annotations
import unittest
from utils.docker.image.architectures import role_capability, runnable_architectures
BOTH = {"linux/amd64", "linux/arm64"}
AMD64 = {"linux/amd64"}
ARM64 = {"linux/arm64"}
class TestRunnableArchitectures(unittest.TestCase):
def test_a_multi_platform_index_offers_both(self) -> None:
self.assertEqual({"amd64", "arm64"}, runnable_architectures(BOTH))
def test_a_foreign_os_carries_no_row(self) -> None:
self.assertEqual(
{"amd64"}, runnable_architectures({"linux/amd64", "windows/amd64"})
)
def test_an_unrotated_architecture_is_dropped(self) -> None:
self.assertEqual(
{"amd64"},
runnable_architectures({"linux/amd64", "linux/ppc64le", "linux/s390x"}),
)
def test_an_empty_or_missing_set_offers_nothing(self) -> None:
for platforms in (set(), None):
with self.subTest(platforms=platforms):
self.assertEqual(set(), runnable_architectures(platforms))
class TestRoleCapability(unittest.TestCase):
def test_one_amd64_only_image_pins_the_whole_role(self) -> None:
capability, blame, unread = role_capability({"app": AMD64, "nginx": BOTH})
self.assertEqual({"amd64"}, capability)
self.assertEqual({"arm64": ["app"]}, blame)
self.assertEqual([], unread)
def test_a_role_whose_images_all_publish_both_runs_on_both(self) -> None:
capability, blame, _unread = role_capability({"app": BOTH, "nginx": BOTH})
self.assertEqual({"amd64", "arm64"}, capability)
self.assertEqual({}, blame)
def test_images_pinned_to_opposite_architectures_run_nowhere(self) -> None:
capability, blame, _unread = role_capability({"app": AMD64, "sidecar": ARM64})
self.assertEqual(set(), capability)
self.assertEqual({"amd64": ["sidecar"], "arm64": ["app"]}, blame)
def test_blame_names_every_image_lacking_the_architecture(self) -> None:
_capability, blame, _unread = role_capability(
{"worker": AMD64, "app": AMD64, "nginx": BOTH}
)
self.assertEqual(["app", "worker"], blame["arm64"])
def test_an_unreadable_image_is_reported_and_not_intersected(self) -> None:
capability, blame, unread = role_capability({"app": BOTH, "unreadable": None})
self.assertEqual(
{"amd64", "arm64"},
capability,
"a throttled probe must understate, not narrow the role to nothing",
)
self.assertEqual({}, blame)
self.assertEqual(
["unreadable"],
unread,
"without this the skipped image reads as one that runs everywhere, "
"and a narrowing it alone justifies looks unnecessary",
)
def test_a_role_nobody_could_read_has_no_capability(self) -> None:
capability, blame, unread = role_capability({"app": None, "nginx": None})
self.assertIsNone(
capability, "None is the skip signal, distinct from an empty set"
)
self.assertEqual({}, blame)
self.assertEqual(["app", "nginx"], unread)
def test_a_role_without_images_has_no_capability(self) -> None:
self.assertEqual((None, {}, []), role_capability({}))
if __name__ == "__main__":
unittest.main()