Blame SOURCES/bz2189400.patch

927684
From c4d1d1c46449c00952a2df7472bd5a8adc9d93a7 Mon Sep 17 00:00:00 2001
927684
From: Achilleas Koutsou <achilleas@koutsou.net>
927684
Date: Wed, 1 Mar 2023 16:48:15 +0100
927684
Subject: [PATCH 1/9] inputs/containers: change archive format to dir
927684
927684
The format so far was assumed to be `docker-archive` if the container
927684
was coming from a source and `oci-archive` if it was coming from a
927684
pipeline.  The source format will now be changed to `dir` instead of
927684
`docker-archive`.  The pipeline format remains `oci-archive`.
927684
927684
With the new archive format being `dir`, the source can't be linked into
927684
the build root and is bind mounted instead with the use of a MountGuard
927684
created with the instance of the service, and torn down when the service
927684
is stopped.
927684
927684
The _data field is removed from the map functions.  It was unused and
927684
these functions aren't part of the abstract class so they don't need to
927684
have consistent signatures.
927684
927684
Update the skopeo stage with support for the newly supported `dir`
927684
format.
927684
---
927684
 inputs/org.osbuild.containers | 27 +++++++++++++++++++--------
927684
 stages/org.osbuild.skopeo     |  4 ++--
927684
 2 files changed, 21 insertions(+), 10 deletions(-)
927684
927684
diff --git a/inputs/org.osbuild.containers b/inputs/org.osbuild.containers
927684
index c50006c..d1e642e 100755
927684
--- a/inputs/org.osbuild.containers
927684
+++ b/inputs/org.osbuild.containers
927684
@@ -21,6 +21,7 @@ import os
927684
 import sys
927684
 
927684
 from osbuild import inputs
927684
+from osbuild.util.mnt import MountGuard
927684
 
927684
 SCHEMA = r"""
927684
 "definitions": {
927684
@@ -159,15 +160,22 @@ SCHEMA = r"""
927684
 
927684
 class ContainersInput(inputs.InputService):
927684
 
927684
-    @staticmethod
927684
-    def map_source_ref(source, ref, _data, target):
927684
-        cache_dir = os.path.join(source, ref)
927684
-        os.link(os.path.join(cache_dir, "container-image.tar"), os.path.join(target, ref))
927684
+    def __init__(self, *args, **kwargs):
927684
+        super().__init__(*args, **kwargs)
927684
+        self.mg = MountGuard()
927684
+
927684
+    def map_source_ref(self, source, ref, target):
927684
+        source_archive = os.path.join(source, ref, "image")
927684
+        dest = os.path.join(target, ref)
927684
 
927684
-        return ref, "docker-archive"
927684
+        # bind mount the input directory to the destination
927684
+        os.makedirs(dest)
927684
+        self.mg.mount(source_archive, dest)
927684
+
927684
+        return ref, "dir"
927684
 
927684
     @staticmethod
927684
-    def map_pipeline_ref(store, ref, _data, target):
927684
+    def map_pipeline_ref(store, ref, target):
927684
         # prepare the mount point
927684
         os.makedirs(target, exist_ok=True)
927684
         print("target", target)
927684
@@ -188,9 +196,9 @@ class ContainersInput(inputs.InputService):
927684
 
927684
         for ref, data in refs.items():
927684
             if origin == "org.osbuild.source":
927684
-                ref, container_format = self.map_source_ref(source, ref, data, target)
927684
+                ref, container_format = self.map_source_ref(source, ref, target)
927684
             else:
927684
-                ref, container_format = self.map_pipeline_ref(store, ref, data, target)
927684
+                ref, container_format = self.map_pipeline_ref(store, ref, target)
927684
 
927684
             images[ref] = {
927684
                 "format": container_format,
927684
@@ -206,6 +214,9 @@ class ContainersInput(inputs.InputService):
927684
         }
927684
         return reply
927684
 
927684
+    def unmap(self):
927684
+        self.mg.umount()
927684
+
927684
 
927684
 def main():
927684
     service = ContainersInput.from_args(sys.argv[1:])
927684
diff --git a/stages/org.osbuild.skopeo b/stages/org.osbuild.skopeo
927684
index 0ed5e02..3323371 100755
927684
--- a/stages/org.osbuild.skopeo
927684
+++ b/stages/org.osbuild.skopeo
927684
@@ -85,8 +85,8 @@ def main(inputs, output, options):
927684
             linkname = os.path.join(tmpdir, "image.tar")
927684
             os.symlink(source, linkname)
927684
 
927684
-            if container_format == "docker-archive":
927684
-                source = f"docker-archive:{linkname}"
927684
+            if container_format == "dir":
927684
+                source = f"dir:{linkname}"
927684
             elif container_format == "oci-archive":
927684
                 source = f"oci-archive:{linkname}"
927684
             else:
927684
-- 
927684
2.40.0
927684
927684
927684
From b703c41a3d5203c16749b1bc1c96bdf2fad154c9 Mon Sep 17 00:00:00 2001
927684
From: Achilleas Koutsou <achilleas@koutsou.net>
927684
Date: Thu, 2 Mar 2023 13:44:50 +0100
927684
Subject: [PATCH 2/9] sources/skopeo: fix comment typo
927684
927684
---
927684
 sources/org.osbuild.skopeo | 2 +-
927684
 1 file changed, 1 insertion(+), 1 deletion(-)
927684
927684
diff --git a/sources/org.osbuild.skopeo b/sources/org.osbuild.skopeo
927684
index 070a35f..bc343da 100755
927684
--- a/sources/org.osbuild.skopeo
927684
+++ b/sources/org.osbuild.skopeo
927684
@@ -103,7 +103,7 @@ class SkopeoSource(sources.SourceService):
927684
                            check=True)
927684
 
