diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java index 53048215980..ccbd2c0a833 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; @@ -142,23 +143,40 @@ class ExportedImageTar implements Closeable { private final Map layerMediaTypes; IndexLayerArchiveFactory(Path tarFile, ImageArchiveIndex index) throws IOException { - Set manifestDigests = getDigests(index, this::isManifest); - List manifestLists = getManifestLists(tarFile, getDigests(index, this::isManifestList)); + this(tarFile, withNestedIndexes(tarFile, index)); + } + + IndexLayerArchiveFactory(Path tarFile, List indexes) throws IOException { + Set manifestDigests = getDigests(indexes, this::isManifest); + Set manifestListDigests = getDigests(indexes, IndexLayerArchiveFactory::isManifestList); + List manifestLists = getManifestLists(tarFile, manifestListDigests); List manifests = getManifests(tarFile, manifestDigests, manifestLists); this.layerMediaTypes = manifests.stream() .flatMap((manifest) -> manifest.getLayers().stream()) - .collect(Collectors.toMap(this::getEntryName, BlobReference::getMediaType)); + .collect(Collectors.toMap(IndexLayerArchiveFactory::getEntryName, BlobReference::getMediaType)); } - private Set getDigests(ImageArchiveIndex index, Predicate predicate) { - return index.getManifests() - .stream() + private static List withNestedIndexes(Path tarFile, ImageArchiveIndex index) + throws IOException { + Set indexDigests = getDigests(Stream.of(index), IndexLayerArchiveFactory::isIndex); + List indexes = new ArrayList<>(); + indexes.add(index); + indexes.addAll(getDigestMatches(tarFile, indexDigests, ImageArchiveIndex::of)); + return indexes; + } + + private static Set getDigests(List indexes, Predicate predicate) { + return getDigests(indexes.stream(), predicate); + } + + private static Set getDigests(Stream indexes, Predicate predicate) { + return indexes.flatMap((index) -> index.getManifests().stream()) .filter(predicate) .map(BlobReference::getDigest) .collect(Collectors.toUnmodifiableSet()); } - private List getManifestLists(Path tarFile, Set digests) throws IOException { + private static List getManifestLists(Path tarFile, Set digests) throws IOException { return getDigestMatches(tarFile, digests, ManifestList::of); } @@ -173,12 +191,14 @@ class ExportedImageTar implements Closeable { return getDigestMatches(tarFile, digests, Manifest::of); } - private List getDigestMatches(Path tarFile, Set digests, + private static List getDigestMatches(Path tarFile, Set digests, ThrowingFunction factory) throws IOException { if (digests.isEmpty()) { return Collections.emptyList(); } - Set names = digests.stream().map(this::getEntryName).collect(Collectors.toUnmodifiableSet()); + Set names = digests.stream() + .map(IndexLayerArchiveFactory::getEntryName) + .collect(Collectors.toUnmodifiableSet()); List result = new ArrayList<>(); try (TarArchiveInputStream tar = openTar(tarFile)) { TarArchiveEntry entry = tar.getNextTarEntry(); @@ -197,19 +217,23 @@ class ExportedImageTar implements Closeable { || isJsonWithPrefix(reference.getMediaType(), "application/vnd.docker.distribution.manifest.v"); } - private boolean isManifestList(BlobReference reference) { + private static boolean isIndex(BlobReference reference) { + return isJsonWithPrefix(reference.getMediaType(), "application/vnd.oci.image.index.v"); + } + + private static boolean isManifestList(BlobReference reference) { return isJsonWithPrefix(reference.getMediaType(), "application/vnd.docker.distribution.manifest.list.v"); } - private boolean isJsonWithPrefix(String mediaType, String prefix) { + private static boolean isJsonWithPrefix(String mediaType, String prefix) { return mediaType.startsWith(prefix) && mediaType.endsWith("+json"); } - private String getEntryName(BlobReference reference) { + private static String getEntryName(BlobReference reference) { return getEntryName(reference.getDigest()); } - private String getEntryName(String digest) { + private static String getEntryName(String digest) { return "blobs/" + digest.replace(':', '/'); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java index 253ed2cb03d..4d9ea93c470 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java @@ -16,6 +16,9 @@ package org.springframework.boot.buildpack.platform.docker; +import java.util.ArrayList; +import java.util.List; + import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -34,7 +37,8 @@ class ExportedImageTarTests { @ParameterizedTest @ValueSource(strings = { "export-docker-desktop.tar", "export-docker-desktop-containerd.tar", - "export-docker-desktop-containerd-manifest-list.tar", "export-docker-engine.tar", "export-podman.tar" }) + "export-docker-desktop-containerd-manifest-list.tar", "export-docker-engine.tar", "export-podman.tar", + "export-docker-desktop-nested-index.tar" }) void test(String tarFile) throws Exception { ImageReference reference = ImageReference.of("test:latest"); try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, @@ -43,10 +47,12 @@ class ExportedImageTarTests { String expectedName = (expectedCompression != Compression.GZIP) ? "5caae51697b248b905dca1a4160864b0e1a15c300981736555cdce6567e8d477" : "f0f1fd1bdc71ac6a4dc99cea5f5e45c86c5ec26fe4d1daceeb78207303606429"; + List names = new ArrayList<>(); exportedImageTar.exportLayers((name, tarArchive) -> { - assertThat(name).contains(expectedName); + names.add(name); assertThat(tarArchive.getCompression()).isEqualTo(expectedCompression); }); + assertThat(names).filteredOn((name) -> name.contains(expectedName)).isNotEmpty(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-docker-desktop-nested-index.tar b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-docker-desktop-nested-index.tar new file mode 100644 index 00000000000..bf423d69ba2 Binary files /dev/null and b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-docker-desktop-nested-index.tar differ