Skip to content

Commit f6dae13

Browse files
committed
Add application directory layer to ephemeral builder for Podman support
Update `EphemeralBuilder` so that it adds an additional layer that containing an empty application (aka workspace) directory owned by the build user. Prior to this commit, the directory was only bound. This could cause issues on Podman where, unlike Docker, the bound directory is owned by `root`. Fixes gh-45233
1 parent 2ffea0f commit f6dae13

File tree

4 files changed

+75
-39
lines changed

4 files changed

+75
-39
lines changed

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

+22-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 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.
@@ -29,6 +29,7 @@
2929
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
3030
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
3131
import org.springframework.boot.buildpack.platform.docker.type.Image;
32+
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
3233
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
3334
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
3435
import org.springframework.boot.buildpack.platform.io.TarArchive;
@@ -110,16 +111,10 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
110111
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata, buildpackLayersMetadata);
111112
EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(),
112113
builderMetadata, request.getCreator(), request.getEnv(), buildpacks);
113-
this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none());
114-
try {
115-
executeLifecycle(request, ephemeralBuilder);
116-
tagImage(request.getName(), request.getTags());
117-
if (request.isPublish()) {
118-
pushImages(request.getName(), request.getTags());
119-
}
120-
}
121-
finally {
122-
this.docker.image().remove(ephemeralBuilder.getName(), true);
114+
executeLifecycle(request, ephemeralBuilder);
115+
tagImage(request.getName(), request.getTags());
116+
if (request.isPublish()) {
117+
pushImages(request.getName(), request.getTags());
123118
}
124119
}
125120

@@ -157,13 +152,25 @@ private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher
157152
}
158153

159154
private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException {
160-
ResolvedDockerHost dockerHost = null;
161-
if (this.dockerConfiguration != null && this.dockerConfiguration.isBindHostToBuilder()) {
162-
dockerHost = ResolvedDockerHost.from(this.dockerConfiguration.getHost());
155+
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, getDockerHost(), request, builder)) {
156+
executeLifecycle(builder, lifecycle);
163157
}
164-
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, dockerHost, request, builder)) {
158+
}
159+
160+
private void executeLifecycle(EphemeralBuilder builder, Lifecycle lifecycle) throws IOException {
161+
ImageArchive archive = builder.getArchive(lifecycle.getApplicationDirectory());
162+
this.docker.image().load(archive, UpdateListener.none());
163+
try {
165164
lifecycle.execute();
166165
}
166+
finally {
167+
this.docker.image().remove(builder.getName(), true);
168+
}
169+
}
170+
171+
private ResolvedDockerHost getDockerHost() {
172+
boolean bindHostToBuilder = this.dockerConfiguration != null && this.dockerConfiguration.isBindHostToBuilder();
173+
return (bindHostToBuilder) ? ResolvedDockerHost.from(this.dockerConfiguration.getHost()) : null;
167174
}
168175

