Skip to content

Commit c11edda

Browse files
committed
Add bindings command-line option for build-image goal
- Allowed volume bind mounts to be supplied through the `spring-boot.build-image.bindings` and `--bindings` properties - Added command-line bindings to those configured in `pom.xml` or the Gradle build script, taking precedence over a configured one when they share the same container destination path Signed-off-by: Tim Ysewyn <Tim.Ysewyn@me.com>
1 parent 1d693b1 commit c11edda

8 files changed

Lines changed: 133 additions & 5 deletions

File tree

build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ Buildpack references must be in one of the following forms:
179179
| None, indicating the builder should use the buildpacks included in it.
180180

181181
| `bindings`
182-
|
182+
| `--bindings`
183183
a|https://docs.docker.com/storage/bind-mounts/[Volume bind mounts] that should be mounted to the builder container when building the image.
184184
The bindings will be passed unparsed and unvalidated to Docker when creating the builder container.
185185
Bindings must be in one of the following forms:
@@ -192,6 +192,9 @@ Where `<options>` can contain:
192192
* `ro` to mount the volume as read-only in the container
193193
* `rw` to mount the volume as readable and writable in the container
194194
* `volume-opt=key=value` to specify key-value pairs consisting of an option name and its value
195+
196+
Bindings provided using the `--bindings` command-line option are added to those configured in the build script.
197+
When a command-line binding shares its container destination path with a configured binding, the command-line binding takes precedence.
195198
|
196199

197200
| `network`

build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package org.springframework.boot.gradle.tasks.bundling;
1818

1919
import java.io.IOException;
20+
import java.util.ArrayList;
2021
import java.util.Collections;
2122
import java.util.LinkedHashMap;
2223
import java.util.List;
2324
import java.util.Map;
25+
import java.util.Set;
26+
import java.util.stream.Collectors;
2427

2528
import org.gradle.api.Action;
2629
import org.gradle.api.DefaultTask;
@@ -108,6 +111,20 @@ public BootBuildImage() {
108111
getSecurityOptions().convention((Iterable<? extends String>) null);
109112
getEffectiveEnvironment().putAll(getEnvironment());
110113
getEffectiveEnvironment().putAll(getEnvironmentFromCommandLine().map(BootBuildImage::asMap));
114+
getEffectiveBindings().set(getBindings().zip(getBindingsFromCommandLine(), BootBuildImage::mergeBindings));
115+
}
116+
117+
private static List<String> mergeBindings(List<String> bindings, List<String> bindingsFromCommandLine) {
118+
Set<String> commandLineContainerDestinationPaths = bindingsFromCommandLine.stream()
119+
.map((binding) -> Binding.of(binding).getContainerDestinationPath())
120+
.collect(Collectors.toSet());
121+
List<String> merged = new ArrayList<>();
122+
bindings.stream()
123+
.filter((binding) -> !commandLineContainerDestinationPaths
124+
.contains(Binding.of(binding).getContainerDestinationPath()))
125+
.forEach(merged::add);
126+
merged.addAll(bindingsFromCommandLine);
127+
return merged;
111128
}
112129

