Use image manifest when exporting layers
A tar archive of a Docker image contains a `mainfest.json` file that lists the path to each embedded tar file containing the contents of a layer in the image. This manifest file should be used to identify the layer files instead of relying on file naming conventions and assumptions on the directory structure that are not consistent between container engine implementations. Fixes gh-34324
This commit is contained in:
@@ -18,7 +18,10 @@ package org.springframework.boot.buildpack.platform.build;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
@@ -33,7 +36,6 @@ import org.mockito.invocation.InvocationOnMock;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
|
||||
import org.springframework.boot.buildpack.platform.io.TarArchive;
|
||||
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -173,20 +175,20 @@ class ImageBuildpackTests extends AbstractJsonTests {
|
||||
|
||||
private Object withMockLayers(InvocationOnMock invocation) {
|
||||
try {
|
||||
IOBiConsumer<String, TarArchive> consumer = invocation.getArgument(1);
|
||||
TarArchive archive = (out) -> {
|
||||
try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) {
|
||||
tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
|
||||
writeTarEntry(tarOut, "/cnb/");
|
||||
writeTarEntry(tarOut, "/cnb/buildpacks/");
|
||||
writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/");
|
||||
writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/");
|
||||
writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/buildpack.toml");
|
||||
writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/" + this.longFilePath);
|
||||
tarOut.finish();
|
||||
}
|
||||
};
|
||||
consumer.accept("test", archive);
|
||||
IOBiConsumer<String, Path> consumer = invocation.getArgument(1);
|
||||
File tarFile = File.createTempFile("create-builder-test-", null);
|
||||
FileOutputStream out = new FileOutputStream(tarFile);
|
||||
try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) {
|
||||
tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
|
||||
writeTarEntry(tarOut, "/cnb/");
|
||||
writeTarEntry(tarOut, "/cnb/buildpacks/");
|
||||
writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/");
|
||||
writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/");
|
||||
writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/buildpack.toml");
|
||||
writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/" + this.longFilePath);
|
||||
tarOut.finish();
|
||||
}
|
||||
consumer.accept("test", tarFile.toPath());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
fail("Error writing mock layers", ex);
|
||||
|
||||
@@ -21,6 +21,9 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||
@@ -310,14 +313,14 @@ class DockerApiTests {
|
||||
|
||||
@Test
|
||||
void exportLayersWhenReferenceIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayers(null, (name, archive) -> {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayerFiles(null, (name, archive) -> {
|
||||
})).withMessage("Reference must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportLayersWhenExportsIsNullThrowsException() {
|
||||
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayers(reference, null))
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayerFiles(reference, null))
|
||||
.withMessage("Exports must not be null");
|
||||
}
|
||||
|
||||
@@ -340,11 +343,62 @@ class DockerApiTests {
|
||||
}
|
||||
});
|
||||
assertThat(contents).hasSize(3)
|
||||
.containsKeys("1bf6c63a1e9ed1dd7cb961273bf60b8e0f440361faf273baf866f408e4910601/layer.tar",
|
||||
"8fdfb915302159a842cbfae6faec5311b00c071ebf14e12da7116ae7532e9319/layer.tar",
|
||||
"93cd584bb189bfca4f51744bd19d836fd36da70710395af5a1523ee88f208c6a/layer.tar");
|
||||
assertThat(contents.get("1bf6c63a1e9ed1dd7cb961273bf60b8e0f440361faf273baf866f408e4910601/layer.tar"))
|
||||
.containsExactly("etc/", "etc/apt/", "etc/apt/sources.list");
|
||||
.containsKeys("70bb7a3115f3d5c01099852112c7e05bf593789e510468edb06b6a9a11fa3b73/layer.tar",
|
||||
"74a9a50ece13c025cf10e9110d9ddc86c995079c34e2a22a28d1a3d523222c6e/layer.tar",
|
||||
"a69532b5b92bb891fbd9fa1a6b3af9087ea7050255f59ba61a796f8555ecd783/layer.tar");
|
||||
assertThat(contents.get("70bb7a3115f3d5c01099852112c7e05bf593789e510468edb06b6a9a11fa3b73/layer.tar"))
|
||||
.containsExactly("/cnb/order.toml");
|
||||
assertThat(contents.get("74a9a50ece13c025cf10e9110d9ddc86c995079c34e2a22a28d1a3d523222c6e/layer.tar"))
|
||||
.containsExactly("/cnb/stack.toml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportLayersWithSymlinksExportsLayerTars() 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");
|
||||
given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export-symlinks.tar"));
|
||||
MultiValueMap<String, String> contents = new LinkedMultiValueMap<>();
|
||||
this.api.exportLayers(reference, (name, archive) -> {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
archive.writeTo(out);
|
||||
try (TarArchiveInputStream in = new TarArchiveInputStream(
|
||||
new ByteArrayInputStream(out.toByteArray()))) {
|
||||
TarArchiveEntry entry = in.getNextTarEntry();
|
||||
while (entry != null) {
|
||||
contents.add(name, entry.getName());
|
||||
entry = in.getNextTarEntry();
|
||||
}
|
||||
}
|
||||
});
|
||||
assertThat(contents).hasSize(3)
|
||||
.containsKeys("6aa3691a73805f608e5fce69fb6bc89aec8362f58a6b4be2682515e9cfa3cc1a.tar",
|
||||
"762e198f655bc2580ef3e56b538810fd2b9981bd707f8a44c70344b58f9aee68.tar",
|
||||
"d3cc975ad97fdfbb73d9daf157e7f658d6117249fd9c237e3856ad173c87e1d2.tar");
|
||||
assertThat(contents.get("d3cc975ad97fdfbb73d9daf157e7f658d6117249fd9c237e3856ad173c87e1d2.tar"))
|
||||
.containsExactly("/cnb/order.toml");
|
||||
assertThat(contents.get("762e198f655bc2580ef3e56b538810fd2b9981bd707f8a44c70344b58f9aee68.tar"))
|
||||
.containsExactly("/cnb/stack.toml");
|
||||
}
|
||||
|
||||
@Test
|
||||
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");
|
||||
given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export.tar"));
|
||||
List<Path> layerFilePaths = new ArrayList<>();
|
||||
this.api.exportLayerFiles(reference, (name, path) -> layerFilePaths.add(path));
|
||||
layerFilePaths.forEach((path) -> assertThat(path.toFile()).doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportLayersWithNoManifestThrowsException() 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");
|
||||
given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export-no-manifest.tar"));
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.api.exportLayerFiles(reference, (name, archive) -> {
|
||||
}))
|
||||
.withMessageContaining("Manifest not found in image " + reference);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.buildpack.platform.docker.type;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ImageArchiveManifest}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ImageArchiveManifestTests extends AbstractJsonTests {
|
||||
|
||||
@Test
|
||||
void getLayersReturnsLayers() throws Exception {
|
||||
ImageArchiveManifest manifest = getManifest();
|
||||
List<String> expectedLayers = new ArrayList<>();
|
||||
for (int blankLayersCount = 0; blankLayersCount < 46; blankLayersCount++) {
|
||||
expectedLayers.add("blank_" + blankLayersCount);
|
||||
}
|
||||
expectedLayers.add("bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216.tar");
|
||||
assertThat(manifest.getEntries()).hasSize(1);
|
||||
assertThat(manifest.getEntries().get(0).getLayers()).hasSize(47);
|
||||
assertThat(manifest.getEntries().get(0).getLayers()).isEqualTo(expectedLayers);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLayersWithNoLayersReturnsEmptyList() throws Exception {
|
||||
String content = "[{\"Layers\": []}]";
|
||||
ImageArchiveManifest manifest = new ImageArchiveManifest(getObjectMapper().readTree(content));
|
||||
assertThat(manifest.getEntries()).hasSize(1);
|
||||
assertThat(manifest.getEntries().get(0).getLayers()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLayersWithEmptyManifestReturnsEmptyList() throws Exception {
|
||||
String content = "[]";
|
||||
ImageArchiveManifest manifest = new ImageArchiveManifest(getObjectMapper().readTree(content));
|
||||
assertThat(manifest.getEntries()).isEmpty();
|
||||
}
|
||||
|
||||
private ImageArchiveManifest getManifest() throws IOException {
|
||||
return new ImageArchiveManifest(getObjectMapper().readTree(getContent("image-archive-manifest.json")));
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user