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 04b9dbe97896..8d0e4959bf2c 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 @@ -107,7 +107,8 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage()); assertStackIdsMatch(runImage, builderImage); BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv()); - Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata); + BuildpackLayersMetadata buildpackLayersMetadata = BuildpackLayersMetadata.fromImage(builderImage); + Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata, buildpackLayersMetadata); EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(), builderMetadata, request.getCreator(), request.getEnv(), buildpacks); this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none()); @@ -143,8 +144,10 @@ private void assertStackIdsMatch(Image runImage, Image builderImage) { + "' does not match builder stack '" + builderImageStackId + "'"); } - private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata) { - BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata); + private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata, + BuildpackLayersMetadata buildpackLayersMetadata) { + BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata, + buildpackLayersMetadata); return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks()); } @@ -245,9 +248,13 @@ private class BuilderResolverContext implements BuildpackResolverContext { private final BuilderMetadata builderMetadata; - BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata) { + private final BuildpackLayersMetadata buildpackLayersMetadata; + + BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata, + BuildpackLayersMetadata buildpackLayersMetadata) { this.imageFetcher = imageFetcher; this.builderMetadata = builderMetadata; + this.buildpackLayersMetadata = buildpackLayersMetadata; } @Override @@ -255,6 +262,11 @@ public List getBuildpackMetadata() { return this.builderMetadata.getBuildpacks(); } + @Override + public BuildpackLayersMetadata getBuildpackLayersMetadata() { + return this.buildpackLayersMetadata; + } + @Override public Image fetchImage(ImageReference reference, ImageType imageType) throws IOException { return this.imageFetcher.fetchImage(imageType, reference); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackLayersMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackLayersMetadata.java new file mode 100644 index 000000000000..1816b0d98770 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackLayersMetadata.java @@ -0,0 +1,194 @@ +/* + * Copyright 2012-2022 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.build; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; +import org.springframework.boot.buildpack.platform.json.MappedObject; +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Buildpack layers metadata information. + * + * @author Scott Frederick + */ +final class BuildpackLayersMetadata extends MappedObject { + + private static final String LABEL_NAME = "io.buildpacks.buildpack.layers"; + + private final Buildpacks buildpacks; + + private BuildpackLayersMetadata(JsonNode node) { + super(node, MethodHandles.lookup()); + this.buildpacks = Buildpacks.fromJson(getNode()); + } + + /** + * Return the metadata details of a buildpack with the given ID and version. + * @param id the buildpack ID + * @param version the buildpack version + * @return the buildpack details or {@code null} if a buildpack with the given ID and + * version does not exist in the metadata + */ + BuildpackLayerDetails getBuildpack(String id, String version) { + return this.buildpacks.getBuildpack(id, version); + } + + /** + * Create a {@link BuildpackLayersMetadata} from an image. + * @param image the source image + * @return the buildpack layers metadata + * @throws IOException on IO error + */ + static BuildpackLayersMetadata fromImage(Image image) throws IOException { + Assert.notNull(image, "Image must not be null"); + return fromImageConfig(image.getConfig()); + } + + /** + * Create a {@link BuildpackLayersMetadata} from image config. + * @param imageConfig the source image config + * @return the buildpack layers metadata + * @throws IOException on IO error + */ + static BuildpackLayersMetadata fromImageConfig(ImageConfig imageConfig) throws IOException { + Assert.notNull(imageConfig, "ImageConfig must not be null"); + String json = imageConfig.getLabels().get(LABEL_NAME); + Assert.notNull(json, () -> "No '" + LABEL_NAME + "' label found in image config labels '" + + StringUtils.collectionToCommaDelimitedString(imageConfig.getLabels().keySet()) + "'"); + return fromJson(json); + } + + /** + * Create a {@link BuildpackLayersMetadata} from JSON. + * @param json the source JSON + * @return the buildpack layers metadata + * @throws IOException on IO error + */ + static BuildpackLayersMetadata fromJson(String json) throws IOException { + return fromJson(SharedObjectMapper.get().readTree(json)); + } + + /** + * Create a {@link BuildpackLayersMetadata} from JSON. + * @param node the source JSON + * @return the buildpack layers metadata + */ + static BuildpackLayersMetadata fromJson(JsonNode node) { + return new BuildpackLayersMetadata(node); + } + + private static class Buildpacks { + + private final Map buildpacks = new HashMap<>(); + + private BuildpackLayerDetails getBuildpack(String id, String version) { + if (this.buildpacks.containsKey(id)) { + return this.buildpacks.get(id).getBuildpack(version); + } + return null; + } + + private void addBuildpackVersions(String id, BuildpackVersions versions) { + this.buildpacks.put(id, versions); + } + + private static Buildpacks fromJson(JsonNode node) { + Buildpacks buildpacks = new Buildpacks(); + node.fields().forEachRemaining((field) -> buildpacks.addBuildpackVersions(field.getKey(), + BuildpackVersions.fromJson(field.getValue()))); + return buildpacks; + } + + } + + private static class BuildpackVersions { + + private final Map versions = new HashMap<>(); + + private BuildpackLayerDetails getBuildpack(String version) { + return this.versions.get(version); + } + + private void addBuildpackVersion(String version, BuildpackLayerDetails details) { + this.versions.put(version, details); + } + + private static BuildpackVersions fromJson(JsonNode node) { + BuildpackVersions versions = new BuildpackVersions(); + node.fields().forEachRemaining((field) -> versions.addBuildpackVersion(field.getKey(), + BuildpackLayerDetails.fromJson(field.getValue()))); + return versions; + } + + } + + static final class BuildpackLayerDetails extends MappedObject { + + private final String name; + + private final String homepage; + + private final String layerDiffId; + + private BuildpackLayerDetails(JsonNode node) { + super(node, MethodHandles.lookup()); + this.name = valueAt("/name", String.class); + this.homepage = valueAt("/homepage", String.class); + this.layerDiffId = valueAt("/layerDiffID", String.class); + } + + /** + * Return the buildpack name. + * @return the name + */ + String getName() { + return this.name; + } + + /** + * Return the buildpack homepage address. + * @return the homepage address + */ + String getHomepage() { + return this.homepage; + } + + /** + * Return the buildpack layer {@code diffID}. + * @return the layer {@code diffID} + */ + String getLayerDiffId() { + return this.layerDiffId; + } + + private static BuildpackLayerDetails fromJson(JsonNode node) { + return new BuildpackLayerDetails(node); + } + + } + +} 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 0dc760115710..b20dd5b969f9 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-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -34,6 +34,8 @@ interface BuildpackResolverContext { List getBuildpackMetadata(); + BuildpackLayersMetadata getBuildpackLayersMetadata(); + /** * Retrieve an image. * @param reference the image reference 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 6b383bbfcfd1..c52bed91297c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -28,10 +28,12 @@ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.springframework.boot.buildpack.platform.build.BuildpackLayersMetadata.BuildpackLayerDetails; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.Image; 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; @@ -59,13 +61,28 @@ private ImageBuildpack(BuildpackResolverContext context, ImageReference imageRef Image image = context.fetchImage(reference, ImageType.BUILDPACK); BuildpackMetadata buildpackMetadata = BuildpackMetadata.fromImage(image); this.coordinates = BuildpackCoordinates.fromBuildpackMetadata(buildpackMetadata); - this.exportedLayers = new ExportedLayers(context, reference); + if (!buildpackExistsInBuilder(context, image.getLayers())) { + this.exportedLayers = new ExportedLayers(context, reference); + } + else { + this.exportedLayers = null; + } } catch (IOException | DockerEngineException ex) { throw new IllegalArgumentException("Error pulling buildpack image '" + reference + "'", ex); } } + private boolean buildpackExistsInBuilder(BuildpackResolverContext context, List imageLayers) { + BuildpackLayerDetails buildpackLayerDetails = context.getBuildpackLayersMetadata() + .getBuildpack(this.coordinates.getId(), this.coordinates.getVersion()); + if (buildpackLayerDetails != null) { + String layerDiffId = buildpackLayerDetails.getLayerDiffId(); + return imageLayers.stream().map(LayerId::toString).anyMatch((layerId) -> layerId.equals(layerDiffId)); + } + return false; + } + @Override public BuildpackCoordinates getCoordinates() { return this.coordinates; @@ -73,7 +90,9 @@ public BuildpackCoordinates getCoordinates() { @Override public void apply(IOConsumer layers) throws IOException { - this.exportedLayers.apply(layers); + if (this.exportedLayers != null) { + this.exportedLayers.apply(layers); + } } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackLayersMetadataTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackLayersMetadataTests.java new file mode 100644 index 000000000000..779c1607d39c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackLayersMetadataTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2022 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.build; + +import java.io.IOException; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BuildpackLayersMetadata}. + * + * @author Scott Frederick + */ +class BuildpackLayersMetadataTests extends AbstractJsonTests { + + @Test + void fromImageLoadsMetadata() throws IOException { + Image image = Image.of(getContent("buildpack-image.json")); + BuildpackLayersMetadata metadata = BuildpackLayersMetadata.fromImage(image); + assertThat(metadata.getBuildpack("example/hello-moon", "0.0.3")).extracting("homepage", "layerDiffId") + .containsExactly("https://github.com/example/tree/main/buildpacks/hello-moon", + "sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2"); + assertThat(metadata.getBuildpack("example/hello-world", "0.0.2")).extracting("homepage", "layerDiffId") + .containsExactly("https://github.com/example/tree/main/buildpacks/hello-world", + "sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940"); + assertThat(metadata.getBuildpack("example/hello-world", "version-does-not-exist")).isNull(); + assertThat(metadata.getBuildpack("id-does-not-exist", "9.9.9")).isNull(); + } + + @Test + void fromImageWhenImageIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackLayersMetadata.fromImage(null)) + .withMessage("Image must not be null"); + } + + @Test + void fromImageWhenImageConfigIsNullThrowsException() { + Image image = mock(Image.class); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackLayersMetadata.fromImage(image)) + .withMessage("ImageConfig must not be null"); + } + + @Test + void fromImageConfigWhenLabelIsMissingThrowsException() { + Image image = mock(Image.class); + ImageConfig imageConfig = mock(ImageConfig.class); + given(image.getConfig()).willReturn(imageConfig); + given(imageConfig.getLabels()).willReturn(Collections.singletonMap("alpha", "a")); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackLayersMetadata.fromImage(image)) + .withMessage("No 'io.buildpacks.buildpack.layers' label found in image config labels 'alpha'"); + } + + @Test + void fromJsonLoadsMetadata() throws IOException { + BuildpackLayersMetadata metadata = BuildpackLayersMetadata + .fromJson(getContentAsString("buildpack-layers-metadata.json")); + assertThat(metadata.getBuildpack("example/hello-moon", "0.0.3")).extracting("name", "homepage", "layerDiffId") + .containsExactly("Example hello-moon buildpack", + "https://github.com/example/tree/main/buildpacks/hello-moon", + "sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2"); + assertThat(metadata.getBuildpack("example/hello-world", "0.0.1")).extracting("name", "homepage", "layerDiffId") + .containsExactly("Example hello-world buildpack", + "https://github.com/example/tree/main/buildpacks/hello-world", + "sha256:1c90e0b80d92555a0523c9ee6500845328fc39ba9dca9d30a877ff759ffbff28"); + assertThat(metadata.getBuildpack("example/hello-world", "0.0.2")).extracting("name", "homepage", "layerDiffId") + .containsExactly("Example hello-world buildpack", + "https://github.com/example/tree/main/buildpacks/hello-world", + "sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940"); + assertThat(metadata.getBuildpack("example/hello-world", "version-does-not-exist")).isNull(); + assertThat(metadata.getBuildpack("id-does-not-exist", "9.9.9")).isNull(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java index 4356f56387bd..12c2038a7dea 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -84,6 +84,7 @@ void resolveAllWithTarGzipBuildpackReferenceReturnsExpectedBuildpack(@TempDir Fi void resolveAllWithImageBuildpackReferenceReturnsExpectedBuildpack() throws IOException { Image image = Image.of(getContent("buildpack-image.json")); BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}")); given(resolverContext.fetchImage(any(), any())).willReturn(image); BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest"); Buildpacks buildpacks = BuildpackResolvers.resolveAll(resolverContext, Collections.singleton(reference)); 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 a9195daa78f1..7a349006cf92 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -64,29 +64,31 @@ void setUp() { } @Test - void resolveWhenFullyQualifiedReferenceReturnsBuilder() throws Exception { + void resolveWhenFullyQualifiedReferenceReturnsBuildpack() throws Exception { Image image = Image.of(getContent("buildpack-image.json")); ImageReference imageReference = ImageReference.of("example/buildpack1:1.0.0"); BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}")); given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any()); BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:1.0.0"); Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); - assertHasExpectedLayers(buildpack); + assertAppliesExpectedLayers(buildpack); } @Test - void resolveWhenUnqualifiedReferenceReturnsBuilder() throws Exception { + void resolveWhenUnqualifiedReferenceReturnsBuildpack() throws Exception { Image image = Image.of(getContent("buildpack-image.json")); ImageReference imageReference = ImageReference.of("example/buildpack1:1.0.0"); BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}")); given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any()); BuildpackReference reference = BuildpackReference.of("example/buildpack1:1.0.0"); Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); - assertHasExpectedLayers(buildpack); + assertAppliesExpectedLayers(buildpack); } @Test @@ -94,12 +96,13 @@ void resolveReferenceWithoutTagUsesLatestTag() throws Exception { Image image = Image.of(getContent("buildpack-image.json")); ImageReference imageReference = ImageReference.of("example/buildpack1:latest"); BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}")); given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any()); BuildpackReference reference = BuildpackReference.of("example/buildpack1"); Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); - assertHasExpectedLayers(buildpack); + assertAppliesExpectedLayers(buildpack); } @Test @@ -108,12 +111,28 @@ void resolveReferenceWithDigestUsesDigest() throws Exception { String digest = "sha256:4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30"; ImageReference imageReference = ImageReference.of("example/buildpack1@" + digest); BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.getBuildpackLayersMetadata()).willReturn(BuildpackLayersMetadata.fromJson("{}")); given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any()); BuildpackReference reference = BuildpackReference.of("example/buildpack1@" + digest); Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); - assertHasExpectedLayers(buildpack); + assertAppliesExpectedLayers(buildpack); + } + + @Test + void resolveWhenBuildpackExistsInBuilderSkipsLayers() throws Exception { + Image image = Image.of(getContent("buildpack-image.json")); + ImageReference imageReference = ImageReference.of("example/buildpack1:1.0.0"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.getBuildpackLayersMetadata()) + .willReturn(BuildpackLayersMetadata.fromJson(getContentAsString("buildpack-layers-metadata.json"))); + given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); + willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any()); + BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:1.0.0"); + Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); + assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); + assertAppliesNoLayers(buildpack); } @Test @@ -181,7 +200,7 @@ private void writeTarEntry(TarArchiveOutputStream tarOut, String name) throws IO tarOut.closeArchiveEntry(); } - private void assertHasExpectedLayers(Buildpack buildpack) throws IOException { + private void assertAppliesExpectedLayers(Buildpack buildpack) throws IOException { List layers = new ArrayList<>(); buildpack.apply((layer) -> { ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -208,4 +227,14 @@ private void assertHasExpectedLayers(Buildpack buildpack) throws IOException { TarArchiveEntry.DEFAULT_FILE_MODE)); } + private void assertAppliesNoLayers(Buildpack buildpack) throws IOException { + List layers = new ArrayList<>(); + buildpack.apply((layer) -> { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + layers.add(out); + }); + assertThat(layers).isEmpty(); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-layers-metadata.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-layers-metadata.json new file mode 100644 index 000000000000..590ff073dac2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-layers-metadata.json @@ -0,0 +1,70 @@ +{ + "example/hello-moon": { + "0.0.3": { + "api": "0.2", + "stacks": [ + { + "id": "io.buildpacks.stacks.alpine" + }, + { + "id": "io.buildpacks.stacks.bionic" + } + ], + "name": "Example hello-moon buildpack", + "layerDiffID": "sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2", + "homepage": "https://github.com/example/tree/main/buildpacks/hello-moon" + } + }, + "example/hello-universe": { + "0.0.1": { + "api": "0.2", + "order": [ + { + "group": [ + { + "id": "example/hello-world", + "version": "0.0.2" + }, + { + "id": "example/hello-moon", + "version": "0.0.2" + } + ] + } + ], + "name": "Example hello-universe buildpack", + "layerDiffID": "sha256:739b4e8f3caae7237584a1bfe029ebdb05403752b1a60a4f9be991b1d51dbb69", + "homepage": "https://github.com/example/tree/main/buildpacks/hello-universe" + } + }, + "example/hello-world": { + "0.0.1": { + "api": "0.2", + "stacks": [ + { + "id": "io.buildpacks.stacks.alpine" + }, + { + "id": "io.buildpacks.stacks.bionic" + } + ], + "name": "Example hello-world buildpack", + "layerDiffID": "sha256:1c90e0b80d92555a0523c9ee6500845328fc39ba9dca9d30a877ff759ffbff28", + "homepage": "https://github.com/example/tree/main/buildpacks/hello-world" + }, + "0.0.2": { + "api": "0.2", + "stacks": [ + { + "id": "io.buildpacks.stacks.alpine" + }, + { + "id": "io.buildpacks.stacks.bionic" + } + ], + "name": "Example hello-world buildpack", + "layerDiffID": "sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940", + "homepage": "https://github.com/example/tree/main/buildpacks/hello-world" + } + } +}