From a620d348ad74daa15edfbcf5e9098e4f9694fb86 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 30 Jan 2024 16:00:59 -0600 Subject: [PATCH] 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 --- .../buildpack/platform/docker/DockerApi.java | 43 ++++++++++--------- .../platform/build/ImageBuildpackTests.java | 2 + .../platform/docker/DockerApiTests.java | 1 + 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 8847290f6c0..ad53e823d27 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -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 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 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); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java index cebc8c0bd47..7978de12eb2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java @@ -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); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index 6bb99134266..f751ada70cd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -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");