mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
An unmounted VOLUME does not fail: docker creates an anonymous volume for
it, the container works, and the gap shows up later as a volume nobody can
attribute. sys-ctl-hlth-volumes fails the deploy over exactly that, and its
whitelist keys on volume ids, which are new on every deploy, so the gap
cannot be suppressed there either. e0b973f9bb fixed one such case in
web-svc-xmpp by hand; this is the check that would have found it.
Attribution is the whole difficulty. meta/services.yml declares an image
under a key, and the compose file often runs it under another name:
web-app-jitsi declares the image under 'jitsi' and renders the service as
'web', web-app-erpnext runs one image as seven services, and the two RDBMS
consumer keys name no service of their own because the container comes from
sys-svc-rdbms. The sweep therefore unions the entry key with its
proxy.service_key and delegates the RDBMS keys to their provider role. A
first version keyed on the entry alone and reported six roles that mount
every path correctly, web-svc-xmpp among them.
Reading a config blob section is now its own module beside the registry
client, because the two readers were the same walk twice and registry.py
sat three lines under the file-size cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
5.0 KiB
Python
149 lines
5.0 KiB
Python
"""What an image's config blob declares: its healthcheck and its volumes.
|
|
|
|
The manifest names a config blob and the blob carries the sections a
|
|
``Dockerfile`` baked in. Reading one section is the same walk as reading
|
|
another, so both callers share it: resolve the index to a platform, read the
|
|
blob, take the section.
|
|
|
|
Every answer is three-valued. ``{}`` means the image definitively declares
|
|
nothing, ``None`` means the walk could not finish, and a caller that reads the
|
|
two as the same thing turns an auth wall or a rate limit into evidence.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from utils.docker.mirror import mirror_image
|
|
from utils.docker.registry import (
|
|
cache_read,
|
|
cache_write,
|
|
fetch_blob,
|
|
fetch_manifest,
|
|
platform_digest,
|
|
probe_order,
|
|
)
|
|
|
|
|
|
def _section_at(
|
|
image: str, reference: str, section: str, os_name: str, architecture: str
|
|
) -> dict | None:
|
|
doc = fetch_manifest(image, reference)
|
|
if doc is None:
|
|
return None
|
|
manifests = doc.get("manifests")
|
|
if isinstance(manifests, list) and manifests:
|
|
digest = platform_digest(manifests, os_name, architecture)
|
|
if digest is None:
|
|
return None
|
|
doc = fetch_manifest(image, digest)
|
|
if doc is None or doc.get("manifests"):
|
|
return None
|
|
config_digest = (doc.get("config") or {}).get("digest")
|
|
if not isinstance(config_digest, str) or not config_digest:
|
|
return None
|
|
blob = fetch_blob(image, config_digest)
|
|
if blob is None:
|
|
return None
|
|
return (blob.get("config") or {}).get(section) or {}
|
|
|
|
|
|
def _section_probed(
|
|
section: str, kind: str, image: str, reference: str, os_name: str, architecture: str
|
|
) -> tuple[dict | None, str]:
|
|
"""The named config section, and which address answered for it.
|
|
|
|
Args:
|
|
section: key under the blob's ``config``, e.g. ``Healthcheck``.
|
|
kind: cache namespace, so two sections never share an entry.
|
|
image: image name, with or without registry host.
|
|
reference: tag or digest.
|
|
os_name: platform to resolve a multi-arch index to.
|
|
architecture: platform to resolve a multi-arch index to.
|
|
|
|
Returns:
|
|
``(section, source)`` where source is ``cache``, ``mirror``,
|
|
``upstream`` or ``none``.
|
|
"""
|
|
cached = cache_read(kind, image, reference, os_name, architecture)
|
|
if isinstance(cached, dict):
|
|
return cached, "cache"
|
|
mirrored = mirror_image(image)
|
|
for candidate in probe_order(image):
|
|
found = _section_at(candidate, reference, section, os_name, architecture)
|
|
if found is not None:
|
|
cache_write(kind, found, image, reference, os_name, architecture)
|
|
return found, "mirror" if candidate == mirrored else "upstream"
|
|
return None, "none"
|
|
|
|
|
|
def image_healthcheck(
|
|
image: str,
|
|
reference: str,
|
|
*,
|
|
os_name: str = "linux",
|
|
architecture: str = "amd64",
|
|
) -> dict | None:
|
|
"""Return the ``HEALTHCHECK`` baked into ``image:reference``.
|
|
|
|
Args:
|
|
image: image name, with or without registry host.
|
|
reference: tag or digest.
|
|
os_name: platform to resolve a multi-arch index to.
|
|
architecture: platform to resolve a multi-arch index to.
|
|
|
|
Returns:
|
|
The image's healthcheck config, ``{}`` when the image definitively
|
|
declares none, and ``None`` when the answer is indeterminate.
|
|
|
|
Reads from the mirror first and falls back to the upstream registry only
|
|
for images the mirror does not carry, so a full sweep does not spend the
|
|
upstream's rate limit. Determinate answers are cached on disk.
|
|
"""
|
|
return image_healthcheck_probed(
|
|
image, reference, os_name=os_name, architecture=architecture
|
|
)[0]
|
|
|
|
|
|
def image_healthcheck_probed(
|
|
image: str,
|
|
reference: str,
|
|
*,
|
|
os_name: str = "linux",
|
|
architecture: str = "amd64",
|
|
) -> tuple[dict | None, str]:
|
|
""":func:`image_healthcheck` plus which address answered.
|
|
|
|
Returns:
|
|
``(healthcheck, source)``. Reading upstream is the documented fallback
|
|
for images the mirror does not carry; a caller that sweeps many images
|
|
can use the counts to tell a fallback from a mirror that stopped
|
|
working.
|
|
"""
|
|
return _section_probed(
|
|
"Healthcheck", "healthcheck", image, reference, os_name, architecture
|
|
)
|
|
|
|
|
|
def image_volumes(
|
|
image: str,
|
|
reference: str,
|
|
*,
|
|
os_name: str = "linux",
|
|
architecture: str = "amd64",
|
|
) -> dict | None:
|
|
"""Return the ``VOLUME`` paths baked into ``image:reference``.
|
|
|
|
Args:
|
|
image: image name, with or without registry host.
|
|
reference: tag or digest.
|
|
os_name: platform to resolve a multi-arch index to.
|
|
architecture: platform to resolve a multi-arch index to.
|
|
|
|
Returns:
|
|
A mapping whose keys are the declared container paths, ``{}`` when the
|
|
image definitively declares none, and ``None`` when the answer is
|
|
indeterminate.
|
|
"""
|
|
return _section_probed(
|
|
"Volumes", "volumes", image, reference, os_name, architecture
|
|
)[0]
|