From 7730eee43998ac6009a71aff4c0a30427f9404fe Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Mon, 27 Feb 2023 14:38:29 -0600 Subject: [PATCH] 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 --- .../buildpack/platform/build/Builder.java | 9 +- .../build/BuildpackResolverContext.java | 6 +- .../platform/build/ImageBuildpack.java | 32 +++--- .../buildpack/platform/docker/DockerApi.java | 67 +++++++++++- .../docker/type/ImageArchiveManifest.java | 95 ++++++++++++++++++ .../platform/build/ImageBuildpackTests.java | 32 +++--- .../platform/docker/DockerApiTests.java | 68 +++++++++++-- .../type/ImageArchiveManifestTests.java | 69 +++++++++++++ .../platform/docker/export-no-manifest.tar | Bin 0 -> 10240 bytes .../platform/docker/export-symlinks.tar | Bin 0 -> 71680 bytes .../boot/buildpack/platform/docker/export.tar | Bin 22016 -> 69120 bytes 11 files changed, 329 insertions(+), 49 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifest.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-no-manifest.tar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-symlinks.tar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index 8d0e4959bf..367e52d616 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -17,6 +17,7 @@ package org.springframework.boot.buildpack.platform.build; import java.io.IOException; +import java.nio.file.Path; import java.util.List; import java.util.function.Consumer; @@ -32,7 +33,6 @@ import org.springframework.boot.buildpack.platform.docker.transport.DockerEngine 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.util.Assert; import org.springframework.util.StringUtils; @@ -273,9 +273,8 @@ public class Builder { } @Override - public void exportImageLayers(ImageReference reference, IOBiConsumer exports) - throws IOException { - Builder.this.docker.image().exportLayers(reference, exports); + public void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException { + Builder.this.docker.image().exportLayerFiles(reference, exports); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java index b20dd5b969..fd8e59feff 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -17,12 +17,12 @@ package org.springframework.boot.buildpack.platform.build; import java.io.IOException; +import java.nio.file.Path; import java.util.List; 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; /** * Context passed to a {@link BuildpackResolver}. @@ -52,6 +52,6 @@ interface BuildpackResolverContext { * during the callback) * @throws IOException on IO error */ - void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException; + void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java index a798b226e6..5a76b8ab0c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java @@ -17,6 +17,7 @@ package org.springframework.boot.buildpack.platform.build; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -35,7 +36,6 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.Layer; import org.springframework.boot.buildpack.platform.docker.type.LayerId; import org.springframework.boot.buildpack.platform.io.IOConsumer; -import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.util.StreamUtils; /** @@ -115,23 +115,16 @@ final class ImageBuildpack implements Buildpack { ExportedLayers(BuildpackResolverContext context, ImageReference imageReference) throws IOException { List layerFiles = new ArrayList<>(); - context.exportImageLayers(imageReference, (name, archive) -> layerFiles.add(copyToTemp(name, archive))); + context.exportImageLayers(imageReference, (name, path) -> layerFiles.add(copyToTemp(path))); this.layerFiles = Collections.unmodifiableList(layerFiles); } - private Path copyToTemp(String name, TarArchive archive) throws IOException { - String[] parts = name.split("/"); - Path path = Files.createTempFile("create-builder-scratch-", parts[0]); - try (OutputStream out = Files.newOutputStream(path)) { - archive.writeTo(out); - } - return path; - } - - void apply(IOConsumer layers) throws IOException { - for (Path path : this.layerFiles) { - layers.accept(Layer.fromTarArchive((out) -> copyLayerTar(path, out))); + private Path copyToTemp(Path path) throws IOException { + Path outputPath = Files.createTempFile("create-builder-scratch-", null); + try (OutputStream out = Files.newOutputStream(outputPath)) { + copyLayerTar(path, out); } + return outputPath; } private void copyLayerTar(Path path, OutputStream out) throws IOException { @@ -147,7 +140,16 @@ final class ImageBuildpack implements Buildpack { } tarOut.finish(); } - Files.delete(path); + } + + void apply(IOConsumer layers) throws IOException { + for (Path path : this.layerFiles) { + layers.accept(Layer.fromTarArchive((out) -> { + InputStream in = Files.newInputStream(path); + StreamUtils.copy(in, out); + })); + Files.delete(path); + } } } 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 6a27db1059..719113f3b2 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 @@ -16,13 +16,24 @@ package org.springframework.boot.buildpack.platform.docker; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +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.stream.Collectors; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; @@ -37,6 +48,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ContainerReferenc import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; +import org.springframework.boot.buildpack.platform.docker.type.ImageArchiveManifest; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.boot.buildpack.platform.io.IOBiConsumer; @@ -250,7 +262,7 @@ public class DockerApi { } /** - * Export the layers of an image. + * Export the layers of an image as {@link TarArchive}s. * @param reference the reference to export * @param exports a consumer to receive the layers (contents can only be accessed * during the callback) @@ -258,20 +270,49 @@ public class DockerApi { */ public void exportLayers(ImageReference reference, IOBiConsumer exports) throws IOException { + exportLayerFiles(reference, (name, path) -> { + try (InputStream in = Files.newInputStream(path)) { + TarArchive archive = (out) -> StreamUtils.copy(in, out); + exports.accept(name, archive); + } + }); + } + + /** + * Export the layers of an image as paths to layer tar files. + * @param reference the reference to export + * @param exports a consumer to receive the layer tar file paths (file can only be + * accessed during the callback) + * @throws IOException on IO error + */ + public void exportLayerFiles(ImageReference reference, IOBiConsumer exports) throws IOException { Assert.notNull(reference, "Reference must not be null"); 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())) { TarArchiveEntry entry = tar.getNextTarEntry(); while (entry != null) { - if (entry.getName().endsWith("/layer.tar")) { - TarArchive archive = (out) -> StreamUtils.copy(tar, out); - exports.accept(entry.getName(), archive); + if (entry.getName().equals("manifest.json")) { + manifest = readManifest(tar); + } + if (entry.getName().endsWith(".tar")) { + layerFiles.put(entry.getName(), copyToTemp(tar)); } entry = tar.getNextTarEntry(); } } + 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); + } } /** @@ -308,6 +349,24 @@ public class DockerApi { http().post(uri).close(); } + 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 { + Path path = Files.createTempFile("create-builder-scratch-", null); + try (OutputStream out = Files.newOutputStream(path)) { + StreamUtils.copy(in, out); + } + return path; + } + + private boolean manifestContainsLayerEntry(ImageArchiveManifest manifest, String layerId) { + return manifest.getEntries().stream().anyMatch((content) -> content.getLayers().contains(layerId)); + } + } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifest.java new file mode 100644 index 0000000000..8096c5c11c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifest.java @@ -0,0 +1,95 @@ +/* + * 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.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.buildpack.platform.json.MappedObject; + +/** + * Image archive manifest information. + * + * @author Scott Frederick + * @since 2.7.9 + */ +public class ImageArchiveManifest extends MappedObject { + + private final List entries = new ArrayList<>(); + + protected ImageArchiveManifest(JsonNode node) { + super(node, MethodHandles.lookup()); + getNode().elements().forEachRemaining((element) -> this.entries.add(ManifestEntry.of(element))); + } + + /** + * Return the entries contained in the manifest. + * @return the manifest entries + */ + public List getEntries() { + return this.entries; + } + + /** + * Create an {@link ImageArchiveManifest} from the provided JSON input stream. + * @param content the JSON input stream + * @return a new {@link ImageArchiveManifest} instance + * @throws IOException on IO error + */ + public static ImageArchiveManifest of(InputStream content) throws IOException { + return of(content, ImageArchiveManifest::new); + } + + public static class ManifestEntry extends MappedObject { + + private final List layers; + + protected ManifestEntry(JsonNode node) { + super(node, MethodHandles.lookup()); + this.layers = extractLayers(); + } + + /** + * Return the collection of layer IDs from a section of the manifest. + * @return a collection of layer IDs + */ + public List getLayers() { + return this.layers; + } + + static ManifestEntry of(JsonNode node) { + return new ManifestEntry(node); + } + + @SuppressWarnings("unchecked") + private List extractLayers() { + List layers = valueAt("/Layers", List.class); + if (layers == null) { + return Collections.emptyList(); + } + return layers; + } + + } + +} 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 20a483a514..2ad43f3ecc 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 @@ -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 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 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); 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 a4e4d54362..8410e401b7 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 @@ -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 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 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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java new file mode 100644 index 0000000000..31cd8a768c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java @@ -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 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"))); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-no-manifest.tar b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-no-manifest.tar new file mode 100644 index 0000000000000000000000000000000000000000..94fceadc0f798b6f208f109472ff75ebb4df6f5f GIT binary patch literal 10240 zcmeIvO$x&x5QgDd=MhYJa+XpF#Wo zPxpySQUnk{009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|00D+&t E2UMULyZ`_I literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-symlinks.tar b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export-symlinks.tar new file mode 100644 index 0000000000000000000000000000000000000000..b03583289b0436c0f8ef37a662cc841d30924b54 GIT binary patch literal 71680 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 AYybcN literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export.tar b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export.tar index 9f2ed1e92261467c54676c371bc84d5a44e83d9c..e850d35a2c84149fdfdcc1d9dd9fa972b6607325 100644 GIT binary patch literal 69120 zcmeHwZFAf( zzx_T9kTVU&=5Mo!3h zlKFwf?99tTmZc649na$|3EVjJSriX{Ddw|p`u|v#6?mTUjb+)PZ#DnL8{73AD>N*} z3GkDP2l&}{9OoOu>L>gAyj>L~TcT-2w&W>a=EjTv(;ztcR8Pz86NyLD6bYI854HJmj&LWd6xa0r+D{nP+Ty_ z4~~7?ce%|H#^QuqX%w{VVovDcY-8~f;94N*R>+o@}m(8Vkgdgj5*ih98@bx z%ul_TC9&oD_Q=XCmxFMcxIyFvS?Z>-l?7qwW?tmDnZqJKOZ*YavMh}J&|@1)QNpq$ zNS!qHd@r@QZ6(Rb@*Hr2gG}Hjwl|8MBu=^QFuaMal!0avJ3$sW4N76`dP$Hup~LJj zjqE6MMyVZHKJZzw9gU*Q8#z1~xuH9y4?3F4Tg{wU(i$}%U2nU%PiYi-)(aL>2HFa_)U1T6vM!iaHBO^TTpj(nG) z4lDBfFp0y!L8E;q8hK!1=6KnLQoxuSjBFOV=$kCCA`b5GV3fr{oS-yMA~$d{KLYc; z7*Y{Azz1j%mP13K?buUS~*G_H{@>Q2Q0NiH;F=su(N3s`jA7b z;v+v!fWYFJ%l#ns-6*mx%(C%l6xneaT49Fy*-Jvp^*q4H(1$#THkAAf0+`tmv!VcE zWZ4iDFN!UX@zh0E*p>^H#GHAUdi^*^(lkSpEtYVg z^Y4E8`Q?kh*_bA)N8kVS`t4ck+4-AiC;y;l`Fyxwm%N-0F;OR%#jwn0SJbYTQ#K~G zROt=JcVi1i5k4j#YWH3)S0{+L)~CbL+yZkjKb^JM`dv(;qs z^((5I(k*PSXFVhtVfdNE;THeXJwhxmp_<0GmHRGVMn z*OV`oJRy??uipvKtHolztRB1{)O4+?dTZ#3daR*e{TsZe;qrdasChTS5>E#~(Ib_$ z_54bqQ)068(zM^N07J5YPh9t7QJ#S=?6F0eSPf! z{My^A^CgxKPz}^2m-Yi+lprfuD;DKA9M+GN0sS&dHkFmXfLx8sYOHbqy+c zD=O(PT7f`mI~rQFfY8ycd4*ycJtg(LxgNOCR6^x<5X)(CqLsRi52h@imH1z<=}b&2 z8~xhV728T5lOi{3rD#AE2r7*hiy!|_CEppDEuo#j{(z~Hgs@Ry2aGVq+Zn^JzUz(B z6sB5?g)huE4^y-4cuoQvAWSnF$6ZkJ&-sk3^>0 z+mG_eC1w+83avW}5KX>ZHis3_#5i$jsFqt}y?WE??=|CYIVGxk3Yd3(CWR5&%QM>ef=kSI&<36 zoMQ?cZI79}oVNPqmtR{CE`5p2-Tv~DUy1SCE-m^k&^JnSYMTx|DCC-?#feFZQR)o6 zHXFoqotl|IueE7>@yu2WCts$lC3Mp{?8#QK=^|;hbh^lvt&ssCY{|$BYM|h4jZr=Q z)T$STNIBsxnzJz(iBNehcEi>kw;nBQ+q}%%%3{H$=5(H}YSU;vjvFsjHAyeAiY;?m zn6IZGC>gx4n$t8r!osc9Ii#HYq0_>GxjR`D?T5vr)epa}SUP3pMT>dM`6Zv(9kbH1 zREBiHiiy*f7WuI14>Ogk^L3XLJR!~0Vq(FC-PMwaf@6AzV?aB_)i0HA7W4MBTuj-r zOy-lKUG`V3PPp41wMAQL7iF4|MWgCT`KHy~(tt=?eOoe`@modV!N}$<-jwQ~TE&`Y zn0s4zRx?^p!2#6j-PKB&;y=NL>(G5yY&tQmHdns@>UMQ--@{stg%QvcU~=14>w${v z^YOOjP{eJ+Zi@9ygT|D7Y8sqETQpu*gS$;+{dlW3Wwn4=F=-mqty!LaZBrJ{YKd+Q zIyvGRtt-g&vmVb_AQX8#;bzL4Ln2VAaC33CbP@LtEL;Uv zQPfM-bZ#TiyM;I3HqqpF40s=N*H1&^`R zHK-y(s##;27YTe#Y{uuS!l*E5&^Giy6Rtb-ho0omUNEmGr^8p*whJzXW#|%>G%a zzl|hzVw6g)3CI9DiO8+Biw+M>wITmH=wOUNKMh>1Jn2vT7UV}z;L_-A4#)d0S#rd z@rrg~uI!y-5hEc~$AjvC6X9c%VrpRP#3(PgL0rFOuV_7V52|OV6WT)XRCG}rn*l3e z%u7W=nx4oebo&^>2zRTA-5)vH-J$d;?m8&Q3~AUCP2ydoJH~|Y`nE?sR5o@Fs1mUu zU}Ver)PRb3e*VHdg%z$K)i_w2ZL+aW)te9f^Q|yRTaMd4x;l(ob+>`icIpyqD{Pgp z2}VD5Sm&A1s!SzxCVcB&ZsVnb*NA30QjxnX^q)4Gj>CU`PHE!!Sds(v49@kXR|9iXWNm zaqIB{SmIK&y{oIDU-aKnUMySOQ zR`I^en_Xz>Ad2c#glDhMU;azD=_gpzlyb{lX@6t4uL;2hgy072=jQHVKRq3iR;}%! zRjtlsoY!h8=D=zNI>i=CwPMpi+}gfuHp?4ERd~#<+7JMDpr!*?)zt>|YHNMDF<^=X zPe{!}I1LG00k*XR+k|BwyO_`0Gg-gkvb~?%MNJ0`b*a+#cWmT~37Rc)gw}Vf`SqUh zR+U401e?H@dazcEDoNb-g7nE?b0_@zR&9Xx`KtElx!(sS zJi>f3UkIF}G8OA`dv|{urw3O-J#Vi=A|HtV_8ouq)c_D36^;>)5KcLuK(Ao|xmQ;(7^Dn20!DtMNg}C6Cak9~;PsD~nWeq4Ry-lcD2Xxq=n>B6aa^8EW5629rtdm&AS_RT+C@``RMxIS&9EWz)F`mK%-~sn zY9J~Ro89pWk@eWK`cHRL2HlAR^4-Pq)57>)UCB2T1 zY=U=&6!KHMp7vaBA^K}kg&SN;Q_pNvuJW3kG&ra7ktJ8gc*U^&Wg103o-sN-YM>_R zU?Yx`p{ppfI`FgECb%|CNBCj)TX?ObNc)KCu%SXdGNSm@;Pj8!7s_N6+pfZOv}&_j zZ2g+mqH9kOb?j>|H6036sYgK*BYAg;32O_k&55lb+h*PFu!>sTRjpi4N!NN=O?!DL zn|nn|_kx#4#l`%Sm_MivgJKN~xUADT1@enu+o-zYgqr~2D`~T+Pl@bBQioL5*G4L% zn8hH$<~UFxy27BkDh;XF)K!pk3(2=>R*d&tKwM9$1BR$fmAMIa8_G|d{+6R=!aq}F z4Vg3)_duIh-1xueuYZKL~dKz*%#Il%*_gO;j z>Gg~zc9L-51L*_c6pfc6Y-L+z5V~~)dI!cjcm%e{t|JdL6>JSMI!yKJwm51WGF1p< zA0~>P?|^f_R_U`eCG~jDFxv7j2mvrngBfJS7*m88ArL@BLmMfltO#=@Mu)gr%o>LT z`no_4(6++q_h&TE9yG_h=+h=a9fvoCRLT889%?ZD!Iy}bpBI?CpPm0WwEK80k34ih z?6j136=7lGD?;2!RtLDl0IwpcYiOi;KIX!I@$=8}9EaHZrIWZuwwek5j-qsl)pWe0 z13p>L*eVZiWWEq#1-X`@4z;XUW$|~POFm1Xiio^0!m)pGoo;F$S7{0zwFo6fnWt`gU^V!;xtYV zb2Y8pRkHt3=+tA)m`Gr)PZ@Qfg%9@dqqo{2I>r;R7AX;PxFzGt`ik(2^HkmFv3YU0 zF*+2&f%)iTo^a#ki(2e;T$U%qI$+}L*aHt&sRz7itWRhtlD;_**VJRM=E8M+z`l!V zR4WfkUPn={!yE(_v@I?f?r|spV}rw`zz9CHI()O6lw zJ}ZnT)y$zY65z2A=b1Z@+c=T1@xLF^W<0rCVCdEh*L7y19#f%tO1lFuIIRm8+f|hB zDg+HDpsvhvZsH`6ArFb0MV@jIZB|x+Xf<%-$@jC>o6~b6o3M{K+@R$;Lgjk=7!SV- zULz~a`T1#IABbc49b$k3`XUGxO1p}$+*5mR&iFU2mrk-eB`yoATKqX*UgAvLmGKcJ zj2DPJ6kCd$3F}=o+&^d(9b(cbCay{}(19L7-F6*UDrTRZ#lIr2nX+0Gb%3|4ZCj63 z9My@)+*U{Qy%L9VMU>IG$dzF{IYF|T#e~x{8TD7qYPw;tom!#EDNSJ4mYcHA1#l0+ zR)`TZ=p?AIKI^Tl34~(Y^ zm89V-C1B}*i5Ur&*nR;Js0b7lxowitiK}?8itFET2m?Fc#@{RFu|dY?5_!FNEvY>| z3bOJ=+JX*v2W(^0GNSEVe}Yx+I>J?WTXho*KzDo64}9U&LaY(iI^|CXM{Rh~ZL>Za zx95ppNN!DD5nnA~og9mTsvMtwe|Bn|P`aY|T5xw!y3J%potSM}FUi~*yP8czR|B#^ zNe_++sOZSe27=;Dim(iknGpx!;K_J0;bRJ8sX1HkcUHE6R)8r>(}Tpi3pkl?sso;L zj0_4x=O*z6NMxn(Q$tS2)lHo$XJ6qkT247#Wq=vHr{x8bH8ysIDy2l}BD7KWG^!|{ z)}IHmPA#p|gZ+76VqJr%X1J7TKUa;WBfg5{b8Z)FDcu9|FRYidh;-Xp(Dd)mOX74m z6CX8~5_+;a!ImXep(b@j-U>h<=OeZ-MXXaje5=PgHY4b(YjQpCsg=AFug>78dHw3_ zuWNPO19hFRu0x`sr@}oa9TJ(Etisj?JsmIUwaUqwgB07f<(>P;CacPkK1CP`#e>pn z4sNQm@2TWsKErBV=UA6n_o{?^o!IT5J~O)SvW}9{X~3%l^}S$g;t^lHe6xu**_LA3 zO)w3r)A;`U+#D~#?St7En(s2+e~PQdLd>PEUKs1Eg0B+>f}qx;He z><7MG=l6P2StUCEmdHx&tW#EZz#QzKsp_$HQ zQuLl_catE!1F{I0ue9^p>$7JTRk|RBDpE#lA93BlZIoAsFL^^wUpzaj(CQ_)o%p@! zglbUVIJgw3eY@ND47Bo&h;1XW>D@PWMP~aBCLge!amv|c>%dWU)(H8WDYRQ2HR=sc zn@|S-4(Pb6>px4|#X(vbq2mf6-E}SkWksm<6m{ZUmsDl>?{uc>^xwj(Ri8*@lkr@* z>g(Z*0bV(VG}=sDQP6=I`M&8~%ATq0wlZ-y$IUi59Z*q1MftCPn=-TH7S3v#)nJl$ zEOdZ6@a=nSsn8h&0fCkhJt!AR&lF2(8}<@WTtb1mq*yn(hUt1uOUEShCpT(3B}iXgeUSSFcbA=HZyw_tRz5qT*nP;^g-*-{;cO-ed|;{8qlTfhkKN=Ak0 zgcSFAbYMGRHB0Mwgn`K=98Ua^R;6R`JBI((SC(`D$8Ka}MnzcYe%nSBbw$&5kkv68 zMO9XBE!@bfPFoHLMVaDA$z&Zdrr~kbpif;}+8qtsbyoRf@fuG)QA{v46qZ=q!>>`f z+jZ-n>h_rqwrl)|J9mM&)<)sDhN6z!H}gyuS5s_*<05-;Rs)+!Cc*>zoRphub~-u$ zQ!M8Awdn7q)GFdi7wBl}`aqUbg}$12jCFV`%|<`Gd2@E;&{5xVj;w9QldYj~rDMIg zOQ-{PfTJq=&QoX?kyP+DY3Xo^_bN48HHovx|+7Nib5@VB~fEmS5V zCaAqQSrJ!|>8a{4%91L`nwO-vF!_o@+LV?*nd5#1C^6x*WHRdlf|SnwadQ5@Y`c#3 z?X@2p7d>@!3K<9y&Lgce7|V+b_H*W-8l_))3G2E?e4zFY4zi5Llv&AmwnD;w+)IUf zJ!w*VC2zJ8>i+E6EBdl^=C^@VjEBn)meUBQXOW8VbJIB$`g?n{B)wjOotvPzONa+SN z)ufK+*OSx`Vg5cc;n7u)B3I5&m7@P{t>}%@bqr7)IMJ2IfDS*AYC&KB_T6VeJ*KFwJoSC;JV1_ z;K>H4@S1a)oVv(Bsq!CTuT6RkNR5jNH;(9yPJ`oyzz*=V(Fd;jH%N+v#w|K_7F!f` zkY&Xx78b1@;uy=4`RDTKv_>u-Q!u@RoMX)j%&(^?As2^ROD{VSQL0cN{jBjv^CX{ zu5>}i*HoYr_RdDB@=r0wxPw$o&Og0DM5Dau4&ejWFGH#eJ3D)ZVhsO-+g8l66-HKI zS#qPf(tB_k?$txd)pAvsqE1!$QJD7*b?G}x|E=0rC)5Jt{230VBKg*~UCUw$Gmws( zZb@q2X8J#~>*y@MTpYAIu{sKAJbCe4r)TTiUORgY@zLuAf5!zs{bmC=S5=}Bu}}`^I zteQ~b^Y0|4+6>%MC;AP-`V!ve4ZT~oD{^qioU3hkeM=kx4xvPdS^oXd!3d_*1)J@^ z8ZPb&&T&3R)+bIiOKs4PxNC>Z=|sGP9RJ>!uY;&N&RcN0K)FX;_nRdJFF{euNJ(mM z0hRo@5;GvWY!h)NR^>QqRi|hZtnJ|A`cCLhs0V$Yp19K*uB*vviu)o~CH7d5X9fpF z8xpK#Z++)=2W*@|Met&+lMzQL7`N*pu8`CfX6vcy7%xkzYWV!W=$1gky*_>n*LQbd zJKPi6*d&7>S1D{a(@cF#y%Koc^ioXVQ|iQvcQq?~`L{x`e(1`XMi65f$43T_@@y*GSrR*3NM^dH0~Un%xJ(OX)gI^jk^ zRkYJ(F*vs*0w9sX zz6?OBQhWCegB60Ax@Kste)(gS7BmO7_ctnuRPhzDuK({Da#+Eh7I~~-E>jc(pXMdSA9WiUUtlP9i8A6}hHiDL=w_Q- zsFx~G;C55iq282Mp^K{%CUY!CZ1>u1)%zAA&^A;ZNWu}E&*!DCoFbg8rLVTt7J zmqbSIfzuGwVU@*HoYD)2%M)e@GL%VhWw2e9XoOby3eY+3#iBI1t$+iBE^o|YWij99 zgu92t%|D7kiQvK0I)CaDWFQBQze=;>T@_2G=2V;`v#f&EKK6#luXmIA2CmL@{%IGA zdPX9jK|6I~bw}xhn%a>Urig_>rBsWfUgFM9E2@T~D6ugM`q(xp;vMPfkZ5XdHwpd z%DVb`C^Gfa9NVlSGK>P5b9s=pJ^H5X)1}e^p~_4T^4` ztqLvHkJ4*buzkbyu7Z}4*#Z%{a?uLKT3#(UmTOqJh`WTl0czCgwfOpn=3ODIv6qcQEFDdJXBEbG$lOiIxd#UJ#qo|>+MWHsqr#B~OM=#F| zQ2jJtUXpamS)<+f8~I>2^F=?SZbV!M^kxp;?Zg+A>TP$xf)&Lljcs{QGd?91LJoLR zFf7HX2rQ3bkBg=wz*syVBQ4-U7&j`yGPzKs6T6AHNu^qTpnpYyT^S2?zbi={J4M~p z%=d$0h3$zYX7l6sgJ+8c){_hqW0uL=*s&RoQHRrZuyh#KJKH*RkGQ^_f!$50;3-=o z721Dez5Krm<4lBxz!){Y`@9YlA=dtc{m~_k8+J^S1Xn*_=wliGV9X?@Q-Bolk$UAL z5LIy=CV^0-^g3jH6X71>p%c=su+I}@&6S`!l66xcUT|{dNl;W*Kv^|~6)hs0>BiFwM zFF}NyA`5jDZm`eLyeV}(Ze3H_!&c_Q1RRl^irD=&#!7KjaU2VV7JdwKx@lM&cz4ii z;b~qZe1fZr_#82@*G(Zi<}cXQeA$tlj*pn)b$4^>(;;+DiC^FZwmVNL9YY%oZF?xL zxxUek7138lgWXM6hqTcMb-a@MsZMTzx~jCj)%JT^N+sH!OW}H2wku2jAjI2F@DCWC zgSQ1=4vq)5Wm(7c*Wl^kzvX)RMNTh{CKOau48DH-=JC%T(*vOu$02iV+t1w8Pb}LS zjUwN%Z6^u2<;R&na>HoEecSSah^Mg?!~q*I+s>F9hwjl1%?9T~-#5Oo;5_%O=D&C& z{}`6z1g`J6D1)Ef(6PQT{2gfDy2z@48G)u187^?=I5vN&Cwblb3I{0BA45;%@Sfh9 z|Ns2%r}LMuf9yks{s=s;$@f|gL|@@Ax5a-i4D4?VtB>gK_3nQ-|B*fC_v*<Ew5$ydgze9uq({ zHCpX|E!)An&HVoj9$0~U&-~vs_hvs4Nc>rYyBU>x|LWa4!Tb*&{_+>YsD2rW-4Hx7 z@F!1=zZrvKL0$zjp3lppk{6{(A;bewpmxfjlyJcf{&Mdi9xHt8fyW+r?19G~*t-XA zZU6mJ%xBlsifOTT{TF&p%l`8MJG|%l|GM@4b??ROFPt0nlW|R#9L($A26+lk+-=1G zo;{q2MEomgIli5_QD7%-kiwATSr|q<9&Mz-Sy zHnRf{w2*I!m7E%8j{;npfeb(P8#_9NAJMW*E5P4WYK~j2ar9&PGu`5yj|a!U4Stvx z<%~@^K1GQabGkll^Lyox#G7Yj$&!on68Yr@#~GUxyzvajcGZWtih2F;O?kEc`8CdA zZqpIJy`Cj#L`C_#*~h{0Y&DrY9h^)7OaAzBihwBD(0|f-VP4d4w!EZh?RT>h7j)9a z9CeA;v*)XPvLp5|98)P z)bQy4yO(Z0*h+3a{_n>B7s6fi=>NY5sS*A^Y=I@0f1v-*_7(r1@82{3Z`;?>{(pG> zKLje6h%_B@a`%ZJ$lr%3%wG@^R}7JWnobj64L?pl!N4CP+Tln#1BEXTUmxT2#+3;- z@}uMLwlehi{bLV2_P}Eg++z>i+Wxx}|DO*}#68dd*ZKFZd;h@wf46i4!AG_B|NRcW z#7F<1ICUtaB=6M!$AXdXIa{=fU_=R>XK*5m)y{y#bx z(2oBLtnktQe=j0)LsBETQ{zi&U^r2NqW znbRoA!%aTk@f%|Be*CvBI{)=J{&(!)TNGRK|A@~%-hxiwr#Ik#;6L)euS*}_J@&w3 z4?On3t$X0s{-3+q|FiG?{NHu^d)K|ENsi+F>wj;}nfoD7#sA+{44V6YUg9TgdYZM0=Uh)vM9{2z5YyXcj?q)d9 zIox3!-&7(oBzn*P1g>Y;o*RaC=((^&&S-M;rkr&M5NS7>U5{Md)wb TTaO<;_P}EgJodn!rw9H&c4;lh literal 22016 zcmeHPS#R7n5cacwg`j8eE)R*44CJA00yIDo7fqT14T6BDtwbvcI?@_J{(FbgdTnpK zm*o}gn#3A9NRBx3aY%7K%2*gC8Rb|fT4ALkDH4WBMBzRXu|_5c5M|hKV{dX3GiCx5 z>mb31Av`Ux@`b@R~Awf+nfz|u8(Pg%MOqQR>F zQyc>RqmKVWm0nS5sJOA}D=oZ>rYTQnGhWGy^sAn>qRpFjDvSB7KlMafIy?L88{ps-@LMCmgc%$ow5msy2R%vs&UfNi8bkA`Xa29YDcxo0Hy#D)K7x@;Y zVQSp8{zDNBSJr1PyM0Yz(4UzUg^BCvM|L_9EXHZqbQOnP*@VI z2o^M?nkE#-gxiQyWFqJqRB4464opB)A|wG>veNL9&^0WH&;()ly+i1<`~7VT6nO)0 zBf9zoXC^)OPrv!U7Il;JxrQb{iI;_aUAeygv({Cg^HbAsdGWSUY3`pIp4IxqU*0QP zP+|3^uAu7T=DX%{d45*v{I;C#>MWPgh(-BV`B(omZ?o*g|78vu^{21r{Cv^S_eIvu zb$yldS5bXQ^YfSKqP4H`rn)SP)S9-hdzSy)rrA=|Z(QhXQT_XgU%;uKrFr|W|9y*J z+U%jg&)&cKekjLSWOu$m{z@8csPLhv zl&(I)spl;&rsq}BmT=31w5h2Rou5OH!M5dx*2K`7{Ul!UD!)^dHU0jgCTR|PQnKt0 zkz`8mC(`bPY3>k+ror9<%c_tTnOMnpQo{F^ST~sbFFFnk z(Hn5ac6i>;eccG*`HK-^?Q4b0a29YDa27a}1qR3e4#WRqLPlQyJ=6H_g8v=b4Q}$k zTp1}%pf!-XimE6im?FUv5*yBGl1R#h6b#cughq-$a;;T@BO-uSzydq|cSQboDDa2k zf0^_DLk;=2im+ zyFT0qW&vLCe>e&|isb5OvuwJ}|6ok){?Adr{~W4*ug8<)e`cNk4x;zBTF#ngKm1Y1 z#vcE-YO$rjQ^gD(_flfBfMC;Ia3A?x@S!Vpd5=%fW4J!N--c#D4~4 z`hl8e?XsW$V-`C64}^vj>nz|b;4IKvU~v5BK>VNm{~s>?bNF7m;QxkpgPZ(c6b6(; zDvAh>;0sW!u{2Vt*f1r8;F=2fr(#CNQb_n1lmt8yjA#;(fF=@0iR1r9f2hvYMfdBvi