diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java index 776593c22283..607f11632041 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTar.java @@ -63,9 +63,20 @@ class ExportedImageTar implements Closeable { 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); + this.tarFile = Files.createTempFile("docker-layers-", ".tar"); + try (inputStream) { + Files.copy(inputStream, this.tarFile, StandardCopyOption.REPLACE_EXISTING); + this.layerArchiveFactory = LayerArchiveFactory.create(reference, this.tarFile); + } + catch (IOException | RuntimeException ex) { + try { + Files.deleteIfExists(this.tarFile); + } + catch (IOException suppressed) { + ex.addSuppressed(suppressed); + } + throw ex; + } } void exportLayers(IOBiConsumer exports) throws IOException { diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java index 5f306b2d4322..daf3f6422ff7 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ExportedImageTarTests.java @@ -16,9 +16,19 @@ package org.springframework.boot.buildpack.platform.docker; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -26,6 +36,9 @@ import org.springframework.boot.buildpack.platform.io.TarArchive.Compression; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; /** * Tests for {@link ExportedImageTar}. @@ -40,9 +53,15 @@ class ExportedImageTarTests { "export-docker-desktop-containerd-manifest-list.tar", "export-docker-engine.tar", "export-podman.tar", "export-docker-desktop-nested-index.tar", "export-docker-desktop-containerd-alt-mediatype.tar" }) void test(String tarFile) throws Exception { + List temporaryDockerLayersTarFilesBefore = scanTemporaryDockerLayersTarFiles(); ImageReference reference = ImageReference.of("test:latest"); - try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, - getClass().getResourceAsStream(tarFile))) { + Path temporaryDockerFile; + InputStream inputStream = spy(Objects.requireNonNull(getClass().getResourceAsStream(tarFile))); + try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, inputStream)) { + List temporaryDockerLayersTarFilesCurrent = scanTemporaryDockerLayersTarFiles(); + temporaryDockerLayersTarFilesCurrent.removeAll(temporaryDockerLayersTarFilesBefore); + assertThat(temporaryDockerLayersTarFilesCurrent).hasSize(1); + temporaryDockerFile = temporaryDockerLayersTarFilesCurrent.get(0); Compression expectedCompression = (!tarFile.contains("containerd")) ? Compression.NONE : Compression.GZIP; String expectedName = (expectedCompression != Compression.GZIP) ? "5caae51697b248b905dca1a4160864b0e1a15c300981736555cdce6567e8d477" @@ -54,6 +73,30 @@ void test(String tarFile) throws Exception { }); assertThat(names).filteredOn((name) -> name.contains(expectedName)).isNotEmpty(); } + then(inputStream).should().close(); + assertThat(temporaryDockerFile).doesNotExist(); + } + + @Test + void constructorWhenTarHasNoIndexOrManifestDeletesTempFile() throws IOException { + List temporaryDockerLayersTarFilesBefore = scanTemporaryDockerLayersTarFiles(); + ImageReference reference = ImageReference.of("test:latest"); + InputStream notATarFile = spy(new ByteArrayInputStream("not-a-tar-file".getBytes(StandardCharsets.UTF_8))); + assertThatIllegalStateException().isThrownBy(() -> new ExportedImageTar(reference, notATarFile)) + .withMessageContaining("does not contain 'index.json' or 'manifest.json'"); + then(notATarFile).should().close(); + assertThat(scanTemporaryDockerLayersTarFiles()).containsOnlyOnceElementsOf(temporaryDockerLayersTarFilesBefore) + .hasSize(temporaryDockerLayersTarFilesBefore.size()); + } + + private static List scanTemporaryDockerLayersTarFiles() throws IOException { + Path tempDir = Path.of(System.getProperty("java.io.tmpdir")); + try (Stream stream = Files.list(tempDir)) { + return stream.filter(Files::isRegularFile).filter((candidate) -> { + String filename = candidate.getFileName().toString(); + return filename.startsWith("docker-layers") && filename.endsWith(".tar"); + }).collect(Collectors.toCollection(ArrayList::new)); + } } }