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.