927684
             # Verify that the digest supplied downloaded the correct container image id.
927684
-            # The image id is the digest of the config, but skopeo can' currently
927684
+            # The image id is the digest of the config, but skopeo can't currently
927684
             # get the config id, only the full config, so we checksum it ourselves.
927684
             res = subprocess.check_output(["skopeo", "inspect", "--raw", "--config", destination])
927684
             downloaded_id = "sha256:" + hashlib.sha256(res).hexdigest()
927684
-- 
927684
2.40.0
927684
927684
927684
From 321c7fd56d40f570a7f641d079d29d4f7c4c0fbf Mon Sep 17 00:00:00 2001
927684
From: Achilleas Koutsou <achilleas@koutsou.net>
927684
Date: Thu, 9 Mar 2023 14:41:53 +0100
927684
Subject: [PATCH 3/9] sources/skopeo: change local container format
927684
927684
Change the local storage format for containers to the `dir` format.
927684
The `dir` format will be used to retain signatures and manifests.
927684
927684
The remove-signatures option is removed since the storage format now
927684
supports them.
927684
927684
The final move (os.rename()) at the end of the fetch_one() method now
927684
creates the checksum directory if it doesn't exist and moves the child
927684
archive into it, adding to any existing archives that might exist in
927684
other formats (from a previous version downloading a `docker-archive`).
927684
927684
Dropped the .tar suffix from the symlink in the skopeo stage since it's
927684
not necessary and the target of the link might be a directory now.
927684
927684
The parent class exists() method checks if there is a *file* in the
927684
sources cache that matches the checksum.  For containers, this used to
927684
be a file called container-image.tar under a directory that matches the
927684
checksum, so for containers it always returned False.  Added an override
927684
for the skopeo source that checks for the new directory archive.
927684
---
927684
 sources/org.osbuild.skopeo | 35 ++++++++++++++++++++++-------------
927684
 stages/org.osbuild.skopeo  |  2 +-
927684
 2 files changed, 23 insertions(+), 14 deletions(-)
927684
927684
diff --git a/sources/org.osbuild.skopeo b/sources/org.osbuild.skopeo
927684
index bc343da..acc20b0 100755
927684
--- a/sources/org.osbuild.skopeo
927684
+++ b/sources/org.osbuild.skopeo
927684
@@ -1,6 +1,16 @@
927684
 #!/usr/bin/python3
927684
 """Fetch container image from a registry using skopeo
927684
 
927684
+The image is stored in a directory called `image` under a directory indexed by
927684
+the "container image id", which is the digest of the container configuration
927684
+file (rather than the outer manifest) and is what will be shown in the "podman
927684
+images" output when the image is installed. This digest is stable as opposed to
927684
+the manifest digest which can change during transfer and storage due to e.g.
927684
+recompression.
927684
+
927684
+The local storage format for containers is the `dir` format which supports
927684
+retaining signatures and manifests.
927684
+
927684
 Buildhost commands used: `skopeo`.
927684
 """
927684
 
927684
@@ -68,6 +78,8 @@ class SkopeoSource(sources.SourceService):
927684
 
927684
     content_type = "org.osbuild.containers"
927684
 
927684
+    dir_name = "image"
927684
+
927684
     def fetch_one(self, checksum, desc):
927684
         image_id = checksum
927684
         image = desc["image"]
927684
@@ -79,22 +91,14 @@ class SkopeoSource(sources.SourceService):
927684
             archive_dir = os.path.join(tmpdir, "container-archive")
927684
             os.makedirs(archive_dir)
927684
             os.chmod(archive_dir, 0o755)
927684
-            archive_path = os.path.join(archive_dir, "container-image.tar")
927684
 
927684
             source = f"docker://{imagename}@{digest}"
927684
 
927684
-            # We use the docker format, not oci, because that is the
927684
-            # default return image type of real world registries,
927684
-            # allowing the image to get the same image id as if you
927684
-            # did "podman pull" (rather than converting the image to
927684
-            # oci format, changing the id)
927684
-            destination = f"docker-archive:{archive_path}"
927684
+            # We use the dir format because it is the most powerful in terms of feature support and is the closest to a
927684
+            # direct serialisation of the registry data.
927684
+            destination = f"dir:{archive_dir}/{self.dir_name}"
927684
 
927684
             extra_args = []
927684
-
927684
-            # The archive format can't store signatures, but we still verify them during download
927684
-            extra_args.append("--remove-signatures")
927684
-
927684
             if not tls_verify:
927684
                 extra_args.append("--src-tls-verify=false")
927684
 
927684
@@ -111,9 +115,14 @@ class SkopeoSource(sources.SourceService):
927684
                 raise RuntimeError(
927684
                     f"Downloaded image {imagename}@{digest} has a id of {downloaded_id}, but expected {image_id}")
927684
 
927684
-            # Atomically move download dir into place on successful download
927684
+            # Atomically move download archive into place on successful download
927684
             with ctx.suppress_oserror(errno.ENOTEMPTY, errno.EEXIST):
927684
-                os.rename(archive_dir, f"{self.cache}/{image_id}")
927684
+                os.makedirs(os.path.join(self.cache, image_id), exist_ok=True)
927684
+                os.rename(os.path.join(archive_dir, self.dir_name), os.path.join(self.cache, image_id, self.dir_name))
927684
+
927684
+    def exists(self, checksum, _desc):
927684
+        path = os.path.join(self.cache, checksum, self.dir_name)
927684
+        return os.path.exists(path)
927684
 
927684
 
927684
 def main():
927684
diff --git a/stages/org.osbuild.skopeo b/stages/org.osbuild.skopeo
927684
index 3323371..08c9292 100755
927684
--- a/stages/org.osbuild.skopeo
927684
+++ b/stages/org.osbuild.skopeo
927684
@@ -82,7 +82,7 @@ def main(inputs, output, options):
927684
         # treats them special, like e.g. /some/path:tag, so we make a symlink to the real name