113130
private static Map<String, String> asMap(List<String> variables) {
@@ -252,9 +269,23 @@ public void setPullPolicy(String pullPolicy) {
252269
* image.
253270
* @return the bindings
254271
*/
272+
@Internal
273+
public abstract ListProperty<String> getBindings();
274+
275+
/**
276+
* Returns the volume bindings contributed from the command line. Added bindings take
277+
* precedence over configured bindings that share the same container destination path.
278+
* @return the bindings from the command line
279+
* @since 4.1.1
280+
*/
281+
@Internal
282+
@Option(option = "bindings", description = "Volume bind mounts that will be mounted to the builder container. "
283+
+ "Can be specified multiple times.")
284+
abstract ListProperty<String> getBindingsFromCommandLine();
285+
255286
@Input
256287
@Optional
257-
public abstract ListProperty<String> getBindings();
288+
abstract ListProperty<String> getEffectiveBindings();
258289

259290
/**
260291
* Returns the tags that will be created for the built image.
@@ -479,7 +510,7 @@ private BuildRequest customizeBuildpacks(BuildRequest request) {
479510
}
480511

481512
private BuildRequest customizeBindings(BuildRequest request) {
482-
List<String> bindings = getBindings().getOrNull();
513+
List<String> bindings = getEffectiveBindings().getOrNull();
483514
if (!CollectionUtils.isEmpty(bindings)) {
484515
return request.withBindings(bindings.stream().map(Binding::of).toList());
485516
}

build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,30 @@ void whenIndividualEntriesAreAddedToBindingsThenRequestHasBindings() {
291291
.containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw"));
292292
}
293293

294+
@Test
295+
void whenBindingsAreSetOnTheCommandLineAndNoneAreConfiguredThenRequestHasCommandLineBindings() {
296+
this.buildImage.getBindingsFromCommandLine().add("host-src:container-a:ro");
297+
this.buildImage.getBindingsFromCommandLine().add("volume-name:container-b:rw");
298+
assertThat(this.buildImage.createRequest().getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
299+
Binding.of("volume-name:container-b:rw"));
300+
}
301+
302+
@Test
303+
void whenBindingsAreSetOnTheCommandLineThenTheyAreAddedToConfiguredBindings() {
304+
this.buildImage.getBindings().set(Arrays.asList("host-src:container-a:ro"));
305+
this.buildImage.getBindingsFromCommandLine().add("volume-name:container-b:rw");
306+
assertThat(this.buildImage.createRequest().getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
307+
Binding.of("volume-name:container-b:rw"));
308+
}
309+
310+
@Test
311+
void whenBindingsFromTheCommandLineShareContainerPathWithConfiguredBindingThenCommandLineTakesPrecedence() {
312+
this.buildImage.getBindings().set(Arrays.asList("host-src:container-a:ro", "volume-name:container-b:ro"));
313+
this.buildImage.getBindingsFromCommandLine().add("other-volume:container-b:rw");
314+
assertThat(this.buildImage.createRequest().getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
315+
Binding.of("other-volume:container-b:rw"));
316+
}
317+
294318
@Test
295319
void whenNetworkIsConfiguredThenRequestHasNetwork() {
296320
this.buildImage.getNetwork().set("test");

build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ Buildpack references must be in one of the following forms:
195195
* Buildpack in an OCI image - `[docker://]<host>/<repo>[:<tag>][@<digest>]`
196196
| None, indicating the builder should use the buildpacks included in it.
197197

198-
| `bindings`
198+
| `bindings` +
199+
(`spring-boot.build-image.bindings`)
199200
a|https://docs.docker.com/storage/bind-mounts/[Volume bind mounts] that should be mounted to the builder container when building the image.
200201
The bindings will be passed unparsed and unvalidated to Docker when creating the builder container.
201202
Bindings must be in one of the following forms:
@@ -208,6 +209,9 @@ Where `<options>` can contain:
208209
* `ro` to mount the volume as read-only in the container
209210
* `rw` to mount the volume as readable and writable in the container
210211
* `volume-opt=key=value` to specify key-value pairs consisting of an option name and its value
212+
213+
Bindings provided on the command line are added to the bindings configured in the `pom.xml`.
214+
When a command-line binding shares its container destination path with a configured binding, the command-line binding takes precedence.
211215
|
212216

213217
| `network` + (`spring-boot.build-image.network`)

build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.IOException;
2121
import java.io.OutputStream;
2222
import java.time.Duration;
23+
import java.util.Arrays;
2324
import java.util.Collections;
2425
import java.util.function.Consumer;
2526
import java.util.function.Function;
@@ -191,6 +192,15 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
191192
@Parameter(property = "spring-boot.build-image.imagePlatform")
192193
@Nullable String imagePlatform;
193194

195+
/**
196+
* Alias for {@link Image#bindings} to support configuration through command-line
197+
* property.
198+
* @since 4.1.1
199+
*/
200+
@Parameter(property = "spring-boot.build-image.bindings")
201+
@SuppressWarnings("NullAway") // maven-maven-plugin can't handle annotated arrays
202+
String[] bindings;
203+
194204
/**
195205
* Docker configuration options.
196206
* @since 2.4.0
@@ -306,6 +316,9 @@ private BuildRequest getBuildRequest(Libraries libraries) {
306316
if (image.imagePlatform == null && this.imagePlatform != null) {
307317
image.setImagePlatform(this.imagePlatform);
308318
}
319+
if (this.bindings != null && this.bindings.length > 0) {
320+
image.addBindings(Arrays.asList(this.bindings));
321+
}
309322
return customize(image.getBuildRequest(this.project.getArtifact(), content));
310323
}
311324

build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

1717
package org.springframework.boot.maven;
1818

19+
import java.util.ArrayList;
1920
import java.util.List;
2021
import java.util.Map;
22+
import java.util.Set;
2123
import java.util.function.Function;
24+
import java.util.stream.Collectors;
2225

2326
import org.apache.maven.artifact.Artifact;
2427
import org.jspecify.annotations.Nullable;
@@ -188,6 +191,26 @@ void setPublish(@Nullable Boolean publish) {
188191
this.publish = publish;
189192
}
190193

194+
/**
195+
* Adds the given bindings to the existing bindings, with the added bindings taking
196+
* precedence over existing bindings that share the same container destination path.
197+
* @param bindings the bindings to add
198+
*/
199+
void addBindings(List<String> bindings) {
200+
List<String> merged = new ArrayList<>();
201+
if (this.bindings != null) {
202+
Set<String> addedContainerDestinationPaths = bindings.stream()
203+
.map((binding) -> Binding.of(binding).getContainerDestinationPath())
204+
.collect(Collectors.toSet());
205+
this.bindings.stream()
206+
.filter((binding) -> !addedContainerDestinationPaths
207+
.contains(Binding.of(binding).getContainerDestinationPath()))
208+
.forEach(merged::add);
209+
}
210+
merged.addAll(bindings);
211+
this.bindings = merged;
212+
}
213+
191214
/**
192215
* Returns the network the build container will connect to.
193216
* @return the network

build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,35 @@ void getBuildRequestWhenHasBindingsUsesBindings() {
180180
Binding.of("volume-name:container-dest:rw"));
181181
}
182182

183+
@Test
184+
void getBuildRequestWhenAddBindingsAndHasNoBindingsUsesAddedBindings() {
185+
Image image = new Image();
186+
image.addBindings(Arrays.asList("host-src:container-a:ro", "volume-name:container-b:rw"));
187+
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
188+
assertThat(request.getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
189+
Binding.of("volume-name:container-b:rw"));
190+
}
191+
192+
@Test
193+
void getBuildRequestWhenAddBindingsAddsToExistingBindings() {
194+
Image image = new Image();
195+
image.bindings = Arrays.asList("host-src:container-a:ro");
196+
image.addBindings(Arrays.asList("volume-name:container-b:rw"));
197+
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
198+
assertThat(request.getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
199+
Binding.of("volume-name:container-b:rw"));
200+
}
201+
202+
@Test
203+
void getBuildRequestWhenAddBindingsWithMatchingContainerPathOverridesExistingBinding() {
204+
Image image = new Image();
205+
image.bindings = Arrays.asList("host-src:container-a:ro", "volume-name:container-b:ro");
206+
image.addBindings(Arrays.asList("other-volume:container-b:rw"));
207+
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
208+
assertThat(request.getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
209+
Binding.of("other-volume:container-b:rw"));
210+
}
211+
183212
@Test
184213
void getBuildRequestWhenNetworkUsesNetwork() {
185214
Image image = new Image();

buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Binding.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ public boolean usesSensitiveContainerPath() {
7777
/**
7878
* Returns the container destination path.
7979
* @return the container destination path
80+
* @since 4.1.1
8081
*/
81-
String getContainerDestinationPath() {
82+
public String getContainerDestinationPath() {
8283
List<String> parts = getParts();
8384
Assert.state(parts.size() >= 2, () -> "Expected 2 or more parts, but found %d".formatted(parts.size()));
8485
return parts.get(1);

0 commit comments

Comments
 (0)