169176
private void tagImage(ImageReference sourceReference, List<ImageReference> tags) throws IOException {

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

+31-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 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.
@@ -21,11 +21,14 @@
2121

2222
import org.springframework.boot.buildpack.platform.docker.type.Image;
2323
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
24+
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive.Update;
2425
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
2526
import org.springframework.boot.buildpack.platform.docker.type.Layer;
2627
import org.springframework.boot.buildpack.platform.io.Content;
28+
import org.springframework.boot.buildpack.platform.io.IOConsumer;
2729
import org.springframework.boot.buildpack.platform.io.Owner;
2830
import org.springframework.util.CollectionUtils;
31+
import org.springframework.util.StringUtils;
2932

3033
/**
3134
* A short-lived builder that is created for each {@link Lifecycle} run.
@@ -37,13 +40,17 @@ class EphemeralBuilder {
3740

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

43+
private ImageReference name;
44+
4045
private final BuildOwner buildOwner;
4146

47+
private final Creator creator;
48+
4249
private final BuilderMetadata builderMetadata;
4350

44-
private final ImageArchive archive;
51+
private final Image builderImage;
4552

46-
private final Creator creator;
53+
private final IOConsumer<Update> archiveUpdate;
4754

4855
/**
4956
* Create a new {@link EphemeralBuilder} instance.
@@ -54,26 +61,25 @@ class EphemeralBuilder {
5461
* @param creator the builder creator
5562
* @param env the builder env
5663
* @param buildpacks an optional set of buildpacks to apply
57-
* @throws IOException on IO error
5864
*/
5965
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, ImageReference targetImage,
60-
BuilderMetadata builderMetadata, Creator creator, Map<String, String> env, Buildpacks buildpacks)
61-
throws IOException {
62-
ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm();
66+
BuilderMetadata builderMetadata, Creator creator, Map<String, String> env, Buildpacks buildpacks) {
67+
this.name = ImageReference.random("pack.local/builder/").inTaggedForm();
6368
this.buildOwner = buildOwner;
6469
this.creator = creator;
6570
this.builderMetadata = builderMetadata.copy(this::updateMetadata);
66-
this.archive = ImageArchive.from(builderImage, (update) -> {
71+
this.builderImage = builderImage;
72+
this.archiveUpdate = (update) -> {
6773
update.withUpdatedConfig(this.builderMetadata::attachTo);
6874
update.withUpdatedConfig((config) -> config.withLabel(BUILDER_FOR_LABEL_NAME, targetImage.toString()));
69-
update.withTag(name);
75+
update.withTag(this.name);
7076
if (!CollectionUtils.isEmpty(env)) {
7177
update.withNewLayer(getEnvLayer(env));
7278
}
7379
if (buildpacks != null) {
7480
buildpacks.apply(update::withNewLayer);
7581
}
76-
});
82+
};
7783
}
7884

