From 0bd0b2a6c06a1977a2b0295113f660f8583f2df2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 22 Jan 2020 14:54:32 +0000 Subject: [PATCH] Add support for building OCI images using the Gradle plugin Closes gh-19831 --- .../platform/io/ZipFileTarArchive.java | 11 +- .../spring-boot-gradle-plugin/build.gradle | 5 +- .../src/docs/asciidoc/index.adoc | 1 + .../src/docs/asciidoc/packaging.adoc | 9 + .../src/docs/asciidoc/reacting.adoc | 9 +- .../boot/gradle/plugin/JavaPluginAction.java | 10 + .../boot/gradle/plugin/SpringBootPlugin.java | 7 + .../gradle/tasks/bundling/BootBuildImage.java | 224 ++++++++++++++++++ .../BootBuildImageIntegrationTests.java | 90 +++++++ .../tasks/bundling/BootBuildImageTests.java | 156 ++++++++++++ .../boot/gradle/testkit/GradleBuild.java | 15 +- .../BootBuildImageIntegrationTests.gradle | 4 + 12 files changed, 533 insertions(+), 8 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.gradle diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java index 09d3e661d1..525aa632d0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java @@ -35,8 +35,9 @@ import org.springframework.util.StreamUtils; * Adapter class to convert a ZIP file to a {@link TarArchive}. * * @author Phillip Webb + * @since 2.3.0 */ -class ZipFileTarArchive implements TarArchive { +public class ZipFileTarArchive implements TarArchive { static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli(); @@ -44,7 +45,13 @@ class ZipFileTarArchive implements TarArchive { private final Owner owner; - ZipFileTarArchive(File zip, Owner owner) { + /** + * Creates an archive from the contents of the given {@code zip}. Each entry in the + * archive will be owned by the given {@code owner}. + * @param zip the zip to use as a source + * @param owner the owner of the tar entries + */ + public ZipFileTarArchive(File zip, Owner owner) { Assert.notNull(zip, "Zip must not be null"); Assert.notNull(owner, "Owner must not be null"); this.zip = zip; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle index 47601af5cb..176e0fba38 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle @@ -30,6 +30,7 @@ dependencies { asciidoctorExtensions("io.spring.asciidoctor:spring-asciidoctor-extensions-block-switch:0.3.0.RELEASE") + implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform")) implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) implementation("io.spring.gradle:dependency-management-plugin") implementation("org.apache.commons:commons-compress") @@ -38,9 +39,11 @@ dependencies { optional(platform(project(":spring-boot-project:spring-boot-dependencies"))) optional("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.50") - testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core") + testImplementation("org.testcontainers:testcontainers") } gradlePlugin { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/index.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/index.adoc index 435be27797..f479f070f8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/index.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/index.adoc @@ -26,6 +26,7 @@ Andy Wilkinson :api-documentation: {spring-boot-docs}/gradle-plugin/api :spring-boot-reference: {spring-boot-docs}/reference/htmlsingle :build-info-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.html +:boot-build-image-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.html :boot-jar-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootJar.html :boot-war-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootWar.html :boot-run-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/run/BootRun.html diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc index 1d518c4d56..e3c9e580dd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc @@ -298,3 +298,12 @@ The jar will then be split into layer folders which may include: * `resources` * `snapshots-dependencies` * `dependencies` + + + +[[packaging-oci-images]] +== Packaging OCI images + +The plugin can create OCI images from executable jars using a https://buildpacks.io[buildpack]. +Images can be built using the `bootBuildImage` task and a local Docker installation. +The task is automatically created when the `java` plugin is applied and is an instance of {boot-build-image-javadoc}[`BootBuildImage`]. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc index 0bd78bf0cb..e8f9920f5b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc @@ -15,10 +15,11 @@ When Gradle's {java-plugin}[`java` plugin] is applied to a project, the Spring B The jar will contain everything on the runtime classpath of the main source set; classes are packaged in `BOOT-INF/classes` and jars are packaged in `BOOT-INF/lib` 2. Configures the `assemble` task to depend on the `bootJar` task. 3. Disables the `jar` task. -4. Creates a {boot-run-javadoc}[`BootRun`] task named `bootRun` that can be used to run your application. -5. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task. -6. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`. -7. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument. +4. Creates a {boot-build-image-javadoc}[`BootBuildImage`] task named `bootBuildImage` that will create a OCI image using a https://buildpacks.io[buildpack]. +5. Creates a {boot-run-javadoc}[`BootRun`] task named `bootRun` that can be used to run your application. +6. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task. +7. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`. +8. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument. 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 15280ae155..a7cec7713d 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 @@ -36,6 +36,7 @@ import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.compile.JavaCompile; +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage; import org.springframework.boot.gradle.tasks.bundling.BootJar; import org.springframework.boot.gradle.tasks.run.BootRun; import org.springframework.util.StringUtils; @@ -65,6 +66,7 @@ final class JavaPluginAction implements PluginApplicationAction { disableJarTask(project); configureBuildTask(project); BootJar bootJar = configureBootJarTask(project); + configureBootBuildImageTask(project, bootJar); configureArtifactPublication(bootJar); configureBootRunTask(project); configureUtf8Encoding(project); @@ -94,6 +96,14 @@ final class JavaPluginAction implements PluginApplicationAction { return bootJar; } + private void configureBootBuildImageTask(Project project, BootJar bootJar) { + BootBuildImage buildImage = project.getTasks().create(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME, + BootBuildImage.class); + buildImage.setDescription("Builds an OCI image of the application using the output of the bootJar task"); + buildImage.setGroup(BasePlugin.BUILD_GROUP); + buildImage.from(bootJar); + } + private void configureArtifactPublication(BootJar bootJar) { ArchivePublishArtifact artifact = new ArchivePublishArtifact(bootJar); this.singlePublishedArtifact.addCandidate(artifact); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java index 60a31c115b..e01163a827 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java @@ -34,6 +34,7 @@ import org.gradle.api.artifacts.ResolvableDependencies; import org.gradle.util.GradleVersion; import org.springframework.boot.gradle.dsl.SpringBootExtension; +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage; import org.springframework.boot.gradle.tasks.bundling.BootJar; import org.springframework.boot.gradle.tasks.bundling.BootWar; @@ -68,6 +69,12 @@ public class SpringBootPlugin implements Plugin { */ public static final String BOOT_WAR_TASK_NAME = "bootWar"; + /** + * The name of the default {@link BootBuildImage} task. + * @since 2.3.0 + */ + public static final String BOOT_BUILD_IMAGE_TASK_NAME = "bootBuildImage"; + /** * The coordinates {@code (group:name:version)} of the * {@code spring-boot-dependencies} bom. 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 new file mode 100644 index 0000000000..39e06f1680 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -0,0 +1,224 @@ +/* + * Copyright 2012-2020 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.springframework.boot.gradle.tasks.bundling; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; + +import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.Builder; +import org.springframework.boot.buildpack.platform.docker.DockerException; +import org.springframework.boot.buildpack.platform.docker.type.ImageName; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.ZipFileTarArchive; +import org.springframework.util.StringUtils; + +/** + * A {@link Task} for bundling an application into an OCI image using a + * buildpack. + * + * @author Andy Wilkinson + * @since 2.3.0 + */ +public class BootBuildImage extends DefaultTask { + + private Supplier jar; + + private String imageName; + + private String builder; + + private Map environment = new HashMap(); + + private boolean cleanCache; + + private boolean verboseLogging; + + /** + * Configures this task to create an image from the given {@code bootJar} task. This + * task is also configured to depend upon the given task. + * @param bootJar the fat jar from which the image should be created. + */ + public void from(BootJar bootJar) { + dependsOn(bootJar); + this.jar = () -> bootJar.getArchiveFile().get().getAsFile(); + } + + /** + * Configures this task to create an image from the given jar file. + * @param jar the jar from which the image should be created. + */ + public void from(File jar) { + this.jar = () -> jar; + } + + /** + * Returns the name of the image that will be built. When {@code null}, the name will + * be derived from the {@link Project Project's} {@link Project#getName() name} and + * {@link Project#getVersion version}. + * @return name of the image + */ + @Input + @Optional + public String getImageName() { + return this.imageName; + } + + /** + * Sets the name of the image that will be built. + * @param imageName name of the image + */ + public void setImageName(String imageName) { + this.imageName = imageName; + } + + /** + * Returns the builder that will be used to build the image. When {@code null}, the + * default builder will be used. + * @return the builder + */ + @Input + @Optional + public String getBuilder() { + return this.builder; + } + + /** + * Sets the builder that will be used to build the image. + * @param builder the builder + */ + public void setBuilder(String builder) { + this.builder = builder; + } + + /** + * Returns the environment that will be used when building the image. + * @return the environment + */ + @Input + public Map getEnvironment() { + return this.environment; + } + + /** + * Sets the environment that will be used when building the image. + * @param environment the environment + */ + public void setEnvironment(Map environment) { + this.environment = environment; + } + + /** + * Add an entry to the environment that will be used when building the image. + * @param name the name of the entry + * @param value the value of the entry + */ + public void environment(String name, String value) { + this.environment.put(name, value); + } + + /** + * Adds entries to the environment that will be used when building the image. + * @param entries the entries to add to the environment + */ + public void environment(Map entries) { + this.environment.putAll(entries); + } + + /** + * Returns whether caches should be cleaned before packaging. + * @return whether caches should be cleaned + */ + @Input + public boolean isCleanCache() { + return this.cleanCache; + } + + /** + * Sets whether caches should be cleaned before packaging. + * @param cleanCache {@code true} to clean the cache, otherwise {@code false}. + */ + public void setCleanCache(boolean cleanCache) { + this.cleanCache = cleanCache; + } + + /** + * Whether verbose logging should be enabled while building the image. + * @return whether verbose logging should be enabled + */ + @Input + public boolean isVerboseLogging() { + return this.verboseLogging; + } + + /** + * Sets whether verbose logging should be enabled while building the image. + * @param verboseLogging {@code true} to enable verbose logging, otherwise + * {@code false}. + */ + public void setVerboseLogging(boolean verboseLogging) { + this.verboseLogging = verboseLogging; + } + + @TaskAction + void buildImage() throws DockerException, IOException { + Builder builder = new Builder(); + BuildRequest request = createRequest(); + builder.build(request); + } + + BuildRequest createRequest() { + BuildRequest request = customize( + BuildRequest.of(determineImageReference(), (owner) -> new ZipFileTarArchive(this.jar.get(), owner))); + return request; + } + + private ImageReference determineImageReference() { + if (StringUtils.hasText(this.imageName)) { + return ImageReference.of(this.imageName); + } + ImageName imageName = ImageName.of(getProject().getName()); + String version = getProject().getVersion().toString(); + if ("unspecified".equals(version)) { + return ImageReference.of(imageName); + } + return ImageReference.of(imageName, version); + } + + private BuildRequest customize(BuildRequest request) { + if (StringUtils.hasText(this.builder)) { + request = request.withBuilder(ImageReference.of(this.builder)); + } + if (this.environment != null && !this.environment.isEmpty()) { + request = request.withEnv(this.environment); + } + request = request.withCleanCache(this.cleanCache); + request = request.withVerboseLogging(this.verboseLogging); + 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 new file mode 100644 index 0000000000..b904d0384a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2020 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.springframework.boot.gradle.tasks.bundling; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; + +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.TaskOutcome; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import org.springframework.boot.buildpack.platform.docker.DockerApi; +import org.springframework.boot.buildpack.platform.docker.type.ImageName; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.testkit.GradleBuild; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link BootBuildImage}. + * + * @author Andy Wilkinson + */ +@ExtendWith(GradleCompatibilityExtension.class) +@DisabledIfDockerUnavailable +class BootBuildImageIntegrationTests { + + GradleBuild gradleBuild; + + @TestTemplate + void bootBuildImageBuildsImage() throws IOException { + writeMainClass(); + BuildResult result = this.gradleBuild.build("bootBuildImage"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + ImageReference imageReference = ImageReference.of(ImageName.of(this.gradleBuild.getProjectDir().getName())); + 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() { + File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); + examplePackage.mkdirs(); + File main = new File(examplePackage, "Main.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(main))) { + writer.println("package example;"); + writer.println(); + writer.println("import java.io.IOException;"); + writer.println(); + writer.println("public class Main {"); + writer.println(); + writer.println(" public static void main(String[] args) throws Exception {"); + writer.println(" System.out.println(\"Launched\");"); + writer.println(" synchronized(args) {"); + writer.println(" args.wait(); // Prevent exit"); + writer.println(" }"); + writer.println(" }"); + writer.println(); + writer.println("}"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java new file mode 100644 index 0000000000..ac5574c42b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2012-2020 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.springframework.boot.gradle.tasks.bundling; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.buildpack.platform.build.BuildRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BootBuildImage}. + * + * @author Andy Wilkinson + */ +class BootBuildImageTests { + + @TempDir + File temp; + + Project project; + + private final BootBuildImage buildImage; + + BootBuildImageTests() { + File projectDir = new File(this.temp, "project"); + projectDir.mkdirs(); + this.project = ProjectBuilder.builder().withProjectDir(projectDir).withName("build-image-test").build(); + this.project.setDescription("Test project for BootBuildImage"); + this.buildImage = this.project.getTasks().create("buildImage", BootBuildImage.class); + } + + @Test + void whenProjectVersionIsUnspecifiedThenItIsIgnoredWhenDerivingImageName() { + BuildRequest request = this.buildImage.createRequest(); + assertThat(request.getName().getDomain()).isEqualTo("docker.io"); + assertThat(request.getName().getName()).isEqualTo("library/build-image-test"); + assertThat(request.getName().getTag()).isEqualTo("latest"); + assertThat(request.getName().getDigest()).isNull(); + } + + @Test + void whenProjectVersionIsSpecifiedThenItIsUsedInTagOfImageName() { + this.project.setVersion("1.2.3"); + BuildRequest request = this.buildImage.createRequest(); + assertThat(request.getName().getDomain()).isEqualTo("docker.io"); + assertThat(request.getName().getName()).isEqualTo("library/build-image-test"); + assertThat(request.getName().getTag()).isEqualTo("1.2.3"); + assertThat(request.getName().getDigest()).isNull(); + } + + @Test + void whenImageNameIsSpecifiedThenItIsUsedInRequest() { + this.project.setVersion("1.2.3"); + this.buildImage.setImageName("example.com/test/build-image:1.0"); + BuildRequest request = this.buildImage.createRequest(); + assertThat(request.getName().getDomain()).isEqualTo("example.com"); + assertThat(request.getName().getName()).isEqualTo("test/build-image"); + assertThat(request.getName().getTag()).isEqualTo("1.0"); + assertThat(request.getName().getDigest()).isNull(); + } + + @Test + void whenIndividualEntriesAreAddedToTheEnvironmentThenTheyAreIncludedInTheRequest() { + this.buildImage.environment("ALPHA", "a"); + this.buildImage.environment("BRAVO", "b"); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void whenEntriesAreAddedToTheEnvironmentThenTheyAreIncludedInTheRequest() { + Map environment = new HashMap(); + environment.put("ALPHA", "a"); + environment.put("BRAVO", "b"); + this.buildImage.environment(environment); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void whenTheEnvironmentIsSetItIsIncludedInTheRequest() { + Map environment = new HashMap(); + environment.put("ALPHA", "a"); + environment.put("BRAVO", "b"); + this.buildImage.setEnvironment(environment); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void whenTheEnvironmentIsSetItReplacesAnyExistingEntriesAndIsIncludedInTheRequest() { + Map environment = new HashMap(); + environment.put("ALPHA", "a"); + environment.put("BRAVO", "b"); + this.buildImage.environment("C", "Charlie"); + this.buildImage.setEnvironment(environment); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void whenUsingDefaultConfigurationThenRequestHasVerboseLoggingDisabled() { + assertThat(this.buildImage.createRequest().isVerboseLogging()).isFalse(); + } + + @Test + void whenVerboseLoggingIsEnabledThenRequestHasVerboseLoggingEnabled() { + this.buildImage.setVerboseLogging(true); + assertThat(this.buildImage.createRequest().isVerboseLogging()).isTrue(); + } + + @Test + void whenUsingDefaultConfigurationThenRequestHasCleanCacheDisabled() { + assertThat(this.buildImage.createRequest().isCleanCache()).isFalse(); + } + + @Test + void whenCleanCacheIsEnabledThenRequestHasCleanCacheEnabled() { + this.buildImage.setCleanCache(true); + assertThat(this.buildImage.createRequest().isCleanCache()).isTrue(); + } + + @Test + void whenNoBuilderIsConfiguredThenRequestHasDefaultBuilder() { + assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("cloudfoundry/cnb"); + } + + @Test + void whenBuilderIsConfiguredThenRequestUsesSpecifiedBuilder() { + this.buildImage.setBuilder("example.com/test/builder:1.2"); + assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("test/builder"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java index b08386c85e..60a6c07113 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java @@ -27,9 +27,16 @@ import java.util.Arrays; import java.util.List; import java.util.jar.JarFile; +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import com.sun.jna.Platform; import io.spring.gradle.dependencymanagement.DependencyManagementPlugin; import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension; import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.http.HttpRequest; +import org.apache.http.conn.HttpClientConnectionManager; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; import org.gradle.util.GradleVersion; @@ -41,6 +48,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin; import org.jetbrains.kotlin.gradle.plugin.KotlinPlugin; import org.springframework.asm.ClassVisitor; +import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.loader.tools.LaunchScript; import org.springframework.util.FileCopyUtils; import org.springframework.util.FileSystemUtils; @@ -94,7 +102,12 @@ public class GradleBuild { new File(pathOfJarContaining(KotlinPlugin.class)), new File(pathOfJarContaining(KotlinProject.class)), new File(pathOfJarContaining(KotlinCompilerClient.class)), new File(pathOfJarContaining(KotlinGradleSubplugin.class)), - new File(pathOfJarContaining(ArchiveEntry.class))); + new File(pathOfJarContaining(ArchiveEntry.class)), new File(pathOfJarContaining(BuildRequest.class)), + new File(pathOfJarContaining(HttpClientConnectionManager.class)), + new File(pathOfJarContaining(HttpRequest.class)), new File(pathOfJarContaining(Module.class)), + new File(pathOfJarContaining(Versioned.class)), + new File(pathOfJarContaining(ParameterNamesModule.class)), + new File(pathOfJarContaining(JsonView.class)), new File(pathOfJarContaining(Platform.class))); } private String pathOfJarContaining(Class type) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.gradle new file mode 100644 index 0000000000..feb1992eeb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.gradle @@ -0,0 +1,4 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +}