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.

Related to discussion at
containers/podman#24818
  • Loading branch information
tkopecek committed Dec 13, 2024
1 parent 061d3b6 commit 2c83b5a
Show file tree
Hide file tree
Showing 7 changed files with 38 additions and 17 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 RootFS layer digests and Config section from 'podman image inspect' command, sha256 string",
"type": "string"
}
}
Expand Down
4 changes: 2 additions & 2 deletions mock/py/mockbuild/buildroot.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,11 @@ def _fallback(message):
"bootstrap.tar")
podman.import_tarball(tarball)

digest_expected = self.config.get("image_assert_digest", None)
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(
f"Expected digest for image {podman.image} is"
Expand Down
4 changes: 2 additions & 2 deletions mock/py/mockbuild/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def setup_default_config_opts():
config_opts['bootstrap_image_ready'] = False
config_opts['bootstrap_image_fallback'] = True
config_opts['bootstrap_image_keep_getting'] = 120
config_opts['bootstrap_image_assert_digest'] = None
config_opts['bootstrap_image_assert_digest'] = True

config_opts['internal_dev_setup'] = True

Expand Down 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
33 changes: 26 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,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.
"""
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)[0]
except json.JSONDecodeError as exc:
raise BootstrapError(f"The manifest data of {self.image} "
"are not json-formatted.") from exc
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
2 changes: 2 additions & 0 deletions releng/release-notes-next/podman-digests.feature
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 2c83b5a

Please sign in to comment.