Skip to content

Commit c8ff874

Browse files
Add builderFor label to ephemeral builder image
Adding a label to the ephemeral builder image cloned from the base builder image eliminates contention between builds that are run concurrently. Without this label, concurrent builds could result in a race condition in the Docker daemon if the ephemeral builder image shared by builds was deleted by both builds at exactly the same time. Fixes gh-27888
1 parent bb693d7 commit c8ff874

File tree

3 files changed

+37
-14
lines changed

3 files changed

+37
-14
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
9696
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
9797
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
9898
request = determineRunImage(request, builderImage, builderMetadata.getStack());
99-
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getCreator(),
100-
request.getEnv());
99+
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, request.getName(), builderMetadata,
100+
request.getCreator(), request.getEnv());
101101
this.docker.image().load(builder.getArchive(), UpdateListener.none());
102102
try {
103103
executeLifecycle(request, builder);

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

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 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 @@
3434
*/
3535
class EphemeralBuilder {
3636

37+
static final String BUILDER_FOR_LABEL_NAME = "org.springframework.boot.builderFor";
38+
3739
private final BuildOwner buildOwner;
3840

3941
private final BuilderMetadata builderMetadata;
@@ -45,20 +47,22 @@ class EphemeralBuilder {
4547
/**
4648
* Create a new {@link EphemeralBuilder} instance.
4749
* @param buildOwner the build owner
48-
* @param builderImage the image
50+
* @param builderImage the base builder image
51+
* @param targetImage the image being built
4952
* @param builderMetadata the builder metadata
5053
* @param creator the builder creator
5154
* @param env the builder env
5255
* @throws IOException on IO error
5356
*/
54-
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata, Creator creator,
55-
Map<String, String> env) throws IOException {
57+
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, ImageReference targetImage,
58+
BuilderMetadata builderMetadata, Creator creator, Map<String, String> env) throws IOException {
5659
ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm();
5760
this.buildOwner = buildOwner;
5861
this.creator = creator;
5962
this.builderMetadata = builderMetadata.copy(this::updateMetadata);
6063
this.archive = ImageArchive.from(builderImage, (update) -> {
6164
update.withUpdatedConfig(this.builderMetadata::attachTo);
65+
update.withUpdatedConfig((config) -> config.withLabel(BUILDER_FOR_LABEL_NAME, targetImage.toString()));
6266
update.withTag(name);
6367
if (env != null && !env.isEmpty()) {
6468
update.withNewLayer(getEnvLayer(env));

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java

+27-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 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.
@@ -42,6 +42,7 @@
4242
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
4343

4444
import static org.assertj.core.api.Assertions.assertThat;
45+
import static org.assertj.core.api.Assertions.entry;
4546

4647
/**
4748
* Tests for {@link EphemeralBuilder}.
@@ -58,15 +59,18 @@ class EphemeralBuilderTests extends AbstractJsonTests {
5859

5960
private Image image;
6061

62+
private ImageReference targetImage;
63+
6164
private BuilderMetadata metadata;
6265

6366
private Map<String, String> env;
6467

65-
private Creator creator = Creator.withVersion("dev");
68+
private final Creator creator = Creator.withVersion("dev");
6669

6770
@BeforeEach
6871
void setup() throws Exception {
6972
this.image = Image.of(getContent("image.json"));
73+
this.targetImage = ImageReference.of("my-image:latest");
7074
this.metadata = BuilderMetadata.fromImage(this.image);
7175
this.env = new HashMap<>();
7276
this.env.put("spring", "boot");
@@ -75,15 +79,18 @@ void setup() throws Exception {
7579

7680
@Test
7781
void getNameHasRandomName() throws Exception {
78-
EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
79-
EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
82+
EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
83+
this.creator, this.env);
84+
EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
85+
this.creator, this.env);
8086
assertThat(b1.getName().toString()).startsWith("pack.local/builder/").endsWith(":latest");
8187
assertThat(b1.getName().toString()).isNotEqualTo(b2.getName().toString());
8288
}
8389

8490
@Test
8591
void getArchiveHasCreatedByConfig() throws Exception {
86-
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
92+
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
93+
this.creator, this.env);
8794
ImageConfig config = builder.getArchive().getImageConfig();
8895
BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config);
8996
assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot");
@@ -92,14 +99,16 @@ void getArchiveHasCreatedByConfig() throws Exception {
9299

93100
@Test
94101
void getArchiveHasTag() throws Exception {
95-
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
102+
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
103+
this.creator, this.env);
96104
ImageReference tag = builder.getArchive().getTag();
97105
assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest");
98106
}
99107

100108
@Test
101109
void getArchiveHasFixedCreateDate() throws Exception {
102-
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
110+
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
111+
this.creator, this.env);
103112
Instant createInstant = builder.getArchive().getCreateDate();
104113
OffsetDateTime createDateTime = OffsetDateTime.ofInstant(createInstant, ZoneId.of("UTC"));
105114
assertThat(createDateTime.getYear()).isEqualTo(1980);
@@ -112,12 +121,22 @@ void getArchiveHasFixedCreateDate() throws Exception {
112121

113122
@Test
114123
void getArchiveContainsEnvLayer() throws Exception {
115-
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
124+
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
125+
this.creator, this.env);
116126
File directory = unpack(getLayer(builder.getArchive(), 0), "env");
117127
assertThat(new File(directory, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot");
118128
assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent("");
119129
}
120130

131+
@Test
132+
void getArchiveHasBuilderForLabel() throws Exception {
133+
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
134+
this.creator, this.env);
135+
ImageConfig config = builder.getArchive().getImageConfig();
136+
assertThat(config.getLabels())
137+
.contains(entry(EphemeralBuilder.BUILDER_FOR_LABEL_NAME, this.targetImage.toString()));
138+
}
139+
121140
private TarArchiveInputStream getLayer(ImageArchive archive, int index) throws Exception {
122141
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
123142
archive.writeTo(outputStream);

0 commit comments

Comments
 (0)