Files
computer-playbook/cli/contributing/mirror/providers.py
T
kevinveenbirkenbachandClaude Opus 5 747aff6b45 fix(docs): let docutils read these class docstrings
The same failure class as 3dee332350, in the places that only became
visible once the louder messages were gone. Nine of them are class
docstrings, which the detector used to find the first round skipped
because it walked modules and functions only.

restart_condition carried a section title inside its docstring, where
docutils allows none, and the indented block below it held a second.

cert_utils indented the closing brace of its example shallower than the
body it closes.

Two are regressions of my own first round: filters lacked the blank line
between its new literal block and the paragraph after it, and
fediwall_active had the wrong one of its two colons corrected.

make i18n-extract domain=docs now finishes with zero warnings under -W.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 19:31:02 +02:00

187 lines
5.6 KiB
Python

from __future__ import annotations
import subprocess
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from utils.docker.mirror import mirror_image_base
if TYPE_CHECKING:
import argparse
from .model import ImageRef
class RegistryProvider(ABC):
@abstractmethod
def image_base(self, image: ImageRef) -> str:
pass
@abstractmethod
def mirror(self, image: ImageRef) -> None:
pass
@abstractmethod
def tag_exists(self, image: ImageRef) -> bool:
pass
class GHCRProvider(RegistryProvider):
def __init__(self, namespace: str, repository: str, prefix: str = "mirror") -> None:
self.namespace = namespace.lower()
self.repository = repository.lower()
self.prefix = prefix.strip("/")
@classmethod
def add_args(cls, parser: argparse.ArgumentParser) -> None:
"""Register --ghcr-namespace, --ghcr-repository, --ghcr-prefix on *parser*."""
parser.add_argument("--ghcr-namespace", required=True)
parser.add_argument("--ghcr-repository", required=True)
parser.add_argument("--ghcr-prefix", default="mirror")
@classmethod
def from_args(cls, args: argparse.Namespace) -> GHCRProvider:
"""Construct a GHCRProvider from parsed CLI args."""
return cls(args.ghcr_namespace, args.ghcr_repository, args.ghcr_prefix)
def image_base(self, image: ImageRef) -> str:
return mirror_image_base(
image.registry,
image.name,
namespace=self.namespace,
repository=self.repository,
prefix=self.prefix,
)
def tag_exists(self, image: ImageRef) -> bool:
"""
Return True if the destination tag already exists in GHCR.
Uses ``skopeo inspect docker://<dest>``, whose exit code is 0 when the
tag exists and non-zero when it does not exist or cannot be accessed
(auth/network).
"""
dest = f"{self.image_base(image)}:{image.version}"
r = subprocess.run(
["skopeo", "inspect", f"docker://{dest}"],
check=False,
capture_output=True,
text=True,
)
return r.returncode == 0
def _run_copy(self, *, src: str, dest: str, extra: list[str] | None = None) -> None:
cmd = [
"skopeo",
"copy",
"--all",
"--retry-times",
"5",
"--dest-precompute-digests",
]
if extra:
cmd += extra
cmd += [src, f"docker://{dest}"]
subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
)
def _looks_like_blob_reuse_problem(self, e: subprocess.CalledProcessError) -> bool:
out = (e.stdout or "") + "\n" + (e.stderr or "")
s = out.lower()
return (
"reuse blob" in s
or "blob mount" in s
or "mount blob" in s
or ("failed to mount" in s and ("403" in s or "401" in s))
or "denied:" in s
or "unauthorized" in s
)
def mirror(self, image: ImageRef) -> None:
dest = f"{self.image_base(image)}:{image.version}"
src = f"docker://{image.source}"
try:
self._run_copy(src=src, dest=dest)
except subprocess.CalledProcessError as e:
output = (e.stdout or "") + (e.stderr or "")
if output.strip():
print(output, flush=True)
if self._looks_like_blob_reuse_problem(e):
self._run_copy(
src=src,
dest=dest,
extra=[
"--dest-compress-format",
"gzip",
"--dest-compress-level",
"1",
"--dest-force-compress-format",
],
)
return
raise
class GiteaProvider(RegistryProvider):
def __init__(self, registry: str, namespace: str, prefix: str = "mirror") -> None:
self.registry = registry.rstrip("/")
self.namespace = namespace
self.prefix = prefix.strip("/")
def image_base(self, image: ImageRef) -> str:
return f"{self.registry}/{self.namespace}/{self.prefix}/{image.registry}/{image.name}"
def tag_exists(self, image: ImageRef) -> bool:
"""
Return True if the destination tag already exists in the target registry.
Uses ``skopeo inspect docker://<dest>``, whose exit code is 0 when the
tag exists and non-zero when it does not exist or cannot be accessed
(auth/network).
"""
dest = f"{self.image_base(image)}:{image.version}"
r = subprocess.run(
["skopeo", "inspect", f"docker://{dest}"],
check=False,
capture_output=True,
text=True,
)
return r.returncode == 0
def mirror(self, image: ImageRef) -> None:
dest = f"{self.image_base(image)}:{image.version}"
src = f"docker://{image.source}"
cmd = [
"skopeo",
"copy",
"--all",
"--retry-times",
"5",
"--dest-precompute-digests",
src,
f"docker://{dest}",
]
try:
subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
)
except subprocess.CalledProcessError as e:
output = (e.stdout or "") + (e.stderr or "")
if output.strip():
print(output, flush=True)
raise