diff --git a/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index c533fc2ad80..b9103ce53ae 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -469,6 +469,20 @@ class BootBuildImageIntegrationTests { removeImages(projectName, builderImage, runImage, buildpackImage); } + @TestTemplate + void buildsImageWithMultipleCommandLineEnvironments() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--environment", "BP_LIVE_RELOAD_ENABLED=true", + "--environment", "MY_CUSTOM_VAR=hello_world"); + BuildTask task = result.task(":bootBuildImage"); + assertThat(task).isNotNull(); + assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("BP_LIVE_RELOAD_ENABLED=true"); + assertThat(result.getOutput()).contains("MY_CUSTOM_VAR=hello_world"); + removeImages(this.gradleBuild.getProjectDir().getName()); + } + @TestTemplate @EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "amd64", disabledReason = "The expected failure condition will not fail on ARM architectures") diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc index 139eeda2adb..8e3b4bbfade 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc @@ -162,7 +162,7 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`. | `ALWAYS` | `environment` -| +| `--environment` | Environment variables that should be passed to the builder. | Empty. @@ -346,6 +346,15 @@ include::example$packaging/boot-build-image-env.gradle.kts[tags=env] ---- ====== +Environment variables can also be specified on the command line, as shown in the following example: + +[source,shell] +---- +$ gradle bootBuildImage --environment BP_JVM_VERSION=17 +---- + +`--environment` can be used multiple times to specify multiple environment variables. + If there is a network proxy between the Docker daemon the builder runs in and network locations that buildpacks download artifacts from, you will need to configure the builder to use the proxy. When using the Paketo builder, this can be accomplished by setting the `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables as show in the following example: diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 1945c156bd6..078180fafae 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -17,11 +17,14 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.gradle.api.Action; import org.gradle.api.DefaultTask; +import org.gradle.api.InvalidUserDataException; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.RegularFileProperty; @@ -30,6 +33,7 @@ import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.PathSensitive; @@ -102,6 +106,23 @@ public abstract class BootBuildImage extends DefaultTask { this.docker = getProject().getObjects().newInstance(DockerSpec.class); this.pullPolicy = getProject().getObjects().property(PullPolicy.class); getSecurityOptions().convention((Iterable) null); + getEffectiveEnvironment().putAll(getEnvironment()); + getEffectiveEnvironment().putAll(getEnvironmentFromCommandLine().map(BootBuildImage::asMap)); + } + + private static Map asMap(List variables) { + Map environment = new LinkedHashMap<>(); + for (String variable : variables) { + int index = variable.indexOf('='); + if (index <= 0) { + throw new InvalidUserDataException( + "Invalid value for option '--environment'. Expected 'NAME=VALUE' but got '" + variable + "'."); + } + String name = variable.substring(0, index); + String value = variable.substring(index + 1); + environment.put(name, value); + } + return environment; } /** @@ -157,9 +178,22 @@ public abstract class BootBuildImage extends DefaultTask { * Returns the environment that will be used when building the image. * @return the environment */ - @Input + @Internal public abstract MapProperty getEnvironment(); + /** + * Returns environment variables contributed from the command line. Each entry must be + * in the form NAME=VALUE. + * @return the environment variables from the command line + */ + @Internal + @Option(option = "environment", description = "Environment variable that will be used when building the image " + + "(NAME=VALUE). Can be specified multiple times.") + abstract ListProperty getEnvironmentFromCommandLine(); + + @Input + abstract MapProperty getEffectiveEnvironment(); + /** * Returns whether caches should be cleaned before packaging. * @return whether caches should be cleaned @@ -413,8 +447,8 @@ public abstract class BootBuildImage extends DefaultTask { } private BuildRequest customizeEnvironment(BuildRequest request) { - Map environment = getEnvironment().getOrNull(); - if (!CollectionUtils.isEmpty(environment)) { + Map environment = getEffectiveEnvironment().getOrElse(Collections.emptyMap()); + if (!environment.isEmpty()) { request = request.withEnv(environment); } return request; diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index 0eef7843ef3..9a917d02382 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -144,6 +144,25 @@ class BootBuildImageTests { .hasSize(2); } + @Test + void whenEnvironmentVariablesAreSetOnTheCommandLineTheyAreIncludedInTheRequest() { + this.buildImage.getEnvironmentFromCommandLine().add("ALPHA=a"); + this.buildImage.getEnvironmentFromCommandLine().add("BRAVO=b"); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a") + .containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void environmentVariablesFromTheCommandLineOverrideThoseInTheBuildScript() { + this.buildImage.getEnvironment().put("ALPHA", "a"); + this.buildImage.getEnvironmentFromCommandLine().add("ALPHA=apple"); + this.buildImage.getEnvironmentFromCommandLine().add("BRAVO=banana"); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "apple") + .containsEntry("BRAVO", "banana") + .hasSize(2); + } + @Test void whenUsingDefaultConfigurationThenRequestHasVerboseLoggingDisabled() { assertThat(this.buildImage.createRequest().isVerboseLogging()).isFalse();