Source code for pubtools._quay.manifest_list_merger

from copy import deepcopy
import logging
from typing import List, cast, Optional

import requests

from .quay_client import QuayClient
from .types import ManifestList, Manifest

LOG = logging.getLogger("pubtools.quay")


[docs]class ManifestListMerger: """Class containing logic for merging manifest lists of two images."""
[docs] def __init__( self, src_image: str, dest_image: str, src_quay_host: Optional[str] = None, src_quay_username: Optional[str] = None, src_quay_password: Optional[str] = None, dest_quay_username: Optional[str] = None, dest_quay_password: Optional[str] = None, host: Optional[str] = None, ) -> None: """ Initialize. Args: src_image (str): Address to a new image whose manifest list contains the newer data. dest_image (str): Address to an older image whose data will be overwritten. src_quay_host (str): Custom hostname to connect to use for pulling src_image. src_quay_username (str): Quay username to get src_image. If ommited, external client instance should be set. src_quay_password (str): Quay password to get src_image. If ommited, external client instance should be set. dest_quay_username (str): Quay username to get dest_image. If ommited, external client instance should be set. dest_quay_password (str): Quay password to get dest_image. If ommited, external client instance should be set. host (str): Custom hostname to connect to. If ommited, standard quay.io will be used. """ self.src_image = src_image self.dest_image = dest_image if src_quay_username and src_quay_password: self._src_quay_client: Optional[QuayClient] = QuayClient( src_quay_username, src_quay_password, src_quay_host or host ) else: self._src_quay_client = None if dest_quay_username and dest_quay_password: self._dest_quay_client: Optional[QuayClient] = QuayClient( dest_quay_username, dest_quay_password, host ) else: self._dest_quay_client = None
[docs] def set_quay_clients(self, src_quay_client: QuayClient, dest_quay_client: QuayClient) -> None: """ Set client instances to be used for the HTTP API operations. Args: src_quay_client (QuayClient): Instance of QuayClient. dest_quay_client (QuayClient): Instance of QuayClient. """ self._src_quay_client = src_quay_client self._dest_quay_client = dest_quay_client
[docs] def merge_manifest_lists(self) -> None: """Merge manifest lists and upload to Quay. Main entrypoint method.""" if not self._src_quay_client or not self._dest_quay_client: raise RuntimeError("QuayClient instance must be set for both source and dest images") LOG.info( "Merging manifest lists of images '{0}' and '{1}'".format( self.src_image, self.dest_image ) ) src_manifest_list = cast( ManifestList, self._src_quay_client.get_manifest( self.src_image, media_type=QuayClient.MANIFEST_LIST_TYPE ), ) dest_manifest_list = cast( ManifestList, self._dest_quay_client.get_manifest( self.dest_image, media_type=QuayClient.MANIFEST_LIST_TYPE ), ) missing_archs = self.get_missing_architectures(src_manifest_list, dest_manifest_list) new_manifest_list = self._add_missing_architectures(src_manifest_list, missing_archs) LOG.info("Uploading the new manifest list to '{0}'".format(self.dest_image)) self._dest_quay_client.upload_manifest(new_manifest_list, self.dest_image) LOG.info("Merging manifests lists: complete.")
[docs] @staticmethod def get_missing_architectures( src_manifest_list: ManifestList, dest_manifest_list: ManifestList ) -> List[Manifest]: """ Get architectures which are missing from the new source image. NOTE: this method assumes that images are built only for one OS. The following logic would need to be overwritten if multiple OS builds started to be made. Args: src_manifest_list (dict): Manifest list of the source image. dest_manifest_list (dict): Manifest list of the destination image. Returns ([dict]): List of arch manifest data present in destination image but missing from source. """ missing_archs: List[Manifest] = [] missing_archs_log = [] src_archs = [arch["platform"]["architecture"] for arch in src_manifest_list["manifests"]] for dest_arch_dict in dest_manifest_list["manifests"]: if dest_arch_dict["platform"]["architecture"] not in src_archs: missing_archs.append(deepcopy(dest_arch_dict)) missing_archs_log.append(dest_arch_dict["platform"]["architecture"]) LOG.info( "Architectures missing from the new image: {0}".format(", ".join(missing_archs_log)) ) return missing_archs
[docs] def _add_missing_architectures( self, src_manifest_list: ManifestList, missing_archs: List[Manifest] ) -> ManifestList: """ Add missing architectures to the source manifest list. Args: src_manifest_list (dict): Source manifest list. missing_archs ([dict]): Manifest data of missing architectures. Retuns (dict): New manifest list containing all the architectures. """ new_manifest_list = deepcopy(src_manifest_list) new_manifest_list["manifests"] = new_manifest_list["manifests"] + missing_archs return new_manifest_list
[docs] def merge_manifest_lists_selected_architectures( self, eligible_archs: List[str] ) -> ManifestList: """ Merge manifests lists. Only specified archs are eligible for merging. This is an alternate workflow used in 'tag-docker'. Args: eligible_archs ([str]): Archs eligible for merging with the old manifest list. Returns (dict): New manifest list. """ if not self._src_quay_client or not self._dest_quay_client: raise RuntimeError("QuayClient instance must be set for both source and dest images") src_manifest_list = cast(ManifestList, self._src_quay_client.get_manifest(self.src_image)) # It's possible that destination doesn't exist in this workflow. ML merging logic is still # necessary due to only some archs being eligible try: dest_manifest_list = cast( ManifestList, self._dest_quay_client.get_manifest(self.dest_image), ) except requests.exceptions.HTTPError as e: if e.response.status_code == 404 or e.response.status_code == 401: dest_manifest_list = None else: raise archs_to_add = [] manifests_to_add = [] for src_arch_dict in src_manifest_list["manifests"]: if src_arch_dict["platform"]["architecture"] in eligible_archs: manifests_to_add.append(deepcopy(src_arch_dict)) archs_to_add.append(src_arch_dict["platform"]["architecture"]) manifests_to_keep = [] if dest_manifest_list: for dest_arch_dict in dest_manifest_list["manifests"]: if dest_arch_dict["platform"]["architecture"] not in archs_to_add: manifests_to_keep.append(deepcopy(dest_arch_dict)) new_manifest_list = deepcopy(src_manifest_list) new_manifest_list["manifests"] = manifests_to_add + manifests_to_keep return new_manifest_list