Repository: containers/image
Pull request #2003 (2023-06-16 13:15:50)

Oci image deletion
Fix attempt for #1812

I made two commit; first a failing test then the feature to make it work, I'll happily fixup in one commit given how small this change is.

mtrmac: > I made two commit; first a failing test then the feature to make it work, I'll happily fixup in one commit given how small this change is.

We prefer the tests to be working on every commit, so that bisection doesn’t have false positives.
Pvlerick: > > I made two commit; first a failing test then the feature to make it work, I'll happily fixup in one commit given how small this change is.
> 
> We prefer the tests to be working on every commit, so that bisection doesn’t have false positives.

All right, I'm doing this out of habit (failing tests first then committing) but I'll squash all of that for next time.
Pvlerick: Thanks for the feedback, I'll get back to this and will ping you once I have something more substantial.
mtrmac: > All right, I'm doing this out of habit (failing tests first then committing) but I'll squash all of that for next time.

That habit makes sense — we really only need it squashed (or just reordered?) immediately before merging.
Pvlerick: Here's something hopefully better. Test setup is ugly but I'd like to get the logic validated before refactoring this into something cleaner.

A couple of questions on this:

- if it's the last image in the index, should it delete the index file altogether? And all sub-folders, which should all be empty at that point?
- I don't think it's possible to check if there is a container running using that image, is it? That should be a test case, but I don't see how it would be possible... Multiple OCI runtime could be running in different locations, etc... Hairy.
- if the given image is empty, should it 1) if there is only one image, delete it from the index (and see point above) 2) if more than one image, do nothing - see `getManifestDescriptor` behavior: https://github.com/containers/image/blob/main/oci/layout/oci_transport.go#L183
- what's in the `archive`folder?
Pvlerick: Style/opinion question: index files are generated using string literals: https://github.com/containers/image/blob/main/oci/layout/oci_transport_test.go#L201

I started refactoring this part for my tests to use imgspecv1 structs that I'm filling then serializing instead; is that ok or do you rather keep literals?
mtrmac: > I started refactoring this part for my tests to use imgspecv1 structs that I'm filling then serializing instead

I think using raw blobs works better as an interoperability test; that way we know if the (de)serialization code changes in a way that breaks compatibility.

(That said, using raw files in `testdata` would be cleaner than long string literals like that.)

---

Refactors are fine and welcome, but please keep any larger ones as separate commits to be clear which parts of the PR are intended to do something new and which are intended not to be a change.
Pvlerick: > I think using raw blobs works better as an interoperability test; that way we know if the (de)serialization code changes in a way that breaks compatibility.
> 
> (That said, using raw files in `testdata` would be cleaner than long string literals like that.)

Now I'm on the fence; it's true that literals have the advantage of indirectly testing the spec, which is always a plus. I'll commit what I have now so you can have a look then I'll make the changes, that's not a big deal.
Pvlerick: I'm fairly happy with what's in there now; test data generation is much clearer (although it uses the spec library, I'll change that to literals as soon as time permits) and allows more flexibility as demonstrated with the test with a layer shared by two images.

(Looks like I'll need to rebase on main too)
Pvlerick: All right, all test are passing now :-)
Pvlerick: A couple of question on top of the review:

- Would it make sense to add some logging (debug or trace) such as "deleting blob xxx" ?
- Would it make sense the parallelize deletion of the blobs?

Thanks
mtrmac: I’m afraid I didn’t get to review the full PR yet

> * Would it make sense to add some logging (debug or trace) such as "deleting blob xxx" ?

Sure; that seems useful, and can’t hurt anything.

> * Would it make sense the parallelize deletion of the blobs?

You know why you are doing this work and whether it is valuable for you. My intuition is that most archives are pretty small, and that the extra complexity and risk are not at all worth it, but that’s a guess with no data.
Pvlerick: @mtrmac I made quite a few changes:

- used fixtures for the tests, it's a bit more static and less readable (you have to go read the fixture to find out what's the expected result), but it tests the spec package indirectly as we discussed previously;
- I addressed both of your comments: used a set and checked for configuration shared use (and added a test case for it in the shared blobs test

I started working on the nested index stuff in another branch, but it's not over yet (although the fixture is there already); there is also the extra complexity that if something changes in the nested index, its sha256 will change too, so all the "root" index will have to be changed too.
Pvlerick: I think it's in a good shape now; there are probably refactorings that could be made here and there but I'd prefer you to review and validate that this is going in the right direction.
Pvlerick: @mtrmac just pinging in case you haven't seen the request for review (if you're too busy right now it can wait)
mtrmac: @Pvlerick I’m sorry — I do receive your updates and it’s something I want to see through soon, but there are some other priorities for this week. I’ll hopefully return to this next week.
Pvlerick: @mtrmac thanks for the extensive review; I hope the code was clear enough in its intent. 

Reading the various comments, what I gather is that you think image deletion should be supported only when there is only a top level index and not when there is a nested index? Or just one level on nested index but no more? Definitely not the theoretical infinite nested indexes.

It may take a little while for me to find time to properly work on this in the coming month, this being the holiday season, but I'm on it.
mtrmac: > Or just one level on nested index but no more?

The transport can read and write two levels nested indices (the top-level `index.json` may refer to one level of other indices). The _name references_ are found only in the top-level `index.json`, and can refer either to a single manifest or to an index (which contains manifests, not indices).
Pvlerick: @mtrmac I finally had time to take a second shot a this.

Going back and forth with tests, I ended up with a different design.
In essence, blobs used in a descriptor in the index.json (descriptor being an image manifest or a nested index) are collected in a map which holds the usage count for each of these blobs (`getBlobsUsedInManifest` or `getBlobsUsedInIndex`). It is then compared to a similar map generated from index.json itself, and blobs that usage count reach 0 in the former map are maked for deletion (`getBlobsToDelete`). Blobs are then deleted and the relevant entry in the index is then deleted too.
Pvlerick: > * AFAICS the updated test for `getManifestDescriptor` is incorrect in a rather misleading way.

