diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c8ea3811..28b8752d7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Usage: * Fix #1929: Docker Image Name parsing fix * Fix #1985: Update outdated methods in Spring Boot CRD Maven Quickstart * Fix #2116: Remove user field from ImageName class +* Fix #2138: Support for Spring Boot Native Image * Fix #2219: Kind/Filename mappings include optional apiVersion configuration * Fix #2224: Quarkus native base image read from properties (configurable) * Fix #2228: Quarkus native base image uses UBI 8.7 diff --git a/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/util/SpringBootUtil.java b/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/util/SpringBootUtil.java index 14bc942065..f9fe454c3d 100644 --- a/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/util/SpringBootUtil.java +++ b/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/util/SpringBootUtil.java @@ -13,6 +13,7 @@ */ package org.eclipse.jkube.kit.common.util; +import java.io.File; import java.net.URL; import java.net.URLClassLoader; import java.util.Collections; @@ -111,5 +112,27 @@ public static boolean isSpringBootRepackage(JavaProject project) { .map(e -> e.contains("repackage")) .orElse(false); } + + public static Plugin getNativePlugin(JavaProject project) { + Plugin plugin = JKubeProjectUtil.getPlugin(project, "org.graalvm.buildtools", "native-maven-plugin"); + if (plugin != null) { + return plugin; + } + return JKubeProjectUtil.getPlugin(project, "org.graalvm.buildtools.native", "org.graalvm.buildtools.native.gradle.plugin"); + } + + public static File getNativeArtifactFile(JavaProject project) { + Plugin plugin = getNativePlugin(project); + String nativeArtifactName = (String) Optional.ofNullable(plugin.getConfiguration()) + .map(c -> c.get("imageName")) + .orElse(project.getArtifactId()); + for (String location : new String[] {"", "native/nativeCompile/"}) { + File nativeArtifact = new File(project.getBuildDirectory(), location + nativeArtifactName); + if (nativeArtifact.exists()) { + return nativeArtifact; + } + } + return null; + } } diff --git a/jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/util/SpringBootUtilTest.java b/jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/util/SpringBootUtilTest.java index b5db038b12..521f3ec1be 100644 --- a/jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/util/SpringBootUtilTest.java +++ b/jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/util/SpringBootUtilTest.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; import java.util.Map; @@ -264,4 +265,124 @@ void isSpringBootRepackage_whenNoExecution_thenReturnFalse() { // Then assertThat(result).isFalse(); } + + @Test + void getNativePlugin_whenNoNativePluginPresent_thenReturnNull() { + assertThat(SpringBootUtil.getNativePlugin(JavaProject.builder().build())).isNull(); + } + + @Test + void getNativePlugin_whenMavenNativePluginPresent_thenReturnPlugin() { + // Given + JavaProject javaProject = JavaProject.builder() + .plugin(Plugin.builder() + .groupId("org.graalvm.buildtools") + .artifactId("native-maven-plugin") + .build()) + .build(); + + // When + Plugin plugin = SpringBootUtil.getNativePlugin(javaProject); + + // Then + assertThat(plugin).isNotNull(); + } + + @Test + void getNativePlugin_whenGradleNativePluginPresent_thenReturnPlugin() { + // Given + JavaProject javaProject = JavaProject.builder() + .plugin(Plugin.builder() + .groupId("org.graalvm.buildtools.native") + .artifactId("org.graalvm.buildtools.native.gradle.plugin") + .build()) + .build(); + + // When + Plugin plugin = SpringBootUtil.getNativePlugin(javaProject); + + // Then + assertThat(plugin).isNotNull(); + } + + @Test + void getNativeArtifactFile_whenNativeExecutableNotFound_thenReturnNull(@TempDir File temporaryFolder) throws IOException { + // Given + JavaProject javaProject = JavaProject.builder() + .artifactId("sample") + .buildDirectory(temporaryFolder) + .plugin(Plugin.builder() + .groupId("org.graalvm.buildtools") + .artifactId("native-maven-plugin") + .build()) + .build(); + + // When + File nativeArtifactFound = SpringBootUtil.getNativeArtifactFile(javaProject); + + // Then + assertThat(nativeArtifactFound).isNull(); + } + + @Test + void getNativeArtifactFile_whenNativeExecutableInStandardMavenBuildDirectory_thenReturnNativeArtifact(@TempDir File temporaryFolder) throws IOException { + // Given + Files.createFile(temporaryFolder.toPath().resolve("sample")); + JavaProject javaProject = JavaProject.builder() + .artifactId("sample") + .buildDirectory(temporaryFolder) + .plugin(Plugin.builder() + .groupId("org.graalvm.buildtools") + .artifactId("native-maven-plugin") + .build()) + .build(); + + // When + File nativeArtifactFound = SpringBootUtil.getNativeArtifactFile(javaProject); + + // Then + assertThat(nativeArtifactFound).hasName("sample"); + } + + @Test + void getNativeArtifactFile_whenNativeExecutableInStandardMavenBuildDirectoryAndImageNameOverridden_thenReturnNativeArtifact(@TempDir File temporaryFolder) throws IOException { + // Given + Files.createFile(temporaryFolder.toPath().resolve("custom-native-name")); + JavaProject javaProject = JavaProject.builder() + .artifactId("sample") + .buildDirectory(temporaryFolder) + .plugin(Plugin.builder() + .groupId("org.graalvm.buildtools") + .artifactId("native-maven-plugin") + .configuration(Collections.singletonMap("imageName", "custom-native-name")) + .build()) + .build(); + + // When + File nativeArtifactFound = SpringBootUtil.getNativeArtifactFile(javaProject); + + // Then + assertThat(nativeArtifactFound).hasName("custom-native-name"); + } + + @Test + void getNativeArtifactFile_whenNativeExecutableInStandardGradleNativeDirectory_thenReturnNativeArtifact(@TempDir File temporaryFolder) throws IOException { + // Given + Files.createDirectories(temporaryFolder.toPath().resolve("native").resolve("nativeCompile")); + Files.createFile(temporaryFolder.toPath().resolve("native").resolve("nativeCompile").resolve("sample")); + JavaProject javaProject = JavaProject.builder() + .artifactId("sample") + .buildDirectory(temporaryFolder) + .plugin(Plugin.builder() + .groupId("org.graalvm.buildtools.native") + .artifactId("org.graalvm.buildtools.native.gradle.plugin") + .build()) + .build(); + + // When + File nativeArtifactFound = SpringBootUtil.getNativeArtifactFile(javaProject); + + // Then + assertThat(nativeArtifactFound).hasName("sample"); + } } diff --git a/jkube-kit/jkube-kit-spring-boot/pom.xml b/jkube-kit/jkube-kit-spring-boot/pom.xml index 507c7e7267..c4aa13f456 100644 --- a/jkube-kit/jkube-kit-spring-boot/pom.xml +++ b/jkube-kit/jkube-kit-spring-boot/pom.xml @@ -81,4 +81,18 @@ + + + + + src/main/resources-filtered + true + + + src/main/resources + false + + + + diff --git a/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/NativeGenerator.java b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/NativeGenerator.java new file mode 100644 index 0000000000..f9bfe6b5fd --- /dev/null +++ b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/NativeGenerator.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.springboot.generator; + +import org.eclipse.jkube.generator.api.FromSelector; +import org.eclipse.jkube.generator.api.GeneratorConfig; +import org.eclipse.jkube.generator.api.GeneratorContext; +import org.eclipse.jkube.kit.common.Arguments; +import org.eclipse.jkube.kit.common.Assembly; +import org.eclipse.jkube.kit.common.AssemblyConfiguration; +import org.eclipse.jkube.kit.common.AssemblyFileSet; +import org.eclipse.jkube.kit.common.JavaProject; + +import java.io.File; +import java.util.List; + +import static org.eclipse.jkube.kit.common.util.FileUtil.getRelativePath; + +public class NativeGenerator extends AbstractSpringBootNestedGenerator { + private final File nativeBinary; + private final FromSelector fromSelector; + + public NativeGenerator(GeneratorContext generatorContext, GeneratorConfig generatorConfig, File nativeBinary) { + super(generatorContext, generatorConfig); + this.nativeBinary = nativeBinary; + fromSelector = new FromSelector.Default(generatorContext, "springboot-native"); + } + + + @Override + public String getFrom() { + return fromSelector.getFrom(); + } + + @Override + public String getDefaultJolokiaPort() { + return "0"; + } + + @Override + public String getDefaultPrometheusPort() { + return "0"; + } + + @Override + public Arguments getBuildEntryPoint() { + return Arguments.builder() + .execArgument("./" + nativeBinary.getName()) + .build(); + } + + @Override + public String getBuildWorkdir() { + return "/"; + } + + @Override + public String getTargetDir() { + return "/"; + } + + @Override + public AssemblyConfiguration createAssemblyConfiguration(List defaultFileSets) { + Assembly.AssemblyBuilder assemblyBuilder = Assembly.builder(); + final JavaProject project = getProject(); + final AssemblyFileSet.AssemblyFileSetBuilder artifactFileSetBuilder = AssemblyFileSet.builder() + .outputDirectory(new File(".")) + .directory(getRelativePath(project.getBaseDirectory(), nativeBinary.getParentFile())) + .fileMode("0755"); + artifactFileSetBuilder.include(nativeBinary.getName()); + + assemblyBuilder.fileSets(defaultFileSets); + assemblyBuilder.fileSet(artifactFileSetBuilder.build()); + + return AssemblyConfiguration.builder() + .targetDir(getTargetDir()) + .excludeFinalOutputArtifact(true) + .layer(assemblyBuilder.build()) + .build(); + } +} \ No newline at end of file diff --git a/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/SpringBootGenerator.java b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/SpringBootGenerator.java index c64e12dda0..9b64f961ae 100644 --- a/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/SpringBootGenerator.java +++ b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/SpringBootGenerator.java @@ -129,7 +129,7 @@ protected String getDefaultWebPort() { @Override protected AssemblyConfiguration createAssembly() { - return Optional.ofNullable(nestedGenerator.createAssemblyConfiguration()) + return Optional.ofNullable(nestedGenerator.createAssemblyConfiguration(addAdditionalFiles())) .orElse(super.createAssembly()); } diff --git a/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/SpringBootNestedGenerator.java b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/SpringBootNestedGenerator.java index 68b5c5f3e1..12ba0ec3ab 100644 --- a/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/SpringBootNestedGenerator.java +++ b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/SpringBootNestedGenerator.java @@ -17,15 +17,21 @@ import org.eclipse.jkube.generator.api.GeneratorContext; import org.eclipse.jkube.kit.common.Arguments; import org.eclipse.jkube.kit.common.AssemblyConfiguration; +import org.eclipse.jkube.kit.common.AssemblyFileSet; import org.eclipse.jkube.kit.common.JavaProject; +import java.io.File; +import java.util.List; + import static org.eclipse.jkube.generator.javaexec.JavaExecGenerator.JOLOKIA_PORT_DEFAULT; import static org.eclipse.jkube.generator.javaexec.JavaExecGenerator.PROMETHEUS_PORT_DEFAULT; +import static org.eclipse.jkube.kit.common.util.SpringBootUtil.getNativeArtifactFile; +import static org.eclipse.jkube.kit.common.util.SpringBootUtil.getNativePlugin; public interface SpringBootNestedGenerator { JavaProject getProject(); - default AssemblyConfiguration createAssemblyConfiguration() { + default AssemblyConfiguration createAssemblyConfiguration(List defaultFileSets) { return null; } @@ -50,6 +56,12 @@ default Arguments getBuildEntryPoint() { String getTargetDir(); static SpringBootNestedGenerator from(GeneratorContext generatorContext, GeneratorConfig generatorConfig) { + if (getNativePlugin(generatorContext.getProject()) != null) { + File nativeBinary = getNativeArtifactFile(generatorContext.getProject()); + if (nativeBinary != null) { + return new NativeGenerator(generatorContext, generatorConfig, nativeBinary); + } + } return new FatJarGenerator(generatorContext, generatorConfig); } } diff --git a/jkube-kit/jkube-kit-spring-boot/src/main/resources-filtered/META-INF/jkube/default-images.properties b/jkube-kit/jkube-kit-spring-boot/src/main/resources-filtered/META-INF/jkube/default-images.properties new file mode 100644 index 0000000000..55db026634 --- /dev/null +++ b/jkube-kit/jkube-kit-spring-boot/src/main/resources-filtered/META-INF/jkube/default-images.properties @@ -0,0 +1,21 @@ +# +# Copyright (c) 2019 Red Hat, Inc. +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at: +# +# https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# Red Hat, Inc. - initial API and implementation +# + +# Properties for specifying the default images version to use +# The replacement values are defined in the parent pom.xml as properties + +# Upstream images +springboot-native.upstream.s2i=${image.springboot-native.upstream.s2i} +springboot-native.upstream.docker=${image.springboot-native.upstream.docker} +springboot-native.upstream.istag=${image.springboot-native.upstream.istag} diff --git a/jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/generator/SpringBootGeneratorIntegrationTest.java b/jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/generator/SpringBootGeneratorIntegrationTest.java index 60633943c3..d54fe9042e 100644 --- a/jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/generator/SpringBootGeneratorIntegrationTest.java +++ b/jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/generator/SpringBootGeneratorIntegrationTest.java @@ -25,10 +25,14 @@ import org.eclipse.jkube.kit.common.Plugin; import org.eclipse.jkube.kit.config.image.ImageConfiguration; import org.eclipse.jkube.kit.config.image.build.BuildConfiguration; +import org.eclipse.jkube.kit.config.resource.RuntimeMode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.MockedConstruction; import java.io.File; @@ -39,6 +43,7 @@ import java.util.List; import java.util.Objects; import java.util.Properties; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -48,6 +53,7 @@ class SpringBootGeneratorIntegrationTest { private File targetDir; private Properties properties; + private JavaProject javaProject; @TempDir Path temporaryFolder; @@ -57,7 +63,7 @@ class SpringBootGeneratorIntegrationTest { void setUp() throws IOException { properties = new Properties(); targetDir = Files.createDirectory(temporaryFolder.resolve("target")).toFile(); - JavaProject javaProject = JavaProject.builder() + javaProject = JavaProject.builder() .baseDirectory(temporaryFolder.toFile()) .buildDirectory(targetDir.getAbsoluteFile()) .buildPackageDirectory(targetDir.getAbsoluteFile()) @@ -74,6 +80,7 @@ void setUp() throws IOException { .artifactId("spring-boot-maven-plugin") .version("2.7.2") .build()) + .artifactId("sample") .buildFinalName("sample") .build(); context = GeneratorContext.builder() @@ -209,6 +216,101 @@ void customize_inKubernetesAndJarArtifact_shouldCreateAssembly() throws IOExcept } } + @Test + @DisplayName("customize, in Kubernetes and native artifact, should create assembly") + void customize_inKubernetesAndNativeArtifact_shouldCreateNativeAssembly() throws IOException { + // Given + withCustomMainClass(); + withNativePluginAndArtifactInProject(); + + // When + final List resultImages = new SpringBootGenerator(context).customize(new ArrayList<>(), false); + + // Then + assertThat(resultImages) + .isNotNull() + .singleElement() + .extracting(ImageConfiguration::getBuild) + .extracting(BuildConfiguration::getAssembly) + .hasFieldOrPropertyWithValue("targetDir", "/") + .hasFieldOrPropertyWithValue("excludeFinalOutputArtifact", true) + .extracting(AssemblyConfiguration::getLayers) + .asList().hasSize(1) + .satisfies(layers -> assertThat(layers).element(0).asInstanceOf(InstanceOfAssertFactories.type(Assembly.class)) + .extracting(Assembly::getFileSets) + .asList().element(2) + .hasFieldOrPropertyWithValue("outputDirectory", new File(".")) + .hasFieldOrPropertyWithValue("fileMode", "0755") + .extracting("includes").asList() + .containsExactly("sample")); + } + + @Test + @DisplayName("customize, with native packaging, disables Jolokia port") + void customize_withNativePackaging_disablesJolokiaPort() throws IOException { + // Given + withCustomMainClass(); + withNativePluginAndArtifactInProject(); + + // When + final List result = new SpringBootGenerator(context).customize(new ArrayList<>(), true); + // Then + assertThat(result).singleElement() + .extracting("buildConfiguration.env") + .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) + .containsEntry("AB_JOLOKIA_OFF", "true"); + } + + @Test + @DisplayName("customize, with native packaging, disables Prometheus port") + void customize_withNativePackaging_disablesPrometheusPort() throws IOException { + // Given + withCustomMainClass(); + withNativePluginAndArtifactInProject(); + // When + final List result = new SpringBootGenerator(context).customize(new ArrayList<>(), true); + // Then + assertThat(result).singleElement() + .extracting("buildConfiguration.env") + .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) + .containsEntry("AB_PROMETHEUS_OFF", "true"); + } + + @ParameterizedTest(name = "{index}: customize, native packaging in ''{0}'' mode, should return ''{1}'' as from image") + @MethodSource("customize_withNativePackaging_fromData") + void customize_withNativePackaging_from(RuntimeMode runtimeMode, String expectedFromStartsWith) throws IOException { + // Given + withCustomMainClass(); + withNativePluginAndArtifactInProject(); + context = context.toBuilder().runtimeMode(runtimeMode).build(); + // When + final List result = new SpringBootGenerator(context).customize(new ArrayList<>(), true); + // Then + assertThat(result).singleElement() + .extracting("buildConfiguration.from").asString() + .startsWith(expectedFromStartsWith); + } + + static Stream customize_withNativePackaging_fromData() { + return Stream.of( + Arguments.of(RuntimeMode.KUBERNETES, "registry.access.redhat.com/ubi8/ubi-minimal:"), + Arguments.of(RuntimeMode.OPENSHIFT, "registry.access.redhat.com/ubi8/ubi-minimal:") + ); + } + + @Test + @DisplayName("customize, with native packaging, should set workDir to root directory") + void customize_withNativePackaging_workDir() throws IOException { + // Given + withNativePluginAndArtifactInProject(); + // When + final List result = new SpringBootGenerator(context).customize(new ArrayList<>(), true); + // Then + assertThat(result).singleElement() + .extracting("buildConfiguration.workdir").asString() + .startsWith("/"); + } + @Test @DisplayName("customize, with standard packaging, has java environment variables") void customize_withStandardPackaging_thenImageHasJavaMainClassAndJavaAppDirEnvVars() { @@ -300,6 +402,16 @@ void customize_withColorConfiguration_shouldAddAnsiEnabledPropertyToJavaOptions( .containsEntry("JAVA_OPTIONS", "-Dspring.output.ansi.enabled=always"); } + private void withNativePluginAndArtifactInProject() throws IOException { + javaProject = javaProject.toBuilder() + .plugin(Plugin.builder().groupId("org.graalvm.buildtools").artifactId("native-maven-plugin").build()) + .build(); + context = context.toBuilder() + .project(javaProject) + .build(); + Files.createFile(targetDir.toPath().resolve("sample")); + } + private void withCustomMainClass() { properties.put("jkube.generator.spring-boot.mainClass", "org.example.Foo"); } diff --git a/jkube-kit/parent/pom.xml b/jkube-kit/parent/pom.xml index f616821c40..13b09524d8 100644 --- a/jkube-kit/parent/pom.xml +++ b/jkube-kit/parent/pom.xml @@ -105,6 +105,11 @@ registry.access.redhat.com/ubi8/ubi-minimal:${version.image.ubi-minimal} registry.access.redhat.com/ubi8/ubi-minimal:${version.image.ubi-minimal} + + registry.access.redhat.com/ubi8/ubi-minimal:${version.image.ubi-minimal} + registry.access.redhat.com/ubi8/ubi-minimal:${version.image.ubi-minimal} + registry.access.redhat.com/ubi8/ubi-minimal:${version.image.ubi-minimal} + quay.io/quarkus/ubi-quarkus-native-binary-s2i:1.0 registry.access.redhat.com/ubi8/ubi-minimal:${version.image.ubi-minimal}