Commit a80c4ad3 authored by Scott Frederick's avatar Scott Frederick

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
parent f09630f7
[[build-image]] [[build-image]]
== Packaging OCI Images == 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. Images can be built using the `bootBuildImage` task.
NOTE: For security reasons, images build and run as non-root users. 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. 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`]. 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 is not supported with projects using <<packaging-executable-wars, war packaging>>.
NOTE: The `bootBuildImage` task can not be used with a <<packaging-executable-configuring-launch-script, fully executable Spring Boot archive>> that includes a launch script. NOTE: The `bootBuildImage` task can not be used with a <<packaging-executable-configuring-launch-script, fully executable Spring Boot archive>> 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`. Disable launch script configuration in the `bootJar` task when building a jar file that is intended to be used with `bootBuildImage`.
......
...@@ -123,7 +123,7 @@ final class JavaPluginAction implements PluginApplicationAction { ...@@ -123,7 +123,7 @@ final class JavaPluginAction implements PluginApplicationAction {
project.getTasks().register(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME, BootBuildImage.class, (buildImage) -> { 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.setDescription("Builds an OCI image of the application using the output of the bootJar task");
buildImage.setGroup(BasePlugin.BUILD_GROUP); buildImage.setGroup(BasePlugin.BUILD_GROUP);
buildImage.getJar().set(bootJar.get().getArchiveFile()); buildImage.getArchiveFile().set(bootJar.get().getArchiveFile());
buildImage.getTargetJavaVersion().set(javaPluginConvention(project).getTargetCompatibility()); buildImage.getTargetJavaVersion().set(javaPluginConvention(project).getTargetCompatibility());
}); });
} }
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -22,7 +22,6 @@ import org.gradle.api.Project; ...@@ -22,7 +22,6 @@ import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCollection;
import org.gradle.api.file.RegularFile;
import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact; import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact;
import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.plugins.WarPlugin; import org.gradle.api.plugins.WarPlugin;
...@@ -56,8 +55,8 @@ class WarPluginAction implements PluginApplicationAction { ...@@ -56,8 +55,8 @@ class WarPluginAction implements PluginApplicationAction {
@Override @Override
public void execute(Project project) { public void execute(Project project) {
disableWarTask(project); disableWarTask(project);
disableBootBuildImageTask(project);
TaskProvider<BootWar> bootWar = configureBootWarTask(project); TaskProvider<BootWar> bootWar = configureBootWarTask(project);
configureBootBuildImageTask(project, bootWar);
configureArtifactPublication(bootWar); configureArtifactPublication(bootWar);
} }
...@@ -65,11 +64,6 @@ class WarPluginAction implements PluginApplicationAction { ...@@ -65,11 +64,6 @@ class WarPluginAction implements PluginApplicationAction {
project.getTasks().named(WarPlugin.WAR_TASK_NAME).configure((war) -> war.setEnabled(false)); 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<BootWar> configureBootWarTask(Project project) { private TaskProvider<BootWar> configureBootWarTask(Project project) {
Configuration developmentOnly = project.getConfigurations() Configuration developmentOnly = project.getConfigurations()
.getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); .getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME);
...@@ -103,6 +97,11 @@ class WarPluginAction implements PluginApplicationAction { ...@@ -103,6 +97,11 @@ class WarPluginAction implements PluginApplicationAction {
return configurations.getByName(WarPlugin.PROVIDED_RUNTIME_CONFIGURATION_NAME); return configurations.getByName(WarPlugin.PROVIDED_RUNTIME_CONFIGURATION_NAME);
} }
private void configureBootBuildImageTask(Project project, TaskProvider<BootWar> 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> bootWar) { private void configureArtifactPublication(TaskProvider<BootWar> bootWar) {
LazyPublishArtifact artifact = new LazyPublishArtifact(bootWar); LazyPublishArtifact artifact = new LazyPublishArtifact(bootWar);
this.singlePublishedArtifact.addCandidate(artifact); this.singlePublishedArtifact.addCandidate(artifact);
......
...@@ -68,7 +68,7 @@ public class BootBuildImage extends DefaultTask { ...@@ -68,7 +68,7 @@ public class BootBuildImage extends DefaultTask {
private final Property<String> projectVersion; private final Property<String> projectVersion;
private RegularFileProperty jar; private RegularFileProperty archiveFile;
private Property<JavaVersion> targetJavaVersion; private Property<JavaVersion> targetJavaVersion;
...@@ -95,7 +95,7 @@ public class BootBuildImage extends DefaultTask { ...@@ -95,7 +95,7 @@ public class BootBuildImage extends DefaultTask {
private final DockerSpec docker = new DockerSpec(); private final DockerSpec docker = new DockerSpec();
public BootBuildImage() { public BootBuildImage() {
this.jar = getProject().getObjects().fileProperty(); this.archiveFile = getProject().getObjects().fileProperty();
this.targetJavaVersion = getProject().getObjects().property(JavaVersion.class); this.targetJavaVersion = getProject().getObjects().property(JavaVersion.class);
this.projectName = getProject().getName(); this.projectName = getProject().getName();
this.projectVersion = getProject().getObjects().property(String.class); this.projectVersion = getProject().getObjects().property(String.class);
...@@ -106,13 +106,22 @@ public class BootBuildImage extends DefaultTask { ...@@ -106,13 +106,22 @@ public class BootBuildImage extends DefaultTask {
} }
/** /**
* Returns the property for the jar file from which the image will be built. * Returns the property for the archive file from which the image will be built.
* @return the jar property * @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 @Input
@Optional
public RegularFileProperty getJar() { public RegularFileProperty getJar() {
return this.jar; return this.archiveFile;
} }
/** /**
...@@ -396,17 +405,14 @@ public class BootBuildImage extends DefaultTask { ...@@ -396,17 +405,14 @@ public class BootBuildImage extends DefaultTask {
@TaskAction @TaskAction
void buildImage() throws DockerEngineException, IOException { 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()); Builder builder = new Builder(this.docker.asDockerConfiguration());
BuildRequest request = createRequest();
builder.build(request); builder.build(request);
} }
BuildRequest createRequest() { BuildRequest createRequest() {
return customize(BuildRequest.of(determineImageReference(), return customize(BuildRequest.of(determineImageReference(),
(owner) -> new ZipFileTarArchive(this.jar.get().getAsFile(), owner))); (owner) -> new ZipFileTarArchive(this.archiveFile.get().getAsFile(), owner)));
} }
private ImageReference determineImageReference() { private ImageReference determineImageReference() {
......
...@@ -79,6 +79,47 @@ class BootBuildImageIntegrationTests { ...@@ -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 @TestTemplate
void buildsImageWithCustomName() throws IOException { void buildsImageWithCustomName() throws IOException {
writeMainClass(); writeMainClass();
...@@ -267,15 +308,6 @@ class BootBuildImageIntegrationTests { ...@@ -267,15 +308,6 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("requires docker.publishRegistry"); 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 @TestTemplate
void failsWithBuildpackNotInBuilder() throws IOException { void failsWithBuildpackNotInBuilder() throws IOException {
writeMainClass(); writeMainClass();
...@@ -285,26 +317,6 @@ class BootBuildImageIntegrationTests { ...@@ -285,26 +317,6 @@ class BootBuildImageIntegrationTests {
assertThat(result.getOutput()).contains("'urn:cnb:builder:example/does-not-exist:0.0.1' not found in builder"); 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 { private void writeMainClass() throws IOException {
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
examplePackage.mkdirs(); examplePackage.mkdirs();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment