From f9e14383ec5520660e6259bf58d20887b8bddbb5 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Fri, 10 Mar 2023 17:18:37 +0100 Subject: [PATCH] sources/skopeo: fetch index manifest for dir When the format is specified as "dir", copy the manifest specified in the source digest and merge it into the final image directory. The effect of this is that the digest of the final image in the container registry will match the manifest digest. This enables users to specify an image's manifest digest or a multi-image manifest list digest which will be preserved in the container store on the final OS and allow them to run the container using the same digest that was specified in the input. This may become a feature of Skopeo (or other tooling) in the future. See https://github.com/containers/skopeo/issues/1935 --- sources/org.osbuild.skopeo | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/sources/org.osbuild.skopeo b/sources/org.osbuild.skopeo index f236ea62bf..5d56a32c47 100755 --- a/sources/org.osbuild.skopeo +++ b/sources/org.osbuild.skopeo @@ -93,9 +93,9 @@ class SkopeoSource(sources.SourceService): os.chmod(archive_dir, 0o755) source = f"docker://{imagename}@{digest}" - archive_name = containers.archive_name(archive_format) - destination = f"{archive_format}:{archive_dir}/{archive_name}" + dest_path = os.path.join(archive_dir, archive_name) + destination = f"{archive_format}:{dest_path}" extra_args = [] @@ -119,6 +119,10 @@ class SkopeoSource(sources.SourceService): raise RuntimeError( f"Downloaded image {imagename}@{digest} has a id of {downloaded_id}, but expected {image_id}") + if archive_format == "dir": + # fetch the manifest and merge it into the archive + self.merge_manifest(source, dest_path) + # Atomically move download archive into place on successful download with ctx.suppress_oserror(errno.ENOTEMPTY, errno.EEXIST): os.makedirs(f"{self.cache}/{image_id}", exist_ok=True) @@ -128,6 +132,23 @@ class SkopeoSource(sources.SourceService): archive_name = containers.archive_name(desc["image"].get("format", "docker-archive")) return os.path.exists(f"{self.cache}/{checksum}/{archive_name}") + def merge_manifest(self, source, destination): + with tempfile.TemporaryDirectory(prefix="tmp-download-", dir=self.cache) as indexdir: + # download the manifest(s) to a temporary directory + subprocess.run(["skopeo", "copy", "--multi-arch=index-only", source, f"dir:{indexdir}"], + encoding="utf-8", check=True) + + # calculate the checksum of the manifest of the container image in the destination + manifest_path = os.path.join(destination, "manifest.json") + with open(manifest_path, encoding="utf-8") as manifest: + manifest_checksum = hashlib.sha256(manifest.read().encode()).hexdigest() + + # rename the manifest to its checksum + os.rename(manifest_path, os.path.join(destination, manifest_checksum + ".manifest.json")) + + # move the index manifest into the destination + os.rename(os.path.join(indexdir, "manifest.json"), manifest_path) + def main(): service = SkopeoSource.from_args(sys.argv[1:])