From 90a5ce6551a299b1ba2256058835632ed404bf33 Mon Sep 17 00:00:00 2001 From: Tomas Kopecek Date: Mon, 2 Dec 2024 11:47:01 +0100 Subject: [PATCH] Use layers digests for comparing podman images Previous method checked default digest provided by podman. This digest is "local" and changed every time image is saved/load or at any other point manifest is modified. This doesn't mean that it is a different image. Viable way for our purposes is to compare that all layers are identical and in the same order. Simple way to distill this into one value is to concatenate individual layers' digests in order of appearance in RootFS. Related to discussion at https://github.com/containers/podman/discussions/24818 --- mock/docs/buildroot-lock-schema-1.0.0.json | 2 +- mock/py/mockbuild/buildroot.py | 6 +- mock/py/mockbuild/plugins/buildroot_lock.py | 2 +- mock/py/mockbuild/podman.py | 64 +++++++++++++++---- mock/tests/test_buildroot_lock.py | 2 +- .../release-notes-next/podman-digests.feature | 2 + 6 files changed, 59 insertions(+), 19 deletions(-) create mode 100644 releng/release-notes-next/podman-digests.feature diff --git a/mock/docs/buildroot-lock-schema-1.0.0.json b/mock/docs/buildroot-lock-schema-1.0.0.json index 9a4cdb25d..a8cd03a32 100644 --- a/mock/docs/buildroot-lock-schema-1.0.0.json +++ b/mock/docs/buildroot-lock-schema-1.0.0.json @@ -83,7 +83,7 @@ "additionalProperties": false, "properties": { "image_digest": { - "description": "Digest got by the 'podman image inspect --format {{ .Digest }}' command, sha256 string", + "description": "SHA256 digest concatenated RootFS layer digests and Config section from 'podman image inspect' command, sha256 string", "type": "string" } } diff --git a/mock/py/mockbuild/buildroot.py b/mock/py/mockbuild/buildroot.py index 6633d0b8c..6ff93e822 100644 --- a/mock/py/mockbuild/buildroot.py +++ b/mock/py/mockbuild/buildroot.py @@ -281,13 +281,13 @@ def _fallback(message): self.chroot_image, podman.image_id) podman.tag_image() - digest_expected = self.image_assert_digest + digest_expected = self.config.get("image_assert_digest") if digest_expected: getLog().info("Checking image digest: %s", digest_expected) - digest = podman.get_image_digest() + digest = podman.get_oci_digest() if digest != digest_expected: - getLog().warning( + raise BootstrapError( f"Expected digest for image {podman.image} is" f"{digest_expected}, but {digest} found.") diff --git a/mock/py/mockbuild/plugins/buildroot_lock.py b/mock/py/mockbuild/plugins/buildroot_lock.py index 32d267f6f..f8f76ac22 100644 --- a/mock/py/mockbuild/plugins/buildroot_lock.py +++ b/mock/py/mockbuild/plugins/buildroot_lock.py @@ -104,7 +104,7 @@ def _executor(cmd): try: podman = Podman(self.buildroot, data["config"]["bootstrap_image"]) - digest = podman.get_image_digest() + digest = podman.get_oci_digest() except PodmanError: digest = "unknown" data["bootstrap"] = { diff --git a/mock/py/mockbuild/podman.py b/mock/py/mockbuild/podman.py index 100c920bd..317991cbf 100644 --- a/mock/py/mockbuild/podman.py +++ b/mock/py/mockbuild/podman.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # vim: noai:ts=4:sw=4:expandtab +import hashlib +import json import os import logging import subprocess @@ -16,6 +18,47 @@ class PodmanError(Exception): """ +def podman_get_oci_digest(image, logger=None, podman_binary=None): + """ + Get sha256 digest of RootFS layers. This must be identical for + all images containing same order of layers, thus it can be used + as the check that we've loaded same image. + """ + logger = logger or logging.getLogger() + podman = podman_binary or "/usr/bin/podman" + logger.info("Calculating %s image OCI digest", image) + check = [podman, "image", "inspect", image] + result = subprocess.run(check, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False, + encoding="utf8") + if result.returncode: + logger.error("Can't get %s podman image digest: %s", image, result.stderr) + return None + result = result.stdout.strip() + + try: + data = json.loads(result)[0] + except json.JSONDecodeError: + logger.error("The manifest data of %s are not json-formatted.", image) + return None + + if 'RootFS' not in data: + logger.error("RootFS section of %s is missing.", image) + return None + if data['RootFS']['Type'] != 'layers': + logger.error("Unexpected format for RootFS in %s.", image) + return None + + # data which should be sufficient to confirm the image + data = { + 'RootFS': data['RootFS'], + 'Config': data['Config'], + } + # convert to json string with ordered dicts and create hash + data = json.dumps(data, sort_keys=True) + return hashlib.sha256(data.encode()).hexdigest() + + def podman_check_native_image_architecture(image, logger=None, podman_binary=None): """ Return True if image's architecture is "native" for this host. @@ -132,26 +175,21 @@ def mounted_image(self): subprocess.run(cmd_umount, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) - def get_image_digest(self): + def get_oci_digest(self): """ - Get the "sha256:..." string for the image we work with. + Get sha256 digest of RootFS layers. This must be identical for + all images containing same order of layers, thus it can be used + as the check that we've loaded same image. """ the_image = self.image if the_image.startswith("oci-archive:"): # We can't query digest from tarball directly, but note # the image needs to be tagged first! the_image = self._tagged_id - check = [self.podman_binary, "image", "inspect", the_image, - "--format", "{{ .Digest }}"] - result = subprocess.run(check, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, check=False, - encoding="utf8") - if result.returncode: - raise PodmanError(f"Can't get {the_image} podman image digest: {result.stderr}") - result = result.stdout.strip() - if len(result.splitlines()) != 1: - raise PodmanError(f"The digest of {the_image} image is not a single-line string") - return result + digest = podman_get_oci_digest(the_image, logger=getLog()) + if digest is None: + raise PodmanError(f"Getting OCI digest for image {self.image} failed") + return digest def check_native_image_architecture(self): """ diff --git a/mock/tests/test_buildroot_lock.py b/mock/tests/test_buildroot_lock.py index 9a89ee3b2..535bc4756 100644 --- a/mock/tests/test_buildroot_lock.py +++ b/mock/tests/test_buildroot_lock.py @@ -99,7 +99,7 @@ def _call_method(plugins, buildroot): _, method = plugins.add_hook.call_args[0] podman_obj = MagicMock() - podman_obj.get_image_digest.return_value = EXPECTED_OUTPUT["bootstrap"]["image_digest"] + podman_obj.get_oci_digest.return_value = EXPECTED_OUTPUT["bootstrap"]["image_digest"] podman_cls = MagicMock(return_value=podman_obj) with patch("mockbuild.plugins.buildroot_lock.Podman", side_effect=podman_cls): method() diff --git a/releng/release-notes-next/podman-digests.feature b/releng/release-notes-next/podman-digests.feature new file mode 100644 index 000000000..de84d3814 --- /dev/null +++ b/releng/release-notes-next/podman-digests.feature @@ -0,0 +1,2 @@ +Hermetic build process is enhanced by adding used imaged digests into the +metadata and confirming that exactly same image is used in the next step.