From a80c4ad38de12493abccca158186f0deef9067d8 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 24 Feb 2021 14:07:03 -0600 Subject: [PATCH] Support image building with Gradle and war packaging This commit updates the Gradle image building task to support building images from executable and non-executable war files. Fixes gh-23825 --- .../docs/asciidoc/packaging-oci-image.adoc | 6 +- .../boot/gradle/plugin/JavaPluginAction.java | 2 +- .../boot/gradle/plugin/WarPluginAction.java | 15 ++-- .../gradle/tasks/bundling/BootBuildImage.java | 28 +++++--- .../BootBuildImageIntegrationTests.java | 70 +++++++++++-------- 5 files changed, 68 insertions(+), 53 deletions(-) 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 5de39a6e79..a684ed4a87 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 @@ -1,14 +1,12 @@ [[build-image]] == Packaging OCI Images -The plugin can create an https://github.com/opencontainers/image-spec[OCI image] from an executable jar file using https://buildpacks.io[Cloud Native Buildpacks] (CNB). +The plugin can create an https://github.com/opencontainers/image-spec[OCI image] from a jar or war file using https://buildpacks.io[Cloud Native Buildpacks] (CNB). Images can be built using the `bootBuildImage` task. NOTE: For security reasons, images build and run as non-root users. See the {buildpacks-reference}/reference/spec/platform-api/#users[CNB specification] for more details. -The task is automatically created when the `java` plugin is applied and is an instance of {boot-build-image-javadoc}[`BootBuildImage`]. - -NOTE: The `bootBuildImage` task is not supported with projects using <>. +The task is automatically created when the `java` or `war` plugin is applied and is an instance of {boot-build-image-javadoc}[`BootBuildImage`]. NOTE: The `bootBuildImage` task can not be used with a <> that includes a launch script. Disable launch script configuration in the `bootJar` task when building a jar file that is intended to be used with `bootBuildImage`. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java index b00f5a1c62..bbf66a5952 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java @@ -123,7 +123,7 @@ final class JavaPluginAction implements PluginApplicationAction { project.getTasks().register(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME, BootBuildImage.class, (buildImage) -> { buildImage.setDescription("Builds an OCI image of the application using the output of the bootJar task"); buildImage.setGroup(BasePlugin.BUILD_GROUP); - buildImage.getJar().set(bootJar.get().getArchiveFile()); + buildImage.getArchiveFile().set(bootJar.get().getArchiveFile()); buildImage.getTargetJavaVersion().set(javaPluginConvention(project).getTargetCompatibility()); }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java index 558d8055a2..555e40b64a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -22,7 +22,6 @@ import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.file.FileCollection; -import org.gradle.api.file.RegularFile; import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact; import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.WarPlugin; @@ -56,8 +55,8 @@ class WarPluginAction implements PluginApplicationAction { @Override public void execute(Project project) { disableWarTask(project); - disableBootBuildImageTask(project); TaskProvider bootWar = configureBootWarTask(project); + configureBootBuildImageTask(project, bootWar); configureArtifactPublication(bootWar); } @@ -65,11 +64,6 @@ class WarPluginAction implements PluginApplicationAction { project.getTasks().named(WarPlugin.WAR_TASK_NAME).configure((war) -> war.setEnabled(false)); } - private void disableBootBuildImageTask(Project project) { - project.getTasks().named(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME, BootBuildImage.class) - .configure((buildImage) -> buildImage.getJar().set((RegularFile) null)); - } - private TaskProvider configureBootWarTask(Project project) { Configuration developmentOnly = project.getConfigurations() .getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); @@ -103,6 +97,11 @@ class WarPluginAction implements PluginApplicationAction { return configurations.getByName(WarPlugin.PROVIDED_RUNTIME_CONFIGURATION_NAME); } + private void configureBootBuildImageTask(Project project, TaskProvider bootWar) { + project.getTasks().named(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME, BootBuildImage.class) + .configure((buildImage) -> buildImage.getArchiveFile().set(bootWar.get().getArchiveFile())); + } + private void configureArtifactPublication(TaskProvider bootWar) { LazyPublishArtifact artifact = new LazyPublishArtifact(bootWar); this.singlePublishedArtifact.addCandidate(artifact); 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 1ab1a876b1..b8eac87706 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 @@ -68,7 +68,7 @@ public class BootBuildImage extends DefaultTask { private final Property projectVersion; - private RegularFileProperty jar; + private RegularFileProperty archiveFile; private Property targetJavaVersion; @@ -95,7 +95,7 @@ public class BootBuildImage extends DefaultTask { private final DockerSpec docker = new DockerSpec(); public BootBuildImage() { - this.jar = getProject().getObjects().fileProperty(); + this.archiveFile = getProject().getObjects().fileProperty(); this.targetJavaVersion = getProject().getObjects().property(JavaVersion.class); this.projectName = getProject().getName(); this.projectVersion = getProject().getObjects().property(String.class); @@ -106,13 +106,22 @@ public class BootBuildImage extends DefaultTask { } /** - * Returns the property for the jar file from which the image will be built. - * @return the jar property + * Returns the property for the archive file from which the image will be built. + * @return the archive file property + */ + @Input + public RegularFileProperty getArchiveFile() { + return this.archiveFile; + } + + /** + * Returns the property for the archive file from which the image will be built. + * @return the archive file property + * @deprecated since 2.5.0 in favor of {@link #getArchiveFile()} */ @Input - @Optional public RegularFileProperty getJar() { - return this.jar; + return this.archiveFile; } /** @@ -396,17 +405,14 @@ public class BootBuildImage extends DefaultTask { @TaskAction void buildImage() throws DockerEngineException, IOException { - if (!this.jar.isPresent()) { - throw new GradleException("Executable jar file required for building image"); - } - BuildRequest request = createRequest(); Builder builder = new Builder(this.docker.asDockerConfiguration()); + BuildRequest request = createRequest(); builder.build(request); } BuildRequest createRequest() { return customize(BuildRequest.of(determineImageReference(), - (owner) -> new ZipFileTarArchive(this.jar.get().getAsFile(), owner))); + (owner) -> new ZipFileTarArchive(this.archiveFile.get().getAsFile(), owner))); } private ImageReference determineImageReference() { 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 b433d743f7..294cb92137 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 @@ -79,6 +79,47 @@ class BootBuildImageIntegrationTests { } } + @TestTemplate + void buildsImageWithWarPackaging() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "-PapplyWarPlugin", + "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs"); + assertThat(buildLibs.listFiles()) + .containsExactly(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war")); + ImageReference imageReference = ImageReference.of(ImageName.of(projectName)); + try (GenericContainer container = new GenericContainer<>(imageReference.toString())) { + container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start(); + } + finally { + new DockerApi().image().remove(imageReference, false); + } + } + + @TestTemplate + void buildsImageWithWarPackagingAndJarConfiguration() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs"); + assertThat(buildLibs.listFiles()) + .containsExactly(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war")); + ImageReference imageReference = ImageReference.of(ImageName.of(projectName)); + try (GenericContainer container = new GenericContainer<>(imageReference.toString())) { + container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start(); + } + finally { + new DockerApi().image().remove(imageReference, false); + } + } + @TestTemplate void buildsImageWithCustomName() throws IOException { writeMainClass(); @@ -267,15 +308,6 @@ class BootBuildImageIntegrationTests { assertThat(result.getOutput()).contains("requires docker.publishRegistry"); } - @TestTemplate - void failsWithWarPackaging() throws IOException { - writeMainClass(); - writeLongNameResource(); - BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "-PapplyWarPlugin"); - assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED); - assertThat(result.getOutput()).contains("Executable jar file required for building image"); - } - @TestTemplate void failsWithBuildpackNotInBuilder() throws IOException { writeMainClass(); @@ -285,26 +317,6 @@ class BootBuildImageIntegrationTests { assertThat(result.getOutput()).contains("'urn:cnb:builder:example/does-not-exist:0.0.1' not found in builder"); } - @TestTemplate - void buildsImageWithWarPackagingAndJarConfiguration() throws IOException { - writeMainClass(); - writeLongNameResource(); - BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); - String projectName = this.gradleBuild.getProjectDir().getName(); - assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(result.getOutput()).contains("docker.io/library/" + projectName); - File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs"); - assertThat(buildLibs.listFiles()) - .containsExactly(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war")); - ImageReference imageReference = ImageReference.of(ImageName.of(projectName)); - try (GenericContainer container = new GenericContainer<>(imageReference.toString())) { - container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start(); - } - finally { - new DockerApi().image().remove(imageReference, false); - } - } - private void writeMainClass() throws IOException { File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); examplePackage.mkdirs();