diff --git a/behave/steps/other.py b/behave/steps/other.py index 61e70444f..8acc4fe88 100644 --- a/behave/steps/other.py +++ b/behave/steps/other.py @@ -245,7 +245,7 @@ def step_impl(context): schemafile = os.path.join(os.path.dirname(__file__), '..', '..', "mock", "docs", - "buildroot-lock-schema-1.0.0.json") + "buildroot-lock-schema-1.1.0.json") with open(schemafile, "r", encoding="utf-8") as fd: schema = json.load(fd) diff --git a/docs/Plugin-BuildrootLock.md b/docs/Plugin-BuildrootLock.md index 7c9f74b69..eb02c9467 100644 --- a/docs/Plugin-BuildrootLock.md +++ b/docs/Plugin-BuildrootLock.md @@ -33,6 +33,7 @@ installed together with the Mock RPM package: rpm -ql mock | grep schema /usr/share/doc/mock/buildroot-lock-schema-1.0.0.json + /usr/share/doc/mock/buildroot-lock-schema-1.1.0.json Currently, we do not provide a compatibility promise. Only the exact same version of Mock that produced the file is guaranteed to read and process it. diff --git a/mock/docs/buildroot-lock-schema-1.1.0.json b/mock/docs/buildroot-lock-schema-1.1.0.json new file mode 100644 index 000000000..4c7c5bf93 --- /dev/null +++ b/mock/docs/buildroot-lock-schema-1.1.0.json @@ -0,0 +1,120 @@ +{ + "$id": "https://raw.githubusercontent.com/rpm-software-management/mock/main/mock/docs/buildroot-lock-schema-1.1.0.json", + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "title": "Mock buildroot_lock.json file specification", + "description": "Version 1.1.0; last updated 2025-02-03", + "additionalProperties": false, + "properties": { + "version": { + "description": "Version of the https://raw.githubusercontent.com/rpm-software-management/mock/main/mock/docs/buildroot-lock-schema.json schema the document conforms to. Semantic versioned. Mock that implements v2.Y.Z versions no longer reads v1.Y.Z.", + "const": "1.1.0" + }, + "buildroot": { + "description": "The object that describes the Mock buildroot", + "type": "object", + "additionalProperties": false, + "properties": { + "rpms": { + "description": "List of RPM packages installed in the buildroot", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "arch": { + "description": "Architecture for which the package was built, 'noarch' for arch agnostic packages", + "type": "string" + }, + "epoch": { + "description": "Epoch number of the package", + "type": ["string", "null"] + }, + "license": { + "description": "The distribution license(s) of the package", + "type": "string" + }, + "name": { + "description": "Name of the package", + "type": "string" + }, + "release": { + "description": "Release (downstream) number of the package", + "type": "string" + }, + "sigmd5": { + "description": "The SIGMD5 tag from the rpm header.", + "type": "string" + }, + "signature": { + "description": "The signature used to sign the rpm (if any), last 8 characters from the \"rpm -q --qf '%{sigpgp:pgpsig}\n'\" output", + "type": ["string", "null"] + }, + "url": { + "description": "Uniform Resource Locator that points to additional information on the packaged software", + "type": "string" + }, + "version": { + "description": "Version (upstream) of the package", + "type": "string" + } + }, + "required": [ + "arch", + "epoch", + "license", + "name", + "release", + "sigmd5", + "signature", + "url", + "version" + ] + } + } + }, + "required": [ + "rpms" + ] + }, + "bootstrap": { + "description": "The object that describes the Mock bootstrap chroot. Optional, only provided when bootstrap (image) is used.", + "type": "object", + "additionalProperties": false, + "required": [ + "image_digest", + "pull_digest", + "architecture", + "id" + ], + "properties": { + "image_digest": { + "description": "SHA256 digest concatenated RootFS layer digests and Config section from 'podman image inspect' command, sha256 string", + "type": "string" + }, + "pull_digest": { + "description": "Image digest, as reported by podman inspect, can be used for podman pull.", + "type": "string" + }, + "architecture": { + "description": "OCI architecture string, as reported by podman inspect .Architecture field.", + "type": "string" + }, + "id": { + "type": "string", + "description": "Image ID, as reported by podman inspect .Id" + } + } + }, + "config": { + "description": "A set of important Mock configuration options used when the buildroot was generated (Mock's internal)", + "type": "object", + "properties": {} + } + }, + "required": [ + "buildroot", + "config", + "version" + ] +} diff --git a/mock/py/mock-hermetic-repo.py b/mock/py/mock-hermetic-repo.py index 10d4fa6d2..b685ba8c5 100755 --- a/mock/py/mock-hermetic-repo.py +++ b/mock/py/mock-hermetic-repo.py @@ -101,11 +101,18 @@ def _argparser(): return parser -def prepare_image(image_specification, outputdir): +def prepare_image(image_specification, bootstrap_data, outputdir): """ Store the tarball into the same directory where the RPMs are """ - subprocess.check_output(["podman", "pull", image_specification]) + pull_cmd = ["podman", "pull"] + if "pull_digest" in bootstrap_data: + image_specification += "@" + bootstrap_data["pull_digest"] + if "architecture" in bootstrap_data: + pull_cmd += ["--arch", bootstrap_data["architecture"]] + pull_cmd += [image_specification] + log.info("Pulling like: %s", ' '.join(pull_cmd)) + subprocess.check_output(pull_cmd) subprocess.check_output(["podman", "save", "--format=oci-archive", "--quiet", "-o", os.path.join(outputdir, "bootstrap.tar"), image_specification]) @@ -136,7 +143,8 @@ def _main(): subprocess.check_call(["createrepo_c", options.output_repo]) - prepare_image(data["config"]["bootstrap_image"], options.output_repo) + prepare_image(data["config"]["bootstrap_image"], data["bootstrap"], + options.output_repo) if __name__ == "__main__": diff --git a/mock/py/mockbuild/plugins/buildroot_lock.py b/mock/py/mockbuild/plugins/buildroot_lock.py index f8f76ac22..469d74f7d 100644 --- a/mock/py/mockbuild/plugins/buildroot_lock.py +++ b/mock/py/mockbuild/plugins/buildroot_lock.py @@ -66,7 +66,7 @@ def _executor(cmd): # the latest Mock implementing with Major == 1 can read any # version from the 1.Y.Z range. Mock implementing v2.Y.Z # no longer reads v1.Y.Z variants. - "version": "1.0.0", + "version": "1.1.0", "buildroot": { "rpms": packages, }, @@ -104,12 +104,10 @@ def _executor(cmd): try: podman = Podman(self.buildroot, data["config"]["bootstrap_image"]) - digest = podman.get_oci_digest() + data["bootstrap"] = podman.inspect_hermetic_metadata() + data["bootstrap"]["image_digest"] = podman.get_oci_digest() except PodmanError: - digest = "unknown" - data["bootstrap"] = { - "image_digest": digest, - } + data["bootstrap"] = {} with open(out_file, "w", encoding="utf-8") as fdlist: fdlist.write(json.dumps(data, indent=4, sort_keys=True) + "\n") diff --git a/mock/py/mockbuild/podman.py b/mock/py/mockbuild/podman.py index 714eb65f8..679eea3c6 100644 --- a/mock/py/mockbuild/podman.py +++ b/mock/py/mockbuild/podman.py @@ -230,11 +230,25 @@ def read_image_id(self): """ cmd = ["podman", "image", "inspect", self.image, "--format", "{{ .Id }}"] - getLog().info("Removing image %s", self.image) + getLog().info("Reading image .ID from %s", self.image) res = subprocess.run(cmd, env=self.buildroot.env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) self.image_id = res.stdout.decode("utf-8").strip() + + def inspect_hermetic_metadata(self): + """ + Get the image metadata needed for the subsequent hermetic build. + """ + get_query = '{"pull_digest": "{{ .Digest }}", "id": "{{.Id}}", "architecture": "{{ .Architecture }}"}' + getLog().info("Reading image %s from %s", get_query, self.image) + cmd = ["podman", "image", "inspect", "--format", get_query, self.image] + res = subprocess.run(cmd, env=self.buildroot.env, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True) + return json.loads(res.stdout.decode("utf-8").strip()) + + def __repr__(self): return "Podman({}({}))".format(self.image, self.image_id) diff --git a/mock/tests/test_buildroot_lock.py b/mock/tests/test_buildroot_lock.py index 535bc4756..0bc41929d 100644 --- a/mock/tests/test_buildroot_lock.py +++ b/mock/tests/test_buildroot_lock.py @@ -2,6 +2,7 @@ Test the methods that generate buildroot_lock.json """ +import copy import json import os import tempfile @@ -32,7 +33,7 @@ """ EXPECTED_OUTPUT = { - 'version': '1.0.0', + 'version': '1.1.0', 'buildroot': { 'rpms': [{ 'arch': 'x86_64', @@ -61,6 +62,9 @@ }, "bootstrap": { "image_digest": "sha256:ba1067bef190fbe88f085bd019464a8c0803b7cd1e3f", + "pull_digest": "sha256:1d9f0eaec60b59a669b285d1e775d970061b9694d4998b5bdb9626c9f33685cd", + "id": "b222730c2ba32385173fe3351026c316c9a294fa8d8c3c0f80d8afdb1b1aeef7", + "architecture": "amd64", }, 'config': { 'bootstrap_image': 'foo', @@ -72,6 +76,12 @@ } +def _exp_output_bootstrap(exp_data): + data = copy.deepcopy(exp_data["bootstrap"]) + del data["image_digest"] + return data + + def _mock_vars(rpm_out, repoquery_out): tc = TestCase() tc.maxDiff = None @@ -100,6 +110,7 @@ def _call_method(plugins, buildroot): podman_obj = MagicMock() podman_obj.get_oci_digest.return_value = EXPECTED_OUTPUT["bootstrap"]["image_digest"] + podman_obj.inspect_hermetic_metadata.return_value = _exp_output_bootstrap(EXPECTED_OUTPUT) 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/hermetic-arch-specific.feature b/releng/release-notes-next/hermetic-arch-specific.feature new file mode 100644 index 000000000..03d6f8143 --- /dev/null +++ b/releng/release-notes-next/hermetic-arch-specific.feature @@ -0,0 +1,9 @@ +The buildroot lockfile generator [has been modified][PR#1548] to include +additional bootstrap image metadata that can be later used for a precise image +pulling. + +The mock-hermetic-repo script has also been modified, to respect the additional +metadata. This allows us to, e.g., download bootstrap image of a different +(cross) architecture then the platform/host architecture is. In turn, the +script is now fully arch-agnostic (any host arch may be used for downloading +files from any arch specific lockfile).