Skip to content

Commit 17bdc52

Browse files
Avoid adding layers for buildpacks that exist in the builder
This commit adds validation of any buildpacks that are specified for image building to match them against buildpacks that are bundled in the builder. If an image buildpack's ID, version, and one layer hash match the same information stored in a label on the builder image, that buildpack won't be added and the buildpack bundled in the builder will be used instead. This reduces the chance of adding to the total count of layers in a builder image unnecessarily. Fixes gh-31233
1 parent 6411f88 commit 17bdc52

File tree

8 files changed

+442
-18
lines changed

8 files changed

+442
-18
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -105,7 +105,8 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
105105
Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage());
106106
assertStackIdsMatch(runImage, builderImage);
107107
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
108-
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata);
108+
BuildpackLayersMetadata buildpackLayersMetadata = BuildpackLayersMetadata.fromImage(builderImage);
109+
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata, buildpackLayersMetadata);
109110
EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(),
110111
builderMetadata, request.getCreator(), request.getEnv(), buildpacks);
111112
this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none());
@@ -141,8 +142,10 @@ private void assertStackIdsMatch(Image runImage, Image builderImage) {
141142
+ "' does not match builder stack '" + builderImageStackId + "'");
142143
}
143144

144-
private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata) {
145-
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata);
145+
private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata,
146+
BuildpackLayersMetadata buildpackLayersMetadata) {
147+
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata,
148+
buildpackLayersMetadata);
146149
return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks());
147150
}
148151

@@ -239,16 +242,25 @@ private class BuilderResolverContext implements BuildpackResolverContext {
239242

240243
private final BuilderMetadata builderMetadata;
241244

242-
BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata) {
245+
private final BuildpackLayersMetadata buildpackLayersMetadata;
246+
247+
BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata,
248+
BuildpackLayersMetadata buildpackLayersMetadata) {
243249
this.imageFetcher = imageFetcher;
244250
this.builderMetadata = builderMetadata;
251+
this.buildpackLayersMetadata = buildpackLayersMetadata;
245252
}
246253

247254
@Override
248255
public List<BuildpackMetadata> getBuildpackMetadata() {
249256
return this.builderMetadata.getBuildpacks();
250257
}
251258

