Skip to content

Commit

Permalink
Replace outdated base images with the latest ones
Browse files Browse the repository at this point in the history
When a base image included in the rebuild list as a dependency is
not the latest published release, replace it with the latest one.
This ensures that outdated base images, which might reference
non-existent resources, do not cause build failures. By using the
latest base images, we can ensure successful image builds.

JIRA: CWFHEALTH-3258
  • Loading branch information
qixiang committed Jul 29, 2024
1 parent 29e7bb5 commit e4bf1f5
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 1 deletion.
6 changes: 6 additions & 0 deletions freshmaker/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down
70 changes: 70 additions & 0 deletions freshmaker/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"].
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

0 comments on commit e4bf1f5

Please sign in to comment.