7985
private void updateMetadata(BuilderMetadata.Update update) {
@@ -95,7 +101,7 @@ private Layer getEnvLayer(Map<String, String> env) throws IOException {
95101
* @return the ephemeral builder name
96102
*/
97103
ImageReference getName() {
98-
return this.archive.getTag();
104+
return this.name;
99105
}
100106

101107
/**
@@ -116,15 +122,26 @@ BuilderMetadata getBuilderMetadata() {
116122

117123
/**
118124
* Return the contents of ephemeral builder for passing to Docker.
125+
* @param applicationDirectory the application directory
119126
* @return the ephemeral builder archive
127+
* @throws IOException on IO error
120128
*/
121-
ImageArchive getArchive() {
122-
return this.archive;
129+
ImageArchive getArchive(String applicationDirectory) throws IOException {
130+
return ImageArchive.from(this.builderImage, (update) -> {
131+
this.archiveUpdate.accept(update);
132+
if (StringUtils.hasLength(applicationDirectory)) {
133+
update.withNewLayer(applicationDirectoryLayer(applicationDirectory));
134+
}
135+
});
136+
}
137+
138+
private Layer applicationDirectoryLayer(String applicationDirectory) throws IOException {
139+
return Layer.of((layout) -> layout.directory(applicationDirectory, this.buildOwner));
123140
}
124141

125142
@Override
126143
public String toString() {
127-
return this.archive.getTag().toString();
144+
return this.name.toString();
128145
}
129146

130147
}

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 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.
@@ -116,6 +116,10 @@ class Lifecycle implements Closeable {
116116
this.securityOptions = getSecurityOptions(request);
117117
}
118118

119+
String getApplicationDirectory() {
120+
return this.applicationDirectory;
121+
}
122+
119123
private Cache getBuildCache(BuildRequest request) {
120124
if (request.getBuildCache() != null) {
121125
return request.getBuildCache();

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

+17-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 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.
@@ -88,7 +88,7 @@ void setup() throws Exception {
8888
}
8989

9090
@Test
91-
void getNameHasRandomName() throws Exception {
91+
void getNameHasRandomName() {
9292
EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
9393
this.creator, this.env, this.buildpacks);
9494
EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
@@ -101,7 +101,7 @@ void getNameHasRandomName() throws Exception {
101101
void getArchiveHasCreatedByConfig() throws Exception {
102102
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
103103
this.creator, this.env, this.buildpacks);
104-
ImageConfig config = builder.getArchive().getImageConfig();
104+
ImageConfig config = builder.getArchive(null).getImageConfig();
105105
BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config);
106106
assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot");
107107
assertThat(ephemeralMetadata.getCreatedBy().getVersion()).isEqualTo("dev");
@@ -111,15 +111,15 @@ void getArchiveHasCreatedByConfig() throws Exception {
111111
void getArchiveHasTag() throws Exception {
112112
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
113113
this.creator, this.env, this.buildpacks);
114-
ImageReference tag = builder.getArchive().getTag();
114+
ImageReference tag = builder.getArchive(null).getTag();
115115
assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest");
116116
}
117117

118118
@Test
119119
void getArchiveHasFixedCreatedDate() throws Exception {
120120
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
121121
this.creator, this.env, this.buildpacks);
122-
Instant createInstant = builder.getArchive().getCreateDate();
122+
Instant createInstant = builder.getArchive(null).getCreateDate();
123123
OffsetDateTime createDateTime = OffsetDateTime.ofInstant(createInstant, ZoneId.of("UTC"));
124124
assertThat(createDateTime.getYear()).isEqualTo(1980);
125125
assertThat(createDateTime.getMonthValue()).isOne();
@@ -133,7 +133,7 @@ void getArchiveHasFixedCreatedDate() throws Exception {
133133
void getArchiveContainsEnvLayer() throws Exception {
134134
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
135135
this.creator, this.env, this.buildpacks);
136-
File directory = unpack(getLayer(builder.getArchive(), EXISTING_IMAGE_LAYER_COUNT), "env");
136+
File directory = unpack(getLayer(builder.getArchive(null), EXISTING_IMAGE_LAYER_COUNT), "env");
137137
assertThat(new File(directory, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot");
138138
assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent("");
139139
}
@@ -142,7 +142,7 @@ void getArchiveContainsEnvLayer() throws Exception {
142142
void getArchiveHasBuilderForLabel() throws Exception {
143143
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
144144
this.creator, this.env, this.buildpacks);
145-
ImageConfig config = builder.getArchive().getImageConfig();
145+
ImageConfig config = builder.getArchive(null).getImageConfig();
146146
assertThat(config.getLabels())
147147
.contains(entry(EphemeralBuilder.BUILDER_FOR_LABEL_NAME, this.targetImage.toString()));
148148
}
@@ -162,13 +162,21 @@ void getArchiveContainsBuildpackLayers() throws Exception {
162162
"/cnb/buildpacks/example_buildpack2/0.0.2/buildpack.toml");
163163
assertBuildpackLayerContent(builder, EXISTING_IMAGE_LAYER_COUNT + 2,
164164
"/cnb/buildpacks/example_buildpack3/0.0.3/buildpack.toml");
165-
File orderDirectory = unpack(getLayer(builder.getArchive(), EXISTING_IMAGE_LAYER_COUNT + 3), "order");
165+
File orderDirectory = unpack(getLayer(builder.getArchive(null), EXISTING_IMAGE_LAYER_COUNT + 3), "order");
166166
assertThat(new File(orderDirectory, "cnb/order.toml")).usingCharset(StandardCharsets.UTF_8)
167167
.hasContent(content("order.toml"));
168168
}
169169

170+
@Test
171+
void getArchiveHasApplicationDirectoryLayer() throws Exception {
172+
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata,
173+
this.creator, this.env, this.buildpacks);
174+
File directory = unpack(getLayer(builder.getArchive("/myapp"), EXISTING_IMAGE_LAYER_COUNT + 1), "appdir");
175+
assertThat(new File(directory, "myapp")).isDirectory();
176+
}
177+
170178
private void assertBuildpackLayerContent(EphemeralBuilder builder, int index, String s) throws Exception {
171-
File buildpackDirectory = unpack(getLayer(builder.getArchive(), index), "buildpack");
179+
File buildpackDirectory = unpack(getLayer(builder.getArchive(null), index), "buildpack");
172180
assertThat(new File(buildpackDirectory, s)).usingCharset(StandardCharsets.UTF_8).hasContent("[test]");
173181
}
174182

0 commit comments

Comments
 (0)