diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java index 4e943015525..c596ade2acd 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java @@ -49,6 +49,7 @@ import org.springframework.util.StreamUtils; * * @author Scott Frederick * @author Phillip Webb + * @author Junhwan Choi */ final class ImageBuildpack implements Buildpack { @@ -137,22 +138,17 @@ final class ImageBuildpack implements Buildpack { tarArchive.writeTo(out); } 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) { - String entryName = entry.getName(); - Path entryPath = Path.of(entryName); - Assert.state(entryPath.toAbsolutePath().equals(entryPath.toAbsolutePath().normalize()), - () -> "Malformed zip entry name '%s'".formatted(entryName)); - out.putArchiveEntry(entry); - StreamUtils.copy(in, out); - out.closeArchiveEntry(); - entry = in.getNextEntry(); - } - out.finish(); + try { + writeLayerFile(layerFile, sourceTarFile); + } + catch (IOException | RuntimeException ex) { + try { + Files.deleteIfExists(layerFile); } + catch (IOException suppressed) { + ex.addSuppressed(suppressed); + } + throw ex; } return layerFile; } @@ -161,6 +157,26 @@ final class ImageBuildpack implements Buildpack { } } + private void writeLayerFile(Path layerFile, Path sourceTarFile) throws IOException { + 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) { + String entryName = entry.getName(); + Path entryPath = Path.of(entryName); + Assert.state(entryPath.toAbsolutePath().equals(entryPath.toAbsolutePath().normalize()), + () -> "Malformed zip entry name '%s'".formatted(entryName)); + out.putArchiveEntry(entry); + StreamUtils.copy(in, out); + out.closeArchiveEntry(); + entry = in.getNextEntry(); + } + out.finish(); + } + } + } + void apply(IOConsumer layers) throws IOException { for (Path path : this.layerFiles) { layers.accept(Layer.fromTarArchive((out) -> { diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java index 4fcd192a3f3..fc61e9b33e5 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java @@ -61,6 +61,7 @@ import static org.mockito.Mockito.mock; * * @author Scott Frederick * @author Phillip Webb + * @author Junhwan Choi */ class ImageBuildpackTests extends AbstractJsonTests { @@ -200,6 +201,25 @@ class ImageBuildpackTests extends AbstractJsonTests { .withMessage("Malformed zip entry name '../cnb/'"); } + @Test + void resolveWhenLayerCreationFailsDeletesLayerTempFiles() throws Exception { + File tempDir = new File(System.getProperty("java.io.tmpdir")); + Set tempsBefore = listTempFileNames(tempDir, "create-builder-scratch-"); + Image image = Image.of(getContent("buildpack-image.json")); + ImageReference imageReference = ImageReference.of("example/buildpack1:latest"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}")); + given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); + willAnswer((invocation) -> withMockLayers(invocation, "..")).given(resolverContext) + .exportImageLayers(eq(imageReference), any()); + BuildpackReference reference = BuildpackReference.of("example/buildpack1"); + assertThatIllegalStateException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference)) + .withMessage("Malformed zip entry name '../cnb/'"); + Set tempsAfter = new HashSet<>(listTempFileNames(tempDir, "create-builder-scratch-")); + tempsAfter.removeAll(tempsBefore); + assertThat(tempsAfter).as("layer temp files must be deleted when layer creation fails").isEmpty(); + } + @Test void resolveDeletesIntermediateSourceTarTempFiles() throws Exception { File tempDir = new File(System.getProperty("java.io.tmpdir"));