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 11, 2024
1 parent 061d3b6 commit 8ef49e3
Show file tree
Hide file tree
Showing 6 changed files with 36 additions and 15 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
35 changes: 28 additions & 7 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,40 @@ 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 }}"]
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 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:
data = json.loads(result)
except json.JSONDecodeError as exc:
raise BootstrapError(f"The manifest data of {self.image} "
"are not json-formatted.") from exc
if 'Config' not in data:
raise BootstrapError(f"Config section of {self.image} is missing.")
if 'RootFS' not in data:
raise BootstrapError(f"RootFS section of {self.image} is missing.")
if data['RootFS']['Type'] != 'layers':
raise BootstrapError(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()

@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 8ef49e3

Please sign in to comment.