From 9e409702807eb5e5d3b0d0756b2578e6c5ddcd1f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 23 Apr 2024 16:36:51 -0700 Subject: [PATCH] Support gzip compressed image layers Update buildpack support to allow gzip compressed image layers to be used when returned by the Docker engine. This update is restores buildpack support when using Docker Desktop with the "Use containerd for pulling and storing images" option enabled. This commit introduces a new `ExportedImageTar` class to deal with the intricacies of determining the mimetype of a layer. The class deals with the parsing of `index.json' and related manifest blobs in order to obtain layer information. The legacy `manifest.json` format is also supported should `index.json` be missing. Tests have been added to ensure that export archives from Docker Engine, Docker Desktop (with and without containerd), and Podman can be used. Fixes gh-40100 Co-authored-by: Moritz Halbritter Co-authored-by: Scott Frederick --- .../buildpack/platform/build/Builder.java | 9 +- .../build/BuildpackResolverContext.java | 6 +- .../platform/build/ImageBuildpack.java | 39 +-- .../buildpack/platform/docker/DockerApi.java | 112 +++----- .../platform/docker/ExportedImageTar.java | 261 ++++++++++++++++++ .../platform/docker/type/BlobReference.java | 59 ++++ .../buildpack/platform/docker/type/Image.java | 14 +- .../docker/type/ImageArchiveIndex.java | 67 +++++ .../docker/type/ImageArchiveManifest.java | 13 +- .../platform/docker/type/Manifest.java | 74 +++++ .../platform/docker/type/ManifestList.java | 79 ++++++ .../buildpack/platform/io/TarArchive.java | 80 +++++- .../buildpack/platform/json/MappedObject.java | 27 +- .../platform/build/ImageBuildpackTests.java | 8 +- .../platform/docker/DockerApiTests.java | 11 +- .../docker/ExportedImageTarTests.java | 53 ++++ .../docker/type/ImageArchiveIndexTests.java | 50 ++++ .../type/ImageArchiveManifestTests.java | 13 +- .../docker/type/ManifestListTests.java | 47 ++++ .../platform/docker/type/ManifestTests.java | 56 ++++ ...ocker-desktop-containerd-manifest-list.tar | Bin 0 -> 29696 bytes .../export-docker-desktop-containerd.tar | Bin 0 -> 9728 bytes .../platform/docker/export-docker-desktop.tar | Bin 0 -> 17920 bytes .../platform/docker/export-docker-engine.tar | Bin 0 -> 17920 bytes .../platform/docker/export-podman.tar | Bin 0 -> 14848 bytes .../type/distribution-manifest-list.json | 24 ++ .../docker/type/distribution-manifest.json | 16 ++ .../docker/type/image-archive-index.json | 15 + .../platform/docker/type/image-manifest.json | 20 ++ 29 files changed, 1014 insertions(+), 139 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/BlobReference.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveIndex.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Manifest.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ManifestList.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveIndexTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ManifestListTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ManifestTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-docker-desktop-containerd-manifest-list.tar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-docker-desktop-containerd.tar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-docker-desktop.tar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-docker-engine.tar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-podman.tar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/distribution-manifest-list.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/distribution-manifest.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-index.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-manifest.json diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index 367e52d616..abe6bf2a69 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.boot.buildpack.platform.build; import java.io.IOException; -import java.nio.file.Path; import java.util.List; import java.util.function.Consumer; @@ -33,6 +32,7 @@ import org.springframework.boot.buildpack.platform.docker.transport.DockerEngine import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -273,8 +273,9 @@ public class Builder { } @Override - public void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException { - Builder.this.docker.image().exportLayerFiles(reference, exports); + public void exportImageLayers(ImageReference reference, IOBiConsumer exports) + throws IOException { + Builder.this.docker.image().exportLayers(reference, exports); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java index fd8e59feff..4357ca8b38 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,12 @@ package org.springframework.boot.buildpack.platform.build; import java.io.IOException; -import java.nio.file.Path; import java.util.List; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; /** * Context passed to a {@link BuildpackResolver}. @@ -52,6 +52,6 @@ interface BuildpackResolverContext { * during the callback) * @throws IOException on IO error */ - void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException; + void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java index 31c84cefc1..272099e245 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java @@ -36,6 +36,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.Layer; import org.springframework.boot.buildpack.platform.docker.type.LayerId; import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.util.StreamUtils; /** @@ -115,31 +116,31 @@ final class ImageBuildpack implements Buildpack { ExportedLayers(BuildpackResolverContext context, ImageReference imageReference) throws IOException { List layerFiles = new ArrayList<>(); - context.exportImageLayers(imageReference, (name, path) -> layerFiles.add(copyToTemp(path))); + context.exportImageLayers(imageReference, + (name, tarArchive) -> layerFiles.add(createLayerFile(tarArchive))); this.layerFiles = Collections.unmodifiableList(layerFiles); } - private Path copyToTemp(Path path) throws IOException { - Path outputPath = Files.createTempFile("create-builder-scratch-", null); - try (OutputStream out = Files.newOutputStream(outputPath)) { - copyLayerTar(path, out); + private Path createLayerFile(TarArchive tarArchive) throws IOException { + Path sourceTarFile = Files.createTempFile("create-builder-scratch-source-", null); + try (OutputStream out = Files.newOutputStream(sourceTarFile)) { + tarArchive.writeTo(out); } - return outputPath; - } - - private void copyLayerTar(Path path, OutputStream out) throws IOException { - try (TarArchiveInputStream tarIn = new TarArchiveInputStream(Files.newInputStream(path)); - TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) { - tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); - TarArchiveEntry entry = tarIn.getNextTarEntry(); - while (entry != null) { - tarOut.putArchiveEntry(entry); - StreamUtils.copy(tarIn, tarOut); - tarOut.closeArchiveEntry(); - entry = tarIn.getNextTarEntry(); + Path layerFile = Files.createTempFile("create-builder-scratch-", null); + try (TarArchiveOutputStream out = new TarArchiveOutputStream(Files.newOutputStream(layerFile))) { + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(sourceTarFile))) { + out.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + TarArchiveEntry entry = in.getNextTarEntry(); + while (entry != null) { + out.putArchiveEntry(entry); + StreamUtils.copy(in, out); + out.closeArchiveEntry(); + entry = in.getNextTarEntry(); + } + out.finish(); } - tarOut.finish(); } + return layerFile; } void apply(IOConsumer layers) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 06bddbdf08..6e7263af88 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -16,16 +16,10 @@ package org.springframework.boot.buildpack.platform.docker; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -33,10 +27,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.hc.core5.net.URIBuilder; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; @@ -48,7 +39,6 @@ import org.springframework.boot.buildpack.platform.docker.type.ContainerReferenc import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; -import org.springframework.boot.buildpack.platform.docker.type.ImageArchiveManifest; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.boot.buildpack.platform.io.IOBiConsumer; @@ -56,7 +46,6 @@ import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.boot.buildpack.platform.json.JsonStream; import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; import org.springframework.util.Assert; -import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; /** @@ -263,7 +252,35 @@ public class DockerApi { } /** - * Export the layers of an image as {@link TarArchive}s. + * Export the layers of an image as paths to layer tar files. + * @param reference the reference to export + * @param exports a consumer to receive the layer tar file paths (file can only be + * accessed during the callback) + * @throws IOException on IO error + * @since 2.7.10 + * @deprecated since 3.2.6 for removal in 3.5.0 in favor of + * {@link #exportLayers(ImageReference, IOBiConsumer)} + */ + @Deprecated(since = "3.2.6", forRemoval = true) + public void exportLayerFiles(ImageReference reference, IOBiConsumer exports) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Assert.notNull(exports, "Exports must not be null"); + exportLayers(reference, (name, archive) -> { + Path path = Files.createTempFile("docker-export-layer-files-", null); + try { + try (OutputStream out = Files.newOutputStream(path)) { + archive.writeTo(out); + exports.accept(name, path); + } + } + finally { + Files.delete(path); + } + }); + } + + /** + * Export the layers of an image as {@link TarArchive TarArchives}. * @param reference the reference to export * @param exports a consumer to receive the layers (contents can only be accessed * during the callback) @@ -271,41 +288,14 @@ public class DockerApi { */ public void exportLayers(ImageReference reference, IOBiConsumer exports) throws IOException { - exportLayerFiles(reference, (name, path) -> { - try (InputStream in = Files.newInputStream(path)) { - TarArchive archive = (out) -> StreamUtils.copy(in, out); - exports.accept(name, archive); - } - }); - } - - /** - * Export the layers of an image as paths to layer tar files. - * @param reference the reference to export - * @param exports a consumer to receive the layer tar file paths (file can only be - * accessed during the callback) - * @throws IOException on IO error - * @since 2.7.10 - */ - public void exportLayerFiles(ImageReference reference, IOBiConsumer exports) throws IOException { Assert.notNull(reference, "Reference must not be null"); Assert.notNull(exports, "Exports must not be null"); - URI saveUri = buildUrl("/images/" + reference + "/get"); - Response response = http().get(saveUri); - Path exportFile = copyToTemp(response.getContent()); - ImageArchiveManifest manifest = getManifest(reference, exportFile); - try (TarArchiveInputStream tar = new TarArchiveInputStream(new FileInputStream(exportFile.toFile()))) { - TarArchiveEntry entry = tar.getNextTarEntry(); - while (entry != null) { - if (manifestContainsLayerEntry(manifest, entry.getName())) { - Path layerFile = copyToTemp(tar); - exports.accept(entry.getName(), layerFile); - Files.delete(layerFile); - } - entry = tar.getNextTarEntry(); + URI uri = buildUrl("/images/" + reference + "/get"); + try (Response response = http().get(uri)) { + try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, response.getContent())) { + exportedImageTar.exportLayers(exports); } } - Files.delete(exportFile); } /** @@ -345,37 +335,6 @@ public class DockerApi { http().post(uri).close(); } - private ImageArchiveManifest getManifest(ImageReference reference, Path exportFile) throws IOException { - try (TarArchiveInputStream tar = new TarArchiveInputStream(new FileInputStream(exportFile.toFile()))) { - TarArchiveEntry entry = tar.getNextTarEntry(); - while (entry != null) { - if (entry.getName().equals("manifest.json")) { - return readManifest(tar); - } - entry = tar.getNextTarEntry(); - } - } - throw new IllegalArgumentException("Manifest not found in image " + reference); - } - - private ImageArchiveManifest readManifest(TarArchiveInputStream tar) throws IOException { - String manifestContent = new BufferedReader(new InputStreamReader(tar, StandardCharsets.UTF_8)).lines() - .collect(Collectors.joining()); - return ImageArchiveManifest.of(new ByteArrayInputStream(manifestContent.getBytes(StandardCharsets.UTF_8))); - } - - private Path copyToTemp(InputStream in) throws IOException { - Path path = Files.createTempFile("create-builder-scratch-", null); - try (OutputStream out = Files.newOutputStream(path)) { - StreamUtils.copy(in, out); - } - return path; - } - - private boolean manifestContainsLayerEntry(ImageArchiveManifest manifest, String layerId) { - return manifest.getEntries().stream().anyMatch((content) -> content.getLayers().contains(layerId)); - } - } /** @@ -458,8 +417,9 @@ public class DockerApi { public ContainerStatus wait(ContainerReference reference) throws IOException { Assert.notNull(reference, "Reference must not be null"); URI uri = buildUrl("/containers/" + reference + "/wait"); - Response response = http().post(uri); - return ContainerStatus.of(response.getContent()); + try (Response response = http().post(uri)) { + return ContainerStatus.of(response.getContent()); + } } /** 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 new file mode 100644 index 0000000000..5304821598 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java @@ -0,0 +1,261 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; + +import org.springframework.boot.buildpack.platform.docker.type.BlobReference; +import org.springframework.boot.buildpack.platform.docker.type.ImageArchiveIndex; +import org.springframework.boot.buildpack.platform.docker.type.ImageArchiveManifest; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.Manifest; +import org.springframework.boot.buildpack.platform.docker.type.ManifestList; +import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.buildpack.platform.io.TarArchive.Compression; +import org.springframework.util.Assert; +import org.springframework.util.function.ThrowingFunction; + +/** + * Internal helper class used by the {@link DockerApi} to extract layers from an exported + * image tar. + * + * @author Phillip Webb + * @author Moritz Halbritter + * @author Scott Frederick + */ +class ExportedImageTar implements Closeable { + + private final Path tarFile; + + private final LayerArchiveFactory layerArchiveFactory; + + ExportedImageTar(ImageReference reference, InputStream inputStream) throws IOException { + this.tarFile = Files.createTempFile("docker-layers-", null); + Files.copy(inputStream, this.tarFile, StandardCopyOption.REPLACE_EXISTING); + this.layerArchiveFactory = LayerArchiveFactory.create(reference, this.tarFile); + } + + void exportLayers(IOBiConsumer exports) throws IOException { + try (TarArchiveInputStream tar = openTar(this.tarFile)) { + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + TarArchive layerArchive = this.layerArchiveFactory.getLayerArchive(tar, entry); + if (layerArchive != null) { + exports.accept(entry.getName(), layerArchive); + } + entry = tar.getNextTarEntry(); + } + } + } + + private static TarArchiveInputStream openTar(Path path) throws IOException { + return new TarArchiveInputStream(Files.newInputStream(path)); + } + + @Override + public void close() throws IOException { + Files.delete(this.tarFile); + } + + /** + * Factory class used to create a {@link TarArchiveEntry} for layer. + */ + private abstract static class LayerArchiveFactory { + + /** + * Create a new {@link TarArchive} if the given entry represents a layer. + * @param tar the tar input stream + * @param entry the candidate entry + * @return a new {@link TarArchive} instance or {@code null} if this entry is not + * a layer. + */ + abstract TarArchive getLayerArchive(TarArchiveInputStream tar, TarArchiveEntry entry); + + /** + * Create a new {@link LayerArchiveFactory} for the given tar file using either + * the {@code index.json} or {@code manifest.json} to detect layers. + * @param reference the image that was referenced + * @param tarFile the source tar file + * @return a new {@link LayerArchiveFactory} instance + * @throws IOException on IO error + */ + static LayerArchiveFactory create(ImageReference reference, Path tarFile) throws IOException { + try (TarArchiveInputStream tar = openTar(tarFile)) { + ImageArchiveIndex index = null; + ImageArchiveManifest manifest = null; + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + if ("index.json".equals(entry.getName())) { + index = ImageArchiveIndex.of(tar); + break; + } + if ("manifest.json".equals(entry.getName())) { + manifest = ImageArchiveManifest.of(tar); + } + entry = tar.getNextTarEntry(); + } + Assert.state(index != null || manifest != null, + "Exported image '%s' does not contain 'index.json' or 'manifest.json'".formatted(reference)); + return (index != null) ? new IndexLayerArchiveFactory(tarFile, index) + : new ManifestLayerArchiveFactory(tarFile, manifest); + } + } + + } + + /** + * {@link LayerArchiveFactory} backed by the more recent {@code index.json} file. + */ + private static class IndexLayerArchiveFactory extends LayerArchiveFactory { + + 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)); + List manifests = getManifests(tarFile, manifestDigests, manifestLists); + this.layerMediaTypes = manifests.stream() + .flatMap((manifest) -> manifest.getLayers().stream()) + .collect(Collectors.toMap(this::getEntryName, BlobReference::getMediaType)); + } + + private Set getDigests(ImageArchiveIndex index, Predicate predicate) { + return index.getManifests() + .stream() + .filter(predicate) + .map(BlobReference::getDigest) + .collect(Collectors.toUnmodifiableSet()); + } + + private List getManifestLists(Path tarFile, Set digests) throws IOException { + return getDigestMatches(tarFile, digests, ManifestList::of); + } + + private List getManifests(Path tarFile, Set manifestDigests, List manifestLists) + throws IOException { + Set digests = new HashSet<>(manifestDigests); + manifestLists.stream() + .flatMap(ManifestList::streamManifests) + .filter(this::isManifest) + .map(BlobReference::getDigest) + .forEach(digests::add); + return getDigestMatches(tarFile, digests, Manifest::of); + } + + private 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()); + List result = new ArrayList<>(); + try (TarArchiveInputStream tar = openTar(tarFile)) { + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + if (names.contains(entry.getName())) { + result.add(factory.apply(tar)); + } + entry = tar.getNextTarEntry(); + } + } + return Collections.unmodifiableList(result); + } + + private boolean isManifest(BlobReference reference) { + return isJsonWithPrefix(reference.getMediaType(), "application/vnd.oci.image.manifest.v") + || isJsonWithPrefix(reference.getMediaType(), "application/vnd.docker.distribution.manifest.v"); + } + + private boolean isManifestList(BlobReference reference) { + return isJsonWithPrefix(reference.getMediaType(), "application/vnd.docker.distribution.manifest.list.v"); + } + + private boolean isJsonWithPrefix(String mediaType, String prefix) { + return mediaType.startsWith(prefix) && mediaType.endsWith("+json"); + } + + private String getEntryName(BlobReference reference) { + return getEntryName(reference.getDigest()); + } + + private String getEntryName(String digest) { + return "blobs/" + digest.replace(':', '/'); + } + + @Override + TarArchive getLayerArchive(TarArchiveInputStream tar, TarArchiveEntry entry) { + String mediaType = this.layerMediaTypes.get(entry.getName()); + if (mediaType == null) { + return null; + } + return TarArchive.fromInputStream(tar, getCompression(mediaType)); + } + + private Compression getCompression(String mediaType) { + if (mediaType.endsWith(".tar.gzip")) { + return Compression.GZIP; + } + if (mediaType.endsWith(".tar.zstd")) { + return Compression.ZSTD; + } + return Compression.NONE; + } + + } + + /** + * {@link LayerArchiveFactory} backed by the legacy {@code manifest.json} file. + */ + private static class ManifestLayerArchiveFactory extends LayerArchiveFactory { + + private Set layers; + + ManifestLayerArchiveFactory(Path tarFile, ImageArchiveManifest manifest) { + this.layers = manifest.getEntries() + .stream() + .flatMap((entry) -> entry.getLayers().stream()) + .collect(Collectors.toUnmodifiableSet()); + } + + @Override + TarArchive getLayerArchive(TarArchiveInputStream tar, TarArchiveEntry entry) { + if (!this.layers.contains(entry.getName())) { + return null; + } + return TarArchive.fromInputStream(tar, Compression.NONE); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/BlobReference.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/BlobReference.java new file mode 100644 index 0000000000..ed2b1a2848 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/BlobReference.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.lang.invoke.MethodHandles; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.buildpack.platform.json.MappedObject; + +/** + * A reference to a blob by its digest. + * + * @author Phillip Webb + * @since 3.2.6 + */ +public class BlobReference extends MappedObject { + + private final String digest; + + private final String mediaType; + + BlobReference(JsonNode node) { + super(node, MethodHandles.lookup()); + this.digest = valueAt("/digest", String.class); + this.mediaType = valueAt("/mediaType", String.class); + } + + /** + * Return the digest of the blob. + * @return the blob digest + */ + public String getDigest() { + return this.digest; + } + + /** + * Return the media type of the blob. + * @return the blob media type + */ + public String getMediaType() { + return this.mediaType; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java index 24a488bd4a..dd224c8dab 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ package org.springframework.boot.buildpack.platform.docker.type; import java.io.IOException; import java.io.InputStream; import java.lang.invoke.MethodHandles; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -48,22 +47,13 @@ public class Image extends MappedObject { Image(JsonNode node) { super(node, MethodHandles.lookup()); - this.digests = getDigests(getNode().at("/RepoDigests")); + this.digests = childrenAt("/RepoDigests", JsonNode::asText); this.config = new ImageConfig(getNode().at("/Config")); this.layers = extractLayers(valueAt("/RootFS/Layers", String[].class)); this.os = valueAt("/Os", String.class); this.created = valueAt("/Created", String.class); } - private List getDigests(JsonNode node) { - if (node.isEmpty()) { - return Collections.emptyList(); - } - List digests = new ArrayList<>(); - node.forEach((child) -> digests.add(child.asText())); - return Collections.unmodifiableList(digests); - } - private List extractLayers(String[] layers) { if (layers == null) { return Collections.emptyList(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveIndex.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveIndex.java new file mode 100644 index 0000000000..df117703c5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveIndex.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.buildpack.platform.json.MappedObject; + +/** + * Image archive index information as provided by {@code index.json}. + * + * @author Phillip Webb + * @since 3.2.6 + * @see OCI Image Index + * Specification + */ +public class ImageArchiveIndex extends MappedObject { + + private final Integer schemaVersion; + + private final List manifests; + + protected ImageArchiveIndex(JsonNode node) { + super(node, MethodHandles.lookup()); + this.schemaVersion = valueAt("/schemaVersion", Integer.class); + this.manifests = childrenAt("/manifests", BlobReference::new); + } + + public Integer getSchemaVersion() { + return this.schemaVersion; + } + + public List getManifests() { + return this.manifests; + } + + /** + * Create an {@link ImageArchiveIndex} from the provided JSON input stream. + * @param content the JSON input stream + * @return a new {@link ImageArchiveIndex} instance + * @throws IOException on IO error + */ + public static ImageArchiveIndex of(InputStream content) throws IOException { + return of(content, ImageArchiveIndex::new); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifest.java index 9c8c1e8cce..1f5f70bdf9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ package org.springframework.boot.buildpack.platform.docker.type; import java.io.IOException; import java.io.InputStream; import java.lang.invoke.MethodHandles; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -28,18 +27,18 @@ import com.fasterxml.jackson.databind.JsonNode; import org.springframework.boot.buildpack.platform.json.MappedObject; /** - * Image archive manifest information. + * Image archive manifest information as provided by {@code manifest.json}. * * @author Scott Frederick * @since 2.7.10 */ public class ImageArchiveManifest extends MappedObject { - private final List entries = new ArrayList<>(); + private final List entries; protected ImageArchiveManifest(JsonNode node) { super(node, MethodHandles.lookup()); - getNode().elements().forEachRemaining((element) -> this.entries.add(ManifestEntry.of(element))); + this.entries = childrenAt(null, ManifestEntry::new); } /** @@ -77,10 +76,6 @@ public class ImageArchiveManifest extends MappedObject { return this.layers; } - static ManifestEntry of(JsonNode node) { - return new ManifestEntry(node); - } - @SuppressWarnings("unchecked") private List extractLayers() { List layers = valueAt("/Layers", List.class); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Manifest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Manifest.java new file mode 100644 index 0000000000..d3d9e50b25 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Manifest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.buildpack.platform.json.MappedObject; + +/** + * A manifest as defined in {@code application/vnd.docker.distribution.manifest} or + * {@code application/vnd.oci.image.manifest} files. + * + * @author Phillip Webb + * @since 3.2.6 + * @see OCI + * Image Manifest Specification + */ +public class Manifest extends MappedObject { + + private final Integer schemaVersion; + + private final String mediaType; + + private final List layers; + + protected Manifest(JsonNode node) { + super(node, MethodHandles.lookup()); + this.schemaVersion = valueAt("/schemaVersion", Integer.class); + this.mediaType = valueAt("/mediaType", String.class); + this.layers = childrenAt("/layers", BlobReference::new); + } + + public Integer getSchemaVersion() { + return this.schemaVersion; + } + + public String getMediaType() { + return this.mediaType; + } + + public List getLayers() { + return this.layers; + } + + /** + * Create an {@link Manifest} from the provided JSON input stream. + * @param content the JSON input stream + * @return a new {@link Manifest} instance + * @throws IOException on IO error + */ + public static Manifest of(InputStream content) throws IOException { + return of(content, Manifest::new); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ManifestList.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ManifestList.java new file mode 100644 index 0000000000..5d5fbcb64f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ManifestList.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.buildpack.platform.json.MappedObject; + +/** + * A distribution manifest list as defined in + * {@code application/vnd.docker.distribution.manifest.list} files. + * + * @author Phillip Webb + * @since 3.2.6 + * @see OCI + * Image Manifest Specification + */ +public class ManifestList extends MappedObject { + + private final Integer schemaVersion; + + private final String mediaType; + + private final List manifests; + + protected ManifestList(JsonNode node) { + super(node, MethodHandles.lookup()); + this.schemaVersion = valueAt("/schemaVersion", Integer.class); + this.mediaType = valueAt("/mediaType", String.class); + this.manifests = childrenAt("/manifests", BlobReference::new); + } + + public Integer getSchemaVersion() { + return this.schemaVersion; + } + + public String getMediaType() { + return this.mediaType; + } + + public Stream streamManifests() { + return getManifests().stream(); + } + + public List getManifests() { + return this.manifests; + } + + /** + * Create an {@link ManifestList} from the provided JSON input stream. + * @param content the JSON input stream + * @return a new {@link ManifestList} instance + * @throws IOException on IO error + */ + public static ManifestList of(InputStream content) throws IOException { + return of(content, ManifestList::new); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarArchive.java index 70a8e7ca97..c5d6e99ba1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarArchive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,15 @@ package org.springframework.boot.buildpack.platform.io; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.zip.GZIPInputStream; + +import org.springframework.util.StreamUtils; +import org.springframework.util.function.ThrowingFunction; /** * A TAR archive that can be written to an output stream. @@ -45,6 +50,15 @@ public interface TarArchive { */ void writeTo(OutputStream outputStream) throws IOException; + /** + * Return the compression being used with the tar archive. + * @return the used compression + * @since 3.2.6 + */ + default Compression getCompression() { + return Compression.NONE; + } + /** * Factory method to create a new {@link TarArchive} instance with a specific layout. * @param layout the TAR layout @@ -68,4 +82,68 @@ public interface TarArchive { return new ZipFileTarArchive(zip, owner); } + /** + * Factory method to adapt a ZIP file to {@link TarArchive}. Assumes that + * {@link #writeTo(OutputStream)} will only be called once. + * @param inputStream the source input stream + * @param compression the compression used + * @return a new {@link TarArchive} instance + * @since 3.2.6 + */ + static TarArchive fromInputStream(InputStream inputStream, Compression compression) { + return new TarArchive() { + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + StreamUtils.copy(compression.uncompress(inputStream), outputStream); + } + + @Override + public Compression getCompression() { + return compression; + } + + }; + } + + /** + * Compression type applied to the archive. + * + * @since 3.2.6 + */ + enum Compression { + + /** + * The tar file is not compressed. + */ + NONE((inputStream) -> inputStream), + + /** + * The tar file is compressed using gzip. + */ + GZIP(GZIPInputStream::new), + + /** + * The tar file is compressed using zstd. + */ + ZSTD("zstd compression is not supported"); + + private final ThrowingFunction uncompressor; + + Compression(String uncompressError) { + this((inputStream) -> { + throw new IllegalStateException(uncompressError); + }); + } + + Compression(ThrowingFunction wrapper) { + this.uncompressor = wrapper; + } + + InputStream uncompress(InputStream inputStream) { + return this.uncompressor.apply(inputStream); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java index 1e51ef5e4b..08a16a206a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,12 +23,16 @@ import java.lang.invoke.MethodHandles.Lookup; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.function.Function; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; /** * Base class for mapped JSON objects. @@ -71,6 +75,25 @@ public class MappedObject { return valueAt(this, this.node, this.lookup, expression, type); } + /** + * Get children at the given JSON path expression by constructing them using the given + * factory. + * @param the child type + * @param expression the JSON path expression + * @param factory factory used to create the child + * @return a list of children + * @since 3.2.6 + */ + protected List childrenAt(String expression, Function factory) { + JsonNode node = (expression != null) ? this.node.at(expression) : this.node; + if (node.isEmpty()) { + return Collections.emptyList(); + } + List children = new ArrayList<>(); + node.elements().forEachRemaining((childNode) -> children.add(factory.apply(childNode))); + return Collections.unmodifiableList(children); + } + @SuppressWarnings("unchecked") protected static T getRoot(Object proxy) { MappedInvocationHandler handler = (MappedInvocationHandler) Proxy.getInvocationHandler(proxy); @@ -128,7 +151,7 @@ public class MappedObject { */ protected static T of(InputStream content, Function factory) throws IOException { - return of(content, ObjectMapper::readTree, factory); + return of(StreamUtils.nonClosing(content), ObjectMapper::readTree, factory); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java index f42d6ee60b..e62cb63da6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java @@ -19,10 +19,10 @@ package org.springframework.boot.buildpack.platform.build; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -37,6 +37,8 @@ import org.mockito.invocation.InvocationOnMock; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.buildpack.platform.io.TarArchive.Compression; import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; import static org.assertj.core.api.Assertions.assertThat; @@ -176,7 +178,7 @@ class ImageBuildpackTests extends AbstractJsonTests { private Object withMockLayers(InvocationOnMock invocation) { try { - IOBiConsumer consumer = invocation.getArgument(1); + IOBiConsumer consumer = invocation.getArgument(1); File tarFile = File.createTempFile("create-builder-test-", null); FileOutputStream out = new FileOutputStream(tarFile); try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) { @@ -189,7 +191,7 @@ class ImageBuildpackTests extends AbstractJsonTests { writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/" + this.longFilePath); tarOut.finish(); } - consumer.accept("test", tarFile.toPath()); + consumer.accept("test", TarArchive.fromInputStream(new FileInputStream(tarFile), Compression.NONE)); Files.delete(tarFile.toPath()); } catch (IOException ex) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index 814914861b..69a32a82f4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -313,12 +313,14 @@ class DockerApiTests { } @Test + @SuppressWarnings("removal") void exportLayersWhenReferenceIsNullThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayerFiles(null, (name, archive) -> { })).withMessage("Reference must not be null"); } @Test + @SuppressWarnings("removal") void exportLayersWhenExportsIsNullThrowsException() { ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayerFiles(reference, null)) @@ -393,14 +395,15 @@ class DockerApiTests { } @Test + @SuppressWarnings("removal") void exportLayersWithNoManifestThrowsException() throws Exception { ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); URI exportUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/get"); given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export-no-manifest.tar")); - assertThatIllegalArgumentException() - .isThrownBy(() -> this.api.exportLayerFiles(reference, (name, archive) -> { - })) - .withMessageContaining("Manifest not found in image " + reference); + String expectedMessage = "Exported image '%s' does not contain 'index.json' or 'manifest.json'" + .formatted(reference); + assertThatIllegalStateException().isThrownBy(() -> this.api.exportLayerFiles(reference, (name, archive) -> { + })).withMessageContaining(expectedMessage); } @Test 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 new file mode 100644 index 0000000000..253ed2cb03 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.TarArchive.Compression; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ExportedImageTar}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +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" }) + void test(String tarFile) throws Exception { + ImageReference reference = ImageReference.of("test:latest"); + try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, + getClass().getResourceAsStream(tarFile))) { + Compression expectedCompression = (!tarFile.contains("containerd")) ? Compression.NONE : Compression.GZIP; + String expectedName = (expectedCompression != Compression.GZIP) + ? "5caae51697b248b905dca1a4160864b0e1a15c300981736555cdce6567e8d477" + : "f0f1fd1bdc71ac6a4dc99cea5f5e45c86c5ec26fe4d1daceeb78207303606429"; + exportedImageTar.exportLayers((name, tarArchive) -> { + assertThat(name).contains(expectedName); + assertThat(tarArchive.getCompression()).isEqualTo(expectedCompression); + }); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveIndexTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveIndexTests.java new file mode 100644 index 0000000000..87117ddd0f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveIndexTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ImageArchiveIndex}. + * + * @author Phillip Webb + */ +class ImageArchiveIndexTests extends AbstractJsonTests { + + @Test + void loadJson() throws IOException { + String content = getContentAsString("image-archive-index.json"); + ImageArchiveIndex index = getIndex(content); + assertThat(index.getSchemaVersion()).isEqualTo(2); + assertThat(index.getManifests()).hasSize(1); + BlobReference manifest = index.getManifests().get(0); + assertThat(manifest.getMediaType()).isEqualTo("application/vnd.docker.distribution.manifest.list.v2+json"); + assertThat(manifest.getDigest()) + .isEqualTo("sha256:3bbe02431d8e5124ffe816ec27bf6508b50edd1d10218be1a03e799a186b9004"); + } + + private ImageArchiveIndex getIndex(String content) throws IOException { + return new ImageArchiveIndex(getObjectMapper().readTree(content)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java index 8f0eaccd84..76fd174cb1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,8 @@ class ImageArchiveManifestTests extends AbstractJsonTests { @Test void getLayersReturnsLayers() throws Exception { - ImageArchiveManifest manifest = getManifest(); + String content = getContentAsString("image-archive-manifest.json"); + ImageArchiveManifest manifest = getManifest(content); List expectedLayers = new ArrayList<>(); for (int blankLayersCount = 0; blankLayersCount < 46; blankLayersCount++) { expectedLayers.add("blank_" + blankLayersCount); @@ -50,7 +51,7 @@ class ImageArchiveManifestTests extends AbstractJsonTests { @Test void getLayersWithNoLayersReturnsEmptyList() throws Exception { String content = "[{\"Layers\": []}]"; - ImageArchiveManifest manifest = new ImageArchiveManifest(getObjectMapper().readTree(content)); + ImageArchiveManifest manifest = getManifest(content); assertThat(manifest.getEntries()).hasSize(1); assertThat(manifest.getEntries().get(0).getLayers()).isEmpty(); } @@ -58,12 +59,12 @@ class ImageArchiveManifestTests extends AbstractJsonTests { @Test void getLayersWithEmptyManifestReturnsEmptyList() throws Exception { String content = "[]"; - ImageArchiveManifest manifest = new ImageArchiveManifest(getObjectMapper().readTree(content)); + ImageArchiveManifest manifest = getManifest(content); assertThat(manifest.getEntries()).isEmpty(); } - private ImageArchiveManifest getManifest() throws IOException { - return new ImageArchiveManifest(getObjectMapper().readTree(getContent("image-archive-manifest.json"))); + private ImageArchiveManifest getManifest(String content) throws IOException { + return new ImageArchiveManifest(getObjectMapper().readTree(content)); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ManifestListTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ManifestListTests.java new file mode 100644 index 0000000000..148b9cd935 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ManifestListTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ManifestList}. + * + * @author Phillip Webb + */ +class ManifestListTests extends AbstractJsonTests { + + @Test + void loadJsonFromDistributionManifestList() throws IOException { + String content = getContentAsString("distribution-manifest-list.json"); + ManifestList manifestList = getManifestList(content); + assertThat(manifestList.getSchemaVersion()).isEqualTo(2); + assertThat(manifestList.getMediaType()).isEqualTo("application/vnd.docker.distribution.manifest.list.v2+json"); + assertThat(manifestList.getManifests()).hasSize(2); + } + + private ManifestList getManifestList(String content) throws IOException { + return new ManifestList(getObjectMapper().readTree(content)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ManifestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ManifestTests.java new file mode 100644 index 0000000000..ac24305d0e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ManifestTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Manifest}. + * + * @author Phillip Webb + */ +class ManifestTests extends AbstractJsonTests { + + @Test + void loadJsonFromDistributionManifest() throws IOException { + String content = getContentAsString("distribution-manifest.json"); + Manifest manifestList = getManifest(content); + assertThat(manifestList.getSchemaVersion()).isEqualTo(2); + assertThat(manifestList.getMediaType()).isEqualTo("application/vnd.docker.distribution.manifest.v2+json"); + assertThat(manifestList.getLayers()).hasSize(1); + } + + @Test + void loadJsonFromImageManifest() throws IOException { + String content = getContentAsString("image-manifest.json"); + Manifest manifestList = getManifest(content); + assertThat(manifestList.getSchemaVersion()).isEqualTo(2); + assertThat(manifestList.getMediaType()).isEqualTo("application/vnd.oci.image.manifest.v1+json"); + assertThat(manifestList.getLayers()).hasSize(1); + } + + private Manifest getManifest(String content) throws IOException { + return new Manifest(getObjectMapper().readTree(content)); + } + +} 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-containerd-manifest-list.tar b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-docker-desktop-containerd-manifest-list.tar new file mode 100644 index 0000000000000000000000000000000000000000..09b9e04564c2a319031dfc36818b16f60d1d15a6 GIT binary patch literal 29696 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 CH+H%J literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-docker-engine.tar b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-docker-engine.tar new file mode 100644 index 0000000000000000000000000000000000000000..2ae031ee47d9d168401a87ed64c1b6cfeca25e32 GIT binary patch literal 17920 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 literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-podman.tar b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-podman.tar new file mode 100644 index 0000000000000000000000000000000000000000..d6f6b0813432ba158bba520ededb4bcc62256c9e GIT binary patch literal 14848 zcmeGiX>Zy{G@tz|rcR`^Rk6o5wt=>hXySCy=2AlU!zL&iJmWA8*sN{RMiKvg-}vB& z2uGk%4_io#=XmoDzjMZP31OyYRqeK3uC%Mzq%P4&S+j7(Dz`Dyh-SJ5#?^{u8xy$=}nq zu3Ltw!-uAAVGWwb7Bbh|cd6xt{1?k##R^umrPw)Zk`VA}^0#3@A^%yt+;rbk`k%^Q zZFB!w+do78rdi1US-f6#(TegGA{L_(3s9f;nMZh6l9<~GkX$O2_D~ao0@Q$@0F9!Cv;|>u~$Ok2fd+72Kl{~cb zkU<-&ho|TJ&h=rvarEWfL9Nz%6nB^}A(Y~N7=$jPDW)t$f~-NvWHJ7o$4HYrp1^g3 zUjK&qsPq}h6VTtvXb-8B-KqYd+a19My3id75%(uidHWvG03hdk1CBMc^BQ=|e9~hu z71j*jeB62cA!`aeXyCKiSmcBYt_;;VsALmUbRMl5;UsYOrltVQ(P)lyw)zp?S?}nDvv=c1_`AJ&jOmglSqld?p}B{7;cE8*ON z&s1<&mY$_1zwM$`?LTEPbK}+6vB>^o8)E(``>$aG;{UaCvSrZx!{h(u?Ef~>6tw@B zv;Q}09HRQ!*->M^{^`icH20OT#j~_>T6a@Ri*cf>+elZ?XD9I#iIecJ`y}oN4*^cZ zP|$(1e^duke&C!rCkM{Se#5yyuTRgKjnk9*(Hluvg1tRqrNNJ+i-$7$@WDAf0sx=# zlos{F;Eq!kfj{68qO8w+%6#`;+Jkt0g72{Uee`X>;NZ$B2aNzX1>DBI4Y!@E-7l9Z3X8BQFekXjeeq6=+4g z*YDmVLMe}V;FB)cap7BucmpJXB?8kFDFJ2dqg~>oLBvoTAiyE$-Z3CQeZ-P6UsAeq zWM|U_JYGh(ia9?$2UfEGxlSo@(CaQwny~-Yll4ES4MMR0$o|{9RoMUKIsE*Yla{i6 z)<9r@BCsVA>2n0O3@iF0E7mwb#HXmoVnRtwq|1?sX-S-lH4`O7b;1^AkW;8oPLf9k z^CGD^qEAqBoJcQ5hEtY0L67yx4HIiSaomq;s(QoY&Y-P8yq?!A!Rmd5*kr1PnIL}= zi_+%UJdQ9%-*~r+z6IeewBin!b0`MGR4{reDLhd6__wS1k^7)@zv9jPm z!GWKM1B@9Jow}xrE0_?AcqSt@9R8IxMhG0*>28&lEzQ)iQK?wEO+7d*v~kt7>?$rB zO!*oGi>kuH|37{HhXFX`f8z&Y0c;w2;s0OaGkK6n=yteB>P0=QCX*hu%JMte4H=0U zg&wV1!6mH0uZeL@{Iq{z0%TKh5Z=qR%ZEjihpl&V5CNzz_Xpo$Z3wR3gN_F|zVC}& zyW}1M4;Z1`^R79Cu}eAX5Z$zD=9)*;a%%|W1ir^_#EwU#&AQ3n;N3@|Ks0VD88S8b z;c9j;a|Lj&AU1+1CT&8r1P#%pU&-%aRT4%(*Cov3G#Q&2OI*poo2!)k10cO|kj&i> zhxL4in7*H!-cm;odI@cQ4{^8d#< zzkrI|yiZaHb&mgsQDa*Ex~7#2|38nV8ynM<7pbyu4BrCKW;w97{%;`uFI)Cw{l~aC z|KF^jpMdss{9gpjBBCz>=A~<`t7kqUU`|rAm~fwka!p*WnCQ69v$m{%QA$b!_m+j% zSt^i&<06S&*Umv=Gm*jmYEk|Z$`9Q=;&TtK^ej`yc^b<6kwx*JVOrDnU(;-@xc@z$ zsT&%F;>~_$>E(^o0zpyESwCo!o1})5AIc~pyMcmHc(2^`zLAhk9NdVz=Hr8+8eA8H z?@@-5(NS6yf=bXPvOp*UwKl}G-v~?yvWhqq@;Hlr6j_V(UnKitPWHh4f6X!r{r^$o ln1cHtudJb>h?*(s2K?$TS23GnU-43Kpx{8kfnSyb{{!@-M(zLr literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/distribution-manifest-list.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/distribution-manifest-list.json new file mode 100644 index 0000000000..d1e4f67643 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/distribution-manifest-list.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 428, + "digest": "sha256:6dba064234a3aa60f7da2e0f1f8b86dccb7df2841136f577b08bd6a89004cb23", + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 428, + "digest": "sha256:c036aba2c51a86a7a338f60af4730df725c2abff1b8b565d753896fd9533dfad", + "platform": { + "architecture": "arm64", + "os": "linux" + } + } + ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/distribution-manifest.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/distribution-manifest.json new file mode 100644 index 0000000000..0d41d2593f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/distribution-manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1175, + "digest": "sha256:b2160a0f9037918d3ca2270fb90f656f425760b337a5ed3813c3a48c09825065" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 4872935, + "digest": "sha256:13ac7da0441b95b1960de1b87ed2c1ef129026cc69b926ffbe734a7dcc4fa40c" + } + ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-index.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-index.json new file mode 100644 index 0000000000..4369b7ce05 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-index.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "digest": "sha256:3bbe02431d8e5124ffe816ec27bf6508b50edd1d10218be1a03e799a186b9004", + "size": 529, + "annotations": { + "containerd.io/distribution.source.gcr.io": "paketo-buildpacks/adoptium", + "io.containerd.image.name": "gcr.io/paketo-buildpacks/adoptium:latest", + "org.opencontainers.image.ref.name": "latest" + } + } + ] +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-manifest.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-manifest.json new file mode 100644 index 0000000000..5a91f5d567 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:ee382dc5c080aa6af5ea716041eaa4442c9d461520388627dfe51709c679043e", + "size": 849, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar", + "digest": "sha256:5caae51697b248b905dca1a4160864b0e1a15c300981736555cdce6567e8d477", + "size": 6656 + } + ] +} \ No newline at end of file