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 <moritz.halbritter@broadcom.com> Co-authored-by: Scott Frederick <scott.frederick@broadcom.com>
This commit is contained in:
parent
79c3f0335b
commit
9e40970280
|
@ -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<String, Path> exports) throws IOException {
|
||||
Builder.this.docker.image().exportLayerFiles(reference, exports);
|
||||
public void exportImageLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports)
|
||||
throws IOException {
|
||||
Builder.this.docker.image().exportLayers(reference, exports);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<String, Path> exports) throws IOException;
|
||||
void exportImageLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports) throws IOException;
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Path> 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<Layer> layers) throws IOException {
|
||||
|
|
|
@ -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<String, Path> 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<String, TarArchive> 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<String, Path> 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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<String, TarArchive> 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<String, String> layerMediaTypes;
|
||||
|
||||
IndexLayerArchiveFactory(Path tarFile, ImageArchiveIndex index) throws IOException {
|
||||
Set<String> manifestDigests = getDigests(index, this::isManifest);
|
||||
List<ManifestList> manifestLists = getManifestLists(tarFile, getDigests(index, this::isManifestList));
|
||||
List<Manifest> manifests = getManifests(tarFile, manifestDigests, manifestLists);
|
||||
this.layerMediaTypes = manifests.stream()
|
||||
.flatMap((manifest) -> manifest.getLayers().stream())
|
||||
.collect(Collectors.toMap(this::getEntryName, BlobReference::getMediaType));
|
||||
}
|
||||
|
||||
private Set<String> getDigests(ImageArchiveIndex index, Predicate<BlobReference> predicate) {
|
||||
return index.getManifests()
|
||||
.stream()
|
||||
.filter(predicate)
|
||||
.map(BlobReference::getDigest)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private List<ManifestList> getManifestLists(Path tarFile, Set<String> digests) throws IOException {
|
||||
return getDigestMatches(tarFile, digests, ManifestList::of);
|
||||
}
|
||||
|
||||
private List<Manifest> getManifests(Path tarFile, Set<String> manifestDigests, List<ManifestList> manifestLists)
|
||||
throws IOException {
|
||||
Set<String> 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 <T> List<T> getDigestMatches(Path tarFile, Set<String> digests,
|
||||
ThrowingFunction<InputStream, T> factory) throws IOException {
|
||||
if (digests.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Set<String> names = digests.stream().map(this::getEntryName).collect(Collectors.toUnmodifiableSet());
|
||||
List<T> 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<String> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String> getDigests(JsonNode node) {
|
||||
if (node.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> digests = new ArrayList<>();
|
||||
node.forEach((child) -> digests.add(child.asText()));
|
||||
return Collections.unmodifiableList(digests);
|
||||
}
|
||||
|
||||
private List<LayerId> extractLayers(String[] layers) {
|
||||
if (layers == null) {
|
||||
return Collections.emptyList();
|
||||
|
|
|
@ -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 <a href=
|
||||
* "https://github.com/opencontainers/image-spec/blob/main/image-index.md">OCI Image Index
|
||||
* Specification</a>
|
||||
*/
|
||||
public class ImageArchiveIndex extends MappedObject {
|
||||
|
||||
private final Integer schemaVersion;
|
||||
|
||||
private final List<BlobReference> 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<BlobReference> 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<ManifestEntry> entries = new ArrayList<>();
|
||||
private final List<ManifestEntry> 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<String> extractLayers() {
|
||||
List<String> layers = valueAt("/Layers", List.class);
|
||||
|
|
|
@ -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 <a href="https://github.com/opencontainers/image-spec/blob/main/manifest.md">OCI
|
||||
* Image Manifest Specification</a>
|
||||
*/
|
||||
public class Manifest extends MappedObject {
|
||||
|
||||
private final Integer schemaVersion;
|
||||
|
||||
private final String mediaType;
|
||||
|
||||
private final List<BlobReference> 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<BlobReference> 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <a href="https://github.com/opencontainers/image-spec/blob/main/manifest.md">OCI
|
||||
* Image Manifest Specification</a>
|
||||
*/
|
||||
public class ManifestList extends MappedObject {
|
||||
|
||||
private final Integer schemaVersion;
|
||||
|
||||
private final String mediaType;
|
||||
|
||||
private final List<BlobReference> 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<BlobReference> streamManifests() {
|
||||
return getManifests().stream();
|
||||
}
|
||||
|
||||
public List<BlobReference> 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<InputStream, InputStream> uncompressor;
|
||||
|
||||
Compression(String uncompressError) {
|
||||
this((inputStream) -> {
|
||||
throw new IllegalStateException(uncompressError);
|
||||
});
|
||||
}
|
||||
|
||||
Compression(ThrowingFunction<InputStream, InputStream> wrapper) {
|
||||
this.uncompressor = wrapper;
|
||||
}
|
||||
|
||||
InputStream uncompress(InputStream inputStream) {
|
||||
return this.uncompressor.apply(inputStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 <T> 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 <T> List<T> childrenAt(String expression, Function<JsonNode, T> factory) {
|
||||
JsonNode node = (expression != null) ? this.node.at(expression) : this.node;
|
||||
if (node.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<T> children = new ArrayList<>();
|
||||
node.elements().forEachRemaining((childNode) -> children.add(factory.apply(childNode)));
|
||||
return Collections.unmodifiableList(children);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected static <T extends MappedObject> T getRoot(Object proxy) {
|
||||
MappedInvocationHandler handler = (MappedInvocationHandler) Proxy.getInvocationHandler(proxy);
|
||||
|
@ -128,7 +151,7 @@ public class MappedObject {
|
|||
*/
|
||||
protected static <T extends MappedObject> T of(InputStream content, Function<JsonNode, T> factory)
|
||||
throws IOException {
|
||||
return of(content, ObjectMapper::readTree, factory);
|
||||
return of(StreamUtils.nonClosing(content), ObjectMapper::readTree, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<String, Path> consumer = invocation.getArgument(1);
|
||||
IOBiConsumer<String, TarArchive> 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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String> 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue