mirror of
https://github.com/quay/quay.git
synced 2026-01-26 06:21:37 +03:00
* feat(mirror): add architecture-filtered mirroring support (PROJQUAY-10257) When architecture_filter is set on a mirror config, copy only the specified architectures instead of using the --all flag. This preserves the original manifest list digest for OpenShift compatibility by pushing the original manifest bytes directly after copying the filtered architecture manifests. Key changes: - Add inspect_raw() and copy_by_digest() methods to SkopeoMirror - Create manifest_utils.py for manifest list parsing and filtering - Modify perform_mirror() to use architecture filtering when configured - Add comprehensive unit tests for the new functionality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(mirror): default media_type to OCI index when None (PROJQUAY-10257) Prevent InvalidHeader error when get_manifest_media_type() returns None by defaulting to OCI_IMAGE_INDEX_CONTENT_TYPE in the Content-Type header of the manifest push request. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
74 lines
2.3 KiB
Python
74 lines
2.3 KiB
Python
"""Utilities for parsing and filtering manifest lists during repository mirroring."""
|
|
|
|
import json
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from image.docker.schema2 import DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE
|
|
from image.oci import OCI_IMAGE_INDEX_CONTENT_TYPE
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MANIFEST_LIST_MEDIA_TYPES = {
|
|
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE,
|
|
OCI_IMAGE_INDEX_CONTENT_TYPE,
|
|
}
|
|
|
|
|
|
def is_manifest_list(manifest_bytes: str) -> bool:
|
|
"""Check if manifest JSON represents a manifest list/index."""
|
|
try:
|
|
parsed = json.loads(manifest_bytes)
|
|
media_type = parsed.get("mediaType", "")
|
|
if media_type in MANIFEST_LIST_MEDIA_TYPES:
|
|
return True
|
|
# OCI index may not have mediaType but has manifests array
|
|
if "manifests" in parsed and isinstance(parsed["manifests"], list):
|
|
return True
|
|
return False
|
|
except (json.JSONDecodeError, TypeError):
|
|
return False
|
|
|
|
|
|
def get_manifest_media_type(manifest_bytes: str) -> Optional[str]:
|
|
"""Extract media type from manifest JSON."""
|
|
try:
|
|
return json.loads(manifest_bytes).get("mediaType")
|
|
except (json.JSONDecodeError, TypeError):
|
|
return None
|
|
|
|
|
|
def filter_manifests_by_architecture(manifest_bytes: str, architectures: list[str]) -> list[dict]:
|
|
"""
|
|
Filter manifest list entries to only include specified architectures.
|
|
|
|
Returns list of manifest entries with digest, size, and platform info.
|
|
"""
|
|
try:
|
|
parsed = json.loads(manifest_bytes)
|
|
except json.JSONDecodeError:
|
|
return []
|
|
|
|
filtered = []
|
|
for manifest in parsed.get("manifests", []):
|
|
platform = manifest.get("platform", {})
|
|
arch = platform.get("architecture", "")
|
|
if arch in architectures:
|
|
filtered.append(manifest)
|
|
logger.debug("Including manifest %s for arch %s", manifest.get("digest"), arch)
|
|
return filtered
|
|
|
|
|
|
def get_available_architectures(manifest_bytes: str) -> list[str]:
|
|
"""Get all architectures present in a manifest list."""
|
|
try:
|
|
parsed = json.loads(manifest_bytes)
|
|
except json.JSONDecodeError:
|
|
return []
|
|
|
|
return [
|
|
m.get("platform", {}).get("architecture")
|
|
for m in parsed.get("manifests", [])
|
|
if m.get("platform", {}).get("architecture")
|
|
]
|