Again, sorry about that :-(
mtrmac: > > * AFAICS the updated test for `getManifestDescriptor` is incorrect in a rather misleading way.
> 
> Again, sorry about that :-(

No worries — I have written a much worse bug yesterday. I just wanted to highlight that as more important than some of the stylistic comments.

Patch:
diff --git a/go.mod b/go.mod
index 14661cb9..03054a9f 100644
--- a/go.mod
+++ b/go.mod
@@ -25,6 +25,7 @@ require (
 	github.com/opencontainers/image-spec v1.1.0-rc5
 	github.com/opencontainers/selinux v1.11.0
 	github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f
+	github.com/otiai10/copy v1.12.0
 	github.com/proglottis/gpgme v0.1.3
 	github.com/secure-systems-lab/go-securesystemslib v0.7.0
 	github.com/sigstore/fulcio v1.4.2
diff --git a/go.sum b/go.sum
index 822a1adf..6b2091fe 100644
--- a/go.sum
+++ b/go.sum
@@ -281,6 +281,9 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+
 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
 github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f h1:/UDgs8FGMqwnHagNDPGOlts35QkhAZ8by3DR7nMih7M=
 github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc=
+github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY=
+github.com/otiai10/copy v1.12.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww=
+github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
 github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/02ea786cb1ff44d997661886a4186cbd8a1dc466938712bf7281379209476022 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/02ea786cb1ff44d997661886a4186cbd8a1dc466938712bf7281379209476022
new file mode 100644
index 00000000..add17979
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/02ea786cb1ff44d997661886a4186cbd8a1dc466938712bf7281379209476022
@@ -0,0 +1,1 @@
+insert binary content here #26559
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/0dc27f36a618c110ae851662c13283e9fbc1b5a5de003befc4bcefa5a05d2eef b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/0dc27f36a618c110ae851662c13283e9fbc1b5a5de003befc4bcefa5a05d2eef
new file mode 100644
index 00000000..b3a7a96c
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/0dc27f36a618c110ae851662c13283e9fbc1b5a5de003befc4bcefa5a05d2eef
@@ -0,0 +1,16 @@
+{
+    "schemaVersion": 2,
+    "mediaType": "application/vnd.oci.image.manifest.v1+json",
+    "config": {
+        "mediaType": "application/vnd.oci.image.config.v1+json",
+        "digest": "sha256:913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8",
+        "size": 585
+    },
+    "layers": [
+        {
+            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+            "digest": "sha256:a6f737ac2b84bc463f2ff721af39588c69646c82f79f3808236178e02e35b922",
+            "size": 33
+        }
+    ]
+}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/39c524417bb4228f9fcb0aef43a680b5fd6b9f3a1df2fd50509d047e47dad8be b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/39c524417bb4228f9fcb0aef43a680b5fd6b9f3a1df2fd50509d047e47dad8be
new file mode 100644
index 00000000..8b7c8e36
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/39c524417bb4228f9fcb0aef43a680b5fd6b9f3a1df2fd50509d047e47dad8be
@@ -0,0 +1,16 @@
+{
+    "schemaVersion": 2,
+    "mediaType": "application/vnd.oci.image.manifest.v1+json",
+    "config": {
+        "mediaType": "application/vnd.oci.image.config.v1+json",
+        "digest": "sha256:f732172ad8d2a666550fa3ec37a5153d59acc95744562ae64cf62ded46de101a",
+        "size": 583
+    },
+    "layers": [
+        {
+            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+            "digest": "sha256:02ea786cb1ff44d997661886a4186cbd8a1dc466938712bf7281379209476022",
+            "size": 34
+        }
+    ]
+}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861
new file mode 100644
index 00000000..19c1c127
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861
@@ -0,0 +1,1 @@
+insert binary content here #9811
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/5b2aba4d3c27bc6493633d0ec446b25c8d0a5c9cfe99894bcdff0aee80813805 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/5b2aba4d3c27bc6493633d0ec446b25c8d0a5c9cfe99894bcdff0aee80813805
new file mode 100644
index 00000000..aba2333e
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/5b2aba4d3c27bc6493633d0ec446b25c8d0a5c9cfe99894bcdff0aee80813805
@@ -0,0 +1,16 @@
+{
+    "schemaVersion": 2,
+    "mediaType": "application/vnd.oci.image.manifest.v1+json",
+    "config": {
+        "mediaType": "application/vnd.oci.image.config.v1+json",
+        "digest": "sha256:df11bc189adeb50dadb3291a3a7f2c34b36e0efdba0df70f2c8a2d761b215cde",
+        "size": 585
+    },
+    "layers": [
+        {
+            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+            "digest": "sha256:986315a0e599fac2b80eb31db2124dab8d3de04d7ca98b254999bd913c1f73fe",
+            "size": 33
+        }
+    ]
+}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/7ffdfe7d276286b39a203dcc247949cf47c91d2d5e10a53a675c0962ed9e4402 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/7ffdfe7d276286b39a203dcc247949cf47c91d2d5e10a53a675c0962ed9e4402
new file mode 100644
index 00000000..f21c2746
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/7ffdfe7d276286b39a203dcc247949cf47c91d2d5e10a53a675c0962ed9e4402
@@ -0,0 +1,1 @@
+{"created":"2023-08-07T19:38:34.915445772Z","architecture":"386","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"]},"rootfs":{"type":"layers","diff_ids":["sha256:53bfdd548f8566a059cd188348b202a50fb9d39ce80eb5b8f0c670dfa9bc6569"]},"history":[{"created":"2023-08-07T19:38:34.803529816Z","created_by":"/bin/sh -c #(nop) ADD file:c06b4f6991638e506d4d0a4d70c4a78ba30b971767802af4c6b837cdf59d4303 in / "},{"created":"2023-08-07T19:38:34.915445772Z","created_by":"/bin/sh -c #(nop)  CMD [\"/bin/sh\"]","empty_layer":true}]}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/861d3c014b0e3edcf80e6221247d6b2921a4f892feb9bafe9515b9975b78c44f b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/861d3c014b0e3edcf80e6221247d6b2921a4f892feb9bafe9515b9975b78c44f
new file mode 100644
index 00000000..85617bd7
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/861d3c014b0e3edcf80e6221247d6b2921a4f892feb9bafe9515b9975b78c44f
@@ -0,0 +1,24 @@
+{
+    "schemaVersion": 2,
+    "mediaType": "application/vnd.oci.image.index.v1+json",
+    "manifests": [
+        {
+            "mediaType": "application/vnd.oci.image.manifest.v1+json",
+            "digest": "sha256:39c524417bb4228f9fcb0aef43a680b5fd6b9f3a1df2fd50509d047e47dad8be",
+            "size": 525,
+            "platform": {
+                "architecture": "amd64",
+                "os": "linux"
+            }
+        },
+        {
+            "mediaType": "application/vnd.oci.image.manifest.v1+json",
+            "digest": "sha256:be6036f9b6a4e120a04868c47f1b8674f58b2fe5e410cba9f585a13ca8946cf0",
+            "size": 525,
+            "platform": {
+                "architecture": "386",
+                "os": "linux"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8
new file mode 100644
index 00000000..ebe323d4
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8
@@ -0,0 +1,1 @@
+{"created":"2023-08-07T19:20:20.894140623Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"]},"rootfs":{"type":"layers","diff_ids":["sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230"]},"history":[{"created":"2023-08-07T19:20:20.71894984Z","created_by":"/bin/sh -c #(nop) ADD file:32ff5e7a78b890996ee4681cc0a26185d3e9acdb4eb1e2aaccb2411f922fed6b in / "},{"created":"2023-08-07T19:20:20.894140623Z","created_by":"/bin/sh -c #(nop)  CMD [\"/bin/sh\"]","empty_layer":true}]}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5
new file mode 100644
index 00000000..ccf025c9
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5
@@ -0,0 +1,16 @@
+{
+    "schemaVersion": 2,
+    "mediaType": "application/vnd.oci.image.manifest.v1+json",
+    "config": {
+        "mediaType": "application/vnd.oci.image.config.v1+json",
+        "digest": "sha256:913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8",
+        "size": 584
+    },
+    "layers": [
+        {
+            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+            "digest": "sha256:557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861",
+            "size": 33
+        }
+    ]
+}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/986315a0e599fac2b80eb31db2124dab8d3de04d7ca98b254999bd913c1f73fe b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/986315a0e599fac2b80eb31db2124dab8d3de04d7ca98b254999bd913c1f73fe
new file mode 100644
index 00000000..a0cd5aab
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/986315a0e599fac2b80eb31db2124dab8d3de04d7ca98b254999bd913c1f73fe
@@ -0,0 +1,1 @@
+insert binary content here #7959
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1
new file mode 100644
index 00000000..aeecdfac
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1
@@ -0,0 +1,24 @@
+{
+    "schemaVersion": 2,
+    "mediaType": "application/vnd.oci.image.index.v1+json",
+    "manifests": [
+        {
+            "mediaType": "application/vnd.oci.image.manifest.v1+json",
+            "digest": "sha256:93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5",
+            "size": 525,
+            "platform": {
+                "architecture": "amd64",
+                "os": "linux"
+            }
+        },
+        {
+            "mediaType": "application/vnd.oci.image.manifest.v1+json",
+            "digest": "sha256:f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3",
+            "size": 525,
+            "platform": {
+                "architecture": "386",
+                "os": "linux"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/a6f737ac2b84bc463f2ff721af39588c69646c82f79f3808236178e02e35b922 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/a6f737ac2b84bc463f2ff721af39588c69646c82f79f3808236178e02e35b922
new file mode 100644
index 00000000..f26e504a
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/a6f737ac2b84bc463f2ff721af39588c69646c82f79f3808236178e02e35b922
@@ -0,0 +1,1 @@
+insert binary content here #1234
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f
new file mode 100644
index 00000000..e1d45d35
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f
@@ -0,0 +1,1 @@
+{"created":"2023-08-07T19:38:27.007952531Z","architecture":"386","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"]},"rootfs":{"type":"layers","diff_ids":["sha256:f05b0759429ba12d5fda46c196f253cc1cab8f56cd874e9e7be674fc1b8337de"]},"history":[{"created":"2023-08-07T19:38:26.69689892Z","created_by":"/bin/sh -c #(nop) ADD file:4b33c52e11b19fde30197c62ead0b77bde28d34edaa08346a5302cd892d3cebe in / "},{"created":"2023-08-07T19:38:27.007952531Z","created_by":"/bin/sh -c #(nop)  CMD [\"/bin/sh\"]","empty_layer":true}]}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/be6036f9b6a4e120a04868c47f1b8674f58b2fe5e410cba9f585a13ca8946cf0 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/be6036f9b6a4e120a04868c47f1b8674f58b2fe5e410cba9f585a13ca8946cf0
new file mode 100644
index 00000000..c2d027aa
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/be6036f9b6a4e120a04868c47f1b8674f58b2fe5e410cba9f585a13ca8946cf0
@@ -0,0 +1,16 @@
+{
+    "schemaVersion": 2,
+    "mediaType": "application/vnd.oci.image.manifest.v1+json",
+    "config": {
+        "mediaType": "application/vnd.oci.image.config.v1+json",
+        "digest": "sha256:7ffdfe7d276286b39a203dcc247949cf47c91d2d5e10a53a675c0962ed9e4402",
+        "size": 583
+    },
+    "layers": [
+        {
+            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+            "digest": "sha256:e2f7e0374fd6a03d9c373f4d9a0c7802045cc3ddcc1433e89d83b81fa7007242",
+            "size": 33
+        }
+    ]
+}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/df11bc189adeb50dadb3291a3a7f2c34b36e0efdba0df70f2c8a2d761b215cde b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/df11bc189adeb50dadb3291a3a7f2c34b36e0efdba0df70f2c8a2d761b215cde
new file mode 100644
index 00000000..1ff4ad54
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/df11bc189adeb50dadb3291a3a7f2c34b36e0efdba0df70f2c8a2d761b215cde
@@ -0,0 +1,1 @@
+{"created":"2023-08-07T19:20:26.426857961Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"]},"rootfs":{"type":"layers","diff_ids":["sha256:36b50b131297b8860da51b2d2b24bb4c08dfbdf2789b08e3cc0f187c98637a19"]},"history":[{"created":"2023-08-07T19:20:26.326707843Z","created_by":"/bin/sh -c #(nop) ADD file:6dd87346b8be240b21b4f4d9296253bf0d28b6579aa52d2118872e3936963b6b in / "},{"created":"2023-08-07T19:20:26.426857961Z","created_by":"/bin/sh -c #(nop)  CMD [\"/bin/sh\"]","empty_layer":true}]}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a
new file mode 100644
index 00000000..832c1185
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a
@@ -0,0 +1,1 @@
+insert binary content here #28017
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/e2f7e0374fd6a03d9c373f4d9a0c7802045cc3ddcc1433e89d83b81fa7007242 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/e2f7e0374fd6a03d9c373f4d9a0c7802045cc3ddcc1433e89d83b81fa7007242
new file mode 100644
index 00000000..a18eab89
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/e2f7e0374fd6a03d9c373f4d9a0c7802045cc3ddcc1433e89d83b81fa7007242
@@ -0,0 +1,1 @@
+insert binary content here #4794
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3 b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3
new file mode 100644
index 00000000..fb85ad20
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3
@@ -0,0 +1,16 @@
+{
+    "schemaVersion": 2,
+    "mediaType": "application/vnd.oci.image.manifest.v1+json",
+    "config": {
+        "mediaType": "application/vnd.oci.image.config.v1+json",
+        "digest": "sha256:aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f",
+        "size": 582
+    },
+    "layers": [
+        {
+            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+            "digest": "sha256:e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a",
+            "size": 34
+        }
+    ]
+}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/f732172ad8d2a666550fa3ec37a5153d59acc95744562ae64cf62ded46de101a b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/f732172ad8d2a666550fa3ec37a5153d59acc95744562ae64cf62ded46de101a
new file mode 100644
index 00000000..016b01bc
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/blobs/sha256/f732172ad8d2a666550fa3ec37a5153d59acc95744562ae64cf62ded46de101a
@@ -0,0 +1,1 @@
+{"created":"2023-08-07T19:20:31.99661329Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"]},"rootfs":{"type":"layers","diff_ids":["sha256:0e182002b05f2ab123995821ef14f1cda765a0c31f7a6d260221558f6466535e"]},"history":[{"created":"2023-08-07T19:20:31.893185238Z","created_by":"/bin/sh -c #(nop) ADD file:76d829bbce3dd420a8419919b0916c0fda917011d1e6752ca5b9e53d5ca890a6 in / "},{"created":"2023-08-07T19:20:31.99661329Z","created_by":"/bin/sh -c #(nop)  CMD [\"/bin/sh\"]","empty_layer":true}]}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/index.json b/oci/layout/fixtures/delete_image_multiple_images/index.json
new file mode 100644
index 00000000..d781143f
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/index.json
@@ -0,0 +1,61 @@
+{
+    "schemaVersion": 2,
+    "manifests": [
+        {
+            "mediaType": "application/vnd.oci.image.index.v1+json",
+            "digest": "sha256:a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1",
+            "size": 759,
+            "annotations": {
+                "org.opencontainers.image.ref.name": "latest"
+            }
+        },
+        {
+            "mediaType": "application/vnd.oci.image.index.v1+json",
+            "digest": "sha256:a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1",
+            "size": 759,
+            "annotations": {
+                "org.opencontainers.image.ref.name": "3.18.3"
+            }
+        },
+        {
+            "mediaType": "application/vnd.oci.image.manifest.v1+json",
+            "digest": "sha256:93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5",
+            "size": 525,
+            "annotations": {
+                "org.opencontainers.image.ref.name": "3"
+            }
+        },
+        {
+            "mediaType": "application/vnd.oci.image.manifest.v1+json",
+            "digest": "sha256:93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5",
+            "size": 525,
+            "annotations": {
+                "org.opencontainers.image.ref.name": "3.18"
+            }
+        },
+        {
+            "mediaType": "application/vnd.oci.image.manifest.v1+json",
+            "digest": "sha256:5b2aba4d3c27bc6493633d0ec446b25c8d0a5c9cfe99894bcdff0aee80813805",
+            "size": 525,
+            "annotations": {
+                "org.opencontainers.image.ref.name": "3.17.5"
+            }
+        },
+        {
+            "mediaType": "application/vnd.oci.image.index.v1+json",
+            "digest": "sha256:861d3c014b0e3edcf80e6221247d6b2921a4f892feb9bafe9515b9975b78c44f",
+            "size": 759,
+            "annotations": {
+                "org.opencontainers.image.ref.name": "3.16.7"
+            }
+        },
+        {
+            "mediaType": "application/vnd.oci.image.manifest.v1+json",
+            "digest": "sha256:0dc27f36a618c110ae851662c13283e9fbc1b5a5de003befc4bcefa5a05d2eef",
+            "size": 525,
+            "annotations": {
+                "org.opencontainers.image.ref.name": "1.0.0"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_multiple_images/info.txt b/oci/layout/fixtures/delete_image_multiple_images/info.txt
new file mode 100644
index 00000000..4ec2b148
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/info.txt
@@ -0,0 +1,61 @@
+This is tree representation of the fixture to help writing the tests:
+
+7 references in the index, 10 descriptors and 19 blobs in the blob directory
+
+index.json
+│
+├── 3.17.5
+│   └── manifest: 5b2aba4d3c27bc6493633d0ec446b25c8d0a5c9cfe99894bcdff0aee80813805
+│       config:   df11bc189adeb50dadb3291a3a7f2c34b36e0efdba0df70f2c8a2d761b215cde
+│       layers:   986315a0e599fac2b80eb31db2124dab8d3de04d7ca98b254999bd913c1f73fe
+│
+├── 3.18
+│   └── manifest: 93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5
+│       config:   913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8
+│       layers:   557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861
+│
+├── 3
+│   └── manifest: 93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5
+│       config:   913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8
+│       layers:   557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861
+│
+├── 1.0.0
+│   └── manifest: 0dc27f36a618c110ae851662c13283e9fbc1b5a5de003befc4bcefa5a05d2eef
+│       config:   913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8
+│       layers:   a6f737ac2b84bc463f2ff721af39588c69646c82f79f3808236178e02e35b922
+│
+├── latest
+│   └── index:    a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1
+│       ├── linux/am64
+│       │   └── manifest: 93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5
+│       │       config:   913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8
+│       │       layers:   557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861
+│       │
+│       └── linux/386
+│           └── manifest: f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3
+│               config:   aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f
+│               layers:   e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a
+│
+├── 3.18.3
+│   └── index:    a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1
+│       ├── linux/am64
+│       │   └── manifest: 93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5
+│       │       config:   913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8
+│       │       layers:   557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861
+│       │
+│       └── linux/386
+│           └── manifest: f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3
+│               config:   aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f
+│               layers:   e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a
+│
+├── 3.16.7
+│   └── index:    861d3c014b0e3edcf80e6221247d6b2921a4f892feb9bafe9515b9975b78c44f
+│       ├── linux/am64
+│       │   └── manifest: 39c524417bb4228f9fcb0aef43a680b5fd6b9f3a1df2fd50509d047e47dad8be
+│       │       config:   f732172ad8d2a666550fa3ec37a5153d59acc95744562ae64cf62ded46de101a
+│       │       layers:   02ea786cb1ff44d997661886a4186cbd8a1dc466938712bf7281379209476022
+│       │
+│       └── linux/386
+│           └── manifest: be6036f9b6a4e120a04868c47f1b8674f58b2fe5e410cba9f585a13ca8946cf0
+│               config:   7ffdfe7d276286b39a203dcc247949cf47c91d2d5e10a53a675c0962ed9e4402
+│               layers:   e2f7e0374fd6a03d9c373f4d9a0c7802045cc3ddcc1433e89d83b81fa7007242
diff --git a/oci/layout/fixtures/delete_image_multiple_images/oci-layout b/oci/layout/fixtures/delete_image_multiple_images/oci-layout
new file mode 100644
index 00000000..21b1439d
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_multiple_images/oci-layout
@@ -0,0 +1,1 @@
+{"imageLayoutVersion": "1.0.0"}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_only_one_image/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 b/oci/layout/fixtures/delete_image_only_one_image/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423
new file mode 100644
index 00000000..f0f06201
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_only_one_image/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423
@@ -0,0 +1,30 @@
+{
+  "created": "2019-08-20T20:19:55.211423266Z",
+  "architecture": "amd64",
+  "os": "linux",
+  "config": {
+    "Env": [
+      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+    ],
+    "Cmd": [
+      "/bin/sh"
+    ]
+  },
+  "rootfs": {
+    "type": "layers",
+    "diff_ids": [
+      "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0"
+    ]
+  },
+  "history": [
+    {
+      "created": "2019-08-20T20:19:55.062606894Z",
+      "created_by": "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / "
+    },
+    {
+      "created": "2019-08-20T20:19:55.211423266Z",
+      "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
+      "empty_layer": true
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_only_one_image/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 b/oci/layout/fixtures/delete_image_only_one_image/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18
new file mode 100644
index 00000000..1ff195d0
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_only_one_image/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18
@@ -0,0 +1,16 @@
+{
+  "schemaVersion": 2,
+  "mediaType": "application/vnd.oci.image.manifest.v1+json",
+  "config": {
+    "mediaType": "application/vnd.oci.image.config.v1+json",
+    "digest": "sha256:a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423",
+    "size": 585
+  },
+  "layers": [
+    {
+      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+      "digest": "sha256:0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc",
+      "size": 33
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_only_one_image/index.json b/oci/layout/fixtures/delete_image_only_one_image/index.json
new file mode 100644
index 00000000..b0a0c984
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_only_one_image/index.json
@@ -0,0 +1,13 @@
+{
+  "schemaVersion": 2,
+  "manifests": [
+    {
+      "mediaType": "application/vnd.oci.image.manifest.v1+json",
+      "digest": "sha256:eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18",
+      "size": 476,
+      "annotations": {
+        "org.opencontainers.image.ref.name": "latest"
+      }
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_only_one_image/oci-layout b/oci/layout/fixtures/delete_image_only_one_image/oci-layout
new file mode 100644
index 00000000..21b1439d
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_only_one_image/oci-layout
@@ -0,0 +1,1 @@
+{"imageLayoutVersion": "1.0.0"}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_shared_blobs_dir/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 b/oci/layout/fixtures/delete_image_shared_blobs_dir/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423
new file mode 100644
index 00000000..f0f06201
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_shared_blobs_dir/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423
@@ -0,0 +1,30 @@
+{
+  "created": "2019-08-20T20:19:55.211423266Z",
+  "architecture": "amd64",
+  "os": "linux",
+  "config": {
+    "Env": [
+      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+    ],
+    "Cmd": [
+      "/bin/sh"
+    ]
+  },
+  "rootfs": {
+    "type": "layers",
+    "diff_ids": [
+      "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0"
+    ]
+  },
+  "history": [
+    {
+      "created": "2019-08-20T20:19:55.062606894Z",
+      "created_by": "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / "
+    },
+    {
+      "created": "2019-08-20T20:19:55.211423266Z",
+      "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
+      "empty_layer": true
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_shared_blobs_dir/index.json b/oci/layout/fixtures/delete_image_shared_blobs_dir/index.json
new file mode 100644
index 00000000..49925c19
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_shared_blobs_dir/index.json
@@ -0,0 +1,13 @@
+{
+  "schemaVersion": 2,
+  "manifests": [
+    {
+      "mediaType": "application/vnd.oci.image.manifest.v1+json",
+      "digest": "sha256:eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18",
+      "size": 405,
+      "annotations": {
+        "org.opencontainers.image.ref.name": "latest"
+      }
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_shared_blobs_dir/oci-layout b/oci/layout/fixtures/delete_image_shared_blobs_dir/oci-layout
new file mode 100644
index 00000000..21b1439d
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_shared_blobs_dir/oci-layout
@@ -0,0 +1,1 @@
+{"imageLayoutVersion": "1.0.0"}
\ No newline at end of file
diff --git a/oci/layout/fixtures/delete_image_shared_blobs_dir/shared_blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 b/oci/layout/fixtures/delete_image_shared_blobs_dir/shared_blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18
new file mode 100644
index 00000000..1ff195d0
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_shared_blobs_dir/shared_blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18
@@ -0,0 +1,16 @@
+{
+  "schemaVersion": 2,
+  "mediaType": "application/vnd.oci.image.manifest.v1+json",
+  "config": {
+    "mediaType": "application/vnd.oci.image.config.v1+json",
+    "digest": "sha256:a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423",
+    "size": 585
+  },
+  "layers": [
+    {
+      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+      "digest": "sha256:0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc",
+      "size": 33
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/49d1584496c6e196f512c4a9f52b17b187642269d84c044538523c5b69a660b3 b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/49d1584496c6e196f512c4a9f52b17b187642269d84c044538523c5b69a660b3
new file mode 100644
index 00000000..e59a5f80
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/49d1584496c6e196f512c4a9f52b17b187642269d84c044538523c5b69a660b3
@@ -0,0 +1,16 @@
+{
+  "schemaVersion": 2,
+  "mediaType": "application/vnd.oci.image.manifest.v1+json",
+  "config": {
+    "mediaType": "application/vnd.oci.image.config.v1+json",
+    "digest": "sha256:a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423",
+    "size": 740
+  },
+  "layers": [
+    {
+      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+      "digest": "sha256:0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc",
+      "size": 33
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423
new file mode 100644
index 00000000..f0f06201
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423
@@ -0,0 +1,30 @@
+{
+  "created": "2019-08-20T20:19:55.211423266Z",
+  "architecture": "amd64",
+  "os": "linux",
+  "config": {
+    "Env": [
+      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+    ],
+    "Cmd": [
+      "/bin/sh"
+    ]
+  },
+  "rootfs": {
+    "type": "layers",
+    "diff_ids": [
+      "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0"
+    ]
+  },
+  "history": [
+    {
+      "created": "2019-08-20T20:19:55.062606894Z",
+      "created_by": "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / "
+    },
+    {
+      "created": "2019-08-20T20:19:55.211423266Z",
+      "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
+      "empty_layer": true
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/ce229a4eb5797ecd3a3a1846613b6b49811f79e38b5b0ce666268ba4b6c68e41 b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/ce229a4eb5797ecd3a3a1846613b6b49811f79e38b5b0ce666268ba4b6c68e41
new file mode 100644
index 00000000..9578f7d9
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/ce229a4eb5797ecd3a3a1846613b6b49811f79e38b5b0ce666268ba4b6c68e41
@@ -0,0 +1,30 @@
+{
+  "created": "2019-08-20T20:20:55.211423266Z",
+  "architecture": "amd64",
+  "os": "linux",
+  "config": {
+    "Env": [
+      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+    ],
+    "Cmd": [
+      "/bin/sh"
+    ]
+  },
+  "rootfs": {
+    "type": "layers",
+    "diff_ids": [
+      "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0"
+    ]
+  },
+  "history": [
+    {
+      "created": "2019-08-20T20:19:55.062606894Z",
+      "created_by": "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / "
+    },
+    {
+      "created": "2019-08-20T20:19:55.211423266Z",
+      "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
+      "empty_layer": true
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/ecfa463536cb5472e238aadc4df81d4785d5d6373027c488a2db8a6e76fe88ed b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/ecfa463536cb5472e238aadc4df81d4785d5d6373027c488a2db8a6e76fe88ed
new file mode 100644
index 00000000..16b7c27b
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/ecfa463536cb5472e238aadc4df81d4785d5d6373027c488a2db8a6e76fe88ed
@@ -0,0 +1,16 @@
+{
+  "schemaVersion": 2,
+  "mediaType": "application/vnd.oci.image.manifest.v1+json",
+  "config": {
+    "mediaType": "application/vnd.oci.image.config.v1+json",
+    "digest": "sha256:ce229a4eb5797ecd3a3a1846613b6b49811f79e38b5b0ce666268ba4b6c68e41",
+    "size": 740
+  },
+  "layers": [
+    {
+      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+      "digest": "sha256:fa00bb4e2adbc73a5da1fd54d2a840020592530a8d4e8de9888b9e9a533419d8",
+      "size": 33
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/fa00bb4e2adbc73a5da1fd54d2a840020592530a8d4e8de9888b9e9a533419d8 b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/fa00bb4e2adbc73a5da1fd54d2a840020592530a8d4e8de9888b9e9a533419d8
new file mode 100644
index 00000000..3badd42d
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/fa00bb4e2adbc73a5da1fd54d2a840020592530a8d4e8de9888b9e9a533419d8
@@ -0,0 +1,1 @@
+insert binary content here 32515
diff --git a/oci/layout/fixtures/delete_image_two_identical_references/index.json b/oci/layout/fixtures/delete_image_two_identical_references/index.json
new file mode 100644
index 00000000..80850d4b
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_two_identical_references/index.json
@@ -0,0 +1,21 @@
+{
+  "schemaVersion": 2,
+  "manifests": [
+    {
+      "mediaType": "application/vnd.oci.image.manifest.v1+json",
+      "digest": "sha256:49d1584496c6e196f512c4a9f52b17b187642269d84c044538523c5b69a660b3",
+      "size": 476,
+      "annotations": {
+        "org.opencontainers.image.ref.name": "1.0.0"
+      }
+    },
+    {
+      "mediaType": "application/vnd.oci.image.manifest.v1+json",
+      "digest": "sha256:ecfa463536cb5472e238aadc4df81d4785d5d6373027c488a2db8a6e76fe88ed",
+      "size": 476,
+      "annotations": {
+        "org.opencontainers.image.ref.name": "1.0.0"
+      }
+    }
+  ]
+}
diff --git a/oci/layout/fixtures/delete_image_two_identical_references/oci-layout b/oci/layout/fixtures/delete_image_two_identical_references/oci-layout
new file mode 100644
index 00000000..21b1439d
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_two_identical_references/oci-layout
@@ -0,0 +1,1 @@
+{"imageLayoutVersion": "1.0.0"}
\ No newline at end of file
diff --git a/oci/layout/fixtures/manifest/index.json b/oci/layout/fixtures/manifest/index.json
index fd6930cf..7e779082 100644
--- a/oci/layout/fixtures/manifest/index.json
+++ b/oci/layout/fixtures/manifest/index.json
@@ -1,1 +1,17 @@
-{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:84afb6189c4d69f2d040c5f1dc4e0a16fed9b539ce9cfb4ac2526ae4e0576cc0","size":496,"annotations":{"org.opencontainers.image.ref.name":"v0.1.1"},"platform":{"architecture":"amd64","os":"linux"}}]}
\ No newline at end of file
+{
+    "schemaVersion": 2,
+    "manifests": [
+        {
+            "mediaType": "application/vnd.oci.image.manifest.v1+json",
+            "digest": "sha256:84afb6189c4d69f2d040c5f1dc4e0a16fed9b539ce9cfb4ac2526ae4e0576cc0",
+            "size": 496,
+            "annotations": {
+                "org.opencontainers.image.ref.name": "v0.1.1"
+            },
+            "platform": {
+                "architecture": "amd64",
+                "os": "linux"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/oci/layout/oci_delete.go b/oci/layout/oci_delete.go
new file mode 100644
index 00000000..8dd54f25
--- /dev/null
+++ b/oci/layout/oci_delete.go
@@ -0,0 +1,240 @@
+package layout
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/fs"
+	"os"
+
+	"github.com/containers/image/v5/internal/set"
+	"github.com/containers/image/v5/types"
+	digest "github.com/opencontainers/go-digest"
+	imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
+	"github.com/sirupsen/logrus"
+	"golang.org/x/exp/slices"
+)
+
+// DeleteImage deletes the named image from the directory, if supported.
+func (ref ociReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error {
+	sharedBlobsDir := ""
+	if sys != nil && sys.OCISharedBlobDirPath != "" {
+		sharedBlobsDir = sys.OCISharedBlobDirPath
+	}
+
+	descriptor, descriptorIndex, err := ref.getManifestDescriptor()
+	if err != nil {
+		return err
+	}
+
+	var blobsUsedByImage map[digest.Digest]int
+
+	switch descriptor.MediaType {
+	case imgspecv1.MediaTypeImageManifest:
+		blobsUsedByImage, err = ref.getBlobsUsedInSingleImage(&descriptor, sharedBlobsDir)
+	case imgspecv1.MediaTypeImageIndex:
+		blobsUsedByImage, err = ref.getBlobsUsedInImageIndex(&descriptor, sharedBlobsDir)
+	default:
+		return fmt.Errorf("unsupported mediaType in index: %q", descriptor.MediaType)
+	}
+	if err != nil {
+		return err
+	}
+
+	blobsToDelete, err := ref.getBlobsToDelete(blobsUsedByImage, sharedBlobsDir)
+	if err != nil {
+		return err
+	}
+
+	err = ref.deleteBlobs(blobsToDelete)
+	if err != nil {
+		return err
+	}
+
+	return ref.deleteReferenceFromIndex(descriptorIndex)
+}
+
+func (ref ociReference) getBlobsUsedInSingleImage(descriptor *imgspecv1.Descriptor, sharedBlobsDir string) (map[digest.Digest]int, error) {
+	manifest, err := ref.getManifest(descriptor, sharedBlobsDir)
+	if err != nil {
+		return nil, err
+	}
+	blobsUsedInManifest := ref.getBlobsUsedInManifest(manifest)
+	blobsUsedInManifest[descriptor.Digest]++ // Add the current manifest to the list of blobs used by this reference
+
+	return blobsUsedInManifest, nil
+}
+
+func (ref ociReference) getBlobsUsedInImageIndex(descriptor *imgspecv1.Descriptor, sharedBlobsDir string) (map[digest.Digest]int, error) {
+	blobPath, err := ref.blobPath(descriptor.Digest, sharedBlobsDir)
+	if err != nil {
+		return nil, err
+	}
+	index, err := parseIndex(blobPath)
+	if err != nil {
+		return nil, err
+	}
+
+	blobsUsedInImageRefIndex := make(map[digest.Digest]int)
+	err = ref.addBlobsUsedInIndex(blobsUsedInImageRefIndex, index, sharedBlobsDir)
+	if err != nil {
+		return nil, err
+	}
+	blobsUsedInImageRefIndex[descriptor.Digest]++ // Add the nested index in the list of blobs used by this reference
+
+	return blobsUsedInImageRefIndex, nil
+}
+
+// Updates a map of digest with the usage count, so a blob that is referenced three times will have 3 in the map
+func (ref ociReference) addBlobsUsedInIndex(destination map[digest.Digest]int, index *imgspecv1.Index, sharedBlobsDir string) error {
+	for _, descriptor := range index.Manifests {
+		destination[descriptor.Digest]++
+		switch descriptor.MediaType {
+		case imgspecv1.MediaTypeImageManifest:
+			manifest, err := ref.getManifest(&descriptor, sharedBlobsDir)
+			if err != nil {
+				return err
+			}
+			for digest, count := range ref.getBlobsUsedInManifest(manifest) {
+				destination[digest] += count
+			}
+		case imgspecv1.MediaTypeImageIndex:
+			blobPath, err := ref.blobPath(descriptor.Digest, sharedBlobsDir)
+			if err != nil {
+				return err
+			}
+			index, err := parseIndex(blobPath)
+			if err != nil {
+				return err
+			}
+			err = ref.addBlobsUsedInIndex(destination, index, sharedBlobsDir)
+			if err != nil {
+				return err
+			}
+		default:
+			return fmt.Errorf("unsupported mediaType in index: %q", descriptor.MediaType)
+		}
+	}
+
+	return nil
+}
+
+func (ref ociReference) getBlobsUsedInManifest(manifest *imgspecv1.Manifest) map[digest.Digest]int {
+	blobsUsedInManifest := make(map[digest.Digest]int, 0)
+
+	blobsUsedInManifest[manifest.Config.Digest]++
+	for _, layer := range manifest.Layers {
+		blobsUsedInManifest[layer.Digest]++
+	}
+
+	return blobsUsedInManifest
+}
+
+// This takes in a map of the digest and their usage count in the manifest to be deleted
+// It will compare it to the digest usage in the root index, and return a set of the blobs that can be safely deleted
+func (ref ociReference) getBlobsToDelete(blobsUsedByDescriptorToDelete map[digest.Digest]int, sharedBlobsDir string) (*set.Set[digest.Digest], error) {
+	rootIndex, err := ref.getIndex()
+	if err != nil {
+		return nil, err
+	}
+	blobsUsedInRootIndex := make(map[digest.Digest]int)
+	err = ref.addBlobsUsedInIndex(blobsUsedInRootIndex, rootIndex, sharedBlobsDir)
+	if err != nil {
+		return nil, err
+	}
+
+	blobsToDelete := set.New[digest.Digest]()
+
+	for digest, count := range blobsUsedInRootIndex {
+		if count-blobsUsedByDescriptorToDelete[digest] == 0 {
+			blobsToDelete.Add(digest)
+		}
+	}
+
+	return blobsToDelete, nil
+}
+
+// This transport never generates layouts where blobs for an image are both in the local blobs directory
+// and the shared one; it’s either one or the other, depending on how OCISharedBlobDirPath is set.
+//
+// But we can’t correctly compute use counts for OCISharedBlobDirPath (because we don't know what
+// the other layouts sharing that directory are, and we might not even have permission to read them),
+// so we can’t really delete any blobs in that case.
+// Checking the _local_ blobs directory, and deleting blobs from there, doesn't really hurt,
+// in case the layout was created using some other tool or without OCISharedBlobDirPath set, so let's silently
+// check for local blobs (but we should make no noise if the blobs are actually in the shared directory).
+//
+// So, NOTE: the blobPath() call below hard-codes "" even in calls where OCISharedBlobDirPath is set
+func (ref ociReference) deleteBlobs(blobsToDelete *set.Set[digest.Digest]) error {
+	for _, digest := range blobsToDelete.Values() {
+		blobPath, err := ref.blobPath(digest, "") //Only delete in the local directory, see comment above
+		if err != nil {
+			return err
+		}
+		err = deleteBlob(blobPath)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func deleteBlob(blobPath string) error {
+	logrus.Debug(fmt.Sprintf("Deleting blob at %q", blobPath))
+
+	err := os.Remove(blobPath)
+	if err != nil && !os.IsNotExist(err) {
+		return err
+	} else {
+		return nil
+	}
+}
+
+func (ref ociReference) deleteReferenceFromIndex(referenceIndex int) error {
+	index, err := ref.getIndex()
+	if err != nil {
+		return err
+	}
+
+	index.Manifests = slices.Delete(index.Manifests, referenceIndex, referenceIndex+1)
+
+	return saveJSON(ref.indexPath(), index)
+}
+
+func saveJSON(path string, content any) error {
+	// If the file already exists, get its mode to preserve it
+	var mode fs.FileMode
+	existingfi, err := os.Stat(path)
+	if err != nil {
+		if !os.IsNotExist(err) {
+			return err
+		} else { // File does not exist, use default mode
+			mode = 0644
+		}
+	} else {
+		mode = existingfi.Mode()
+	}
+
+	file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+
+	return json.NewEncoder(file).Encode(content)
+}
+
+func (ref ociReference) getManifest(descriptor *imgspecv1.Descriptor, sharedBlobsDir string) (*imgspecv1.Manifest, error) {
+	manifestPath, err := ref.blobPath(descriptor.Digest, sharedBlobsDir)
+	if err != nil {
+		return nil, err
+	}
+
+	manifest, err := parseJSON[imgspecv1.Manifest](manifestPath)
+	if err != nil {
+		return nil, err
+	}
+
+	return manifest, nil
+}
diff --git a/oci/layout/oci_src.go b/oci/layout/oci_src.go
index 6b423f3b..f5f1debc 100644
--- a/oci/layout/oci_src.go
+++ b/oci/layout/oci_src.go
@@ -60,7 +60,7 @@ func newImageSource(sys *types.SystemContext, ref ociReference) (private.ImageSo
 
 	client := &http.Client{}
 	client.Transport = tr
-	descriptor, err := ref.getManifestDescriptor()
+	descriptor, _, err := ref.getManifestDescriptor()
 	if err != nil {
 		return nil, err
 	}
diff --git a/oci/layout/oci_transport.go b/oci/layout/oci_transport.go
index dcb83607..1e26dc52 100644
--- a/oci/layout/oci_transport.go
+++ b/oci/layout/oci_transport.go
@@ -160,48 +160,56 @@ func (ref ociReference) NewImage(ctx context.Context, sys *types.SystemContext)
 // getIndex returns a pointer to the index references by this ociReference. If an error occurs opening an index nil is returned together
 // with an error.
 func (ref ociReference) getIndex() (*imgspecv1.Index, error) {
-	indexJSON, err := os.Open(ref.indexPath())
+	return parseIndex(ref.indexPath())
+}
+
+func parseIndex(path string) (*imgspecv1.Index, error) {
+	return parseJSON[imgspecv1.Index](path)
+}
+
+func parseJSON[T any](path string) (*T, error) {
+	content, err := os.Open(path)
 	if err != nil {
 		return nil, err
 	}
-	defer indexJSON.Close()
+	defer content.Close()
 
-	index := &imgspecv1.Index{}
-	if err := json.NewDecoder(indexJSON).Decode(index); err != nil {
+	obj := new(T)
+	if err := json.NewDecoder(content).Decode(obj); err != nil {
 		return nil, err
 	}
-	return index, nil
+	return obj, nil
 }
 
-func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, error) {
+func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, int, error) {
 	index, err := ref.getIndex()
 	if err != nil {
-		return imgspecv1.Descriptor{}, err
+		return imgspecv1.Descriptor{}, -1, err
 	}
 
 	if ref.image == "" {
 		// return manifest if only one image is in the oci directory
 		if len(index.Manifests) != 1 {
 			// ask user to choose image when more than one image in the oci directory
-			return imgspecv1.Descriptor{}, ErrMoreThanOneImage
+			return imgspecv1.Descriptor{}, -1, ErrMoreThanOneImage
 		}
-		return index.Manifests[0], nil
+		return index.Manifests[0], 0, nil
 	} else {
 		// if image specified, look through all manifests for a match
 		var unsupportedMIMETypes []string
-		for _, md := range index.Manifests {
+		for i, md := range index.Manifests {
 			if refName, ok := md.Annotations[imgspecv1.AnnotationRefName]; ok && refName == ref.image {
 				if md.MediaType == imgspecv1.MediaTypeImageManifest || md.MediaType == imgspecv1.MediaTypeImageIndex {
-					return md, nil
+					return md, i, nil
 				}
 				unsupportedMIMETypes = append(unsupportedMIMETypes, md.MediaType)
 			}
 		}
 		if len(unsupportedMIMETypes) != 0 {
-			return imgspecv1.Descriptor{}, fmt.Errorf("reference %q matches unsupported manifest MIME types %q", ref.image, unsupportedMIMETypes)
+			return imgspecv1.Descriptor{}, -1, fmt.Errorf("reference %q matches unsupported manifest MIME types %q", ref.image, unsupportedMIMETypes)
 		}
 	}
-	return imgspecv1.Descriptor{}, ImageNotFoundError{ref}
+	return imgspecv1.Descriptor{}, -1, ImageNotFoundError{ref}
 }
 
 // LoadManifestDescriptor loads the manifest descriptor to be used to retrieve the image name
@@ -211,7 +219,8 @@ func LoadManifestDescriptor(imgRef types.ImageReference) (imgspecv1.Descriptor,
 	if !ok {
 		return imgspecv1.Descriptor{}, errors.New("error typecasting, need type ociRef")
 	}
-	return ociRef.getManifestDescriptor()
+	md, _, err := ociRef.getManifestDescriptor()
+	return md, err
 }
 
 // NewImageSource returns a types.ImageSource for this reference.
@@ -226,11 +235,6 @@ func (ref ociReference) NewImageDestination(ctx context.Context, sys *types.Syst
 	return newImageDestination(sys, ref)
 }
 
-// DeleteImage deletes the named image from the registry, if supported.
-func (ref ociReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error {
-	return errors.New("Deleting images not implemented for oci: images")
-}
-
 // ociLayoutPath returns a path for the oci-layout within a directory using OCI conventions.
 func (ref ociReference) ociLayoutPath() string {
 	return filepath.Join(ref.dir, imgspecv1.ImageLayoutFile)


Test patch:
diff --git a/oci/layout/fixtures/delete_image_only_one_image/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc b/oci/layout/fixtures/delete_image_only_one_image/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc
new file mode 100644
index 00000000..e7e64ba4
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_only_one_image/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc
@@ -0,0 +1,1 @@
+insert binary content here #9671
diff --git a/oci/layout/fixtures/delete_image_shared_blobs_dir/shared_blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc b/oci/layout/fixtures/delete_image_shared_blobs_dir/shared_blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc
new file mode 100644
index 00000000..e7e64ba4
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_shared_blobs_dir/shared_blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc
@@ -0,0 +1,1 @@
+insert binary content here #9671
diff --git a/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc
new file mode 100644
index 00000000..e7e64ba4
--- /dev/null
+++ b/oci/layout/fixtures/delete_image_two_identical_references/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc
@@ -0,0 +1,1 @@
+insert binary content here #9671
diff --git a/oci/layout/oci_delete_test.go b/oci/layout/oci_delete_test.go
new file mode 100644
index 00000000..7e06456f
--- /dev/null
+++ b/oci/layout/oci_delete_test.go
@@ -0,0 +1,298 @@
+package layout
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/containers/image/v5/types"
+	digest "github.com/opencontainers/go-digest"
+	imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
+	cp "github.com/otiai10/copy"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestReferenceDeleteImage_onlyOneImage(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_only_one_image")
+
+	ref, err := NewReference(tmpDir, "latest")
+	require.NoError(t, err)
+
+	err = ref.DeleteImage(context.Background(), nil)
+	require.NoError(t, err)
+
+	// Check that all blobs were deleted
+	blobsDir := filepath.Join(tmpDir, "blobs")
+	files, err := os.ReadDir(filepath.Join(blobsDir, "sha256"))
+	require.NoError(t, err)
+	require.Empty(t, files)
+
+	// Check that the index is empty as there is only one image in the fixture
+	ociRef, ok := ref.(ociReference)
+	require.True(t, ok)
+	index, err := ociRef.getIndex()
+	require.NoError(t, err)
+	require.Equal(t, 0, len(index.Manifests))
+}
+
+func TestReferenceDeleteImage_onlyOneImage_emptyImageName(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_only_one_image")
+
+	ref, err := NewReference(tmpDir, "")
+	require.NoError(t, err)
+
+	err = ref.DeleteImage(context.Background(), nil)
+	require.NoError(t, err)
+
+	// Check that all blobs were deleted
+	blobsDir := filepath.Join(tmpDir, "blobs")
+	files, err := os.ReadDir(filepath.Join(blobsDir, "sha256"))
+	require.NoError(t, err)
+	require.Empty(t, files)
+
+	// Check that the index is empty as there is only one image in the fixture
+	ociRef, ok := ref.(ociReference)
+	require.True(t, ok)
+	index, err := ociRef.getIndex()
+	require.NoError(t, err)
+	require.Equal(t, 0, len(index.Manifests))
+}
+
+func TestReferenceDeleteImage_sharedBlobDir(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_shared_blobs_dir")
+
+	ref, err := NewReference(tmpDir, "latest")
+	require.NoError(t, err)
+
+	sys := &types.SystemContext{OCISharedBlobDirPath: filepath.Join(tmpDir, "shared_blobs")}
+	err = ref.DeleteImage(context.Background(), sys)
+	require.NoError(t, err)
+
+	// Check that the only blob in the local directory was deleted
+	blobsDir := filepath.Join(tmpDir, "blobs")
+	files, err := os.ReadDir(filepath.Join(blobsDir, "sha256"))
+	require.NoError(t, err)
+	require.Empty(t, files)
+
+	// Check that the blobs in the shared blob directory are still present
+	sharedBlobsDir := filepath.Join(tmpDir, "shared_blobs")
+	files, err = os.ReadDir(filepath.Join(sharedBlobsDir, "sha256"))
+	require.NoError(t, err)
+	require.Equal(t, 2, len(files))
+
+	// Check that the index is empty as there is only one image in the fixture
+	ociRef, ok := ref.(ociReference)
+	require.True(t, ok)
+	index, err := ociRef.getIndex()
+	require.NoError(t, err)
+	require.Equal(t, 0, len(index.Manifests))
+}
+
+func TestReferenceDeleteImage_multipleImages(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_multiple_images")
+
+	ref, err := NewReference(tmpDir, "3.17.5")
+	require.NoError(t, err)
+
+	err = ref.DeleteImage(context.Background(), nil)
+	require.NoError(t, err)
+
+	// Check that the relevant blobs were deleted/preserved
+	blobsDir := filepath.Join(tmpDir, "blobs")
+	files, err := os.ReadDir(filepath.Join(blobsDir, "sha256"))
+	require.NoError(t, err)
+	require.Equal(t, 16, len(files))
+	assertBlobDoesNotExist(t, blobsDir, "sha256:5b2aba4d3c27bc6493633d0ec446b25c8d0a5c9cfe99894bcdff0aee80813805")
+	assertBlobDoesNotExist(t, blobsDir, "sha256:df11bc189adeb50dadb3291a3a7f2c34b36e0efdba0df70f2c8a2d761b215cde")
+	assertBlobDoesNotExist(t, blobsDir, "sha256:986315a0e599fac2b80eb31db2124dab8d3de04d7ca98b254999bd913c1f73fe")
+
+	// Check the index
+	ociRef, ok := ref.(ociReference)
+	require.True(t, ok)
+	// .. Check that the index has been reduced to the correct size
+	index, err := ociRef.getIndex()
+	require.NoError(t, err)
+	require.Equal(t, 6, len(index.Manifests))
+	// .. Check that the image is not in the index anymore
+	for _, descriptor := range index.Manifests {
+		switch descriptor.Annotations[imgspecv1.AnnotationRefName] {
+		case "3.17.5":
+			assert.Fail(t, "image still present in the index after deletion")
+		default:
+			continue
+		}
+	}
+}
+
+func TestReferenceDeleteImage_multipleImages_blobsUsedByOtherImages(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_multiple_images")
+
+	ref, err := NewReference(tmpDir, "1.0.0")
+	require.NoError(t, err)
+
+	err = ref.DeleteImage(context.Background(), nil)
+	require.NoError(t, err)
+
+	// Check that the relevant blobs were deleted/preserved
+	blobsDir := filepath.Join(tmpDir, "blobs")
+	files, err := os.ReadDir(filepath.Join(blobsDir, "sha256"))
+	require.NoError(t, err)
+	require.Equal(t, 17, len(files))
+	assertBlobExists(t, blobsDir, "sha256:df11bc189adeb50dadb3291a3a7f2c34b36e0efdba0df70f2c8a2d761b215cde")
+	assertBlobDoesNotExist(t, blobsDir, "sha256:0dc27f36a618c110ae851662c13283e9fbc1b5a5de003befc4bcefa5a05d2eef")
+	assertBlobDoesNotExist(t, blobsDir, "sha256:a6f737ac2b84bc463f2ff721af39588c69646c82f79f3808236178e02e35b922")
+
+	// Check the index
+	ociRef, ok := ref.(ociReference)
+	require.True(t, ok)
+	// .. Check that the index has been reduced to the correct size
+	index, err := ociRef.getIndex()
+	require.NoError(t, err)
+	require.Equal(t, 6, len(index.Manifests))
+	// .. Check that the image is not in the index anymore
+	for _, descriptor := range index.Manifests {
+		switch descriptor.Annotations[imgspecv1.AnnotationRefName] {
+		case "1.0.0":
+			assert.Fail(t, "image still present in the index after deletion")
+		default:
+			continue
+		}
+	}
+}
+
+func TestReferenceDeleteImage_multipleImages_imageDoesNotExist(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_multiple_images")
+
+	ref, err := NewReference(tmpDir, "does-not-exist")
+	assert.NoError(t, err)
+
+	err = ref.DeleteImage(context.Background(), nil)
+	assert.Error(t, err)
+}
+
+func TestReferenceDeleteImage_multipleImages_emptyImageName(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_multiple_images")
+
+	ref, err := NewReference(tmpDir, "")
+	require.NoError(t, err)
+
+	err = ref.DeleteImage(context.Background(), nil)
+	require.Error(t, err)
+}
+
+func TestReferenceDeleteImage_multipleImages_nestedIndexImage(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_multiple_images")
+
+	ref, err := NewReference(tmpDir, "3.16.7")
+	require.NoError(t, err)
+
+	err = ref.DeleteImage(context.Background(), nil)
+	require.NoError(t, err)
+
+	// Check that the relevant blobs were deleted/preserved
+	blobsDir := filepath.Join(tmpDir, "blobs")
+	files, err := os.ReadDir(filepath.Join(blobsDir, "sha256"))
+	require.NoError(t, err)
+	require.Equal(t, 12, len(files))
+	assertBlobDoesNotExist(t, blobsDir, "sha256:861d3c014b0e3edcf80e6221247d6b2921a4f892feb9bafe9515b9975b78c44f")
+	assertBlobDoesNotExist(t, blobsDir, "sha256:39c524417bb4228f9fcb0aef43a680b5fd6b9f3a1df2fd50509d047e47dad8be")
+	assertBlobDoesNotExist(t, blobsDir, "sha256:f732172ad8d2a666550fa3ec37a5153d59acc95744562ae64cf62ded46de101a")
+	assertBlobDoesNotExist(t, blobsDir, "sha256:02ea786cb1ff44d997661886a4186cbd8a1dc466938712bf7281379209476022")
+	assertBlobDoesNotExist(t, blobsDir, "sha256:be6036f9b6a4e120a04868c47f1b8674f58b2fe5e410cba9f585a13ca8946cf0")
+	assertBlobDoesNotExist(t, blobsDir, "sha256:7ffdfe7d276286b39a203dcc247949cf47c91d2d5e10a53a675c0962ed9e4402")
+	assertBlobDoesNotExist(t, blobsDir, "sha256:e2f7e0374fd6a03d9c373f4d9a0c7802045cc3ddcc1433e89d83b81fa7007242")
+
+	// Check the index
+	ociRef, ok := ref.(ociReference)
+	require.True(t, ok)
+	// .. Check that the index has been reduced to the correct size
+	index, err := ociRef.getIndex()
+	require.NoError(t, err)
+	require.Equal(t, 6, len(index.Manifests))
+	// .. Check that the image is not in the index anymore
+	for _, descriptor := range index.Manifests {
+		switch descriptor.Annotations[imgspecv1.AnnotationRefName] {
+		case "3.16.7":
+			assert.Fail(t, "image still present in the index after deletion")
+		default:
+			continue
+		}
+	}
+}
+
+func TestReferenceDeleteImage_multipleImages_nestedIndexImage_refWithSameContent(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_multiple_images")
+
+	ref, err := NewReference(tmpDir, "3.18.3")
+	require.NoError(t, err)
+
+	err = ref.DeleteImage(context.Background(), nil)
+	require.NoError(t, err)
+
+	// Check that the relevant blobs were deleted/preserved
+	blobsDir := filepath.Join(tmpDir, "blobs")
+	files, err := os.ReadDir(filepath.Join(blobsDir, "sha256"))
+	require.NoError(t, err)
+	require.Equal(t, 19, len(files))
+
+	// Check the index
+	ociRef, ok := ref.(ociReference)
+	require.True(t, ok)
+	// .. Check that the index has been reduced to the correct size
+	index, err := ociRef.getIndex()
+	require.NoError(t, err)
+	require.Equal(t, 6, len(index.Manifests))
+}
+
+func TestReferenceDeleteImage_multipleImages_twoIdenticalReferences(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_two_identical_references")
+
+	ref, err := NewReference(tmpDir, "1.0.0")
+	require.NoError(t, err)
+
+	err = ref.DeleteImage(context.Background(), nil)
+	require.NoError(t, err)
+
+	// Check that the relevant blobs were deleted/preserved - in this case only the first reference should be deleted
+	blobsDir := filepath.Join(tmpDir, "blobs")
+	files, err := os.ReadDir(filepath.Join(blobsDir, "sha256"))
+	require.NoError(t, err)
+	require.Equal(t, 3, len(files))
+	assertBlobExists(t, blobsDir, "sha256:ecfa463536cb5472e238aadc4df81d4785d5d6373027c488a2db8a6e76fe88ed")
+	assertBlobExists(t, blobsDir, "sha256:ce229a4eb5797ecd3a3a1846613b6b49811f79e38b5b0ce666268ba4b6c68e41")
+	assertBlobExists(t, blobsDir, "sha256:fa00bb4e2adbc73a5da1fd54d2a840020592530a8d4e8de9888b9e9a533419d8")
+
+	// Check the index
+	ociRef, ok := ref.(ociReference)
+	require.True(t, ok)
+	// .. Check that the index has been reduced to the correct size
+	index, err := ociRef.getIndex()
+	require.NoError(t, err)
+	require.Equal(t, 1, len(index.Manifests))
+}
+
+func loadFixture(t *testing.T, fixtureName string) string {
+	tmpDir := t.TempDir()
+	err := cp.Copy(fmt.Sprintf("fixtures/%v/", fixtureName), tmpDir)
+	require.NoError(t, err)
+	return tmpDir
+}
+
+func assertBlobExists(t *testing.T, blobsDir string, blobDigest string) {
+	digest, err := digest.Parse(blobDigest)
+	require.NoError(t, err)
+	blobPath := filepath.Join(blobsDir, digest.Algorithm().String(), digest.Hex())
+	_, err = os.Stat(blobPath)
+	require.NoError(t, err)
+}
+
+func assertBlobDoesNotExist(t *testing.T, blobsDir string, blobDigest string) {
+	digest, err := digest.Parse(blobDigest)
+	require.NoError(t, err)
+	blobPath := filepath.Join(blobsDir, digest.Algorithm().String(), digest.Hex())
+	_, err = os.Stat(blobPath)
+	require.True(t, os.IsNotExist(err))
+}
diff --git a/oci/layout/oci_transport_test.go b/oci/layout/oci_transport_test.go
index fd348deb..8beb52dd 100644
--- a/oci/layout/oci_transport_test.go
+++ b/oci/layout/oci_transport_test.go
@@ -17,20 +17,21 @@ func TestGetManifestDescriptor(t *testing.T) {
 	emptyDir := t.TempDir()
 
 	for _, c := range []struct {
-		dir, image string
-		expected   *imgspecv1.Descriptor // nil if a failure ie expected. errorIs / errorAs allows more specific checks.
-		errorIs    error
-		errorAs    any
+		dir, image         string
+		expectedDescriptor *imgspecv1.Descriptor // nil if a failure ie expected. errorIs / errorAs allows more specific checks.
+		expectedIndex      int
+		errorIs            error
+		errorAs            any
 	}{
 		{ // Index is missing
-			dir:      emptyDir,
-			image:    "",
-			expected: nil,
+			dir:                emptyDir,
+			image:              "",
+			expectedDescriptor: nil,
 		},
 		{ // A valid reference to the only manifest
 			dir:   "fixtures/manifest",
 			image: "",
-			expected: &imgspecv1.Descriptor{
+			expectedDescriptor: &imgspecv1.Descriptor{
 				MediaType:   "application/vnd.oci.image.manifest.v1+json",
 				Digest:      "sha256:84afb6189c4d69f2d040c5f1dc4e0a16fed9b539ce9cfb4ac2526ae4e0576cc0",
 				Size:        496,
@@ -40,52 +41,56 @@ func TestGetManifestDescriptor(t *testing.T) {
 					OS:           "linux",
 				},
 			},
+			expectedIndex: 0,
 		},
 		{ // An ambiguous reference to a multi-manifest directory
-			dir:      "fixtures/two_images_manifest",
-			image:    "",
-			expected: nil,
-			errorIs:  ErrMoreThanOneImage,
+			dir:                "fixtures/two_images_manifest",
+			image:              "",
+			expectedDescriptor: nil,
+			errorIs:            ErrMoreThanOneImage,
 		},
 		{ // A valid reference in a multi-manifest directory
 			dir:   "fixtures/name_lookups",
 			image: "a",
-			expected: &imgspecv1.Descriptor{
+			expectedDescriptor: &imgspecv1.Descriptor{
 				MediaType:   "application/vnd.oci.image.manifest.v1+json",
 				Digest:      "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
 				Size:        1,
 				Annotations: map[string]string{"org.opencontainers.image.ref.name": "a"},
 			},
+			expectedIndex: 0,
 		},
 		{ // A valid reference in a multi-manifest directory
 			dir:   "fixtures/name_lookups",
 			image: "b",
-			expected: &imgspecv1.Descriptor{
+			expectedDescriptor: &imgspecv1.Descriptor{
 				MediaType:   "application/vnd.oci.image.manifest.v1+json",
 				Digest:      "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
 				Size:        2,
 				Annotations: map[string]string{"org.opencontainers.image.ref.name": "b"},
 			},
+			expectedIndex: 1,
 		},
 		{ // No entry found
-			dir:      "fixtures/name_lookups",
-			image:    "this-does-not-exist",
-			expected: nil,
-			errorAs:  &ImageNotFoundError{},
+			dir:                "fixtures/name_lookups",
+			image:              "this-does-not-exist",
+			expectedDescriptor: nil,
+			errorAs:            &ImageNotFoundError{},
 		},
 		{ // Entries with invalid MIME types found
-			dir:      "fixtures/name_lookups",
-			image:    "invalid-mime",
-			expected: nil,
+			dir:                "fixtures/name_lookups",
+			image:              "invalid-mime",
+			expectedDescriptor: nil,
 		},
 	} {
 		ref, err := NewReference(c.dir, c.image)
 		require.NoError(t, err)
 
-		res, err := ref.(ociReference).getManifestDescriptor()
-		if c.expected != nil {
+		res, i, err := ref.(ociReference).getManifestDescriptor()
+		if c.expectedDescriptor != nil {
 			require.NoError(t, err)
-			assert.Equal(t, *c.expected, res)
+			assert.Equal(t, c.expectedIndex, i)
+			assert.Equal(t, *c.expectedDescriptor, res)
 		} else {
 			require.Error(t, err)
 			if c.errorIs != nil {
@@ -319,12 +324,6 @@ func TestReferenceNewImageDestination(t *testing.T) {
 	defer dest.Close()
 }
 
-func TestReferenceDeleteImage(t *testing.T) {
-	ref, _ := refToTempOCI(t)
-	err := ref.DeleteImage(context.Background(), nil)
-	assert.Error(t, err)
-}
-
 func TestReferenceOCILayoutPath(t *testing.T) {
 	ref, tmpDir := refToTempOCI(t)
 	ociRef, ok := ref.(ociReference)