Skip to content

Commit

Permalink
Add builderFor label to ephemeral builder image
Browse files Browse the repository at this point in the history
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
  • Loading branch information
scottfrederick committed Sep 14, 2021
1 parent bb693d7 commit c8ff874
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 14 deletions.
Expand Up @@ -96,8 +96,8 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
request = determineRunImage(request, builderImage, builderMetadata.getStack());
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getCreator(),
request.getEnv());
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, request.getName(), builderMetadata,
request.getCreator(), request.getEnv());
this.docker.image().load(builder.getArchive(), UpdateListener.none());
try {
executeLifecycle(request, builder);
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
Expand Down Expand Up @@ -34,6 +34,8 @@
*/
class EphemeralBuilder {

static final String BUILDER_FOR_LABEL_NAME = "org.springframework.boot.builderFor";

private final BuildOwner buildOwner;

private final BuilderMetadata builderMetadata;
Expand All @@ -45,20 +47,22 @@ class EphemeralBuilder {
/**
* Create a new {@link EphemeralBuilder} instance.
* @param buildOwner the build owner
* @param builderImage the image
* @param builderImage the base builder image
* @param targetImage the image being built
* @param builderMetadata the builder metadata
* @param creator the builder creator
* @param env the builder env
* @throws IOException on IO error
*/
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata, Creator creator,
Map<String, String> env) throws IOException {
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, ImageReference targetImage,
BuilderMetadata builderMetadata, Creator creator, Map<String, String> env) throws IOException {
ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm();
this.buildOwner = buildOwner;
this.creator = creator;
this.builderMetadata = builderMetadata.copy(this::updateMetadata);
this.archive = ImageArchive.from(builderImage, (update) -> {
update.withUpdatedConfig(this.builderMetadata::attachTo);
update.withUpdatedConfig((config) -> config.withLabel(BUILDER_FOR_LABEL_NAME, targetImage.toString()));
update.withTag(name);
if (env != null && !env.isEmpty()) {
update.withNewLayer(getEnvLayer(env));
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
Expand Down Expand Up @@ -42,6 +42,7 @@
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;

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

private Image image;

private ImageReference targetImage;

private BuilderMetadata metadata;

private Map<String, String> env;

private Creator creator = Creator.withVersion("dev");
private final Creator creator = Creator.withVersion("dev");

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

@Test
void getNameHasRandomName() throws Exception {
EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
this.creator, this.env);
EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
this.creator, this.env);
assertThat(b1.getName().toString()).startsWith("pack.local/builder/").endsWith(":latest");
assertThat(b1.getName().toString()).isNotEqualTo(b2.getName().toString());
}

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

@Test
void getArchiveHasTag() throws Exception {
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
this.creator, this.env);
ImageReference tag = builder.getArchive().getTag();
assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest");
}

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

@Test
void getArchiveContainsEnvLayer() throws Exception {
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
this.creator, this.env);
File directory = unpack(getLayer(builder.getArchive(), 0), "env");
assertThat(new File(directory, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot");
assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent("");
}

@Test
void getArchiveHasBuilderForLabel() throws Exception {
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
this.creator, this.env);
ImageConfig config = builder.getArchive().getImageConfig();
assertThat(config.getLabels())
.contains(entry(EphemeralBuilder.BUILDER_FOR_LABEL_NAME, this.targetImage.toString()));
}

private TarArchiveInputStream getLayer(ImageArchive archive, int index) throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
archive.writeTo(outputStream);
Expand Down

0 comments on commit c8ff874

Please sign in to comment.