927684
         # and pass the symlink name to skopeo to make it work with anything
927684
         with tempfile.TemporaryDirectory() as tmpdir:
927684
-            linkname = os.path.join(tmpdir, "image.tar")
927684
+            linkname = os.path.join(tmpdir, "image")
927684
             os.symlink(source, linkname)
927684
 
927684
             if container_format == "dir":
927684
-- 
927684
2.40.0
927684
927684
927684
From 8d1b22d0a29637d1b661b0a8633ab457c4391c7d Mon Sep 17 00:00:00 2001
927684
From: Achilleas Koutsou <achilleas@koutsou.net>
927684
Date: Tue, 14 Mar 2023 17:25:23 +0100
927684
Subject: [PATCH 4/9] osbuild-mpp: extract is_manifest_list() function
927684
927684
Extract the is_manifest_list() function from the ImageManifest object in
927684
osbuild-mpp into a util function to be reused by the skopeo source.
927684
---
927684
 osbuild/util/containers.py | 14 ++++++++++++++
927684
 tools/osbuild-mpp          |  9 ++-------
927684
 2 files changed, 16 insertions(+), 7 deletions(-)
927684
 create mode 100644 osbuild/util/containers.py
927684
927684
diff --git a/osbuild/util/containers.py b/osbuild/util/containers.py
927684
new file mode 100644
927684
index 0000000..65b5d05
927684
--- /dev/null
927684
+++ b/osbuild/util/containers.py
927684
@@ -0,0 +1,14 @@
927684
+def is_manifest_list(data):
927684
+    """Inspect a manifest determine if it's a multi-image manifest-list."""
927684
+    media_type = data.get("mediaType")
927684
+    #  Check if mediaType is set according to docker or oci specifications
927684
+    if media_type in ("application/vnd.docker.distribution.manifest.list.v2+json",
927684
+                      "application/vnd.oci.image.index.v1+json"):
927684
+        return True
927684
+
927684
+    # According to the OCI spec, setting mediaType is not mandatory. So, if it is not set at all, check for the
927684
+    # existence of manifests
927684
+    if media_type is None and data.get("manifests") is not None:
927684
+        return True
927684
+
927684
+    return False
927684
diff --git a/tools/osbuild-mpp b/tools/osbuild-mpp
927684
index b8619f4..2f44dd6 100755
927684
--- a/tools/osbuild-mpp
927684
+++ b/tools/osbuild-mpp
927684
@@ -322,6 +322,7 @@ import hawkey
927684
 import rpm
927684
 import yaml
927684
 
927684
+from osbuild.util import containers
927684
 from osbuild.util.rhsm import Subscriptions
927684
 
927684
 # We need to resolve an image name to a resolved image manifest digest
927684
@@ -407,13 +408,7 @@ class ImageManifest:
927684
         self.digest = "sha256:" + hashlib.sha256(raw).hexdigest()
927684
 
927684
     def is_manifest_list(self):
927684
-        #  Check if mediaType is set according to docker or oci specifications
927684
-        if self.media_type in ("application/vnd.docker.distribution.manifest.list.v2+json", "application/vnd.oci.image.index.v1+json"):
927684
-            return True
927684
-        # According to the OCI spec, setting mediaType is not mandatory. So, if it is not set at all, check for the existance of manifests
927684
-        if self.media_type == "" and self.json.get("manifests") is not None:
927684
-            return True
927684
-        return False
927684
+        return containers.is_manifest_list(self.json)
927684
 
927684
     def _match_platform(self, wanted_arch, wanted_os, wanted_variant):
927684
         for m in self.json.get("manifests", []):
927684
-- 
927684
2.40.0
927684
927684
927684
From b5bada59383c6b275077309924ec086c0f1c467e Mon Sep 17 00:00:00 2001
927684
From: Achilleas Koutsou <achilleas@koutsou.net>
927684
Date: Thu, 16 Mar 2023 13:43:06 +0100
927684
Subject: [PATCH 5/9] sources: add org.osbuild.skopeo-index source
927684
927684
A new source module that can download a multi-image manifest list from a
927684
container registry.  This module is very similar to the skopeo source,
927684
but instead downloads a manifest list with `--multi-arch=index-only`.
927684
The checksum of the source object must be the digest of the manifest
927684
list that will be stored and the manifest that is downloaded must be a
927684
manifest-list.
927684
---
927684
 sources/org.osbuild.skopeo-index | 117 +++++++++++++++++++++++++++++++
927684
 1 file changed, 117 insertions(+)
927684
 create mode 100755 sources/org.osbuild.skopeo-index
