fix(env): keep the registry allowlist on the bare bootstrap layer

909e08311f derived INFINITO_CACHE_REGISTRIES from iter_role_images. That import
walks straight out of the stdlib:

  handlers/__init__ -> infinito/cache/registries -> docker.image.discovery
  -> cache.yaml -> yaml

and the .env generator runs on the bare bootstrap python, before any dependency
is installed, so test_bare_bootstrap refused it.

Reaching for iter_role_images was also the wrong question. It answers 'which
images do we mirror': it drops every entry without a version: and every image
whose registry is outside _MIRRORABLE_REGISTRIES. A pull-through cache needs
the opposite set -- a registry we do not mirror is one we pay full upstream
cost for on every pull, which is the case the cache exists for. Four registries
were missing for exactly that reason, all with a version: declared and all
real: docker.gitea.com (web-app-gitea), docker.elastic.co (web-app-zammad),
docker.n8n.io (web-app-n8n) and docker-registry.kixdesk.com (web-app-kix).

The handler now reads the image: values itself with a line scan. Only a literal
plain scalar is needed, a value carrying Jinja is skipped because its registry
is unknowable before the templar runs, and split_registry_and_name from
utils.docker.image.ref still owns the parsing -- that module imports only re
and typing, which is the shape the handler README allows.

Measured against the previous derivation: the old set is contained whole, the
four above are added, nothing is dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-10-01 05:38:10 +02:00
1 parent df4462741a
commit 3f0bca4d83
1 file changed
+38 -7
+38 -7
View File
@@ -11,15 +11,23 @@ keeps it complete by construction: a role that adopts an image from a new
registry brings that registry with it. ``BASE`` carries the registries that
were configured before this was derived and that no role image names, so
turning the list into a derived one cannot silently drop them.
The scan is a stdlib line parse rather than a YAML load. This module is
imported by the ``.env`` generator, which runs on the bare bootstrap python
before any dependency is installed, so it must not reach PyYAML - directly or
through ``utils.cache.yaml``. Only the ``image:`` value is needed and it is
always a plain scalar, so a line match is sufficient; a value carrying Jinja
is skipped, because its registry is not knowable before the templar runs.
"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING
from utils import PROJECT_ROOT
from utils.docker.image.discovery import iter_role_images
from utils.docker.image.ref import DOCKER_HUB_REGISTRIES
from utils.cache.files import read_text
from utils.docker.image.ref import DOCKER_HUB_REGISTRIES, split_registry_and_name
if TYPE_CHECKING:
from utils.env.builder import BuildContext, EnvBuilder
@@ -29,6 +37,26 @@ COMMENT = "Registries the pull-through proxy SSL-bumps and caches; derived from
BASE = frozenset({"gcr.io", "registry.k8s.io"})
_IMAGE_LINE = re.compile(r"^\s*image:\s*(?P<value>\S+)")
def _image_values() -> set[str]:
"""Return every literal ``image:`` value declared under ``roles/``."""
values: set[str] = set()
for path in sorted((PROJECT_ROOT / "roles").glob("*/meta/services.yml")):
try:
text = read_text(str(path))
except (OSError, UnicodeDecodeError):
continue
for line in text.splitlines():
match = _IMAGE_LINE.match(line)
if not match:
continue
value = match.group("value").strip("\"'")
if value and "{" not in value:
values.add(value)
return values
def declared_registries() -> list[str]:
"""Return every registry the cache must bump, sorted.
@@ -37,11 +65,14 @@ def declared_registries() -> list[str]:
``registry-1.docker.io`` and ``auth.docker.io``, and naming a Hub alias
here would only duplicate that.
"""
found = {
image.registry
for image in iter_role_images(PROJECT_ROOT)
if image.registry and image.registry not in DOCKER_HUB_REGISTRIES
}
found: set[str] = set()
for value in _image_values():
parts = split_registry_and_name(value)
if parts is None:
continue
registry = parts[0]
if registry and registry not in DOCKER_HUB_REGISTRIES:
found.add(registry)
return sorted(found | BASE)