From c8ff874e91d8b39e461fd5ffbc8827c5e237adb1 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Mon, 13 Sep 2021 16:39:15 -0500 Subject: [PATCH] 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 --- .../buildpack/platform/build/Builder.java | 4 +-- .../platform/build/EphemeralBuilder.java | 12 ++++--- .../platform/build/EphemeralBuilderTests.java | 35 ++++++++++++++----- 3 files changed, 37 insertions(+), 14 deletions(-) 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 7e8f2c72a1..b2bfae0a7f 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 @@ -96,8 +96,8 @@ public class Builder { 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); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java index 67bbaf555e..b816f01acf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java @@ -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. @@ -34,6 +34,8 @@ import org.springframework.boot.buildpack.platform.io.Owner; */ class EphemeralBuilder { + static final String BUILDER_FOR_LABEL_NAME = "org.springframework.boot.builderFor"; + private final BuildOwner buildOwner; private final BuilderMetadata builderMetadata; @@ -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 env) throws IOException { + EphemeralBuilder(BuildOwner buildOwner, Image builderImage, ImageReference targetImage, + BuilderMetadata builderMetadata, Creator creator, Map 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)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java index 3f8f6411f1..fb77b87606 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java @@ -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. @@ -42,6 +42,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference; 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}. @@ -58,15 +59,18 @@ class EphemeralBuilderTests extends AbstractJsonTests { private Image image; + private ImageReference targetImage; + private BuilderMetadata metadata; private Map 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"); @@ -75,15 +79,18 @@ class EphemeralBuilderTests extends AbstractJsonTests { @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"); @@ -92,14 +99,16 @@ class EphemeralBuilderTests extends AbstractJsonTests { @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); @@ -112,12 +121,22 @@ class EphemeralBuilderTests extends AbstractJsonTests { @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);