927684
927684
diff --git a/sources/org.osbuild.skopeo-index b/sources/org.osbuild.skopeo-index
927684
new file mode 100755
927684
index 0000000..9d608f9
927684
--- /dev/null
927684
+++ b/sources/org.osbuild.skopeo-index
927684
@@ -0,0 +1,117 @@
927684
+#!/usr/bin/python3
927684
+"""Fetch container manifest list from a registry using skopeo
927684
+
927684
+The manifest is stored as a single file indexed by its content hash.
927684
+
927684
+Buildhost commands used: `skopeo`.
927684
+"""
927684
+
927684
+import errno
927684
+import json
927684
+import os
927684
+import subprocess
927684
+import sys
927684
+import tempfile
927684
+
927684
+from osbuild import sources
927684
+from osbuild.util import containers, ctx
927684
+
927684
+SCHEMA = """
927684
+"additionalProperties": false,
927684
+"definitions": {
927684
+  "item": {
927684
+    "description": "The manifest list to fetch",
927684
+    "type": "object",
927684
+    "additionalProperties": false,
927684
+    "patternProperties": {
927684
+      "sha256:[0-9a-f]{64}": {
927684
+        "type": "object",
927684
+        "additionalProperties": false,
927684
+        "required": ["image"],
927684
+        "properties": {
927684
+          "image": {
927684
+            "type": "object",
927684
+            "additionalProperties": false,
927684
+            "required": ["name"],
927684
+            "properties": {
927684
+              "name": {
927684
+                "type": "string",
927684
+                "description": "Name of the image (including registry)."
927684
+              },
927684
+              "tls-verify": {
927684
+                "type": "boolean",
927684
+                "description": "Require https (default true)."
927684
+              }
927684
+            }
927684
+          }
927684
+        }
927684
+      }
927684
+    }
927684
+  }
927684
+},
927684
+"properties": {
927684
+  "items": {"$ref": "#/definitions/item"},
927684
+  "digests": {"$ref": "#/definitions/item"}
927684
+},
927684
+"oneOf": [{
927684
+  "required": ["items"]
927684
+}, {
927684
+  "required": ["digests"]
927684
+}]
927684
+"""
927684
+
927684
+
927684
+class SkopeoIndexSource(sources.SourceService):
927684
+
927684
+    content_type = "org.osbuild.files"
927684
+
927684
+    def fetch_one(self, checksum, desc):
927684
+        digest = checksum
927684
+        image = desc["image"]
927684
+        imagename = image["name"]
927684
+        tls_verify = image.get("tls-verify", True)
927684
+
927684
+        with tempfile.TemporaryDirectory(prefix="tmp-download-", dir=self.cache) as tmpdir:
927684
+            archive_dir = os.path.join(tmpdir, "index")
927684
+            os.makedirs(archive_dir)
927684
+            os.chmod(archive_dir, 0o755)
927684
+
927684
+            source = f"docker://{imagename}@{digest}"
927684
+
927684
+            destination = f"dir:{archive_dir}"
927684
+
927684
+            extra_args = []
927684
+            if not tls_verify:
927684
+                extra_args.append("--src-tls-verify=false")
927684
+
927684
+            subprocess.run(["skopeo", "copy", "--multi-arch=index-only", *extra_args, source, destination],
927684
+                           encoding="utf-8", check=True)
927684
+
927684
+            # Verify that the digest supplied downloaded a manifest-list.
927684
+            res = subprocess.check_output(["skopeo", "inspect", "--raw", destination])
927684
+            if not containers.is_manifest_list(json.loads(res)):
927684
+                raise RuntimeError(
927684
+                    f"{imagename}@{digest} is not a manifest-list")
927684
+
927684
+            # use skopeo to calculate the checksum instead of our verify utility to make sure it's computed properly for
927684
+            # all types of manifests and handles any potential future changes to the way it's calculated
927684
+            manifest_path = os.path.join(archive_dir, "manifest.json")
927684
+            dl_checksum = subprocess.check_output(["skopeo", "manifest-digest", manifest_path]).decode().strip()
927684
+            if dl_checksum != checksum:
927684
+                raise RuntimeError(
927684
+                    f"Downloaded manifest-list {imagename}@{digest} has a checksum of {dl_checksum}, "
927684
+                    f"but expected {checksum}"
927684
+                )
927684
+
927684
+            # Move manifest into place on successful download
927684
+            with ctx.suppress_oserror(errno.ENOTEMPTY, errno.EEXIST):
927684
+                os.rename(f"{archive_dir}/manifest.json", f"{self.cache}/{digest}")
927684
+
927684
+
927684
+def main():
927684
+    service = SkopeoIndexSource.from_args(sys.argv[1:])
927684
+    service.main()
927684
+
927684
+
927684
+if __name__ == '__main__':
927684
+    main()
927684
-- 
927684
2.40.0
927684
927684
927684
From 5aabd8ac9a74e3f33fb34ae25c586b4bd7aabfd9 Mon Sep 17 00:00:00 2001
927684
From: Achilleas Koutsou <achilleas@koutsou.net>
927684
Date: Thu, 16 Mar 2023 15:23:20 +0100
927684
Subject: [PATCH 6/9] stages/skopeo: add manifest-lists input
927684
927684
Add an extra optional input type to the skopeo stage called
927684
`manifest-lists`.  This is a list of file-type inputs that must be a
927684
list of manifest lists, downloaded by the skopeo-index source.
927684
927684
The manifests are parsed and automatically associated with an image from
927684
the required `images` inputs.  If any manifest list is specified and not
927684
used, this is an error.
927684
927684
Adding manifest-lists currently has no effect.
927684
---
927684
 stages/org.osbuild.skopeo | 70 +++++++++++++++++++++++++++++++++++----
927684
 1 file changed, 64 insertions(+), 6 deletions(-)
927684
927684
diff --git a/stages/org.osbuild.skopeo b/stages/org.osbuild.skopeo
927684
index 08c9292..a26fa11 100755
927684
--- a/stages/org.osbuild.skopeo
927684
+++ b/stages/org.osbuild.skopeo
927684
@@ -7,6 +7,7 @@ input (reading from a skopeo source or a file in a pipeline).
927684
 Buildhost commands used: `skopeo`.
927684
 """
927684
 
927684
+import json
927684
 import os
927684
 import subprocess
927684
 import sys
927684
@@ -23,6 +24,11 @@ SCHEMA_2 = r"""
927684
     "images": {
927684
       "type": "object",
927684
       "additionalProperties": true
927684
+    },
927684
+    "manifest-lists": {
927684
+      "type": "object",
927684
+      "description": "Optional manifest lists to merge into images. The metadata must specify an image ID to merge to.",
927684
+      "additionalProperties": true
927684
     }
927684
   }
927684
 },
927684
@@ -53,20 +59,70 @@ SCHEMA_2 = r"""
927684
 """
927684
 
927684
 
