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
This commit is contained in:
parent
83e7ccd638
commit
a293560237
|
@ -30,6 +30,7 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
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.TarArchiveEntry;
|
||||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||||
|
@ -142,23 +143,40 @@ class ExportedImageTar implements Closeable {
|
||||||
private final Map<String, String> layerMediaTypes;
|
private final Map<String, String> layerMediaTypes;
|
||||||
|
|
||||||
IndexLayerArchiveFactory(Path tarFile, ImageArchiveIndex index) throws IOException {
|
IndexLayerArchiveFactory(Path tarFile, ImageArchiveIndex index) throws IOException {
|
||||||
Set<String> manifestDigests = getDigests(index, this::isManifest);
|
this(tarFile, withNestedIndexes(tarFile, index));
|
||||||
List<ManifestList> manifestLists = getManifestLists(tarFile, getDigests(index, this::isManifestList));
|
}
|
||||||
|
|
||||||
|
IndexLayerArchiveFactory(Path tarFile, List<ImageArchiveIndex> indexes) throws IOException {
|
||||||
|
Set<String> manifestDigests = getDigests(indexes, this::isManifest);
|
||||||
|
Set<String> manifestListDigests = getDigests(indexes, IndexLayerArchiveFactory::isManifestList);
|
||||||
|
List<ManifestList> manifestLists = getManifestLists(tarFile, manifestListDigests);
|
||||||
List<Manifest> manifests = getManifests(tarFile, manifestDigests, manifestLists);
|
List<Manifest> manifests = getManifests(tarFile, manifestDigests, manifestLists);
|
||||||
this.layerMediaTypes = manifests.stream()
|
this.layerMediaTypes = manifests.stream()
|
||||||
.flatMap((manifest) -> manifest.getLayers().stream())
|
.flatMap((manifest) -> manifest.getLayers().stream())
|
||||||
.collect(Collectors.toMap(this::getEntryName, BlobReference::getMediaType));
|
.collect(Collectors.toMap(IndexLayerArchiveFactory::getEntryName, BlobReference::getMediaType));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> getDigests(ImageArchiveIndex index, Predicate<BlobReference> predicate) {
|
private static List<ImageArchiveIndex> withNestedIndexes(Path tarFile, ImageArchiveIndex index)
|
||||||
return index.getManifests()
|
throws IOException {
|
||||||
.stream()
|
Set<String> indexDigests = getDigests(Stream.of(index), IndexLayerArchiveFactory::isIndex);
|
||||||
|
List<ImageArchiveIndex> indexes = new ArrayList<>();
|
||||||
|
indexes.add(index);
|
||||||
|
indexes.addAll(getDigestMatches(tarFile, indexDigests, ImageArchiveIndex::of));
|
||||||
|
return indexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> getDigests(List<ImageArchiveIndex> indexes, Predicate<BlobReference> predicate) {
|
||||||
|
return getDigests(indexes.stream(), predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> getDigests(Stream<ImageArchiveIndex> indexes, Predicate<BlobReference> predicate) {
|
||||||
|
return indexes.flatMap((index) -> index.getManifests().stream())
|
||||||
.filter(predicate)
|
.filter(predicate)
|
||||||
.map(BlobReference::getDigest)
|
.map(BlobReference::getDigest)
|
||||||
.collect(Collectors.toUnmodifiableSet());
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ManifestList> getManifestLists(Path tarFile, Set<String> digests) throws IOException {
|
private static List<ManifestList> getManifestLists(Path tarFile, Set<String> digests) throws IOException {
|
||||||
return getDigestMatches(tarFile, digests, ManifestList::of);
|
return getDigestMatches(tarFile, digests, ManifestList::of);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,12 +191,14 @@ class ExportedImageTar implements Closeable {
|
||||||
return getDigestMatches(tarFile, digests, Manifest::of);
|
return getDigestMatches(tarFile, digests, Manifest::of);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> List<T> getDigestMatches(Path tarFile, Set<String> digests,
|
private static <T> List<T> getDigestMatches(Path tarFile, Set<String> digests,
|
||||||
ThrowingFunction<InputStream, T> factory) throws IOException {
|
ThrowingFunction<InputStream, T> factory) throws IOException {
|
||||||
if (digests.isEmpty()) {
|
if (digests.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
Set<String> names = digests.stream().map(this::getEntryName).collect(Collectors.toUnmodifiableSet());
|
Set<String> names = digests.stream()
|
||||||
|
.map(IndexLayerArchiveFactory::getEntryName)
|
||||||
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
List<T> result = new ArrayList<>();
|
List<T> result = new ArrayList<>();
|
||||||
try (TarArchiveInputStream tar = openTar(tarFile)) {
|
try (TarArchiveInputStream tar = openTar(tarFile)) {
|
||||||
TarArchiveEntry entry = tar.getNextTarEntry();
|
TarArchiveEntry entry = tar.getNextTarEntry();
|
||||||
|
@ -197,19 +217,23 @@ class ExportedImageTar implements Closeable {
|
||||||
|| isJsonWithPrefix(reference.getMediaType(), "application/vnd.docker.distribution.manifest.v");
|
|| 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");
|
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");
|
return mediaType.startsWith(prefix) && mediaType.endsWith("+json");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getEntryName(BlobReference reference) {
|
private static String getEntryName(BlobReference reference) {
|
||||||
return getEntryName(reference.getDigest());
|
return getEntryName(reference.getDigest());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getEntryName(String digest) {
|
private static String getEntryName(String digest) {
|
||||||
return "blobs/" + digest.replace(':', '/');
|
return "blobs/" + digest.replace(':', '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
|
|
||||||
package org.springframework.boot.buildpack.platform.docker;
|
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.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
|
||||||
|
@ -34,7 +37,8 @@ class ExportedImageTarTests {
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = { "export-docker-desktop.tar", "export-docker-desktop-containerd.tar",
|
@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 {
|
void test(String tarFile) throws Exception {
|
||||||
ImageReference reference = ImageReference.of("test:latest");
|
ImageReference reference = ImageReference.of("test:latest");
|
||||||
try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference,
|
try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference,
|
||||||
|
@ -43,10 +47,12 @@ class ExportedImageTarTests {
|
||||||
String expectedName = (expectedCompression != Compression.GZIP)
|
String expectedName = (expectedCompression != Compression.GZIP)
|
||||||
? "5caae51697b248b905dca1a4160864b0e1a15c300981736555cdce6567e8d477"
|
? "5caae51697b248b905dca1a4160864b0e1a15c300981736555cdce6567e8d477"
|
||||||
: "f0f1fd1bdc71ac6a4dc99cea5f5e45c86c5ec26fe4d1daceeb78207303606429";
|
: "f0f1fd1bdc71ac6a4dc99cea5f5e45c86c5ec26fe4d1daceeb78207303606429";
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
exportedImageTar.exportLayers((name, tarArchive) -> {
|
exportedImageTar.exportLayers((name, tarArchive) -> {
|
||||||
assertThat(name).contains(expectedName);
|
names.add(name);
|
||||||
assertThat(tarArchive.getCompression()).isEqualTo(expectedCompression);
|
assertThat(tarArchive.getCompression()).isEqualTo(expectedCompression);
|
||||||
});
|
});
|
||||||
|
assertThat(names).filteredOn((name) -> name.contains(expectedName)).isNotEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue