Skip to content

Commit a293560

Browse files
committed
Support nested OCI indexes
Update `ExportedImageTar.IndexLayerArchiveFactory` to support nested indexes. Nested indexes support a layer of interaction where the `index.json` file points to a blob that contains the read index to use. Prior to this commit, we only supported indexes provided directly by the `index.json` file. This missing support results in "buildpack.toml: no such file or directory" errors when referencing specific buildpacks and using Docker Engine 27.3.1 or above. See gh-43126
1 parent 83e7ccd commit a293560

File tree

3 files changed

+45
-15
lines changed

3 files changed

+45
-15
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.Set;
3131
import java.util.function.Predicate;
3232
import java.util.stream.Collectors;
33+
import java.util.stream.Stream;
3334

3435
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
3536
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
@@ -142,23 +143,40 @@ private static class IndexLayerArchiveFactory extends LayerArchiveFactory {
142143
private final Map<String, String> layerMediaTypes;
143144

144145
IndexLayerArchiveFactory(Path tarFile, ImageArchiveIndex index) throws IOException {
145-
Set<String> manifestDigests = getDigests(index, this::isManifest);
146-
List<ManifestList> manifestLists = getManifestLists(tarFile, getDigests(index, this::isManifestList));
146+
this(tarFile, withNestedIndexes(tarFile, index));
147+
}
148+
149+
IndexLayerArchiveFactory(Path tarFile, List<ImageArchiveIndex> indexes) throws IOException {
150+
Set<String> manifestDigests = getDigests(indexes, this::isManifest);
151+
Set<String> manifestListDigests = getDigests(indexes, IndexLayerArchiveFactory::isManifestList);
152+
List<ManifestList> manifestLists = getManifestLists(tarFile, manifestListDigests);
147153
List<Manifest> manifests = getManifests(tarFile, manifestDigests, manifestLists);
148154
this.layerMediaTypes = manifests.stream()
149155
.flatMap((manifest) -> manifest.getLayers().stream())
150-
.collect(Collectors.toMap(this::getEntryName, BlobReference::getMediaType));
156+
.collect(Collectors.toMap(IndexLayerArchiveFactory::getEntryName, BlobReference::getMediaType));
151157
}
152158

153-
private Set<String> getDigests(ImageArchiveIndex index, Predicate<BlobReference> predicate) {
154-
return index.getManifests()
155-
.stream()
159+
private static List<ImageArchiveIndex> withNestedIndexes(Path tarFile, ImageArchiveIndex index)
160+
throws IOException {
161+
Set<String> indexDigests = getDigests(Stream.of(index), IndexLayerArchiveFactory::isIndex);
162+
List<ImageArchiveIndex> indexes = new ArrayList<>();
163+
indexes.add(index);
164+
indexes.addAll(getDigestMatches(tarFile, indexDigests, ImageArchiveIndex::of));
165+
return indexes;
166+
}
167+
168+
private static Set<String> getDigests(List<ImageArchiveIndex> indexes, Predicate<BlobReference> predicate) {
169+
return getDigests(indexes.stream(), predicate);
170+
}
171+
172+
private static Set<String> getDigests(Stream<ImageArchiveIndex> indexes, Predicate<BlobReference> predicate) {
173+
return indexes.flatMap((index) -> index.getManifests().stream())
156174
.filter(predicate)
157175
.map(BlobReference::getDigest)
158176
.collect(Collectors.toUnmodifiableSet());
159177
}
160178

161-
private List<ManifestList> getManifestLists(Path tarFile, Set<String> digests) throws IOException {
179+
private static List<ManifestList> getManifestLists(Path tarFile, Set<String> digests) throws IOException {
162180
return getDigestMatches(tarFile, digests, ManifestList::of);
163181
}
164182

@@ -173,12 +191,14 @@ private List<Manifest> getManifests(Path tarFile, Set<String> manifestDigests, L
173191
return getDigestMatches(tarFile, digests, Manifest::of);
174192
}
175193

176-
private <T> List<T> getDigestMatches(Path tarFile, Set<String> digests,
194+
private static <T> List<T> getDigestMatches(Path tarFile, Set<String> digests,
177195
ThrowingFunction<InputStream, T> factory) throws IOException {
178196
if (digests.isEmpty()) {
179197
return Collections.emptyList();
180198
}
181-
Set<String> names = digests.stream().map(this::getEntryName).collect(Collectors.toUnmodifiableSet());
199+
Set<String> names = digests.stream()
200+
.map(IndexLayerArchiveFactory::getEntryName)
201+
.collect(Collectors.toUnmodifiableSet());
182202
List<T> result = new ArrayList<>();
183203
try (TarArchiveInputStream tar = openTar(tarFile)) {
184204
TarArchiveEntry entry = tar.getNextTarEntry();
@@ -197,19 +217,23 @@ private boolean isManifest(BlobReference reference) {
197217
|| isJsonWithPrefix(reference.getMediaType(), "application/vnd.docker.distribution.manifest.v");
198218
}
199219

200-
private boolean isManifestList(BlobReference reference) {
220+
private static boolean isIndex(BlobReference reference) {
221+
return isJsonWithPrefix(reference.getMediaType(), "application/vnd.oci.image.index.v");
222+
}
223+
224+
private static boolean isManifestList(BlobReference reference) {
201225
return isJsonWithPrefix(reference.getMediaType(), "application/vnd.docker.distribution.manifest.list.v");
202226
}
203227

204-
private boolean isJsonWithPrefix(String mediaType, String prefix) {
228+
private static boolean isJsonWithPrefix(String mediaType, String prefix) {
205229
return mediaType.startsWith(prefix) && mediaType.endsWith("+json");
206230
}
207231

208-
private String getEntryName(BlobReference reference) {
232+
private static String getEntryName(BlobReference reference) {
209233
return getEntryName(reference.getDigest());
210234
}
211235

212-
private String getEntryName(String digest) {
236+
private static String getEntryName(String digest) {
213237
return "blobs/" + digest.replace(':', '/');
214238
}
215239

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.boot.buildpack.platform.docker;
1818

19+
import java.util.ArrayList;
20+
import java.util.List;
21+
1922
import org.junit.jupiter.params.ParameterizedTest;
2023
import org.junit.jupiter.params.provider.ValueSource;
2124

@@ -34,7 +37,8 @@ class ExportedImageTarTests {
3437

3538
@ParameterizedTest
3639
@ValueSource(strings = { "export-docker-desktop.tar", "export-docker-desktop-containerd.tar",
37-
"export-docker-desktop-containerd-manifest-list.tar", "export-docker-engine.tar", "export-podman.tar" })
40+
"export-docker-desktop-containerd-manifest-list.tar", "export-docker-engine.tar", "export-podman.tar",
41+
"export-docker-desktop-nested-index.tar" })
3842
void test(String tarFile) throws Exception {
3943
ImageReference reference = ImageReference.of("test:latest");
4044
try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference,
@@ -43,10 +47,12 @@ void test(String tarFile) throws Exception {
4347
String expectedName = (expectedCompression != Compression.GZIP)
4448
? "5caae51697b248b905dca1a4160864b0e1a15c300981736555cdce6567e8d477"
4549
: "f0f1fd1bdc71ac6a4dc99cea5f5e45c86c5ec26fe4d1daceeb78207303606429";
50+
List<String> names = new ArrayList<>();
4651
exportedImageTar.exportLayers((name, tarArchive) -> {
47-
assertThat(name).contains(expectedName);
52+
names.add(name);
4853
assertThat(tarArchive.getCompression()).isEqualTo(expectedCompression);
4954
});
55+
assertThat(names).filteredOn((name) -> name.contains(expectedName)).isNotEmpty();
5056
}
5157
}
5258

0 commit comments

Comments
 (0)