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
This commit is contained in:
parent
bb693d73a6
commit
c8ff874e91
|
|
@ -96,8 +96,8 @@ public class Builder {
|
||||||
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
|
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
|
||||||
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
|
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
|
||||||
request = determineRunImage(request, builderImage, builderMetadata.getStack());
|
request = determineRunImage(request, builderImage, builderMetadata.getStack());
|
||||||
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getCreator(),
|
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, request.getName(), builderMetadata,
|
||||||
request.getEnv());
|
request.getCreator(), request.getEnv());
|
||||||
this.docker.image().load(builder.getArchive(), UpdateListener.none());
|
this.docker.image().load(builder.getArchive(), UpdateListener.none());
|
||||||
try {
|
try {
|
||||||
executeLifecycle(request, builder);
|
executeLifecycle(request, builder);
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 {
|
class EphemeralBuilder {
|
||||||
|
|
||||||
|
static final String BUILDER_FOR_LABEL_NAME = "org.springframework.boot.builderFor";
|
||||||
|
|
||||||
private final BuildOwner buildOwner;
|
private final BuildOwner buildOwner;
|
||||||
|
|
||||||
private final BuilderMetadata builderMetadata;
|
private final BuilderMetadata builderMetadata;
|
||||||
|
|
@ -45,20 +47,22 @@ class EphemeralBuilder {
|
||||||
/**
|
/**
|
||||||
* Create a new {@link EphemeralBuilder} instance.
|
* Create a new {@link EphemeralBuilder} instance.
|
||||||
* @param buildOwner the build owner
|
* @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 builderMetadata the builder metadata
|
||||||
* @param creator the builder creator
|
* @param creator the builder creator
|
||||||
* @param env the builder env
|
* @param env the builder env
|
||||||
* @throws IOException on IO error
|
* @throws IOException on IO error
|
||||||
*/
|
*/
|
||||||
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata, Creator creator,
|
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, ImageReference targetImage,
|
||||||
Map<String, String> env) throws IOException {
|
BuilderMetadata builderMetadata, Creator creator, Map<String, String> env) throws IOException {
|
||||||
ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm();
|
ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm();
|
||||||
this.buildOwner = buildOwner;
|
this.buildOwner = buildOwner;
|
||||||
this.creator = creator;
|
this.creator = creator;
|
||||||
this.builderMetadata = builderMetadata.copy(this::updateMetadata);
|
this.builderMetadata = builderMetadata.copy(this::updateMetadata);
|
||||||
this.archive = ImageArchive.from(builderImage, (update) -> {
|
this.archive = ImageArchive.from(builderImage, (update) -> {
|
||||||
update.withUpdatedConfig(this.builderMetadata::attachTo);
|
update.withUpdatedConfig(this.builderMetadata::attachTo);
|
||||||
|
update.withUpdatedConfig((config) -> config.withLabel(BUILDER_FOR_LABEL_NAME, targetImage.toString()));
|
||||||
update.withTag(name);
|
update.withTag(name);
|
||||||
if (env != null && !env.isEmpty()) {
|
if (env != null && !env.isEmpty()) {
|
||||||
update.withNewLayer(getEnvLayer(env));
|
update.withNewLayer(getEnvLayer(env));
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.entry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link EphemeralBuilder}.
|
* Tests for {@link EphemeralBuilder}.
|
||||||
|
|
@ -58,15 +59,18 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
||||||
|
|
||||||
private Image image;
|
private Image image;
|
||||||
|
|
||||||
|
private ImageReference targetImage;
|
||||||
|
|
||||||
private BuilderMetadata metadata;
|
private BuilderMetadata metadata;
|
||||||
|
|
||||||
private Map<String, String> env;
|
private Map<String, String> env;
|
||||||
|
|
||||||
private Creator creator = Creator.withVersion("dev");
|
private final Creator creator = Creator.withVersion("dev");
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setup() throws Exception {
|
void setup() throws Exception {
|
||||||
this.image = Image.of(getContent("image.json"));
|
this.image = Image.of(getContent("image.json"));
|
||||||
|
this.targetImage = ImageReference.of("my-image:latest");
|
||||||
this.metadata = BuilderMetadata.fromImage(this.image);
|
this.metadata = BuilderMetadata.fromImage(this.image);
|
||||||
this.env = new HashMap<>();
|
this.env = new HashMap<>();
|
||||||
this.env.put("spring", "boot");
|
this.env.put("spring", "boot");
|
||||||
|
|
@ -75,15 +79,18 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getNameHasRandomName() throws Exception {
|
void getNameHasRandomName() throws Exception {
|
||||||
EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
|
EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
|
||||||
EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env);
|
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()).startsWith("pack.local/builder/").endsWith(":latest");
|
||||||
assertThat(b1.getName().toString()).isNotEqualTo(b2.getName().toString());
|
assertThat(b1.getName().toString()).isNotEqualTo(b2.getName().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getArchiveHasCreatedByConfig() throws Exception {
|
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();
|
ImageConfig config = builder.getArchive().getImageConfig();
|
||||||
BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config);
|
BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config);
|
||||||
assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot");
|
assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot");
|
||||||
|
|
@ -92,14 +99,16 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getArchiveHasTag() throws Exception {
|
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();
|
ImageReference tag = builder.getArchive().getTag();
|
||||||
assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest");
|
assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getArchiveHasFixedCreateDate() throws Exception {
|
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();
|
Instant createInstant = builder.getArchive().getCreateDate();
|
||||||
OffsetDateTime createDateTime = OffsetDateTime.ofInstant(createInstant, ZoneId.of("UTC"));
|
OffsetDateTime createDateTime = OffsetDateTime.ofInstant(createInstant, ZoneId.of("UTC"));
|
||||||
assertThat(createDateTime.getYear()).isEqualTo(1980);
|
assertThat(createDateTime.getYear()).isEqualTo(1980);
|
||||||
|
|
@ -112,12 +121,22 @@ class EphemeralBuilderTests extends AbstractJsonTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getArchiveContainsEnvLayer() throws Exception {
|
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");
|
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/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot");
|
||||||
assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent("");
|
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 {
|
private TarArchiveInputStream getLayer(ImageArchive archive, int index) throws Exception {
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
archive.writeTo(outputStream);
|
archive.writeTo(outputStream);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue