Merge branch '4.0.x' into 4.1.x

Closes gh-51132
This commit is contained in:
Stéphane Nicoll
2026-07-27 10:04:09 +02:00
2 changed files with 59 additions and 5 deletions
@@ -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<String, TarArchive> exports) throws IOException {
@@ -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.docker.type.ImageReference;
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<Path> 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<Path> 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 @@ class ExportedImageTarTests {
});
assertThat(names).filteredOn((name) -> name.contains(expectedName)).isNotEmpty();
}
then(inputStream).should().close();
assertThat(temporaryDockerFile).doesNotExist();
}
@Test
void constructorWhenTarHasNoIndexOrManifestDeletesTempFile() throws IOException {
List<Path> 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<Path> scanTemporaryDockerLayersTarFiles() throws IOException {
Path tempDir = Path.of(System.getProperty("java.io.tmpdir"));
try (Stream<Path> 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));
}
}
}