Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ Buildpack references must be in one of the following forms:
| None, indicating the builder should use the buildpacks included in it.

| `bindings`
|
| `--bindings`
a|https://docs.docker.com/storage/bind-mounts/[Volume bind mounts] that should be mounted to the builder container when building the image.
The bindings will be passed unparsed and unvalidated to Docker when creating the builder container.
Bindings must be in one of the following forms:
Expand All @@ -192,6 +192,9 @@ Where `<options>` can contain:
* `ro` to mount the volume as read-only in the container
* `rw` to mount the volume as readable and writable in the container
* `volume-opt=key=value` to specify key-value pairs consisting of an option name and its value

Bindings provided using the `--bindings` command-line option are added to those configured in the build script.
When a command-line binding shares its container destination path with a configured binding, the command-line binding takes precedence.
|

| `network`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
package org.springframework.boot.gradle.tasks.bundling;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
Expand Down Expand Up @@ -108,6 +111,20 @@ public BootBuildImage() {
getSecurityOptions().convention((Iterable<? extends String>) null);
getEffectiveEnvironment().putAll(getEnvironment());
getEffectiveEnvironment().putAll(getEnvironmentFromCommandLine().map(BootBuildImage::asMap));
getEffectiveBindings().set(getBindings().zip(getBindingsFromCommandLine(), BootBuildImage::mergeBindings));
}

private static List<String> mergeBindings(List<String> bindings, List<String> bindingsFromCommandLine) {
Set<String> commandLineContainerDestinationPaths = bindingsFromCommandLine.stream()
.map((binding) -> Binding.of(binding).getContainerDestinationPath())
.collect(Collectors.toSet());
List<String> merged = new ArrayList<>();
bindings.stream()
.filter((binding) -> !commandLineContainerDestinationPaths
.contains(Binding.of(binding).getContainerDestinationPath()))
.forEach(merged::add);
merged.addAll(bindingsFromCommandLine);
return merged;
}