927684
+def parse_manifest_list(manifests):
927684
+    """Return a map with single-image manifest digests as keys and the manifest-list digest as the value for each"""
927684
+    manifest_files = manifests["data"]["files"]
927684
+    manifest_map = {}
927684
+    for fname in manifest_files:
927684
+        filepath = os.path.join(manifests["path"], fname)
927684
+        with open(filepath, mode="r", encoding="utf-8") as mfile:
927684
+            data = json.load(mfile)
927684
+
927684
+        for manifest in data["manifests"]:
927684
+            digest = manifest["digest"]  # single image manifest digest
927684
+            manifest_map[digest] = fname
927684
+
927684
+    return manifest_map
927684
+
927684
+
927684
+def manifest_digest(path):
927684
+    """Get the manifest digest for a container at path, stored in dir: format"""
927684
+    return subprocess.check_output(["skopeo", "manifest-digest", os.path.join(path, "manifest.json")]).decode().strip()
927684
+
927684
+
927684
 def parse_input(inputs):
927684
+    manifests = inputs.get("manifest-lists")
927684
+    manifest_map = {}
927684
+    manifest_files = {}
927684
+    if manifests:
927684
+        manifest_files = manifests["data"]["files"]
927684
+        # reverse map manifest-digest -> manifest-list path
927684
+        manifest_map = parse_manifest_list(manifests)
927684
+
927684
     images = inputs["images"]
927684
     archives = images["data"]["archives"]
927684
 
927684
-    res = []
927684
-    for filename, data in archives.items():
927684
-        filepath = os.path.join(images["path"], filename)
927684
+    res = {}
927684
+    for checksum, data in archives.items():
927684
+        filepath = os.path.join(images["path"], checksum)
927684
+        list_path = None
927684
+        if data["format"] == "dir":
927684
+            digest = manifest_digest(filepath)
927684
+
927684
+            # get the manifest list path for this image
927684
+            list_digest = manifest_map.get(digest)
927684
+            if list_digest:
927684
+                # make sure all manifest files are used
927684
+                del manifest_files[list_digest]
927684
+                list_path = os.path.join(manifests["path"], list_digest)
927684
+
927684
+        res[checksum] = {
927684
+            "filepath": filepath,
927684
+            "manifest-list": list_path,
927684
+            "data": data,
927684
+        }
927684
+
927684
+    if manifest_files:
927684
+        raise RuntimeError(
927684
+            "The following manifest lists specified in the input did not match any of the container images: " +
927684
+            ", ".join(manifest_files)
927684
+        )
927684
 
927684
-        res.append((filepath, data))
927684
     return res
927684
 
927684
 
927684
 def main(inputs, output, options):
927684
-    files = parse_input(inputs)
927684
+    images = parse_input(inputs)
927684
 
927684
     destination = options["destination"]
927684
     # The destination type is always containers-storage atm, so ignore "type"
927684
@@ -74,7 +130,9 @@ def main(inputs, output, options):
927684
     storage_root = destination.get("storage-path", "/var/lib/containers/storage")
927684
     storage_driver = destination.get("storage-driver", "overlay")
927684
 
927684
-    for source, source_data in files:
927684
+    for image in images.values():
927684
+        source = image["filepath"]
927684
+        source_data = image["data"]
927684
         container_format = source_data["format"]
927684
         image_name = source_data["name"]
927684
 
927684
-- 
927684
2.40.0
927684
927684
927684
From 4e59d3a383e6cd54c92dd5e2c738df840e026b3f Mon Sep 17 00:00:00 2001
927684
From: Achilleas Koutsou <achilleas@koutsou.net>
927684
Date: Thu, 16 Mar 2023 15:50:41 +0100
927684
Subject: [PATCH 7/9] stages/skopeo: merge manifest into image directory
927684
927684
When a manifest list is matched with a container image, the skopeo
927684
stage will merge the specified manifest into the container image dir
927684
before copying it to the registry in the OS tree.
927684
927684
If there is no manifest to merge, we maintain the old behaviour of
927684
symlinking the source to work around the ":" in filename issue.
927684
Otherwise, we copy the container directory so that we can merge the
927684
manifest in the new location.
927684
---
927684
 stages/org.osbuild.skopeo | 46 +++++++++++++++++++++++++++++----------
927684
 1 file changed, 34 insertions(+), 12 deletions(-)
927684
927684
diff --git a/stages/org.osbuild.skopeo b/stages/org.osbuild.skopeo
927684
index a26fa11..6454ea7 100755
927684
--- a/stages/org.osbuild.skopeo
927684
+++ b/stages/org.osbuild.skopeo
927684
@@ -121,6 +121,27 @@ def parse_input(inputs):
927684
     return res
927684
 
927684
 
927684
+def merge_manifest(list_manifest, destination):
927684
+    """
927684
+    Merge the list manifest into the image directory. This preserves the manifest list with the image in the registry so
927684
+    that users can run or inspect a container using the original manifest list digest used to pull the container.
927684
+
927684
+    See https://github.com/containers/skopeo/issues/1935
927684
+    """
927684
+    # calculate the checksum of the manifest of the container image in the destination
927684
+    dest_manifest = os.path.join(destination, "manifest.json")
927684
+    manifest_checksum = subprocess.check_output(["skopeo", "manifest-digest", dest_manifest]).decode().strip()
927684
+    parts = manifest_checksum.split(":")
927684
+    assert len(parts) == 2, f"unexpected output for skopeo manifest-digest: {manifest_checksum}"
927684
+    manifest_checksum = parts[1]
927684
+
927684
+    # rename the manifest to its checksum
927684
+    os.rename(dest_manifest, os.path.join(destination, manifest_checksum + ".manifest.json"))
927684
+
927684
+    # copy the index manifest into the destination
927684
+    subprocess.run(["cp", "--reflink=auto", "-a", list_manifest, dest_manifest], check=True)
927684
+
927684
+
927684
 def main(inputs, output, options):
