Merge branch '3.2.x'

Closes gh-40526
This commit is contained in:
Phillip Webb 2024-04-25 12:54:13 -07:00
commit abdff95ad0
29 changed files with 1014 additions and 139 deletions

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
}
}
]
}

View File

@ -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"
}
]
}

View File

@ -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"
}
}
]
}

View File

@ -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
}
]
}