private static Map<String, String> asMap(List<String> variables) {
Expand Down Expand Up @@ -252,9 +269,23 @@ public void setPullPolicy(String pullPolicy) {
* image.
* @return the bindings
*/
@Internal
public abstract ListProperty<String> getBindings();

/**
* Returns the volume bindings contributed from the command line. Added bindings take
* precedence over configured bindings that share the same container destination path.
* @return the bindings from the command line
* @since 4.1.1
*/
@Internal
@Option(option = "bindings", description = "Volume bind mounts that will be mounted to the builder container. "
+ "Can be specified multiple times.")
abstract ListProperty<String> getBindingsFromCommandLine();

@Input
@Optional
public abstract ListProperty<String> getBindings();
abstract ListProperty<String> getEffectiveBindings();

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

private BuildRequest customizeBindings(BuildRequest request) {
List<String> bindings = getBindings().getOrNull();
List<String> bindings = getEffectiveBindings().getOrNull();
if (!CollectionUtils.isEmpty(bindings)) {
return request.withBindings(bindings.stream().map(Binding::of).toList());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,30 @@ void whenIndividualEntriesAreAddedToBindingsThenRequestHasBindings() {
.containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw"));
}

@Test
void whenBindingsAreSetOnTheCommandLineAndNoneAreConfiguredThenRequestHasCommandLineBindings() {
this.buildImage.getBindingsFromCommandLine().add("host-src:container-a:ro");
this.buildImage.getBindingsFromCommandLine().add("volume-name:container-b:rw");
assertThat(this.buildImage.createRequest().getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
Binding.of("volume-name:container-b:rw"));
}

@Test
void whenBindingsAreSetOnTheCommandLineThenTheyAreAddedToConfiguredBindings() {
this.buildImage.getBindings().set(Arrays.asList("host-src:container-a:ro"));
this.buildImage.getBindingsFromCommandLine().add("volume-name:container-b:rw");
assertThat(this.buildImage.createRequest().getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
Binding.of("volume-name:container-b:rw"));
}

@Test
void whenBindingsFromTheCommandLineShareContainerPathWithConfiguredBindingThenCommandLineTakesPrecedence() {
this.buildImage.getBindings().set(Arrays.asList("host-src:container-a:ro", "volume-name:container-b:ro"));
this.buildImage.getBindingsFromCommandLine().add("other-volume:container-b:rw");
assertThat(this.buildImage.createRequest().getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
Binding.of("other-volume:container-b:rw"));
}

@Test
void whenNetworkIsConfiguredThenRequestHasNetwork() {
this.buildImage.getNetwork().set("test");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ Buildpack references must be in one of the following forms:
* Buildpack in an OCI image - `[docker://]<host>/<repo>[:<tag>][@<digest>]`
| None, indicating the builder should use the buildpacks included in it.

| `bindings`
| `bindings` +
(`spring-boot.build-image.bindings`)
a|https://docs.docker.com/storage/bind-mounts/[Volume bind mounts] that should be mounted to the builder container when building the image.
The bindings will be passed unparsed and unvalidated to Docker when creating the builder container.
Bindings must be in one of the following forms:
Expand All @@ -208,6 +209,9 @@ Where `<options>` can contain:
* `ro` to mount the volume as read-only in the container
* `rw` to mount the volume as readable and writable in the container
* `volume-opt=key=value` to specify key-value pairs consisting of an option name and its value

Bindings provided on the command line are added to the bindings configured in the `pom.xml`.
When a command-line binding shares its container destination path with a configured binding, the command-line binding takes precedence.
|

| `network` + (`spring-boot.build-image.network`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.IOException;
import java.io.OutputStream;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.function.Consumer;
import java.util.function.Function;
Expand Down Expand Up @@ -191,6 +192,15 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
@Parameter(property = "spring-boot.build-image.imagePlatform")
@Nullable String imagePlatform;

/**
* Alias for {@link Image#bindings} to support configuration through command-line
* property.
* @since 4.1.1
*/
@Parameter(property = "spring-boot.build-image.bindings")
@SuppressWarnings("NullAway") // maven-maven-plugin can't handle annotated arrays
String[] bindings;

/**
* Docker configuration options.
* @since 2.4.0
Expand Down Expand Up @@ -306,6 +316,9 @@ private BuildRequest getBuildRequest(Libraries libraries) {
if (image.imagePlatform == null && this.imagePlatform != null) {
image.setImagePlatform(this.imagePlatform);
}
if (this.bindings != null && this.bindings.length > 0) {
image.addBindings(Arrays.asList(this.bindings));
}
return customize(image.getBuildRequest(this.project.getArtifact(), content));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

package org.springframework.boot.maven;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.maven.artifact.Artifact;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -188,6 +191,26 @@ void setPublish(@Nullable Boolean publish) {
this.publish = publish;
}

/**
* Adds the given bindings to the existing bindings, with the added bindings taking
* precedence over existing bindings that share the same container destination path.
* @param bindings the bindings to add
*/
void addBindings(List<String> bindings) {
List<String> merged = new ArrayList<>();
if (this.bindings != null) {
Set<String> addedContainerDestinationPaths = bindings.stream()
.map((binding) -> Binding.of(binding).getContainerDestinationPath())
.collect(Collectors.toSet());
this.bindings.stream()
.filter((binding) -> !addedContainerDestinationPaths
.contains(Binding.of(binding).getContainerDestinationPath()))
.forEach(merged::add);
}
merged.addAll(bindings);
this.bindings = merged;
}

/**
* Returns the network the build container will connect to.
* @return the network
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,35 @@ void getBuildRequestWhenHasBindingsUsesBindings() {
Binding.of("volume-name:container-dest:rw"));
}

@Test
void getBuildRequestWhenAddBindingsAndHasNoBindingsUsesAddedBindings() {
Image image = new Image();
image.addBindings(Arrays.asList("host-src:container-a:ro", "volume-name:container-b:rw"));
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
Binding.of("volume-name:container-b:rw"));
}

@Test
void getBuildRequestWhenAddBindingsAddsToExistingBindings() {
Image image = new Image();
image.bindings = Arrays.asList("host-src:container-a:ro");
image.addBindings(Arrays.asList("volume-name:container-b:rw"));
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
Binding.of("volume-name:container-b:rw"));
}

@Test
void getBuildRequestWhenAddBindingsWithMatchingContainerPathOverridesExistingBinding() {
Image image = new Image();
image.bindings = Arrays.asList("host-src:container-a:ro", "volume-name:container-b:ro");
image.addBindings(Arrays.asList("other-volume:container-b:rw"));
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getBindings()).containsExactly(Binding.of("host-src:container-a:ro"),
Binding.of("other-volume:container-b:rw"));
}

@Test
void getBuildRequestWhenNetworkUsesNetwork() {
Image image = new Image();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ public boolean usesSensitiveContainerPath() {
/**
* Returns the container destination path.
* @return the container destination path
* @since 4.1.1
*/
String getContainerDestinationPath() {
public String getContainerDestinationPath() {
List<String> parts = getParts();
Assert.state(parts.size() >= 2, () -> "Expected 2 or more parts, but found %d".formatted(parts.size()));
return parts.get(1);
Expand Down
Loading