927684
     images = parse_input(inputs)
927684
 
927684
@@ -136,24 +157,25 @@ def main(inputs, output, options):
927684
         container_format = source_data["format"]
927684
         image_name = source_data["name"]
927684
 
927684
-        # We can't have special characters like ":" in the source names because containers/image
927684
-        # treats them special, like e.g. /some/path:tag, so we make a symlink to the real name
927684
-        # and pass the symlink name to skopeo to make it work with anything
927684
         with tempfile.TemporaryDirectory() as tmpdir:
927684
-            linkname = os.path.join(tmpdir, "image")
927684
-            os.symlink(source, linkname)
927684
+            tmp_source = os.path.join(tmpdir, "image")
927684
 
927684
-            if container_format == "dir":
927684
-                source = f"dir:{linkname}"
927684
-            elif container_format == "oci-archive":
927684
-                source = f"oci-archive:{linkname}"
927684
+            if container_format == "dir" and image["manifest-list"]:
927684
+                # copy the source container to the tmp source so we can merge the manifest into it
927684
+                subprocess.run(["cp", "-a", "--reflink=auto", source, tmp_source], check=True)
927684
+                merge_manifest(image["manifest-list"], tmp_source)
927684
             else:
927684
+                # We can't have special characters like ":" in the source names because containers/image
927684
+                # treats them special, like e.g. /some/path:tag, so we make a symlink to the real name
927684
+                # and pass the symlink name to skopeo to make it work with anything
927684
+                os.symlink(source, tmp_source)
927684
+
927684
+            if container_format not in ("dir", "oci-archive"):
927684
                 raise RuntimeError(f"Unknown container format {container_format}")
927684
 
927684
+            source = f"{container_format}:{tmp_source}"
927684
             dest = f"containers-storage:[{storage_driver}@{output}{storage_root}+/run/containers/storage]{image_name}"
927684
-
927684
-            subprocess.run(["skopeo", "copy", source, dest],
927684
-                           check=True)
927684
+            subprocess.run(["skopeo", "copy", source, dest], check=True)
927684
 
927684
     if storage_driver == "overlay":
927684
         # Each time the overlay backend runs on an xfs fs it creates this file:
927684
-- 
927684
2.40.0
927684
927684
927684
From 5096c6e11f060eb8f550544db7ce6960c5601a43 Mon Sep 17 00:00:00 2001
927684
From: Achilleas Koutsou <achilleas@koutsou.net>
927684
Date: Mon, 27 Mar 2023 18:14:50 +0200
927684
Subject: [PATCH 8/9] tools/osbuild-mpp: resolve manifest lists
927684
927684
Add support for resolving manifest lists in osbuild-mpp.
927684
Adds an `index` boolean field to the container image struct for
927684
mpp-resolve-images.  When enabled, the preprocessor will also store the
927684
manifest-list digest as a separate skopeo-index source and add it to the
927684
skopeo stage under the `manifest-lists` input.
927684
---
927684
 tools/osbuild-mpp | 19 +++++++++++++++++++
927684
 1 file changed, 19 insertions(+)
927684
927684
diff --git a/tools/osbuild-mpp b/tools/osbuild-mpp
927684
index 2f44dd6..8ac8634 100755
927684
--- a/tools/osbuild-mpp
927684
+++ b/tools/osbuild-mpp
927684
@@ -1495,12 +1495,14 @@ class ManifestFileV2(ManifestFile):
927684
             return
927684
 
927684
         refs = element_enter(inputs_images, "references", {})
927684
+        manifest_lists = []
927684
 
927684
         for image in element_enter(mpp, "images", []):
927684
             source = image["source"]
927684
             name = image.get("name", source)
927684
             digest = image.get("digest", None)
927684
             tag = image.get("tag", None)
927684
+            index = image.get("index", False)
927684
 
927684
             main_manifest = ImageManifest.load(source, tag=tag, digest=digest)
927684
 
927684
@@ -1529,6 +1531,23 @@ class ManifestFileV2(ManifestFile):
927684
                 "name": name
927684
             }
927684
 
927684
+            if index:
927684
+                manifest_lists.append(main_manifest.digest)
927684
+                container_index_source = element_enter(self.sources, "org.osbuild.skopeo-index", {})
927684
+                index_items = element_enter(container_index_source, "items", {})
927684
+                index_items[main_manifest.digest] = {
927684
+                    "image": {
927684
+                        "name": source
927684
+                    }
927684
+                }
927684
+
927684
+        # if we collected manifest lists, create the manifest-lists input array for the stage
927684
+        if manifest_lists:
927684
+            inputs_manifests = element_enter(inputs, "manifest-lists", {})
927684
+            inputs_manifests["type"] = "org.osbuild.files"
927684
+            inputs_manifests["origin"] = "org.osbuild.source"
927684
+            inputs_manifests["references"] = manifest_lists
927684
+
927684
 
927684
 def main():
927684
     parser = argparse.ArgumentParser(description="Manifest pre processor")
927684
-- 
927684
2.40.0
927684
927684
927684
From 4832a08422b9d8d022937fb9b6fc0ca43d395543 Mon Sep 17 00:00:00 2001
927684
From: Achilleas Koutsou <achilleas@koutsou.net>
927684
Date: Mon, 27 Mar 2023 17:33:31 +0200
927684
Subject: [PATCH 9/9] test: add manifest-list test for skopeo stage
927684
927684
Added another skopeo stage to skopeo/a.mpp.json with a skopeo source for
927684
a container hosted on the osbuild-composer gitlab registry.  The name
927684
points to a manifest list, which refers to two containers (amd64 and
927684
arm64) that contain a single text file (README.md).  The `index` field
927684
is enabled to include the manifest-list as an extra input to the stage.
927684
927684
The diff is updated with the new expected file list.
927684
The containers were created with buildah:
927684
927684
  amd=$(buildah from --arch=amd64 scratch)
