Fix exporting of Docker image layers

The logic to extract layers from a downloaded Docker image assumed that
the layer entries in the image tar archive always had the file extension
`.tar`. This was the case with Docker and other compatible daemons until
Docker 25.0. With this commit, the extension is no longer assumed, but
any entries listed in `manifest.json` will be recognized.

Fixes gh-39323
This commit is contained in:
Scott Frederick 2024-01-30 16:00:59 -06:00
parent 1c2a622f7f
commit a620d348ad
3 changed files with 26 additions and 20 deletions

View File

@ -18,6 +18,7 @@ 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;
@ -30,9 +31,7 @@ import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@ -293,29 +292,20 @@ public class DockerApi {
Assert.notNull(exports, "Exports must not be null");
URI saveUri = buildUrl("/images/" + reference + "/get");
Response response = http().get(saveUri);
ImageArchiveManifest manifest = null;
Map<String, Path> layerFiles = new HashMap<>();
try (TarArchiveInputStream tar = new TarArchiveInputStream(response.getContent())) {
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 (entry.getName().equals("manifest.json")) {
manifest = readManifest(tar);
}
if (entry.getName().endsWith(".tar")) {
layerFiles.put(entry.getName(), copyToTemp(tar));
if (manifestContainsLayerEntry(manifest, entry.getName())) {
Path layerFile = copyToTemp(tar);
exports.accept(entry.getName(), layerFile);
Files.delete(layerFile);
}
entry = tar.getNextEntry();
}
}
Assert.notNull(manifest, "Manifest not found in image " + reference);
for (Map.Entry<String, Path> entry : layerFiles.entrySet()) {
String name = entry.getKey();
Path path = entry.getValue();
if (manifestContainsLayerEntry(manifest, name)) {
exports.accept(name, path);
}
Files.delete(path);
}
Files.delete(exportFile);
}
/**
@ -355,13 +345,26 @@ 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(TarArchiveInputStream in) throws IOException {
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);

View File

@ -21,6 +21,7 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
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;
@ -189,6 +190,7 @@ class ImageBuildpackTests extends AbstractJsonTests {
tarOut.finish();
}
consumer.accept("test", tarFile.toPath());
Files.delete(tarFile.toPath());
}
catch (IOException ex) {
fail("Error writing mock layers", ex);

View File

@ -382,6 +382,7 @@ class DockerApiTests {
}
@Test
@SuppressWarnings("removal")
void exportLayerFilesDeletesTempFiles() 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");