mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
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>
71 lines
2.8 KiB
Python
71 lines
2.8 KiB
Python
"""Reduce the platforms a role's images publish to where the role can run.
|
|
|
|
A role is schedulable only where *every* image it pins can run, so its
|
|
capability is the intersection over its services rather than a property of
|
|
any one of them. Kept apart from the registry probe so the reduction can be
|
|
tested without a network.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from utils.github.variant.pools import ARCHITECTURES
|
|
|
|
LINUX = "linux"
|
|
|
|
|
|
def runnable_architectures(platforms) -> set[str]:
|
|
"""Return the matrix architectures *platforms* can be scheduled on.
|
|
|
|
Args:
|
|
platforms: ``os/architecture`` strings as a registry reports them.
|
|
|
|
Returns:
|
|
The subset of :data:`ARCHITECTURES` offered for ``linux``. Anything
|
|
else a manifest lists - another operating system, an architecture the
|
|
matrix does not rotate - is dropped, because it cannot carry a row.
|
|
"""
|
|
offered = set()
|
|
for entry in platforms or ():
|
|
os_name, _, architecture = str(entry).partition("/")
|
|
if os_name == LINUX and architecture in ARCHITECTURES:
|
|
offered.add(architecture)
|
|
return offered
|
|
|
|
|
|
def role_capability(
|
|
offered,
|
|
) -> tuple[set[str] | None, dict[str, list[str]], list[str]]:
|
|
"""Intersect what a role's images publish, and name what each one lacks.
|
|
|
|
Args:
|
|
offered: mapping of image label to its ``os/architecture`` set, or to
|
|
``None`` for an image whose registry answer was indeterminate.
|
|
|
|
Returns:
|
|
``(capability, blame, unread)``. ``capability`` is the intersection
|
|
over the images that answered, or ``None`` when none did - which a
|
|
caller must not read as "runs nowhere", since a throttled sweep
|
|
produces it. ``blame`` maps an architecture to the labels that cannot
|
|
run it, so a failure names the pin to change rather than only the
|
|
role. ``unread`` lists the labels that did not answer.
|
|
|
|
An incomplete read can only ever overstate the capability, because the
|
|
intersection skips what it could not see. A caller may therefore trust
|
|
``capability`` to decide an architecture is missing, but must not conclude
|
|
from it that a narrowing is unnecessary while ``unread`` is non-empty: the
|
|
image it could not read may be the one the narrowing exists for.
|
|
"""
|
|
capability: set[str] | None = None
|
|
blame: dict[str, list[str]] = {}
|
|
unread: list[str] = []
|
|
for label in sorted(offered):
|
|
platforms = offered[label]
|
|
if platforms is None:
|
|
unread.append(label)
|
|
continue
|
|
runnable = runnable_architectures(platforms)
|
|
for architecture in sorted(set(ARCHITECTURES) - runnable):
|
|
blame.setdefault(architecture, []).append(label)
|
|
capability = runnable if capability is None else capability & runnable
|
|
return capability, blame, unread
|