927684
  arm=$(buildah from --arch=arm64 scratch)
927684
  buildah config --created-by "Achilleas Koutsou" "${amd}"
927684
  buildah config --created-by "Achilleas Koutsou" "${arm}"
927684
  buildah copy "${amd}" README.md
927684
  buildah copy "${arm}" README.md
927684
  amdid=$(buildah commit --format=docker --rm "${amd}")
927684
  armid=$(buildah commit --format=docker --rm "${arm}")
927684
  name="registry.gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/manifest-list-test"
927684
  buildah manifest create "${name}" "${amdid}" "${armid}"
927684
927684
  podman manifest push --all "${name}" dir:container
927684
---
927684
 test/data/stages/skopeo/a.json     | 46 ++++++++++++++++++++++++++++++
927684
 test/data/stages/skopeo/a.mpp.json | 24 ++++++++++++++++
927684
 test/data/stages/skopeo/diff.json  | 18 +++++++++---
927684
 3 files changed, 84 insertions(+), 4 deletions(-)
927684
927684
diff --git a/test/data/stages/skopeo/a.json b/test/data/stages/skopeo/a.json
927684
index 7c31e18..b29c318 100644
927684
--- a/test/data/stages/skopeo/a.json
927684
+++ b/test/data/stages/skopeo/a.json
927684
@@ -401,6 +401,33 @@
927684
               "storage-driver": "vfs"
927684
             }
927684
           }
927684
+        },
927684
+        {
927684
+          "type": "org.osbuild.skopeo",
927684
+          "inputs": {
927684
+            "images": {
927684
+              "type": "org.osbuild.containers",
927684
+              "origin": "org.osbuild.source",
927684
+              "references": {
927684
+                "sha256:dbb63178dc9157068107961f11397df3fb62c02fa64f697d571bf84aad71cb99": {
927684
+                  "name": "manifest-list-test"
927684
+                }
927684
+              }
927684
+            },
927684
+            "manifest-lists": {
927684
+              "type": "org.osbuild.files",
927684
+              "origin": "org.osbuild.source",
927684
+              "references": [
927684
+                "sha256:58150862447d05feeb263ddb7257bf11d2ce2a697362ac117de2184d10f028fc"
927684
+              ]
927684
+            }
927684
+          },
927684
+          "options": {
927684
+            "destination": {
927684
+              "type": "containers-storage",
927684
+              "storage-driver": "vfs"
927684
+            }
927684
+          }
927684
         }
927684
       ]
927684
     }
927684
@@ -733,6 +760,25 @@
927684
           "data": "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AAAAAAAAAAAAAAAAAAAAAABhbGV4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDAwMDAAMDAwMDAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHsiaW1hZ2VMYXlvdXRWZXJzaW9uIjogIjEuMC4wIn0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
927684
         }
927684
       }
927684
+    },
927684
+    "org.osbuild.skopeo": {
927684
+      "items": {
927684
+        "sha256:dbb63178dc9157068107961f11397df3fb62c02fa64f697d571bf84aad71cb99": {
927684
+          "image": {
927684
+            "name": "registry.gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/manifest-list-test",
927684
+            "digest": "sha256:601c98c8148720ec5c29b8e854a1d5d88faddbc443eca12920d76cf993d7290e"
927684
+          }
927684
+        }
927684
+      }
927684
+    },
927684
+    "org.osbuild.skopeo-index": {
927684
+      "items": {
927684
+        "sha256:58150862447d05feeb263ddb7257bf11d2ce2a697362ac117de2184d10f028fc": {
927684
+          "image": {
927684
+            "name": "registry.gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/manifest-list-test"
927684
+          }
927684
+        }
927684
+      }
927684
     }
927684
   }
927684
 }
927684
diff --git a/test/data/stages/skopeo/a.mpp.json b/test/data/stages/skopeo/a.mpp.json
927684
index 25886ad..9dd3f0b 100644
927684
--- a/test/data/stages/skopeo/a.mpp.json
927684
+++ b/test/data/stages/skopeo/a.mpp.json
927684
@@ -60,6 +60,30 @@
927684
               "storage-driver": "vfs"
927684
             }
927684
           }
927684
+        },
927684
+        {
927684
+          "type": "org.osbuild.skopeo",
927684
+          "inputs": {
927684
+            "images": {
927684
+              "type": "org.osbuild.containers",
927684
+              "origin": "org.osbuild.source",
927684
+              "mpp-resolve-images": {
927684
+                "images": [
927684
+                  {
927684
+                    "source": "registry.gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/manifest-list-test",
927684
+                    "name": "manifest-list-test",
927684
+                    "index": true
927684
+                  }
927684
+                ]
927684
+              }
927684
+            }
927684
+          },
927684
+          "options": {
927684
+            "destination": {
927684
+              "type": "containers-storage",
927684
+              "storage-driver": "vfs"
927684
+            }
927684
+          }
927684
         }
927684
       ]
927684
     }
927684
diff --git a/test/data/stages/skopeo/diff.json b/test/data/stages/skopeo/diff.json
927684
index f3cfe01..2118db3 100644
927684
--- a/test/data/stages/skopeo/diff.json
927684
+++ b/test/data/stages/skopeo/diff.json
927684
@@ -12,20 +12,30 @@
927684
     "/var/lib/containers/storage/vfs",
927684
     "/var/lib/containers/storage/vfs-containers",
927684
     "/var/lib/containers/storage/vfs-containers/containers.lock",
