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:
Phillip Webb 2024-11-14 13:43:28 -08:00
parent 83e7ccd638
commit a293560237
3 changed files with 45 additions and 15 deletions

View File

@ -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(':', '/');
} }

View File

@ -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();
} }
} }