259+
@Override
260+
public BuildpackLayersMetadata getBuildpackLayersMetadata() {
261+
return this.buildpackLayersMetadata;
262+
}
263+
252264
@Override
253265
public Image fetchImage(ImageReference reference, ImageType imageType) throws IOException {
254266
return this.imageFetcher.fetchImage(imageType, reference);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.buildpack.platform.build;
18+
19+
import java.io.IOException;
20+
import java.lang.invoke.MethodHandles;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
24+
import com.fasterxml.jackson.databind.JsonNode;
25+
26+
import org.springframework.boot.buildpack.platform.docker.type.Image;
27+
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
28+
import org.springframework.boot.buildpack.platform.json.MappedObject;
29+
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
30+
import org.springframework.util.Assert;
31+
import org.springframework.util.StringUtils;
32+
33+
/**
34+
* Buildpack layers metadata information.
35+
*
36+
* @author Scott Frederick
37+
*/
38+
final class BuildpackLayersMetadata extends MappedObject {
39+
40+
private static final String LABEL_NAME = "io.buildpacks.buildpack.layers";
41+
42+
private final Buildpacks buildpacks;
43+
44+
private BuildpackLayersMetadata(JsonNode node) {
45+
super(node, MethodHandles.lookup());
46+
this.buildpacks = Buildpacks.fromJson(getNode());
47+
}
48+
49+
/**
50+
* Return the metadata details of a buildpack with the given ID and version.
51+
* @param id the buildpack ID
52+
* @param version the buildpack version
53+
* @return the buildpack details or {@code null} if a buildpack with the given ID and
54+
* version does not exist in the metadata
55+
*/
56+
BuildpackLayerDetails getBuildpack(String id, String version) {
57+
return this.buildpacks.getBuildpack(id, version);
58+
}
59+
60+
/**
61+
* Create a {@link BuildpackLayersMetadata} from an image.
62+
* @param image the source image
63+
* @return the buildpack layers metadata
64+
* @throws IOException on IO error
65+
*/
66+
static BuildpackLayersMetadata fromImage(Image image) throws IOException {
67+
Assert.notNull(image, "Image must not be null");
68+
return fromImageConfig(image.getConfig());
69+
}
70+
71+
/**
72+
* Create a {@link BuildpackLayersMetadata} from image config.
73+
* @param imageConfig the source image config
74+
* @return the buildpack layers metadata
75+
* @throws IOException on IO error
76+
*/
77+
static BuildpackLayersMetadata fromImageConfig(ImageConfig imageConfig) throws IOException {
78+
Assert.notNull(imageConfig, "ImageConfig must not be null");
79+
String json = imageConfig.getLabels().get(LABEL_NAME);
80+
Assert.notNull(json, () -> "No '" + LABEL_NAME + "' label found in image config labels '"
81+
+ StringUtils.collectionToCommaDelimitedString(imageConfig.getLabels().keySet()) + "'");
82+
return fromJson(json);
83+
}
84+
85+
/**
86+
* Create a {@link BuildpackLayersMetadata} from JSON.
87+
* @param json the source JSON
88+
* @return the buildpack layers metadata
89+
* @throws IOException on IO error
90+
*/
91+
static BuildpackLayersMetadata fromJson(String json) throws IOException {
92+
return fromJson(SharedObjectMapper.get().readTree(json));
93+
}
94+
95+
/**
96+
* Create a {@link BuildpackLayersMetadata} from JSON.
97+
* @param node the source JSON
98+
* @return the buildpack layers metadata
99+
*/
100+
static BuildpackLayersMetadata fromJson(JsonNode node) {
101+
return new BuildpackLayersMetadata(node);
102+
}
103+
104+
private static class Buildpacks {
105+
106+
private final Map<String, BuildpackVersions> buildpacks = new HashMap<>();
107+
108+
private BuildpackLayerDetails getBuildpack(String id, String version) {
109+
if (this.buildpacks.containsKey(id)) {
110+
return this.buildpacks.get(id).getBuildpack(version);
111+
}
112+
return null;
113+
}
114+
115+
private void addBuildpackVersions(String id, BuildpackVersions versions) {
116+
this.buildpacks.put(id, versions);
117+
}
118+
119+
private static Buildpacks fromJson(JsonNode node) {
120+
Buildpacks buildpacks = new Buildpacks();
121+
node.fields().forEachRemaining((field) -> buildpacks.addBuildpackVersions(field.getKey(),
122+
BuildpackVersions.fromJson(field.getValue())));
123+
return buildpacks;
124+
}
125+
126+
}
127+
128+
private static class BuildpackVersions {
129+
130+
private final Map<String, BuildpackLayerDetails> versions = new HashMap<>();
131+
132+
private BuildpackLayerDetails getBuildpack(String version) {
133+
return this.versions.get(version);
134+
}
135+
136+
private void addBuildpackVersion(String version, BuildpackLayerDetails details) {
137+
this.versions.put(version, details);
138+
}
139+
140+
private static BuildpackVersions fromJson(JsonNode node) {
141+
BuildpackVersions versions = new BuildpackVersions();
142+
node.fields().forEachRemaining((field) -> versions.addBuildpackVersion(field.getKey(),
143+
BuildpackLayerDetails.fromJson(field.getValue())));
144+
return versions;
145+
}
146+
147+
}
148+
149+
static final class BuildpackLayerDetails extends MappedObject {
150+
151+
private final String name;
152+
153+
private final String homepage;
154+
155+
private final String layerDiffId;
156+
157+
private BuildpackLayerDetails(JsonNode node) {
158+
super(node, MethodHandles.lookup());
159+
this.name = valueAt("/name", String.class);
160+
this.homepage = valueAt("/homepage", String.class);
161+
this.layerDiffId = valueAt("/layerDiffID", String.class);
162+
}
163+
164+
/**
165+
* Return the buildpack name.
166+
* @return the name
167+
*/
168+
String getName() {
169+
return this.name;
170+
}
171+
172+
/**
173+
* Return the buildpack homepage address.
174+
* @return the homepage address
175+
*/
176+
String getHomepage() {
177+
return this.homepage;
178+
}
179+
180+
/**
181+
* Return the buildpack layer {@code diffID}.
182+
* @return the layer {@code diffID}
183+
*/
184+
String getLayerDiffId() {
185+
return this.layerDiffId;
186+
}
187+
188+
private static BuildpackLayerDetails fromJson(JsonNode node) {
189+
return new BuildpackLayerDetails(node);
190+
}
191+
192+
}
193+
194+
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,6 +34,8 @@ interface BuildpackResolverContext {
3434

3535
List<BuildpackMetadata> getBuildpackMetadata();
3636

37+
BuildpackLayersMetadata getBuildpackLayersMetadata();
38+
3739
/**
3840
* Retrieve an image.
3941
* @param reference the image reference

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,10 +28,12 @@
2828
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
2929
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
3030

31+
import org.springframework.boot.buildpack.platform.build.BuildpackLayersMetadata.BuildpackLayerDetails;
3132
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
3233
import org.springframework.boot.buildpack.platform.docker.type.Image;
3334
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
3435
import org.springframework.boot.buildpack.platform.docker.type.Layer;
36+
import org.springframework.boot.buildpack.platform.docker.type.LayerId;
3537
import org.springframework.boot.buildpack.platform.io.IOConsumer;
3638
import org.springframework.boot.buildpack.platform.io.TarArchive;
3739
import org.springframework.util.StreamUtils;
@@ -59,21 +61,38 @@ private ImageBuildpack(BuildpackResolverContext context, ImageReference imageRef
5961
Image image = context.fetchImage(reference, ImageType.BUILDPACK);
6062
BuildpackMetadata buildpackMetadata = BuildpackMetadata.fromImage(image);
6163
this.coordinates = BuildpackCoordinates.fromBuildpackMetadata(buildpackMetadata);
62-
this.exportedLayers = new ExportedLayers(context, reference);
64+
if (!buildpackExistsInBuilder(context, image.getLayers())) {
65+
this.exportedLayers = new ExportedLayers(context, reference);
66+
}
67+
else {
68+
this.exportedLayers = null;
69+
}
6370
}
6471
catch (IOException | DockerEngineException ex) {
6572
throw new IllegalArgumentException("Error pulling buildpack image '" + reference + "'", ex);
6673
}
6774
}
6875

76+
private boolean buildpackExistsInBuilder(BuildpackResolverContext context, List<LayerId> imageLayers) {
77+
BuildpackLayerDetails buildpackLayerDetails = context.getBuildpackLayersMetadata()
78+
.getBuildpack(this.coordinates.getId(), this.coordinates.getVersion());
79+
if (buildpackLayerDetails != null) {
80+
String layerDiffId = buildpackLayerDetails.getLayerDiffId();
81+
return imageLayers.stream().map(LayerId::toString).anyMatch((layerId) -> layerId.equals(layerDiffId));
82+
}
83+
return false;
84+
}
85+
6986
@Override
7087
public BuildpackCoordinates getCoordinates() {
7188
return this.coordinates;
7289
}
7390

7491
@Override
7592
public void apply(IOConsumer<Layer> layers) throws IOException {
76-
this.exportedLayers.apply(layers);
93+
if (this.exportedLayers != null) {
94+
this.exportedLayers.apply(layers);
95+
}
7796
}
7897

7998
/**

0 commit comments

Comments
 (0)