commit
abdff95ad0
|
@ -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.getNextEntry();
|
||||
while (entry != null) {
|
||||
tarOut.putArchiveEntry(entry);
|
||||
StreamUtils.copy(tarIn, tarOut);
|
||||
tarOut.closeArchiveEntry();
|
||||
entry = tarIn.getNextEntry();
|
||||
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.getNextEntry();
|
||||
while (entry != null) {
|
||||
out.putArchiveEntry(entry);
|
||||
StreamUtils.copy(in, out);
|
||||
out.closeArchiveEntry();
|
||||
entry = in.getNextEntry();
|
||||
}
|
||||
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.getNextEntry();
|
||||
while (entry != null) {
|
||||
if (manifestContainsLayerEntry(manifest, entry.getName())) {
|
||||
Path layerFile = copyToTemp(tar);
|
||||
exports.accept(entry.getName(), layerFile);
|
||||
Files.delete(layerFile);
|
||||
}
|
||||
entry = tar.getNextEntry();
|
||||
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.getNextEntry();
|
||||
while (entry != null) {
|
||||
if (entry.getName().equals("manifest.json")) {
|
||||
return readManifest(tar);
|
||||
}
|
||||
entry = tar.getNextEntry();
|
||||
}
|
||||
}
|
||||
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.getNextEntry();
|
||||
while (entry != null) {
|
||||
TarArchive layerArchive = this.layerArchiveFactory.getLayerArchive(tar, entry);
|
||||
if (layerArchive != null) {
|
||||
exports.accept(entry.getName(), layerArchive);
|
||||
}
|
||||
entry = tar.getNextEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.getNextEntry();
|
||||
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.getNextEntry();
|
||||
}
|
||||
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.getNextEntry();
|
||||
while (entry != null) {
|
||||
if (names.contains(entry.getName())) {
|
||||
result.add(factory.apply(tar));
|
||||
}
|
||||
entry = tar.getNextEntry();
|
||||
}
|
||||
}
|
||||
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