From 7de770f6a191d16e3c120671de8b29349d95d57d Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 20 Sep 2023 13:30:20 -0500 Subject: [PATCH] Add support for security options in CNB builder container config Closes gh-37479 --- .../platform/build/BuildRequest.java | 63 +++++++++++++------ .../buildpack/platform/build/Lifecycle.java | 17 ++++- .../platform/build/BuildRequestTests.java | 7 +++ .../platform/build/LifecycleTests.java | 12 ++++ .../lifecycle-creator-security-opts.json | 40 ++++++++++++ .../docs/asciidoc/packaging-oci-image.adoc | 5 ++ .../gradle/tasks/bundling/BootBuildImage.java | 18 ++++++ .../BootBuildImageIntegrationTests.java | 13 ++++ ...buildsImageWithEmptySecurityOptions.gradle | 15 +++++ .../docs/asciidoc/packaging-oci-image.adoc | 4 ++ .../boot/maven/BuildImageTests.java | 15 +++++ .../build-image-security-opts/pom.xml | 35 +++++++++++ .../main/java/org/test/SampleApplication.java | 28 +++++++++ .../org/springframework/boot/maven/Image.java | 5 ++ .../boot/maven/ImageTests.java | 17 +++++ 15 files changed, 273 insertions(+), 21 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-security-opts.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithEmptySecurityOptions.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/src/main/java/org/test/SampleApplication.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index c88b7527040..7ae4d8419a8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -87,6 +87,8 @@ public class BuildRequest { private final String applicationDirectory; + private final List securityOptions; + BuildRequest(ImageReference name, Function applicationContent) { Assert.notNull(name, "Name must not be null"); Assert.notNull(applicationContent, "ApplicationContent must not be null"); @@ -109,13 +111,14 @@ public class BuildRequest { this.launchCache = null; this.createdDate = null; this.applicationDirectory = null; + this.securityOptions = null; } BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, ImageReference runImage, Creator creator, Map env, boolean cleanCache, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks, List bindings, String network, List tags, Cache buildWorkspace, Cache buildCache, - Cache launchCache, Instant createdDate, String applicationDirectory) { + Cache launchCache, Instant createdDate, String applicationDirectory, List securityOptions) { this.name = name; this.applicationContent = applicationContent; this.builder = builder; @@ -135,6 +138,7 @@ public class BuildRequest { this.launchCache = launchCache; this.createdDate = createdDate; this.applicationDirectory = applicationDirectory; + this.securityOptions = securityOptions; } /** @@ -147,7 +151,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory); + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions); } /** @@ -159,7 +163,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory); + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions); } /** @@ -172,7 +176,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -189,7 +193,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory); + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions); } /** @@ -204,7 +208,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, - this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); + this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions); } /** @@ -216,7 +220,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -228,7 +232,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -240,7 +244,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -252,7 +256,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -277,7 +281,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -302,7 +306,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -315,7 +319,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -338,7 +342,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -351,7 +355,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -364,7 +368,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -377,7 +381,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -390,7 +394,7 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, - parseCreatedDate(createdDate), this.applicationDirectory); + parseCreatedDate(createdDate), this.applicationDirectory, this.securityOptions); } private Instant parseCreatedDate(String createdDate) { @@ -415,7 +419,20 @@ public class BuildRequest { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - applicationDirectory); + applicationDirectory, this.securityOptions); + } + + /** + * Return a new {@link BuildRequest} with an updated security options. + * @param securityOptions the security options + * @return an updated build request + */ + public BuildRequest withSecurityOptions(List securityOptions) { + Assert.notNull(securityOptions, "SecurityOption must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, + this.applicationDirectory, securityOptions); } /** @@ -571,6 +588,14 @@ public class BuildRequest { return this.applicationDirectory; } + /** + * Return the security options that should be used by the lifecycle. + * @return the security options + */ + public List getSecurityOptions() { + return this.securityOptions; + } + /** * Factory method to create a new {@link BuildRequest} from a JAR file. * @param jarFile the source jar file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index d12c27dab84..1c7e7183e7a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -20,6 +20,7 @@ import java.io.Closeable; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -58,6 +59,8 @@ class Lifecycle implements Closeable { private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock"; + private static final List DEFAULT_SECURITY_OPTIONS = List.of("label=disable"); + private final BuildLog log; private final DockerApi docker; @@ -82,6 +85,8 @@ class Lifecycle implements Closeable { private final String applicationDirectory; + private final List securityOptions; + private boolean executed; private boolean applicationVolumePopulated; @@ -108,6 +113,7 @@ class Lifecycle implements Closeable { this.buildCache = getBuildCache(request); this.launchCache = getLaunchCache(request); this.applicationDirectory = getApplicationDirectory(request); + this.securityOptions = getSecurityOptions(request); } private Cache getBuildCache(BuildRequest request) { @@ -128,6 +134,13 @@ class Lifecycle implements Closeable { return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION; } + private List getSecurityOptions(BuildRequest request) { + if (request.getSecurityOptions() != null) { + return request.getSecurityOptions(); + } + return (Platform.isWindows()) ? Collections.emptyList() : DEFAULT_SECURITY_OPTIONS; + } + private ApiVersion getPlatformVersion(BuilderMetadata.Lifecycle lifecycle) { if (lifecycle.getApis().getPlatform() != null) { String[] supportedVersions = lifecycle.getApis().getPlatform(); @@ -240,8 +253,8 @@ class Lifecycle implements Closeable { else { phase.withBinding(Binding.from(DOMAIN_SOCKET_PATH, DOMAIN_SOCKET_PATH)); } - if (!Platform.isWindows()) { - phase.withSecurityOption("label=disable"); + if (this.securityOptions != null) { + this.securityOptions.forEach(phase::withSecurityOption); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 464d218a50f..6e6fce7f50a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -333,6 +333,13 @@ class BuildRequestTests { assertThat(withAppDir.getApplicationDirectory()).isEqualTo("/application"); } + @Test + void withSecurityOptionsSetsSecurityOptions() throws Exception { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withAppDir = request.withSecurityOptions(List.of("label=user:USER", "label=role:ROLE")); + assertThat(withAppDir.getSecurityOptions()).containsExactly("label=user:USER", "label=role:ROLE"); + } + private void hasExpectedJarContent(TarArchive archive) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 64f54b450e4..f55e0ed5cda 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -23,6 +23,7 @@ import java.io.InputStreamReader; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; @@ -254,6 +255,17 @@ class LifecycleTests { assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } + @Test + void executeWithSecurityOptionsExecutesPhases() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + BuildRequest request = getTestRequest().withSecurityOptions(List.of("label=user:USER", "label=role:ROLE")); + createLifecycle(request).execute(); + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-security-opts.json")); + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + @Test void executeWithDockerHostAndRemoteAddressExecutesPhases() throws Exception { given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-security-opts.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-security-opts.json new file mode 100644 index 00000000000..c47bd7f9ffd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-security-opts.json @@ -0,0 +1,40 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/creator", + "-app", + "/workspace", + "-platform", + "/platform", + "-run-image", + "docker.io/cloudfoundry/run:latest", + "-layers", + "/layers", + "-cache-dir", + "/cache", + "-launch-cache", + "/launch-cache", + "-daemon", + "docker.io/library/my-application:latest" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "/var/run/docker.sock:/var/run/docker.sock", + "pack-layers-aaaaaaaaaa:/layers", + "pack-app-aaaaaaaaaa:/workspace", + "pack-cache-b35197ac41ea.build:/cache", + "pack-cache-b35197ac41ea.launch:/launch-cache" + ], + "SecurityOpt" : [ + "label=user:USER", + "label=role:ROLE" + ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 3474dc37d4b..f0abfb2dfe2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -223,6 +223,11 @@ The value must be a string in the ISO 8601 instant format, or `now` to use the c Application contents will also be in this location in the generated image. | `/workspace` +| `securityOptions` +| `--securityOptions` +| https://docs.docker.com/engine/reference/run/#security-configuration[Security options] that will be applied to the builder container, provided as an array of string values +| `["label=disable"]` + |=== NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 04ebf45c74d..143f54d62b8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -302,6 +302,15 @@ public abstract class BootBuildImage extends DefaultTask { @Option(option = "applicationDirectory", description = "The directory containing application content in the image") public abstract Property getApplicationDirectory(); + /** + * Returns the security options that will be applied to the builder container. + * @return the security options + */ + @Input + @Optional + @Option(option = "securityOptions", description = "Security options that will be applied to the builder container") + public abstract ListProperty getSecurityOptions(); + /** * Returns the Docker configuration the builder will use. * @return docker configuration. @@ -349,6 +358,7 @@ public abstract class BootBuildImage extends DefaultTask { request = request.withNetwork(getNetwork().getOrNull()); request = customizeCreatedDate(request); request = customizeApplicationDirectory(request); + request = customizeSecurityOptions(request); return request; } @@ -450,4 +460,12 @@ public abstract class BootBuildImage extends DefaultTask { return request; } + private BuildRequest customizeSecurityOptions(BuildRequest request) { + List securityOptions = getSecurityOptions().getOrNull(); + if (securityOptions != null) { + return request.withSecurityOptions(securityOptions); + } + return request; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index 06bc731d634..b20924ad7d6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -368,6 +368,19 @@ class BootBuildImageIntegrationTests { removeImages(projectName); } + @TestTemplate + void buildsImageWithEmptySecurityOptions() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + removeImages(projectName); + } + @TestTemplate void failsWithInvalidCreatedDate() throws IOException { writeMainClass(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithEmptySecurityOptions.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithEmptySecurityOptions.gradle new file mode 100644 index 00000000000..a44b7807756 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithEmptySecurityOptions.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +java { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' +} + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" + pullPolicy = "IF_NOT_PRESENT" + securityOptions = [] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 67b23f539b4..2766e6b12fd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -230,6 +230,10 @@ The value must be a string in the ISO 8601 instant format, or `now` to use the c Application contents will also be in this location in the generated image. | `/workspace` +| `securityOptions` +| https://docs.docker.com/engine/reference/run/#security-configuration[Security options] that will be applied to the builder container, provided as an array of string values +| `["label=disable"]` + |=== NOTE: The plugin detects the target Java compatibility of the project using the compiler's plugin configuration or the `maven.compiler.target` property. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java index a729404432a..12ca33cdb26 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -480,6 +480,21 @@ class BuildImageTests extends AbstractArchiveIntegrationTests { }); } + @TestTemplate + void whenBuildImageIsInvokedWithEmptySecurityOptions(MavenBuild mavenBuild) { + String testBuildId = randomString(); + mavenBuild.project("build-image-security-opts") + .goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .systemProperty("test-build-id", testBuildId) + .execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-security-opts:0.0.1.BUILD-SNAPSHOT") + .contains("Successfully built image"); + removeImage("build-image-security-opts", "0.0.1.BUILD-SNAPSHOT"); + }); + } + @TestTemplate void failsWhenBuildImageIsInvokedOnMultiModuleProjectWithBuildImageGoal(MavenBuild mavenBuild) { mavenBuild.project("build-image-multi-module") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/pom.xml new file mode 100644 index 00000000000..5eee589a466 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-security-opts + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image-no-fork + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2 + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..58ebebbbb23 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index 699c450c633..c19ac62465a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -79,6 +79,8 @@ public class Image { String applicationDirectory; + List securityOptions; + /** * The name of the created image. * @return the image name @@ -260,6 +262,9 @@ public class Image { if (StringUtils.hasText(this.applicationDirectory)) { request = request.withApplicationDirectory(this.applicationDirectory); } + if (this.securityOptions != null) { + request = request.withSecurityOptions(this.securityOptions); + } return request; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index a829b25dbfb..1ec018db860 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.maven; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.function.Function; import org.apache.maven.artifact.Artifact; @@ -234,6 +235,22 @@ class ImageTests { assertThat(request.getApplicationDirectory()).isEqualTo("/application"); } + @Test + void getBuildRequestWhenHasSecurityOptionsUsesSecurityOptions() { + Image image = new Image(); + image.securityOptions = List.of("label=user:USER", "label=role:ROLE"); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getSecurityOptions()).containsExactly("label=user:USER", "label=role:ROLE"); + } + + @Test + void getBuildRequestWhenHasEmptySecurityOptionsUsesSecurityOptions() { + Image image = new Image(); + image.securityOptions = Collections.emptyList(); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getSecurityOptions()).isEmpty(); + } + private Artifact createArtifact() { return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile", "jar", null, new DefaultArtifactHandler());