diff --git a/freshmaker/config.py b/freshmaker/config.py index f034c77a..851b747f 100644 --- a/freshmaker/config.py +++ b/freshmaker/config.py @@ -371,6 +371,12 @@ class Config(object): 'the keys "groups" and "users" which have values that are lists. Any roles not ' "provided as keys, will contain defaut empty values.", }, + "update_base_image": { + "type": bool, + "default": False, + "desc": "When True, replace base images that are not the latest and are used as " + "dependency, the latest published image with the same name and version will be used.", + }, "rebuilt_nvr_release_suffix": { "type": str, "default": "", diff --git a/freshmaker/image.py b/freshmaker/image.py index d37ab16f..600f6c5e 100644 --- a/freshmaker/image.py +++ b/freshmaker/image.py @@ -111,6 +111,10 @@ def __hash__(self): def nvr(self): return self["brew"]["build"] + @property + def is_base_image(self): + return self["filesystem_koji_task_id"] is not None + def log_error(self, err): """ Logs the error associated with this image and sets self["error"]. @@ -153,6 +157,7 @@ def _get_default_additional_data(): "arches": None, "odcs_compose_ids": None, "parent_image_builds": None, + "filesystem_koji_task_id": None, } @classmethod @@ -187,6 +192,7 @@ def get_additional_data_from_koji(cls, nvr): fs_koji_task_id = build.get("extra", {}).get("filesystem_koji_task_id") if fs_koji_task_id: + data["filesystem_koji_task_id"] = fs_koji_task_id parsed_nvr = koji.parse_NVR(nvr) name_version = f'{parsed_nvr["name"]}-{parsed_nvr["version"]}' if name_version not in conf.image_extra_repo: @@ -1467,6 +1473,10 @@ def _get_images_to_rebuild(image): to_rebuild, directly_affected_nvrs, rpm_nvrs, content_sets ) + # Replace base images that are not the latest and are used as dependency images + if conf.update_base_image: + self._replace_base_images(to_rebuild, rpm_nvrs) + # Now generate batches from deduplicated list and return it. return self._images_to_rebuild_to_batches(to_rebuild, directly_affected_nvrs) @@ -1554,6 +1564,66 @@ def _filter_out_already_fixed_published_images( child_image["parent"] = fixed_published_image del image_group[not_directly_affected_index:] + def _replace_base_images(self, to_rebuild, rpm_nvrs): + """ + Replace base images that are not the latest and are used as dependency images. + + :param Iterable to_rebuild: the list of images to rebuild; each element is + an iterable with the first element being the child image and each subsequent + image being the parent of the previous image + :param Iterable rpm_nvrs: the list of RPM NVRs with the fixes in the advisory + """ + rpm_name_to_nvrs = {kobo.rpmlib.parse_nvr(nvr)["name"]: nvr for nvr in rpm_nvrs} + + replacements = {} + for image_group in to_rebuild: + # Base image can only be present as the last image in list + image = image_group[-1] + + if image.nvr in replacements: + new_image = replacements[image.nvr] + if not new_image: + continue + image_group[-1] = new_image + image_group[-2]["parent"] = new_image + continue + + # Skip non-base images + if not image.is_base_image: + continue + # Skip directly affected images + if image.get("directly_affected", False): + continue + + parsed_image_nvr = kobo.rpmlib.parse_nvr(image.nvr) + images = self.pyxis.find_latest_images_by_name_version( + parsed_image_nvr["name"], parsed_image_nvr["version"], published=True + ) + if not images: + continue + + candidate_nvr = images[0]["brew"]["build"] + parsed_candidate_nvr = kobo.rpmlib.parse_nvr(candidate_nvr) + # Skip if the latest published NVR <= current NVR + if kobo.rpmlib.compare_nvr(parsed_candidate_nvr, parsed_image_nvr) < 1: + replacements[image.nvr] = None + continue + + # Now that the latest base image to be used as replacement is determined, + # get it from pyxis with all the metadata required by Freshmaker + images = self.pyxis.find_images_by_nvr(candidate_nvr) + if not images: + log.error("Image not found: %s", candidate_nvr) + continue + + images = self.postprocess_images(images, rpm_name_to_nvrs) + new_image = images[0] + new_image.resolve(self) + + image_group[-1] = new_image + image_group[-2]["parent"] = new_image + replacements[image.nvr] = new_image + def postprocess_images(self, images, rpm_name_to_nvrs): # Avoid manipulating the images directly, uses a copy instead. image_dicts = copy.deepcopy(images) diff --git a/requirements.txt b/requirements.txt index fa641184..a5440cf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -126,7 +126,7 @@ moksha-common==1.2.5 # via moksha-hub moksha-hub==1.5.17 # via -r requirements.in -multidict==6.0.4 +multidict==6.0.5 # via # aiohttp # yarl diff --git a/tests/test_image.py b/tests/test_image.py index d3eceae0..5586200c 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -3335,3 +3335,57 @@ def test_get_fixed_published_image_not_found_by_nvr( ) assert image is None + + +@patch("freshmaker.pyxis_gql.PyxisGQL.find_latest_images_by_name_version") +@patch("freshmaker.pyxis_gql.PyxisGQL.find_images_by_nvr") +@patch("freshmaker.image.ContainerImage.resolve") +def test_replace_base_images( + mocked_resolve, mocked_find_images_by_nvr, mocked_find_images_by_name_version +): + vulerable_bash_rpm_manifest = { + "rpms": [ + { + "name": "bash", + "nvra": "bash-6.1.8-9.el9.x86_64", + "srpm_name": "bash", + "srpm_nevra": "bash-0:6.1.8-9.el9.src", + "version": "6.1.8", + } + ] + } + base_image = ContainerImage.create( + { + "brew": {"build": "ubi9-container-9.4-1123"}, + "content_sets": ["rhel-9-for-x86_64-baseos-rpms"], + "rpm_manifest": vulerable_bash_rpm_manifest, + "filesystem_koji_task_id": 12345, + } + ) + child_image = ContainerImage.create( + { + "brew": {"build": "dpdk-base-container-v4.13.8-4"}, + "content_sets": ["rhel-9-for-x86_64-baseos-rpms"], + "directly_affected": True, + "parent": base_image, + "rpm_manifest": vulerable_bash_rpm_manifest, + } + ) + latest_base_image = ContainerImage.create( + { + "brew": {"build": "ubi9-container-9.4-1200"}, + "content_sets": ["rhel-9-for-x86_64-baseos-rpms"], + "edges": {"rpm_manifest": {"data": {"rpms": vulerable_bash_rpm_manifest["rpms"]}}}, + } + ) + + mocked_find_images_by_name_version.return_value = [latest_base_image] + mocked_find_images_by_nvr.return_value = [latest_base_image] + + to_rebuild = [[child_image, base_image]] + rpm_nvrs = ["bash-6.1.8-10.el9"] + + pyxis = PyxisAPI("pyxis.domain.local") + pyxis._replace_base_images(to_rebuild, rpm_nvrs) + assert to_rebuild[0][0]["parent"]["brew"]["build"] == "ubi9-container-9.4-1200" + assert to_rebuild[0][1]["brew"]["build"] == "ubi9-container-9.4-1200"