From 42fabed88ce2add5da1a73e6f654d093eeba04be 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/podman.py | 40 +++++++++++++------ mock/tests/test_buildroot_lock.py | 2 +- .../release-notes-next/podman-digests.feature | 2 + 5 files changed, 34 insertions(+), 18 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..29e981f64 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", True) if digest_expected: getLog().info("Checking image digest: %s", digest_expected) - digest = podman.get_image_digest() + digest = podman.get_layers_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/podman.py b/mock/py/mockbuild/podman.py index 100c920bd..0d5f80463 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 @@ -132,26 +134,38 @@ def mounted_image(self): subprocess.run(cmd_umount, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) - def get_image_digest(self): + def get_layers_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 }}"] + check = [self.podman_binary, "image", "inspect", self.image] 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}") + raise PodmanError(f"Can't get {self.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 + + try: + data = json.loads(result)[0] + except json.JSONDecodeError as exc: + raise PodmanError(f"The manifest data of {self.image} " + "are not json-formatted.") from exc + if 'RootFS' not in data: + raise PodmanError(f"RootFS section of {self.image} is missing.") + if data['RootFS']['Type'] != 'layers': + raise PodmanError(f"Unexpected format for RootFS in {self.image}.") + + # 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 check_native_image_architecture(self): """ diff --git a/mock/tests/test_buildroot_lock.py b/mock/tests/test_buildroot_lock.py index 9a89ee3b2..4e5eef046 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_layers_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.