Skip to content

Commit 9e40970

Browse files
philwebbmhalbritterscottfrederick
committed
Support gzip compressed image layers
Update buildpack support to allow gzip compressed image layers to be used when returned by the Docker engine. This update is restores buildpack support when using Docker Desktop with the "Use containerd for pulling and storing images" option enabled. This commit introduces a new `ExportedImageTar` class to deal with the intricacies of determining the mimetype of a layer. The class deals with the parsing of `index.json' and related manifest blobs in order to obtain layer information. The legacy `manifest.json` format is also supported should `index.json` be missing. Tests have been added to ensure that export archives from Docker Engine, Docker Desktop (with and without containerd), and Podman can be used. Fixes gh-40100 Co-authored-by: Moritz Halbritter <[email protected]> Co-authored-by: Scott Frederick <[email protected]>
1 parent 79c3f03 commit 9e40970

29 files changed

+1010
-135
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@
1717
package org.springframework.boot.buildpack.platform.build;
1818

1919
import java.io.IOException;
20-
import java.nio.file.Path;
2120
import java.util.List;
2221
import java.util.function.Consumer;
2322

@@ -33,6 +32,7 @@
3332
import org.springframework.boot.buildpack.platform.docker.type.Image;
3433
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
3534
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
35+
import org.springframework.boot.buildpack.platform.io.TarArchive;
3636
import org.springframework.util.Assert;
3737
import org.springframework.util.StringUtils;
3838

@@ -273,8 +273,9 @@ public Image fetchImage(ImageReference reference, ImageType imageType) throws IO
273273
}
274274

275275
@Override
276-
public void exportImageLayers(ImageReference reference, IOBiConsumer<String, Path> exports) throws IOException {
277-
Builder.this.docker.image().exportLayerFiles(reference, exports);
276+
public void exportImageLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports)
277+
throws IOException {
278+
Builder.this.docker.image().exportLayers(reference, exports);
278279
}
279280

280281
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,12 +17,12 @@
1717
package org.springframework.boot.buildpack.platform.build;
1818

1919
import java.io.IOException;
20-
import java.nio.file.Path;
2120
import java.util.List;
2221

2322
import org.springframework.boot.buildpack.platform.docker.type.Image;
2423
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
2524
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
25+
import org.springframework.boot.buildpack.platform.io.TarArchive;
2626

2727
/**
2828
* Context passed to a {@link BuildpackResolver}.
@@ -52,6 +52,6 @@ interface BuildpackResolverContext {
5252
* during the callback)
5353
* @throws IOException on IO error
5454
*/
55-
void exportImageLayers(ImageReference reference, IOBiConsumer<String, Path> exports) throws IOException;
55+
void exportImageLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports) throws IOException;
5656

5757
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java

+20-19
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.boot.buildpack.platform.docker.type.Layer;
3737
import org.springframework.boot.buildpack.platform.docker.type.LayerId;
3838
import org.springframework.boot.buildpack.platform.io.IOConsumer;
39+
import org.springframework.boot.buildpack.platform.io.TarArchive;
3940
import org.springframework.util.StreamUtils;
4041

4142
/**
@@ -115,31 +116,31 @@ private static class ExportedLayers {
115116

116117
ExportedLayers(BuildpackResolverContext context, ImageReference imageReference) throws IOException {
117118
List<Path> layerFiles = new ArrayList<>();
118-
context.exportImageLayers(imageReference, (name, path) -> layerFiles.add(copyToTemp(path)));
119+
context.exportImageLayers(imageReference,
120+
(name, tarArchive) -> layerFiles.add(createLayerFile(tarArchive)));
119121
this.layerFiles = Collections.unmodifiableList(layerFiles);
120122
}
121123

122-
private Path copyToTemp(Path path) throws IOException {
123-
Path outputPath = Files.createTempFile("create-builder-scratch-", null);
124-
try (OutputStream out = Files.newOutputStream(outputPath)) {
125-
copyLayerTar(path, out);
124+
private Path createLayerFile(TarArchive tarArchive) throws IOException {
125+
Path sourceTarFile = Files.createTempFile("create-builder-scratch-source-", null);
126+
try (OutputStream out = Files.newOutputStream(sourceTarFile)) {
127+
tarArchive.writeTo(out);
126128
}
127-
return outputPath;
128-
}
129-
130-
private void copyLayerTar(Path path, OutputStream out) throws IOException {
131-
try (TarArchiveInputStream tarIn = new TarArchiveInputStream(Files.newInputStream(path));
132-
TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) {
133-
tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
134-
TarArchiveEntry entry = tarIn.getNextTarEntry();
135-
while (entry != null) {
136-
tarOut.putArchiveEntry(entry);
137-
StreamUtils.copy(tarIn, tarOut);
138-
tarOut.closeArchiveEntry();
139-
entry = tarIn.getNextTarEntry();
129+
Path layerFile = Files.createTempFile("create-builder-scratch-", null);
130+
try (TarArchiveOutputStream out = new TarArchiveOutputStream(Files.newOutputStream(layerFile))) {
131+
try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(sourceTarFile))) {
132+
out.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
133+
TarArchiveEntry entry = in.getNextTarEntry();
134+
while (entry != null) {
135+
out.putArchiveEntry(entry);
136+
StreamUtils.copy(in, out);
137+
out.closeArchiveEntry();
138+
entry = in.getNextTarEntry();
139+
}
140+
out.finish();
140141
}
141-
tarOut.finish();
142142
}
143+
return layerFile;
143144
}
144145

145146
void apply(IOConsumer<Layer> layers) throws IOException {

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java

+32-72
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,18 @@
1616

1717
package org.springframework.boot.buildpack.platform.docker;
1818

19-
import java.io.BufferedReader;
20-
import java.io.ByteArrayInputStream;
21-
import java.io.FileInputStream;
2219
import java.io.IOException;
23-
import java.io.InputStream;
24-
import java.io.InputStreamReader;
2520
import java.io.OutputStream;
2621
import java.net.URI;
2722
import java.net.URISyntaxException;
28-
import java.nio.charset.StandardCharsets;
2923
import java.nio.file.Files;
3024
import java.nio.file.Path;
3125
import java.util.Arrays;
3226
import java.util.Collection;
3327
import java.util.Collections;
3428
import java.util.List;
3529
import java.util.Objects;
36-
import java.util.stream.Collectors;
3730

38-
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
39-
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
4031
import org.apache.hc.core5.net.URIBuilder;
4132

4233
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration;
@@ -48,15 +39,13 @@
4839
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
4940
import org.springframework.boot.buildpack.platform.docker.type.Image;
5041
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
51-
import org.springframework.boot.buildpack.platform.docker.type.ImageArchiveManifest;
5242
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
5343
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
5444
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
5545
import org.springframework.boot.buildpack.platform.io.TarArchive;
5646
import org.springframework.boot.buildpack.platform.json.JsonStream;
5747
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
5848
import org.springframework.util.Assert;
59-
import org.springframework.util.StreamUtils;
6049
import org.springframework.util.StringUtils;
6150

6251
/**
@@ -263,49 +252,50 @@ public void load(ImageArchive archive, UpdateListener<LoadImageUpdateEvent> list
263252
}
264253

265254
/**
266-
* Export the layers of an image as {@link TarArchive}s.
255+
* Export the layers of an image as paths to layer tar files.
267256
* @param reference the reference to export
268-
* @param exports a consumer to receive the layers (contents can only be accessed
269-
* during the callback)
257+
* @param exports a consumer to receive the layer tar file paths (file can only be
258+
* accessed during the callback)
270259
* @throws IOException on IO error
260+
* @since 2.7.10
261+
* @deprecated since 3.2.6 for removal in 3.5.0 in favor of
262+
* {@link #exportLayers(ImageReference, IOBiConsumer)}
271263
*/
272-
public void exportLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports)
273-
throws IOException {
274-
exportLayerFiles(reference, (name, path) -> {
275-
try (InputStream in = Files.newInputStream(path)) {
276-
TarArchive archive = (out) -> StreamUtils.copy(in, out);
277-
exports.accept(name, archive);
264+
@Deprecated(since = "3.2.6", forRemoval = true)
265+
public void exportLayerFiles(ImageReference reference, IOBiConsumer<String, Path> exports) throws IOException {
266+
Assert.notNull(reference, "Reference must not be null");
267+
Assert.notNull(exports, "Exports must not be null");
268+
exportLayers(reference, (name, archive) -> {
269+
Path path = Files.createTempFile("docker-export-layer-files-", null);
270+
try {
271+
try (OutputStream out = Files.newOutputStream(path)) {
272+
archive.writeTo(out);
273+
exports.accept(name, path);
274+
}
275+
}
276+
finally {
277+
Files.delete(path);
278278
}
279279
});
280280
}
281281

282282
/**
283-
* Export the layers of an image as paths to layer tar files.
283+
* Export the layers of an image as {@link TarArchive TarArchives}.
284284
* @param reference the reference to export
285-
* @param exports a consumer to receive the layer tar file paths (file can only be
286-
* accessed during the callback)
285+
* @param exports a consumer to receive the layers (contents can only be accessed
286+
* during the callback)
287287
* @throws IOException on IO error
288-
* @since 2.7.10
289288
*/
290-
public void exportLayerFiles(ImageReference reference, IOBiConsumer<String, Path> exports) throws IOException {
289+
public void exportLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports)
290+
throws IOException {
291291
Assert.notNull(reference, "Reference must not be null");
292292
Assert.notNull(exports, "Exports must not be null");
293-
URI saveUri = buildUrl("/images/" + reference + "/get");
294-
Response response = http().get(saveUri);
295-
Path exportFile = copyToTemp(response.getContent());
296-
ImageArchiveManifest manifest = getManifest(reference, exportFile);
297-
try (TarArchiveInputStream tar = new TarArchiveInputStream(new FileInputStream(exportFile.toFile()))) {
298-
TarArchiveEntry entry = tar.getNextTarEntry();
299-
while (entry != null) {
300-
if (manifestContainsLayerEntry(manifest, entry.getName())) {
301-
Path layerFile = copyToTemp(tar);
302-
exports.accept(entry.getName(), layerFile);
303-
Files.delete(layerFile);
304-
}
305-
entry = tar.getNextTarEntry();
293+
URI uri = buildUrl("/images/" + reference + "/get");
294+
try (Response response = http().get(uri)) {
295+
try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, response.getContent())) {
296+
exportedImageTar.exportLayers(exports);
306297
}
307298
}
308-
Files.delete(exportFile);
309299
}
310300

311301
/**
@@ -345,37 +335,6 @@ public void tag(ImageReference sourceReference, ImageReference targetReference)
345335
http().post(uri).close();
346336
}
347337

348-
private ImageArchiveManifest getManifest(ImageReference reference, Path exportFile) throws IOException {
349-
try (TarArchiveInputStream tar = new TarArchiveInputStream(new FileInputStream(exportFile.toFile()))) {
350-
TarArchiveEntry entry = tar.getNextTarEntry();
351-
while (entry != null) {
352-
if (entry.getName().equals("manifest.json")) {
353-
return readManifest(tar);
354-
}
355-
entry = tar.getNextTarEntry();
356-
}
357-
}
358-
throw new IllegalArgumentException("Manifest not found in image " + reference);
359-
}
360-
361-
private ImageArchiveManifest readManifest(TarArchiveInputStream tar) throws IOException {
362-
String manifestContent = new BufferedReader(new InputStreamReader(tar, StandardCharsets.UTF_8)).lines()
363-
.collect(Collectors.joining());
364-
return ImageArchiveManifest.of(new ByteArrayInputStream(manifestContent.getBytes(StandardCharsets.UTF_8)));
365-
}
366-
367-
private Path copyToTemp(InputStream in) throws IOException {
368-
Path path = Files.createTempFile("create-builder-scratch-", null);
369-
try (OutputStream out = Files.newOutputStream(path)) {
370-
StreamUtils.copy(in, out);
371-
}
372-
return path;
373-
}
374-
375-
private boolean manifestContainsLayerEntry(ImageArchiveManifest manifest, String layerId) {
376-
return manifest.getEntries().stream().anyMatch((content) -> content.getLayers().contains(layerId));
377-
}
378-
379338
}
380339

381340
/**
@@ -458,8 +417,9 @@ public void logs(ContainerReference reference, UpdateListener<LogUpdateEvent> li
458417
public ContainerStatus wait(ContainerReference reference) throws IOException {
459418
Assert.notNull(reference, "Reference must not be null");
460419
URI uri = buildUrl("/containers/" + reference + "/wait");
461-
Response response = http().post(uri);
462-
return ContainerStatus.of(response.getContent());
420+
try (Response response = http().post(uri)) {
421+
return ContainerStatus.of(response.getContent());
422+
}
463423
}
464424

465425
/**

0 commit comments

Comments
 (0)