Skip to content

Commit

Permalink
Use layers digests for comparing podman images
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tkopecek committed Dec 2, 2024
1 parent cb2834b commit d89e87c
Show file tree
Hide file tree
Showing 6 changed files with 28 additions and 14 deletions.
4 changes: 2 additions & 2 deletions mock/docs/buildroot-lock-schema-1.0.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@
"type": "object",
"additionalProperties": false,
"properties": {
"image_digest": {
"description": "Digest got by the 'podman image inspect --format {{ .Digest }}' command, sha256 string",
"image_layers_digest": {
"description": "SHA256 digest concatenated layer digests from 'podman image inspect --format {{ .RootFS }}' command, sha256 string",
"type": "string"
}
}
Expand Down
2 changes: 1 addition & 1 deletion mock/py/mockbuild/buildroot.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ def _fallback(message):
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(
f"Expected digest for image {podman.image} is"
Expand Down
2 changes: 1 addition & 1 deletion mock/py/mockbuild/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ def process_hermetic_build_config(cmdline_opts, config_opts):

# With hermetic builds, we always assert that we are reproducing the build
# with the same image.
config_opts["bootstrap_image_assert_digest"] = data["bootstrap"]["image_digest"]
config_opts["bootstrap_image_assert_digest"] = data["bootstrap"]["image_layers_digest"]


@traceLog()
Expand Down
4 changes: 2 additions & 2 deletions mock/py/mockbuild/plugins/buildroot_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ def _executor(cmd):
with self.buildroot.uid_manager.elevated_privileges():
podman = Podman(self.buildroot,
data["config"]["bootstrap_image"])
digest = podman.get_image_digest()
digest = podman.get_layers_digest()
data["bootstrap"] = {
"image_digest": digest,
"image_layers_digest": digest,
}

with open(out_file, "w", encoding="utf-8") as fdlist:
Expand Down
26 changes: 20 additions & 6 deletions mock/py/mockbuild/podman.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# vim: noai:ts=4:sw=4:expandtab

import hashlib
import json
import os
import logging
import subprocess
Expand Down Expand Up @@ -114,21 +116,33 @@ 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.
"""
check = [self.podman_binary, "image", "inspect", self.image,
"--format", "{{ .Digest }}"]
"--format", "{{ .RootFS }}"]
result = subprocess.run(check, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, check=False,
encoding="utf8")
if result.returncode:
raise BootstrapError(f"Can't get {self.image} podman image digest: {result.stderr}")
result = result.stdout.strip()
if len(result.splitlines()) != 1:
raise BootstrapError(f"The digest of {self.image} image is not a single-line string")
return result
try:
rootfs = json.loads(result)
layers = rootfs['layers']
except json.JSONDecodeError as exc:
raise BootstrapError(f"The RootFS layers of {self.image} "
"are not json-formatted.") from exc
except (KeyError, TypeError) as exc:
raise BootstrapError(f"The Layers of {self.image} "
"are not in expected format.") from exc
sha = hashlib.sha256()
for layer in layers:
sha.update(layer.encode()) # as it was decoded from json, it is unicode again
return sha.hexdigest()

@traceLog()
def cp(self, destination, tar_cmd):
Expand Down
4 changes: 2 additions & 2 deletions mock/tests/test_buildroot_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
}]
},
"bootstrap": {
"image_digest": "sha256:ba1067bef190fbe88f085bd019464a8c0803b7cd1e3f",
"image_layers_digest": "sha256:ba1067bef190fbe88f085bd019464a8c0803b7cd1e3f",
},
'config': {
'bootstrap_image': 'foo',
Expand Down Expand Up @@ -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_layers_digest"]
podman_cls = MagicMock(return_value=podman_obj)
with patch("mockbuild.plugins.buildroot_lock.Podman", side_effect=podman_cls):
method()
Expand Down

0 comments on commit d89e87c

Please sign in to comment.