def handle(self, event): if event.dry_run: self.force_dry_run() self.event = event db_event = Event.get_or_create_from_event(db.session, event) self.set_context(db_event) # Check if event is allowed by internal policies if not self.event.is_allowed(self): msg = ("This image rebuild is not allowed by internal policy. " f"message_id: {event.msg_id}") db_event.transition(EventState.SKIPPED, msg) self.log_info(msg) return [] # Mapping of original build nvrs to rebuilt nvrs in advisory nvrs_mapping = self._create_original_to_rebuilt_nvrs_map() original_nvrs = nvrs_mapping.keys() self.log_info( "Orignial nvrs of build in the advisory #{0} are: {1}".format( event.advisory.errata_id, " ".join(original_nvrs))) # Get image manifest_list_digest for all original images, manifest_list_digest is used # in pullspecs in bundle's related images original_digests_by_nvr = {} original_nvrs_by_digest = {} for nvr in original_nvrs: digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr) if digest: original_digests_by_nvr[nvr] = digest original_nvrs_by_digest[digest] = nvr else: log.warning( f"Image manifest_list_digest not found for original image {nvr} in Pyxis, " "skip this image") if not original_digests_by_nvr: msg = f"None of the original images have digests in Pyxis: {','.join(original_nvrs)}" log.warning(msg) db_event.transition(EventState.SKIPPED, msg) return [] # Get image manifest_list_digest for all rebuilt images, manifest_list_digest is used # in pullspecs of bundle's related images rebuilt_digests_by_nvr = {} rebuilt_nvrs = nvrs_mapping.values() for nvr in rebuilt_nvrs: digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr) if digest: rebuilt_digests_by_nvr[nvr] = digest else: log.warning( f"Image manifest_list_digest not found for rebuilt image {nvr} in Pyxis, " "skip this image") if not rebuilt_digests_by_nvr: msg = f"None of the rebuilt images have digests in Pyxis: {','.join(rebuilt_nvrs)}" log.warning(msg) db_event.transition(EventState.SKIPPED, msg) return [] index_images = self._pyxis.get_operator_indices() # get latest bundle images per channel per index image filtered # by the highest semantic version all_bundles = self._pyxis.get_latest_bundles(index_images) # A mapping of digests to bundle metadata. This metadata is used to # for the CSV metadata updates. bundle_mds_by_digest = {} # get bundle digests for original images bundle_digests_by_related_nvr = {} for image_nvr, image_digest in original_digests_by_nvr.items(): bundles = self._pyxis.get_bundles_by_related_image_digest( image_digest, all_bundles) if not bundles: log.info( f"No latest bundle image with the related image of {image_nvr}" ) continue for bundle in bundles: bundle_digest = bundle['bundle_path_digest'] bundle_mds_by_digest[bundle_digest] = bundle bundle_digests_by_related_nvr.setdefault( image_nvr, []).append(bundle_digest) if not bundle_digests_by_related_nvr: msg = "None of the original images have related bundles, skip." log.warning(msg) db_event.transition(EventState.SKIPPED, msg) return [] # Mapping of bundle digest to bundle data # { # digest: { # "images": [image_amd64, image_aarch64], # "nvr": NVR, # "auto_rebuild": True/False, # "osbs_pinning": True/False, # "pullspecs": [...], # } # } bundles_by_digest = {} default_bundle_data = { 'images': [], 'nvr': None, 'auto_rebuild': False, 'osbs_pinning': False, # CSV modifications for the rebuilt bundle image 'pullspecs': [], 'append': {}, 'update': {}, } # Get images for each bundle digest, a bundle digest can have multiple images # with different arches. for digest in bundle_mds_by_digest: bundles = self._pyxis.get_images_by_digest(digest) # If no bundle image found, just skip this bundle digest if not bundles: continue bundles_by_digest.setdefault(digest, copy.deepcopy(default_bundle_data)) bundles_by_digest[digest]['nvr'] = bundles[0]['brew']['build'] bundles_by_digest[digest]['images'] = bundles # Unauthenticated koji session to fetch build info of bundles koji_api = KojiService(conf.koji_profile) # For each bundle, check whether it should be rebuilt by comparing the # auto_rebuild_tags of repository and bundle's tags for digest, bundle_data in bundles_by_digest.items(): bundle_nvr = bundle_data['nvr'] # Images are for different arches, just check against the first image image = bundle_data['images'][0] if self.image_has_auto_rebuild_tag(image): bundle_data['auto_rebuild'] = True # Fetch buildinfo buildinfo = koji_api.get_build(bundle_nvr) related_images = (buildinfo.get('extra', {}).get('image', {}).get( 'operator_manifests', {}).get('related_images', {})) bundle_data['osbs_pinning'] = related_images.get( 'created_by_osbs', False) # Save the original pullspecs bundle_data['pullspecs'] = related_images.get('pullspecs', []) # Digests of bundles to be rebuilt to_rebuild_digests = set() # Now for each bundle, replace the original digest with rebuilt # digest (override pullspecs) for digest, bundle_data in bundles_by_digest.items(): # Override pullspecs only when auto_rebuild is enabled and OSBS-pinning # mechanism is used. if not (bundle_data['auto_rebuild'] and bundle_data['osbs_pinning']): continue csv_name = bundle_mds_by_digest[digest]['csv_name'] version = bundle_mds_by_digest[digest]['version'] bundle_data.update(self._get_csv_updates(csv_name, version)) for pullspec in bundle_data['pullspecs']: # A pullspec item example: # { # 'new': 'registry.exampe.io/repo/example-operator@sha256:<sha256-value>' # 'original': 'registry.example.io/repo/example-operator:v2.2.0', # 'pinned': True # } # A pullspec path is in format of "registry/repository@digest" pullspec_elems = pullspec.get('new').split('@') old_digest = pullspec_elems[1] if old_digest not in original_nvrs_by_digest: # This related image is not one of the original images continue # This related image is one of our original images old_nvr = original_nvrs_by_digest[old_digest] new_nvr = nvrs_mapping[old_nvr] new_digest = rebuilt_digests_by_nvr[new_nvr] # Replace the old digest with new digest pullspec_elems[1] = new_digest new_pullspec = '@'.join(pullspec_elems) pullspec['new'] = new_pullspec # Always set pinned to True when it was replaced by Freshmaker # since it indicates that the pullspec was modified from the # original pullspec pullspec['pinned'] = True # Once a pullspec in this bundle has been overrided, add this bundle # to rebuild list to_rebuild_digests.add(digest) if not to_rebuild_digests: msg = f"No bundle images to rebuild for advisory {event.advisory.name}" self.log_info(msg) db_event.transition(EventState.SKIPPED, msg) db.session.commit() return [] builds = self._prepare_builds(db_event, bundles_by_digest, to_rebuild_digests) # Reset context to db_event. self.set_context(db_event) self.start_to_build_images(builds) msg = f"Advisory {db_event.search_key}: Rebuilding " \ f"{len(db_event.builds.all())} bundle images." db_event.transition(EventState.BUILDING, msg) return []
def _handle_auto_rebuild(self, db_event): """ Handle auto rebuild for an advisory created by Botas :param db_event: database event that represent rebuild event :rtype: list :return: list of advisories that should be rebuilt """ # Mapping of original build nvrs to rebuilt nvrs in advisory nvrs_mapping = self._create_original_to_rebuilt_nvrs_map() original_nvrs = nvrs_mapping.keys() self.log_info( "Orignial nvrs of build in the advisory #{0} are: {1}".format( self.event.advisory.errata_id, " ".join(original_nvrs))) # Get image manifest_list_digest for all original images, manifest_list_digest is used # in pullspecs in bundle's related images original_digests_by_nvr = {} original_nvrs_by_digest = {} for nvr in original_nvrs: digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr) if digest: original_digests_by_nvr[nvr] = digest original_nvrs_by_digest[digest] = nvr else: log.warning( f"Image manifest_list_digest not found for original image {nvr} in Pyxis, " "skip this image") if not original_digests_by_nvr: msg = f"None of the original images have digests in Pyxis: {','.join(original_nvrs)}" log.warning(msg) db_event.transition(EventState.SKIPPED, msg) return [] # Get image manifest_list_digest for all rebuilt images, manifest_list_digest is used # in pullspecs of bundle's related images rebuilt_digests_by_nvr = {} rebuilt_nvrs = nvrs_mapping.values() for nvr in rebuilt_nvrs: # Don't require that the manifest list digest be published in this case because # there's a delay from after an advisory is shipped and when the published repositories # entry is populated digest = self._pyxis.get_manifest_list_digest_by_nvr( nvr, must_be_published=False) if digest: rebuilt_digests_by_nvr[nvr] = digest else: log.warning( f"Image manifest_list_digest not found for rebuilt image {nvr} in Pyxis, " "skip this image") if not rebuilt_digests_by_nvr: msg = f"None of the rebuilt images have digests in Pyxis: {','.join(rebuilt_nvrs)}" log.warning(msg) db_event.transition(EventState.SKIPPED, msg) return [] index_images = self._pyxis.get_operator_indices() # get latest bundle images per channel per index image filtered # by the highest semantic version all_bundles = self._pyxis.get_latest_bundles(index_images) self.log_debug( "There are %d bundles that are latest in a channel in the found index images", len(all_bundles), ) # A mapping of digests to bundle metadata. This metadata is used to # for the CSV metadata updates. bundle_mds_by_digest = {} # get bundle digests for original images bundle_digests_by_related_nvr = {} for image_nvr, image_digest in original_digests_by_nvr.items(): bundles = self._pyxis.get_bundles_by_related_image_digest( image_digest, all_bundles) if not bundles: log.info( f"No latest bundle image with the related image of {image_nvr}" ) continue for bundle in bundles: bundle_digest = bundle['bundle_path_digest'] bundle_mds_by_digest[bundle_digest] = bundle bundle_digests_by_related_nvr.setdefault( image_nvr, []).append(bundle_digest) if not bundle_digests_by_related_nvr: msg = "None of the original images have related bundles, skip." log.warning(msg) db_event.transition(EventState.SKIPPED, msg) return [] self.log_info("Found %d bundles with relevant related images", len(bundle_digests_by_related_nvr)) # Mapping of bundle digest to bundle data # { # digest: { # "images": [image_amd64, image_aarch64], # "nvr": NVR, # "auto_rebuild": True/False, # "osbs_pinning": True/False, # "pullspecs": [...], # } # } bundles_by_digest = {} default_bundle_data = { 'images': [], 'nvr': None, 'auto_rebuild': False, 'osbs_pinning': False, # CSV modifications for the rebuilt bundle image 'pullspec_replacements': [], 'update': {}, } # Get images for each bundle digest, a bundle digest can have multiple images # with different arches. for digest in bundle_mds_by_digest: bundles = self._pyxis.get_images_by_digest(digest) # If no bundle image found, just skip this bundle digest if not bundles: self.log_warn( 'The bundle digest %r was not found in Pyxis. Skipping.', digest) continue bundles_by_digest.setdefault(digest, copy.deepcopy(default_bundle_data)) bundles_by_digest[digest]['nvr'] = bundles[0]['brew']['build'] bundles_by_digest[digest]['images'] = bundles # Unauthenticated koji session to fetch build info of bundles koji_api = KojiService(conf.koji_profile) # For each bundle, check whether it should be rebuilt by comparing the # auto_rebuild_tags of repository and bundle's tags for digest, bundle_data in bundles_by_digest.items(): bundle_nvr = bundle_data['nvr'] # Images are for different arches, just check against the first image image = bundle_data['images'][0] if self.image_has_auto_rebuild_tag(image): bundle_data['auto_rebuild'] = True # Fetch buildinfo buildinfo = koji_api.get_build(bundle_nvr) related_images = (buildinfo.get('extra', {}).get('image', {}).get( 'operator_manifests', {}).get('related_images', {})) bundle_data['osbs_pinning'] = related_images.get( 'created_by_osbs', False) # Save the original pullspecs bundle_data['pullspec_replacements'] = related_images.get( 'pullspecs', []) # Digests of bundles to be rebuilt to_rebuild_digests = set() # Now for each bundle, replace the original digest with rebuilt # digest (override pullspecs) for digest, bundle_data in bundles_by_digest.items(): # Override pullspecs only when auto_rebuild is enabled and OSBS-pinning # mechanism is used. if not (bundle_data['auto_rebuild'] and bundle_data['osbs_pinning']): self.log_info( 'The bundle %r does not have auto-rebuild tags (%r) and/or OSBS pinning (%r)', bundle_data['nvr'], bundle_data['auto_rebuild'], bundle_data['osbs_pinning'], ) continue csv_name = bundle_mds_by_digest[digest]['csv_name'] version = bundle_mds_by_digest[digest]['version'] bundle_data.update(self._get_csv_updates(csv_name, version)) for pullspec in bundle_data['pullspec_replacements']: # A pullspec item example: # { # 'new': 'registry.exampe.io/repo/example-operator@sha256:<sha256-value>', # 'original': 'registry.example.io/repo/example-operator:v2.2.0', # 'pinned': True, # # value used for internal purpose during manual rebuilds, it's an old pullspec that was replaced # '_old': 'registry.exampe.io/repo/example-operator@sha256:<previous-sha256-value>, # } # A pullspec path is in format of "registry/repository@digest" pullspec_elems = pullspec.get('new').split('@') old_digest = pullspec_elems[1] if old_digest not in original_nvrs_by_digest: # This related image is not one of the original images continue # This related image is one of our original images old_nvr = original_nvrs_by_digest[old_digest] new_nvr = nvrs_mapping[old_nvr] new_digest = rebuilt_digests_by_nvr[new_nvr] # save pullspec that image had before rebuild pullspec['_old'] = pullspec.get('new') # Replace the old digest with new digest pullspec_elems[1] = new_digest new_pullspec = '@'.join(pullspec_elems) pullspec['new'] = new_pullspec # Always set pinned to True when it was replaced by Freshmaker # since it indicates that the pullspec was modified from the # original pullspec pullspec['pinned'] = True # Once a pullspec in this bundle has been overrided, add this bundle # to rebuild list self.log_info( 'Changing pullspec %r to %r in the bundle %r', pullspec['_old'], pullspec['new'], bundle_data['nvr'], ) to_rebuild_digests.add(digest) if not to_rebuild_digests: msg = self._no_bundle_prefix + "No bundle images to rebuild for " \ f"advisory {self.event.advisory.name}" self.log_info(msg) db_event.transition(EventState.SKIPPED, msg) db.session.commit() return [] bundles_to_rebuild = list( map(lambda x: bundles_by_digest[x], to_rebuild_digests)) return bundles_to_rebuild
def _handle_manual_rebuild(self, db_event): """ Handle manual rebuild submitted by Release Driver for an advisory created by Botas :param db_event: database event that represents a rebuild event :rtype: list :return: list of advisories that should be rebuilt """ old_to_new_pullspec_map = self._get_pullspecs_mapping() if not old_to_new_pullspec_map: msg = self._no_bundle_prefix + 'None of the bundle images have ' \ 'applicable pullspecs to replace' log.warning(msg) db_event.transition(EventState.SKIPPED, msg) return [] # Unauthenticated koji session to fetch build info of bundles koji_api = KojiService(conf.koji_profile) rebuild_nvr_to_pullspecs_map = dict() # compare replaced pullspecs with pullspecs in 'container_images' and # create map for bundles that should be rebuilt with their nvrs for container_image_nvr in self.event.container_images: artifact_build = db.session.query(ArtifactBuild).filter( ArtifactBuild.rebuilt_nvr == container_image_nvr, ArtifactBuild.type == ArtifactType.IMAGE.value, ).one_or_none() pullspecs = [] # Try to find build in FM database, if it's not there check in Brew if artifact_build: pullspecs = artifact_build.bundle_pullspec_overrides[ "pullspec_replacements"] else: # Fetch buildinfo from Koji buildinfo = koji_api.get_build(container_image_nvr) # Get the original pullspecs pullspecs = (buildinfo.get('extra', {}).get('image', {}).get( 'operator_manifests', {}).get('related_images', {}).get('pullspecs', [])) for pullspec in pullspecs: if pullspec.get('new') not in old_to_new_pullspec_map: continue # use newer pullspecs in the image pullspec['new'] = old_to_new_pullspec_map[pullspec['new']] rebuild_nvr_to_pullspecs_map[container_image_nvr] = pullspecs if not rebuild_nvr_to_pullspecs_map: msg = self._no_bundle_prefix + 'None of the container images have ' \ 'applicable pullspecs from the input bundle images' log.info(msg) db_event.transition(EventState.SKIPPED, msg) return [] # list with metadata about every bundle to do rebuild to_rebuild_bundles = [] # fill 'append' and 'update' fields for bundles to rebuild for nvr, pullspecs in rebuild_nvr_to_pullspecs_map.items(): bundle_digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr) if bundle_digest is not None: bundles = self._pyxis.get_bundles_by_digest(bundle_digest) temp_bundle = bundles[0] csv_updates = (self._get_csv_updates(temp_bundle['csv_name'], temp_bundle['version'])) to_rebuild_bundles.append({ 'nvr': nvr, 'update': csv_updates['update'], 'pullspec_replacements': pullspecs, }) else: log.warning('Can\'t find manifest_list_digest for bundle ' f'"{nvr}" in Pyxis') if not to_rebuild_bundles: msg = 'Can\'t find digests for any of the bundles to rebuild' log.warning(msg) db_event.transition(EventState.FAILED, msg) return [] return to_rebuild_bundles
def handle(self, event): if event.dry_run: self.force_dry_run() self.event = event db_event = Event.get_or_create_from_event(db.session, event) self.set_context(db_event) # Check if event is allowed by internal policies if not self.event.is_allowed(self): msg = ("This image rebuild is not allowed by internal policy. " f"message_id: {event.msg_id}") db_event.transition(EventState.SKIPPED, msg) self.log_info(msg) return [] # Get builds NVRs from the advisory attached to the message/event and # then get original NVR for every build # Mapping of original build nvrs to rebuilt nvrs in advisory nvrs_mapping = {} for product_info in event.advisory.builds.values(): for build in product_info['builds']: # Search for the first build that triggered the chain of rebuilds # for every shipped NVR to get original NVR from it original_nvr = self.get_published_original_nvr(build['nvr']) if original_nvr is None: continue nvrs_mapping[original_nvr] = build['nvr'] original_nvrs = nvrs_mapping.keys() self.log_info( "Orignial nvrs of build in the advisory #{0} are: {1}".format( event.advisory.errata_id, " ".join(original_nvrs))) # Get image manifest_list_digest for all original images, manifest_list_digest is used # in pullspecs in bundle's related images original_digests_by_nvr = {} original_nvrs_by_digest = {} for nvr in original_nvrs: digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr) if digest: original_digests_by_nvr[nvr] = digest original_nvrs_by_digest[digest] = nvr else: log.warning( f"Image manifest_list_digest not found for original image {nvr} in Pyxis, " "skip this image" ) if not original_digests_by_nvr: msg = f"None of the original images have digests in Pyxis: {','.join(original_nvrs)}" log.warning(msg) db_event.transition(EventState.SKIPPED, msg) return [] # Get image manifest_list_digest for all rebuilt images, manifest_list_digest is used # in pullspecs of bundle's related images rebuilt_digests_by_nvr = {} rebuilt_nvrs = nvrs_mapping.values() for nvr in rebuilt_nvrs: digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr) if digest: rebuilt_digests_by_nvr[nvr] = digest else: log.warning( f"Image manifest_list_digest not found for rebuilt image {nvr} in Pyxis, " "skip this image" ) if not rebuilt_digests_by_nvr: msg = f"None of the rebuilt images have digests in Pyxis: {','.join(rebuilt_nvrs)}" log.warning(msg) db_event.transition(EventState.SKIPPED, msg) return [] index_images = self._pyxis.get_operator_indices() # get latest bundle images per channel per index image filtered # by the highest semantic version all_bundles = self._pyxis.get_latest_bundles(index_images) # A set of unique bundle digests bundle_digests = set() # get bundle digests for original images bundle_digests_by_related_nvr = {} for image_nvr, image_digest in original_digests_by_nvr.items(): bundles = self._pyxis.get_bundles_by_related_image_digest( image_digest, all_bundles ) if not bundles: log.info(f"No latest bundle image with the related image of {image_nvr}") continue for bundle in bundles: bundle_digest = bundle['bundle_path_digest'] bundle_digests.add(bundle_digest) bundle_digests_by_related_nvr.setdefault(image_nvr, []).append(bundle_digest) if not bundle_digests_by_related_nvr: msg = "None of the original images have related bundles, skip." log.warning(msg) db_event.transition(EventState.SKIPPED, msg) return [] # Mapping of bundle digest to bundle data # { # digest: { # "images": [image_amd64, image_aarch64], # "nvr": NVR, # "auto_rebuild": True/False, # "osbs_pinning": True/False, # "pullspecs": [...], # } # } bundles_by_digest = {} default_bundle_data = { 'images': [], 'nvr': None, 'auto_rebuild': False, 'osbs_pinning': False, 'pullspecs': [], } # Get images for each bundle digest, a bundle digest can have multiple images # with different arches. for digest in bundle_digests: bundles = self._pyxis.get_images_by_digest(digest) # If no bundle image found, just skip this bundle digest if not bundles: continue bundles_by_digest.setdefault(digest, copy.deepcopy(default_bundle_data)) bundles_by_digest[digest]['nvr'] = bundles[0]['brew']['build'] bundles_by_digest[digest]['images'] = bundles # Unauthenticated koji session to fetch build info of bundles koji_api = KojiService(conf.koji_profile) # For each bundle, check whether it should be rebuilt by comparing the # auto_rebuild_tags of repository and bundle's tags for digest, bundle_data in bundles_by_digest.items(): bundle_nvr = bundle_data['nvr'] # Images are for different arches, just check against the first image image = bundle_data['images'][0] if self.image_has_auto_rebuild_tag(image): bundle_data['auto_rebuild'] = True # Fetch buildinfo buildinfo = koji_api.get_build(bundle_nvr) related_images = ( buildinfo.get("extra", {}) .get("image", {}) .get("operator_manifests", {}) .get("related_images", {}) ) bundle_data['osbs_pinning'] = related_images.get('created_by_osbs', False) # Save the original pullspecs bundle_data['pullspecs'] = related_images.get('pullspecs', []) # Digests of bundles to be rebuilt to_rebuild_digests = set() # Now for each bundle, replace the original digest with rebuilt # digest (override pullspecs) for digest, bundle_data in bundles_by_digest.items(): # Override pullspecs only when auto_rebuild is enabled and OSBS-pinning # mechanism is used. if not (bundle_data['auto_rebuild'] and bundle_data['osbs_pinning']): continue for pullspec in bundle_data['pullspecs']: # A pullspec item example: # { # 'new': 'registry.exampe.io/repo/example-operator@sha256:<sha256-value>' # 'original': 'registry.example.io/repo/example-operator:v2.2.0', # 'pinned': True # } # If related image is not pinned by OSBS, skip if not pullspec.get('pinned', False): continue # A pullspec path is in format of "registry/repository@digest" pullspec_elems = pullspec.get('new').split('@') old_digest = pullspec_elems[1] if old_digest not in original_nvrs_by_digest: # This related image is not one of the original images continue # This related image is one of our original images old_nvr = original_nvrs_by_digest[old_digest] new_nvr = nvrs_mapping[old_nvr] new_digest = rebuilt_digests_by_nvr[new_nvr] # Replace the old digest with new digest pullspec_elems[1] = new_digest new_pullspec = '@'.join(pullspec_elems) pullspec['new'] = new_pullspec # Once a pullspec in this bundle has been overrided, add this bundle # to rebuild list to_rebuild_digests.add(digest) # Skip that event because we can't proceed with processing it. # TODO # Now when we have bundle images' nvrs we can procceed with rebuilding it msg = f"Skipping the rebuild of {len(to_rebuild_digests)} bundle images " \ "due to being blocked on further implementation for now." db_event.transition(EventState.SKIPPED, msg) return []