927684
+    "/var/lib/containers/storage/vfs/dir",
927684
+    "/var/lib/containers/storage/vfs/dir/0da97f9fb353a8d6285145bc7a1b75a28652e80f60ab7479bf5b4ae67ffa37e8",
927684
+    "/var/lib/containers/storage/vfs/dir/0da97f9fb353a8d6285145bc7a1b75a28652e80f60ab7479bf5b4ae67ffa37e8/hello.txt",
927684
+    "/var/lib/containers/storage/vfs/dir/8c42be08e0e5abaacad063054dccf9ce176453c50f14aebc5b335077c79cef77",
927684
+    "/var/lib/containers/storage/vfs/dir/8c42be08e0e5abaacad063054dccf9ce176453c50f14aebc5b335077c79cef77/README.md",
927684
     "/var/lib/containers/storage/vfs-images",
927684
     "/var/lib/containers/storage/vfs-images/805e972fbc4dfa74a616dcaafe0d9e9b4c548b8909b14ffb032aa20fa23d9ad6",
927684
     "/var/lib/containers/storage/vfs-images/805e972fbc4dfa74a616dcaafe0d9e9b4c548b8909b14ffb032aa20fa23d9ad6/=bWFuaWZlc3Qtc2hhMjU2OjIxY2E2OGE2OWJiNTlhZDFhY2M5NzgwN2M2NDMyZmFjODJlMGVlNjk5YWJkODgyNmZjZTdlZDdhOWY0OTc3NjE=",
927684
     "/var/lib/containers/storage/vfs-images/805e972fbc4dfa74a616dcaafe0d9e9b4c548b8909b14ffb032aa20fa23d9ad6/=c2hhMjU2OjgwNWU5NzJmYmM0ZGZhNzRhNjE2ZGNhYWZlMGQ5ZTliNGM1NDhiODkwOWIxNGZmYjAzMmFhMjBmYTIzZDlhZDY=",
927684
     "/var/lib/containers/storage/vfs-images/805e972fbc4dfa74a616dcaafe0d9e9b4c548b8909b14ffb032aa20fa23d9ad6/=c2lnbmF0dXJlLTIxY2E2OGE2OWJiNTlhZDFhY2M5NzgwN2M2NDMyZmFjODJlMGVlNjk5YWJkODgyNmZjZTdlZDdhOWY0OTc3NjE=",
927684
     "/var/lib/containers/storage/vfs-images/805e972fbc4dfa74a616dcaafe0d9e9b4c548b8909b14ffb032aa20fa23d9ad6/manifest",
927684
+    "/var/lib/containers/storage/vfs-images/dbb63178dc9157068107961f11397df3fb62c02fa64f697d571bf84aad71cb99",
927684
+    "/var/lib/containers/storage/vfs-images/dbb63178dc9157068107961f11397df3fb62c02fa64f697d571bf84aad71cb99/=bWFuaWZlc3Qtc2hhMjU2OjU4MTUwODYyNDQ3ZDA1ZmVlYjI2M2RkYjcyNTdiZjExZDJjZTJhNjk3MzYyYWMxMTdkZTIxODRkMTBmMDI4ZmM=",
927684
+    "/var/lib/containers/storage/vfs-images/dbb63178dc9157068107961f11397df3fb62c02fa64f697d571bf84aad71cb99/=bWFuaWZlc3Qtc2hhMjU2OjYwMWM5OGM4MTQ4NzIwZWM1YzI5YjhlODU0YTFkNWQ4OGZhZGRiYzQ0M2VjYTEyOTIwZDc2Y2Y5OTNkNzI5MGU=",
927684
+    "/var/lib/containers/storage/vfs-images/dbb63178dc9157068107961f11397df3fb62c02fa64f697d571bf84aad71cb99/=c2hhMjU2OmRiYjYzMTc4ZGM5MTU3MDY4MTA3OTYxZjExMzk3ZGYzZmI2MmMwMmZhNjRmNjk3ZDU3MWJmODRhYWQ3MWNiOTk=",
927684
+    "/var/lib/containers/storage/vfs-images/dbb63178dc9157068107961f11397df3fb62c02fa64f697d571bf84aad71cb99/=c2lnbmF0dXJlLTYwMWM5OGM4MTQ4NzIwZWM1YzI5YjhlODU0YTFkNWQ4OGZhZGRiYzQ0M2VjYTEyOTIwZDc2Y2Y5OTNkNzI5MGU=",
927684
+    "/var/lib/containers/storage/vfs-images/dbb63178dc9157068107961f11397df3fb62c02fa64f697d571bf84aad71cb99/manifest",
927684
     "/var/lib/containers/storage/vfs-images/images.json",
927684
     "/var/lib/containers/storage/vfs-images/images.lock",
927684
     "/var/lib/containers/storage/vfs-layers",
927684
     "/var/lib/containers/storage/vfs-layers/0da97f9fb353a8d6285145bc7a1b75a28652e80f60ab7479bf5b4ae67ffa37e8.tar-split.gz",
927684
+    "/var/lib/containers/storage/vfs-layers/8c42be08e0e5abaacad063054dccf9ce176453c50f14aebc5b335077c79cef77.tar-split.gz",
927684
     "/var/lib/containers/storage/vfs-layers/layers.json",
927684
-    "/var/lib/containers/storage/vfs-layers/layers.lock",
927684
-    "/var/lib/containers/storage/vfs/dir",
927684
-    "/var/lib/containers/storage/vfs/dir/0da97f9fb353a8d6285145bc7a1b75a28652e80f60ab7479bf5b4ae67ffa37e8",
927684
-    "/var/lib/containers/storage/vfs/dir/0da97f9fb353a8d6285145bc7a1b75a28652e80f60ab7479bf5b4ae67ffa37e8/hello.txt"],
927684
+    "/var/lib/containers/storage/vfs-layers/layers.lock"
927684
+  ],
927684
   "differences": {}
927684
 }
927684
-- 
927684
2.40.0
927684