From 00c8b8536489c5bf90b72baca1d52b8f411eab27 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 14 Dec 2021 14:56:17 -0600 Subject: [PATCH] Add validation of SBOM to Paketo system tests Fixes gh-29027 --- .../assertions/ContainerConfigAssert.java | 31 ++- .../boot/image/assertions/ImageAssert.java | 55 ++++- .../boot/image/paketo/PaketoBuilderTests.java | 209 ++++++++++++------ 3 files changed, 204 insertions(+), 91 deletions(-) diff --git a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java index d8297ac471e..b9c8229ea54 100644 --- a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java +++ b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ContainerConfigAssert.java @@ -18,13 +18,13 @@ package org.springframework.boot.image.assertions; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import com.github.dockerjava.api.model.ContainerConfig; import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.AbstractListAssert; +import org.assertj.core.api.AbstractMapAssert; import org.assertj.core.api.AbstractObjectAssert; -import org.assertj.core.api.AbstractStringAssert; -import org.assertj.core.api.AssertionsForClassTypes; import org.assertj.core.api.ListAssert; import org.assertj.core.api.ObjectAssert; @@ -45,16 +45,16 @@ public class ContainerConfigAssert extends AbstractAssert assertConsumer) { + assertConsumer.accept(new BuildMetadataAssert(jsonLabel(BUILD_METADATA_LABEL))); } - public LifecycleMetadataAssert lifecycleMetadata() { - return new LifecycleMetadataAssert(jsonLabel(LIFECYCLE_METADATA_LABEL)); + public void lifecycleMetadata(Consumer assertConsumer) { + assertConsumer.accept(new LifecycleMetadataAssert(jsonLabel(LIFECYCLE_METADATA_LABEL))); } - public AbstractStringAssert label(String label) { - return AssertionsForClassTypes.assertThat(getLabel(label)); + public void labels(Consumer assertConsumer) { + assertConsumer.accept(new LabelsAssert(this.actual.getLabels())); } private JsonContentAssert jsonLabel(String label) { @@ -72,6 +72,17 @@ public class ContainerConfigAssert extends AbstractAssert, String, String> { + + protected LabelsAssert(Map labels) { + super(labels, LabelsAssert.class); + } + + } + /** * Asserts for the JSON content in the {@code io.buildpacks.build.metadata} label. * @@ -116,6 +127,10 @@ public class ContainerConfigAssert extends AbstractAssert sbomLayerSha() { + return this.actual.extractingJsonPathValue("$.sbom.sha"); + } + } } diff --git a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java index 5baf0764917..39b879011cb 100644 --- a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java +++ b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java @@ -22,9 +22,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.function.Consumer; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.utils.IOUtils; import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.Assertions; import org.assertj.core.api.ListAssert; @@ -32,6 +34,7 @@ import org.assertj.core.api.ListAssert; import org.springframework.boot.buildpack.platform.docker.DockerApi; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.test.json.JsonContentAssert; /** * AssertJ {@link org.assertj.core.api.Assert} for Docker image contents. @@ -47,11 +50,11 @@ public class ImageAssert extends AbstractAssert { getLayers(); } - public LayerContentAssert hasLayer(String layerDigest) { + public void layer(String layerDigest, Consumer assertConsumer) { if (!this.layers.containsKey(layerDigest)) { failWithMessage("Layer with digest '%s' not found in image", layerDigest); } - return new LayerContentAssert(this.layers.get(layerDigest)); + assertConsumer.accept(new LayerContentAssert(this.layers.get(layerDigest))); } private void getLayers() throws IOException { @@ -70,22 +73,52 @@ public class ImageAssert extends AbstractAssert { super(layer, LayerContentAssert.class); } - public ListAssert entries() throws IOException { + public ListAssert entries() { List entryNames = new ArrayList<>(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - this.actual.writeTo(out); - try (TarArchiveInputStream in = new TarArchiveInputStream(new ByteArrayInputStream(out.toByteArray()))) { - TarArchiveEntry entry = in.getNextTarEntry(); - while (entry != null) { - if (!entry.isDirectory()) { - entryNames.add(entry.getName().replaceFirst("^/workspace/", "")); + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.actual.writeTo(out); + try (TarArchiveInputStream in = new TarArchiveInputStream( + new ByteArrayInputStream(out.toByteArray()))) { + TarArchiveEntry entry = in.getNextTarEntry(); + while (entry != null) { + if (!entry.isDirectory()) { + entryNames.add(entry.getName().replaceFirst("^/workspace/", "")); + } + entry = in.getNextTarEntry(); } - entry = in.getNextTarEntry(); } } + catch (IOException ex) { + failWithMessage("IOException while reading image layer archive: '%s'", ex.getMessage()); + } return Assertions.assertThat(entryNames); } + public void jsonEntry(String name, Consumer assertConsumer) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.actual.writeTo(out); + try (TarArchiveInputStream in = new TarArchiveInputStream( + new ByteArrayInputStream(out.toByteArray()))) { + TarArchiveEntry entry = in.getNextTarEntry(); + while (entry != null) { + if (entry.getName().equals(name)) { + ByteArrayOutputStream entryOut = new ByteArrayOutputStream(); + IOUtils.copy(in, entryOut); + assertConsumer.accept(new JsonContentAssert(LayerContentAssert.class, entryOut.toString())); + return; + } + entry = in.getNextTarEntry(); + } + } + failWithMessage("Expected JSON entry '%s' in layer with digest '%s'", name, this.actual.getId()); + } + catch (IOException ex) { + failWithMessage("IOException while reading image layer archive: '%s'", ex.getMessage()); + } + } + } } diff --git a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java index 98f4dbf8c00..9dc2a856aef 100644 --- a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java +++ b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java @@ -49,6 +49,7 @@ import org.springframework.boot.testsupport.gradle.testkit.GradleBuildExtension; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; /** * Integration tests for the Paketo builder and buildpacks. @@ -80,14 +81,16 @@ class PaketoBuilderTests { container.waitingFor(Wait.forHttp("/test")).start(); ContainerConfig config = container.getContainerInfo().getConfig(); assertLabelsMatchManifestAttributes(config); - ImageAssertions.assertThat(config).buildMetadata().buildpacks().contains( - "paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica", - "paketo-buildpacks/executable-jar", "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); - ImageAssertions.assertThat(config).buildMetadata().processOfType("web").extracting("command", "args") - .containsExactly("java", Collections.singletonList("org.springframework.boot.loader.JarLauncher")); - ImageAssertions.assertThat(config).buildMetadata().processOfType("executable-jar") - .extracting("command", "args") - .containsExactly("java", Collections.singletonList("org.springframework.boot.loader.JarLauncher")); + ImageAssertions.assertThat(config).buildMetadata((metadata) -> { + metadata.buildpacks().contains("paketo-buildpacks/ca-certificates", + "paketo-buildpacks/bellsoft-liberica", "paketo-buildpacks/executable-jar", + "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); + metadata.processOfType("web").extracting("command", "args").containsExactly("java", + Collections.singletonList("org.springframework.boot.loader.JarLauncher")); + metadata.processOfType("executable-jar").extracting("command", "args").containsExactly("java", + Collections.singletonList("org.springframework.boot.loader.JarLauncher")); + }); + assertImageHasSbomLayer(imageReference, config, "executable-jar"); assertImageLayersMatchLayersIndex(imageReference, config); } finally { @@ -143,17 +146,22 @@ class PaketoBuilderTests { try (GenericContainer container = new GenericContainer<>(imageName).withExposedPorts(8080)) { container.waitingFor(Wait.forHttp("/test")).start(); ContainerConfig config = container.getContainerInfo().getConfig(); - ImageAssertions.assertThat(config).buildMetadata().buildpacks().contains( - "paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica", - "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); - ImageAssertions.assertThat(config).buildMetadata().processOfType("web").extracting("command", "args") - .containsExactly("/workspace/" + projectName + "-boot/bin/" + projectName, Collections.emptyList()); - ImageAssertions.assertThat(config).buildMetadata().processOfType("dist-zip").extracting("command", "args") - .containsExactly("/workspace/" + projectName + "-boot/bin/" + projectName, Collections.emptyList()); - DigestCapturingCondition digests = new DigestCapturingCondition(); - ImageAssertions.assertThat(config).lifecycleMetadata().appLayerShas().haveExactly(1, digests); - ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(0)).entries().contains( - projectName + "-boot/bin/" + projectName, projectName + "-boot/lib/" + projectName + ".jar"); + ImageAssertions.assertThat(config).buildMetadata((metadata) -> { + metadata.buildpacks().contains("paketo-buildpacks/ca-certificates", + "paketo-buildpacks/bellsoft-liberica", "paketo-buildpacks/dist-zip", + "paketo-buildpacks/spring-boot"); + metadata.processOfType("web").extracting("command", "args").containsExactly( + "/workspace/" + projectName + "-boot/bin/" + projectName, Collections.emptyList()); + metadata.processOfType("dist-zip").extracting("command", "args").containsExactly( + "/workspace/" + projectName + "-boot/bin/" + projectName, Collections.emptyList()); + }); + assertImageHasSbomLayer(imageReference, config, "dist-zip"); + DigestCapturingCondition digest = new DigestCapturingCondition(); + ImageAssertions.assertThat(config) + .lifecycleMetadata((metadata) -> metadata.appLayerShas().haveExactly(1, digest)); + ImageAssertions.assertThat(imageReference).layer(digest.getDigest(), + (layer) -> layer.entries().contains(projectName + "-boot/bin/" + projectName, + projectName + "-boot/lib/" + projectName + ".jar")); } finally { removeImage(imageReference); @@ -171,20 +179,24 @@ class PaketoBuilderTests { try (GenericContainer container = new GenericContainer<>(imageName).withExposedPorts(8080)) { container.waitingFor(Wait.forHttp("/test")).start(); ContainerConfig config = container.getContainerInfo().getConfig(); - ImageAssertions.assertThat(config).buildMetadata().buildpacks().contains( - "paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica", - "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); - ImageAssertions.assertThat(config).buildMetadata().processOfType("web").extracting("command", "args") - .containsExactly("/workspace/" + projectName + "/bin/" + projectName, Collections.emptyList()); - ImageAssertions.assertThat(config).buildMetadata().processOfType("dist-zip").extracting("command", "args") - .containsExactly("/workspace/" + projectName + "/bin/" + projectName, Collections.emptyList()); - DigestCapturingCondition digests = new DigestCapturingCondition(); - ImageAssertions.assertThat(config).lifecycleMetadata().appLayerShas().haveExactly(1, digests); - ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(0)).entries() + ImageAssertions.assertThat(config).buildMetadata((metadata) -> { + metadata.buildpacks().contains("paketo-buildpacks/ca-certificates", + "paketo-buildpacks/bellsoft-liberica", "paketo-buildpacks/dist-zip", + "paketo-buildpacks/spring-boot"); + metadata.processOfType("web").extracting("command", "args") + .containsExactly("/workspace/" + projectName + "/bin/" + projectName, Collections.emptyList()); + metadata.processOfType("dist-zip").extracting("command", "args") + .containsExactly("/workspace/" + projectName + "/bin/" + projectName, Collections.emptyList()); + }); + assertImageHasSbomLayer(imageReference, config, "dist-zip"); + DigestCapturingCondition digest = new DigestCapturingCondition(); + ImageAssertions.assertThat(config) + .lifecycleMetadata((metadata) -> metadata.appLayerShas().haveExactly(1, digest)); + ImageAssertions.assertThat(imageReference).layer(digest.getDigest(), (layer) -> layer.entries() .contains(projectName + "/bin/" + projectName, projectName + "/lib/" + projectName + "-plain.jar") .anyMatch((s) -> s.startsWith(projectName + "/lib/spring-boot-")) .anyMatch((s) -> s.startsWith(projectName + "/lib/spring-core-")) - .anyMatch((s) -> s.startsWith(projectName + "/lib/spring-web-")); + .anyMatch((s) -> s.startsWith(projectName + "/lib/spring-web-"))); } finally { removeImage(imageReference); @@ -203,14 +215,16 @@ class PaketoBuilderTests { container.waitingFor(Wait.forHttp("/test")).start(); ContainerConfig config = container.getContainerInfo().getConfig(); assertLabelsMatchManifestAttributes(config); - ImageAssertions.assertThat(config).buildMetadata().buildpacks().contains( - "paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica", - "paketo-buildpacks/executable-jar", "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); - ImageAssertions.assertThat(config).buildMetadata().processOfType("web").extracting("command", "args") - .containsExactly("java", Collections.singletonList("org.springframework.boot.loader.WarLauncher")); - ImageAssertions.assertThat(config).buildMetadata().processOfType("executable-jar") - .extracting("command", "args") - .containsExactly("java", Collections.singletonList("org.springframework.boot.loader.WarLauncher")); + ImageAssertions.assertThat(config).buildMetadata((metadata) -> { + metadata.buildpacks().contains("paketo-buildpacks/ca-certificates", + "paketo-buildpacks/bellsoft-liberica", "paketo-buildpacks/executable-jar", + "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); + metadata.processOfType("web").extracting("command", "args").containsExactly("java", + Collections.singletonList("org.springframework.boot.loader.WarLauncher")); + metadata.processOfType("executable-jar").extracting("command", "args").containsExactly("java", + Collections.singletonList("org.springframework.boot.loader.WarLauncher")); + }); + assertImageHasSbomLayer(imageReference, config, "executable-jar"); assertImageLayersMatchLayersIndex(imageReference, config); } finally { @@ -229,21 +243,26 @@ class PaketoBuilderTests { try (GenericContainer container = new GenericContainer<>(imageName).withExposedPorts(8080)) { container.waitingFor(Wait.forHttp("/test")).start(); ContainerConfig config = container.getContainerInfo().getConfig(); - ImageAssertions.assertThat(config).buildMetadata().buildpacks().contains( - "paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica", - "paketo-buildpacks/apache-tomcat", "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); - ImageAssertions.assertThat(config).buildMetadata().processOfType("web").extracting("command", "args") - .containsExactly("bash", Arrays.asList("catalina.sh", "run")); - ImageAssertions.assertThat(config).buildMetadata().processOfType("tomcat").extracting("command", "args") - .containsExactly("bash", Arrays.asList("catalina.sh", "run")); - DigestCapturingCondition digests = new DigestCapturingCondition(); - ImageAssertions.assertThat(config).lifecycleMetadata().appLayerShas().haveExactly(1, digests); - ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(0)).entries() - .contains("WEB-INF/classes/example/ExampleApplication.class", - "WEB-INF/classes/example/HelloController.class", "META-INF/MANIFEST.MF") - .anyMatch((s) -> s.startsWith("WEB-INF/lib/spring-boot-")) - .anyMatch((s) -> s.startsWith("WEB-INF/lib/spring-core-")) - .anyMatch((s) -> s.startsWith("WEB-INF/lib/spring-web-")); + ImageAssertions.assertThat(config).buildMetadata((metadata) -> { + metadata.buildpacks().contains("paketo-buildpacks/ca-certificates", + "paketo-buildpacks/bellsoft-liberica", "paketo-buildpacks/apache-tomcat", + "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); + metadata.processOfType("web").extracting("command", "args").containsExactly("bash", + Arrays.asList("catalina.sh", "run")); + metadata.processOfType("tomcat").extracting("command", "args").containsExactly("bash", + Arrays.asList("catalina.sh", "run")); + }); + assertImageHasSbomLayer(imageReference, config, "apache-tomcat"); + DigestCapturingCondition digest = new DigestCapturingCondition(); + ImageAssertions.assertThat(config) + .lifecycleMetadata((metadata) -> metadata.appLayerShas().haveExactly(1, digest)); + ImageAssertions.assertThat(imageReference).layer(digest.getDigest(), + (layer) -> layer.entries() + .contains("WEB-INF/classes/example/ExampleApplication.class", + "WEB-INF/classes/example/HelloController.class", "META-INF/MANIFEST.MF") + .anyMatch((s) -> s.startsWith("WEB-INF/lib/spring-boot-")) + .anyMatch((s) -> s.startsWith("WEB-INF/lib/spring-core-")) + .anyMatch((s) -> s.startsWith("WEB-INF/lib/spring-web-"))); } finally { removeImage(imageReference); @@ -318,29 +337,54 @@ class PaketoBuilderTests { private void assertLabelsMatchManifestAttributes(ContainerConfig config) throws IOException { JarFile jarFile = new JarFile(projectArchiveFile()); Attributes attributes = jarFile.getManifest().getMainAttributes(); - ImageAssertions.assertThat(config).label("org.springframework.boot.version") - .isEqualTo(attributes.getValue("Spring-Boot-Version")); - ImageAssertions.assertThat(config).label("org.opencontainers.image.title") - .isEqualTo(attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE)); - ImageAssertions.assertThat(config).label("org.opencontainers.image.version") - .isEqualTo(attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION)); + ImageAssertions.assertThat(config).labels((labels) -> { + labels.contains(entry("org.springframework.boot.version", attributes.getValue("Spring-Boot-Version"))); + labels.contains( + entry("org.opencontainers.image.title", attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE))); + labels.contains(entry("org.opencontainers.image.version", + attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION))); + }); + } + + private void assertImageHasSbomLayer(ImageReference imageReference, ContainerConfig config, String buildpack) + throws IOException { + DigestCapturingCondition digest = new DigestCapturingCondition(); + ImageAssertions.assertThat(config).lifecycleMetadata((metadata) -> metadata.sbomLayerSha().has(digest)); + ImageAssertions.assertThat(imageReference).layer(digest.getDigest(), (layer) -> { + layer.entries().contains("/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/jre/sbom.syft.json", + "/layers/sbom/launch/paketo-buildpacks_" + buildpack + "/sbom.syft.json", + "/layers/sbom/launch/paketo-buildpacks_" + buildpack + "/sbom.cdx.json"); + layer.jsonEntry("/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/jre/sbom.syft.json", (json) -> { + json.extractingJsonPathStringValue("$.Artifacts[0].Name").isEqualTo("BellSoft Liberica JRE"); + json.extractingJsonPathStringValue("$.Artifacts[0].Version").startsWith(javaMajorVersion()); + }); + layer.jsonEntry("/layers/sbom/launch/paketo-buildpacks_" + buildpack + "/sbom.syft.json", + (json) -> json.extractingJsonPathArrayValue("$.artifacts.[*].name").contains("spring-beans", + "spring-boot", "spring-boot-autoconfigure", "spring-context", "spring-core", + "spring-expression", "spring-jcl", "spring-web", "spring-webmvc")); + layer.jsonEntry("/layers/sbom/launch/paketo-buildpacks_" + buildpack + "/sbom.cdx.json", + (json) -> json.extractingJsonPathArrayValue("$.components.[*].name").contains("spring-beans", + "spring-boot", "spring-boot-autoconfigure", "spring-context", "spring-core", + "spring-expression", "spring-jcl", "spring-web", "spring-webmvc")); + }); } private void assertImageLayersMatchLayersIndex(ImageReference imageReference, ContainerConfig config) throws IOException { - DigestCapturingCondition digests = new DigestCapturingCondition(); - ImageAssertions.assertThat(config).lifecycleMetadata().appLayerShas().haveExactly(5, digests); + DigestsCapturingCondition digests = new DigestsCapturingCondition(); + ImageAssertions.assertThat(config) + .lifecycleMetadata((metadata) -> metadata.appLayerShas().haveExactly(5, digests)); LayersIndex layersIndex = LayersIndex.fromArchiveFile(projectArchiveFile()); - ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(0)).entries() - .allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("dependencies"))); - ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(1)).entries() - .allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("spring-boot-loader"))); - ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(2)).entries() - .allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("snapshot-dependencies"))); - ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(3)).entries() - .allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("application"))); - ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(4)).entries() - .allMatch((entry) -> entry.contains("lib/spring-cloud-bindings-")); + ImageAssertions.assertThat(imageReference).layer(digests.getDigest(0), (layer) -> layer.entries() + .allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("dependencies")))); + ImageAssertions.assertThat(imageReference).layer(digests.getDigest(1), (layer) -> layer.entries() + .allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("spring-boot-loader")))); + ImageAssertions.assertThat(imageReference).layer(digests.getDigest(2), (layer) -> layer.entries() + .allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("snapshot-dependencies")))); + ImageAssertions.assertThat(imageReference).layer(digests.getDigest(3), (layer) -> layer.entries() + .allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("application")))); + ImageAssertions.assertThat(imageReference).layer(digests.getDigest(4), + (layer) -> layer.entries().allMatch((entry) -> entry.contains("lib/spring-cloud-bindings-"))); } private File projectArchiveFile() { @@ -376,12 +420,33 @@ class PaketoBuilderTests { private static class DigestCapturingCondition extends Condition { - private static List digests; + private static String digest = null; DigestCapturingCondition() { super(predicate(), "a value starting with 'sha256:'"); } + private static Predicate predicate() { + return (sha) -> { + digest = sha.toString(); + return sha.toString().startsWith("sha256:"); + }; + } + + String getDigest() { + return digest; + } + + } + + private static class DigestsCapturingCondition extends Condition { + + private static List digests; + + DigestsCapturingCondition() { + super(predicate(), "a value starting with 'sha256:'"); + } + private static Predicate predicate() { digests = new ArrayList<>(); return (sha) -> {