From c1be7a1538de85ef17bd3f5082d41e33979b5096 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 container With the local format being `dir` now, we can copy the manifest specified in the source digest. We can then merge the list manifest into the image directory when requested by the user. 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. Then, they can 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 | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/sources/org.osbuild.skopeo b/sources/org.osbuild.skopeo index 9232c9217d..f986f4a181 100755 --- a/sources/org.osbuild.skopeo +++ b/sources/org.osbuild.skopeo @@ -6,13 +6,14 @@ Buildhost commands used: `skopeo`. import errno import hashlib +import json import os import subprocess import sys import tempfile from osbuild import sources -from osbuild.util import ctx +from osbuild.util import containers, ctx SCHEMA = """ "additionalProperties": false, @@ -86,6 +87,8 @@ class SkopeoSource(sources.SourceService): # direct serialisation of the registry data. dir_name = "image" destination = f"dir:{archive_dir}/{dir_name}" + image_path = os.path.join(archive_dir, dir_name) + destination = f"dir:{image_path}" extra_args = [] if not tls_verify: @@ -104,13 +107,34 @@ class SkopeoSource(sources.SourceService): raise RuntimeError( f"Downloaded image {imagename}@{digest} has a id of {downloaded_id}, but expected {image_id}") - # Atomically move download archive into place on successful download + # Move image directory into place on successful download with ctx.suppress_oserror(errno.ENOTEMPTY, errno.EEXIST): os.makedirs(f"{self.cache}/{image_id}", exist_ok=True) os.rename(f"{archive_dir}/{dir_name}", f"{self.cache}/{image_id}/{dir_name}") - def exists(self, checksum, _desc): - return os.path.exists(f"{self.cache}/{checksum}/image") + if self.is_manifest_list(source): + # the digest points to a manifest-list: download it and store it separately in the archive_dir, next to + # the image directory + manifest_path = os.path.join(archive_dir, digest) + subprocess.run(["skopeo", "copy", "--multi-arch=index-only", *extra_args, + source, f"dir:{manifest_path}"], + encoding="utf-8", check=True) + + # Move manifest directory into place on successful download + with ctx.suppress_oserror(errno.ENOTEMPTY, errno.EEXIST): + os.rename(f"{archive_dir}/{digest}", f"{self.cache}/{image_id}/{digest}") + + def exists(self, checksum, desc): + digest = desc["image"]["digest"] + # check that both the image and the digest exist + return (os.path.exists(f"{self.cache}/{checksum}/image") and + os.path.exists(f"{self.cache}/{checksum}/{digest}")) + + @staticmethod + def is_manifest_list(source): + """Inspect the manifest at the source and determine if it's a multi-image manifest-list.""" + res = subprocess.check_output(["skopeo", "inspect", "--raw", source]) + return containers.is_manifest_list(json.loads(res)) def main():