From ebbfe133acb5c44e3178bf9bf68b221392bec5f8 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Wed, 26 Jul 2023 10:19:38 +0530 Subject: [PATCH] feat (jkube-kit/spring-boot) : SpringBootGenerator utilizes layered jar if present and use it as Docker layers (#1674) + Add LayeredJarGenerator for layered container assembly for spring boot + Add gradle integration test for spring boot layered image generation Signed-off-by: Rohan Kumar --- CHANGELOG.md | 1 + .../plugin/tests/ITGradleRunnerExtension.java | 4 + .../gradle/plugin/tests/SpringBootIT.java | 23 +++ .../jkube/kit/common/util/SpringBootUtil.java | 11 ++ .../kit/common/util/SpringBootUtilTest.java | 32 ++++ .../SpringBootLayeredJarExecUtils.java | 82 ++++++++++ .../AbstractSpringBootNestedGenerator.java | 5 + .../generator/LayeredJarGenerator.java | 82 ++++++++++ .../generator/SpringBootGenerator.java | 5 +- .../generator/SpringBootNestedGenerator.java | 19 ++- .../SpringBootLayeredJarExecUtilsTest.java | 57 +++++++ .../SpringBootGeneratorIntegrationTest.java | 150 +++++++++++++++++- 12 files changed, 466 insertions(+), 5 deletions(-) create mode 100644 jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/SpringBootLayeredJarExecUtils.java create mode 100644 jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/LayeredJarGenerator.java create mode 100644 jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/SpringBootLayeredJarExecUtilsTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c8ea3811..7241199030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Usage: ./scripts/extract-changelog-for-version.sh 1.3.37 5 ``` ### 1.14-SNAPSHOT +* Fix #1674: SpringBootGenerator utilizes the layered jar if present and use it as Docker layers * Fix #1713: Add HelidonHealthCheckEnricher to add Kubernetes health checks for Helidon applications * Fix #1714: Add HelidonGenerator to add opinionated container image for Helidon applications * Fix #1929: Docker Image Name parsing fix diff --git a/gradle-plugin/it/src/test/java/org/eclipse/jkube/gradle/plugin/tests/ITGradleRunnerExtension.java b/gradle-plugin/it/src/test/java/org/eclipse/jkube/gradle/plugin/tests/ITGradleRunnerExtension.java index d4916c8b7d..8d2dde078d 100644 --- a/gradle-plugin/it/src/test/java/org/eclipse/jkube/gradle/plugin/tests/ITGradleRunnerExtension.java +++ b/gradle-plugin/it/src/test/java/org/eclipse/jkube/gradle/plugin/tests/ITGradleRunnerExtension.java @@ -86,6 +86,10 @@ public File resolveDefaultOpenShiftResourceDir() { return resolveFile("build", "classes", "java", "main", "META-INF", "jkube", "openshift"); } + public File resolveDefaultDockerfile(String registry, String imageNamespace, String imageName, String imageTag) { + return resolveFile("build", "docker", registry, imageNamespace, imageName, imageTag, "build", "Dockerfile"); + } + public BuildResult build() { return gradleRunner.build(); } diff --git a/gradle-plugin/it/src/test/java/org/eclipse/jkube/gradle/plugin/tests/SpringBootIT.java b/gradle-plugin/it/src/test/java/org/eclipse/jkube/gradle/plugin/tests/SpringBootIT.java index 5ce5719574..b72bc99a6e 100644 --- a/gradle-plugin/it/src/test/java/org/eclipse/jkube/gradle/plugin/tests/SpringBootIT.java +++ b/gradle-plugin/it/src/test/java/org/eclipse/jkube/gradle/plugin/tests/SpringBootIT.java @@ -14,6 +14,7 @@ package org.eclipse.jkube.gradle.plugin.tests; import java.io.IOException; +import java.nio.file.Files; import org.eclipse.jkube.kit.common.ResourceVerify; @@ -66,4 +67,26 @@ void ocResource_whenRun_generatesOpenShiftManifests() throws IOException, ParseE .contains("jkube-service-discovery: Using first mentioned service port '8080' ") .contains("jkube-revision-history: Adding revision history limit to 2"); } + + @Test + void k8sBuild_whenRunWithJibBuildStrategy_generatesLayeredImage() throws IOException { + // When + final BuildResult result = gradleRunner.withITProject("spring-boot") + .withArguments("clean", "build", "k8sBuild", "-Pjkube.build.strategy=jib", "--stacktrace") + .build(); + // Then + String generatedDockerfileContent = new String(Files.readAllBytes(gradleRunner.resolveDefaultDockerfile("docker.io", "gradle", "spring-boot", "latest").toPath())); + assertThat(generatedDockerfileContent) + .contains("FROM quay.io/jkube/jkube-java:") + .contains("ENV JAVA_MAIN_CLASS=org.springframework.boot.loader.JarLauncher JAVA_APP_DIR=/deployments") + .contains("EXPOSE 8080 8778 9779") + .contains("COPY /dependencies/deployments /deployments/") + .contains("COPY /spring-boot-loader/deployments /deployments/") + .contains("COPY /application/deployments /deployments/") + .contains("WORKDIR /deployments") + .contains("ENTRYPOINT [\"java\",\"org.springframework.boot.loader.JarLauncher\"]"); + assertThat(result).extracting(BuildResult::getOutput).asString() + .contains("Running generator spring-boot") + .contains("Spring Boot layered jar detected"); + } } 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..2b0f394e0b 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,12 +13,15 @@ */ package org.eclipse.jkube.kit.common.util; +import java.io.File; +import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.Properties; +import java.util.jar.JarFile; import org.eclipse.jkube.kit.common.JavaProject; import org.eclipse.jkube.kit.common.Plugin; @@ -111,5 +114,13 @@ public static boolean isSpringBootRepackage(JavaProject project) { .map(e -> e.contains("repackage")) .orElse(false); } + + public static boolean isLayeredJar(File fatJar) { + try (JarFile jarFile = new JarFile(fatJar)) { + return jarFile.getEntry("BOOT-INF/layers.idx") != null; + } catch (IOException ioException) { + throw new IllegalStateException("Failure in inspecting fat jar for layers.idx file", ioException); + } + } } 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..948b551764 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,13 +25,19 @@ 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; import java.util.Objects; import java.util.Optional; import java.util.Properties; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -264,4 +270,30 @@ void isSpringBootRepackage_whenNoExecution_thenReturnFalse() { // Then assertThat(result).isFalse(); } + + @Test + void isLayeredJar_whenInvalidFile_thenThrowException() { + // When + Then + assertThatIllegalStateException() + .isThrownBy(() -> SpringBootUtil.isLayeredJar(new File("i-dont-exist.jar"))) + .withMessage("Failure in inspecting fat jar for layers.idx file"); + } + + @Test + void isLayeredJar_whenJarContainsLayers_thenReturnTrue(@TempDir File temporaryFolder) throws IOException { + // Given + File jarFile = new File(temporaryFolder, "fat.jar"); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, "org.example.Foo"); + try (JarOutputStream jarOutputStream = new JarOutputStream(Files.newOutputStream(jarFile.toPath()), manifest)) { + jarOutputStream.putNextEntry(new JarEntry("BOOT-INF/layers.idx")); + } + + // When + boolean result = SpringBootUtil.isLayeredJar(jarFile); + + // Then + assertThat(result).isTrue(); + } } diff --git a/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/SpringBootLayeredJarExecUtils.java b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/SpringBootLayeredJarExecUtils.java new file mode 100644 index 0000000000..4bf89e8bbe --- /dev/null +++ b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/SpringBootLayeredJarExecUtils.java @@ -0,0 +1,82 @@ +/* + * 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; + +import org.eclipse.jkube.kit.common.ExternalCommand; +import org.eclipse.jkube.kit.common.KitLogger; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class SpringBootLayeredJarExecUtils { + private SpringBootLayeredJarExecUtils() { } + + public static List listLayers(KitLogger kitLogger, File layeredJar) { + LayerListCommand layerListCommand = new LayerListCommand(kitLogger, layeredJar); + try { + layerListCommand.execute(); + return layerListCommand.getLayers(); + } catch (IOException ioException) { + throw new IllegalStateException("Failure in getting spring boot jar layers information", ioException); + } + } + + public static void extractLayers(KitLogger kitLogger, File extractionDir, File layeredJar) { + LayerExtractorCommand layerExtractorCommand = new LayerExtractorCommand(kitLogger, extractionDir, layeredJar); + try { + layerExtractorCommand.execute(); + } catch (IOException ioException) { + throw new IllegalStateException("Failure in extracting spring boot jar layers", ioException); + } + } + + private static class LayerExtractorCommand extends ExternalCommand { + private final File layeredJar; + protected LayerExtractorCommand(KitLogger log, File workDir, File layeredJar) { + super(log, workDir); + this.layeredJar = layeredJar; + } + + @Override + protected String[] getArgs() { + return new String[] { "java", "-Djarmode=layertools", "-jar", layeredJar.getAbsolutePath(), "extract"}; + } + } + + private static class LayerListCommand extends ExternalCommand { + private final List layers; + private final File layeredJar; + protected LayerListCommand(KitLogger log, File layeredJar) { + super(log); + layers = new ArrayList<>(); + this.layeredJar = layeredJar; + } + + @Override + protected String[] getArgs() { + return new String[] { "java", "-Djarmode=layertools", "-jar", layeredJar.getAbsolutePath(), "list"}; + } + + @Override + protected void processLine(String line) { + layers.add(line); + } + + public List getLayers() { + return layers; + } + } +} diff --git a/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/AbstractSpringBootNestedGenerator.java b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/AbstractSpringBootNestedGenerator.java index 6cd1134d68..a46f8c5618 100644 --- a/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/AbstractSpringBootNestedGenerator.java +++ b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/AbstractSpringBootNestedGenerator.java @@ -17,6 +17,7 @@ import org.eclipse.jkube.generator.api.GeneratorContext; import org.eclipse.jkube.generator.javaexec.JavaExecGenerator; import org.eclipse.jkube.kit.common.JavaProject; +import org.eclipse.jkube.kit.common.KitLogger; public abstract class AbstractSpringBootNestedGenerator implements SpringBootNestedGenerator { @@ -42,4 +43,8 @@ public String getBuildWorkdir() { public String getTargetDir() { return generatorConfig.get(JavaExecGenerator.Config.TARGET_DIR); } + + protected KitLogger getLogger() { + return generatorContext.getLogger(); + } } diff --git a/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/LayeredJarGenerator.java b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/LayeredJarGenerator.java new file mode 100644 index 0000000000..23ac72bcb0 --- /dev/null +++ b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/LayeredJarGenerator.java @@ -0,0 +1,82 @@ +/* + * 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.GeneratorConfig; +import org.eclipse.jkube.generator.api.GeneratorContext; +import org.eclipse.jkube.generator.javaexec.FatJarDetector; +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 java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.eclipse.jkube.kit.common.util.FileUtil.getRelativePath; +import static org.eclipse.jkube.springboot.SpringBootLayeredJarExecUtils.extractLayers; +import static org.eclipse.jkube.springboot.SpringBootLayeredJarExecUtils.listLayers; + +public class LayeredJarGenerator extends AbstractSpringBootNestedGenerator { + private final FatJarDetector.Result fatJarDetectorResult; + public LayeredJarGenerator(GeneratorContext generatorContext, GeneratorConfig generatorConfig, FatJarDetector.Result result) { + super(generatorContext, generatorConfig); + fatJarDetectorResult = result; + } + + @Override + public Arguments getBuildEntryPoint() { + return Arguments.builder() + .exec(Arrays.asList("java", "org.springframework.boot.loader.JarLauncher")) + .build(); + } + + @Override + public Map getEnv() { + return Collections.singletonMap("JAVA_MAIN_CLASS", "org.springframework.boot.loader.JarLauncher"); + } + + @Override + public AssemblyConfiguration createAssemblyConfiguration(List defaultFileSets) { + getLogger().info("Spring Boot layered jar detected"); + + List layerNames = listLayers(getLogger(), fatJarDetectorResult.getArchiveFile()); + List layerAssemblies = new ArrayList<>(); + layerAssemblies.add(Assembly.builder().id("jkube-includes").fileSets(defaultFileSets).build()); + extractLayers(getLogger(), getProject().getBuildPackageDirectory(), fatJarDetectorResult.getArchiveFile()); + + for (String springBootLayer : layerNames) { + File layerDir = new File(getProject().getBuildPackageDirectory(), springBootLayer); + layerAssemblies.add(Assembly.builder() + .id(springBootLayer) + .fileSet(AssemblyFileSet.builder() + .outputDirectory(new File(".")) + .directory(getRelativePath(getProject().getBaseDirectory(), layerDir)) + .exclude("*") + .fileMode("0640") + .build()) + .build()); + } + + return AssemblyConfiguration.builder() + .targetDir(getTargetDir()) + .excludeFinalOutputArtifact(true) + .layers(layerAssemblies) + .build(); + } +} 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..db79c02d22 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 @@ -58,7 +58,7 @@ public enum Config implements Configs.Config { public SpringBootGenerator(GeneratorContext context) { super(context, "spring-boot"); - nestedGenerator = SpringBootNestedGenerator.from(context, getGeneratorConfig()); + nestedGenerator = SpringBootNestedGenerator.from(context, getGeneratorConfig(), detectFatJar()); } @Override @@ -96,6 +96,7 @@ protected Map getEnv(boolean prePackagePhase) { res.put(SpringBootConfigurationHelper.DEV_TOOLS_REMOTE_SECRET_ENV, secret); } } + res.putAll(nestedGenerator.getEnv()); return res; } @@ -129,7 +130,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..68af5316fe 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 @@ -15,17 +15,24 @@ import org.eclipse.jkube.generator.api.GeneratorConfig; import org.eclipse.jkube.generator.api.GeneratorContext; +import org.eclipse.jkube.generator.javaexec.FatJarDetector; 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.util.Collections; +import java.util.List; +import java.util.Map; + 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.isLayeredJar; public interface SpringBootNestedGenerator { JavaProject getProject(); - default AssemblyConfiguration createAssemblyConfiguration() { + default AssemblyConfiguration createAssemblyConfiguration(List defaultFileSets) { return null; } @@ -49,7 +56,15 @@ default Arguments getBuildEntryPoint() { String getTargetDir(); - static SpringBootNestedGenerator from(GeneratorContext generatorContext, GeneratorConfig generatorConfig) { + default Map getEnv() { + return Collections.emptyMap(); + } + + static SpringBootNestedGenerator from(GeneratorContext generatorContext, GeneratorConfig generatorConfig, FatJarDetector.Result fatJarDetectorResult) { + if (fatJarDetectorResult != null && fatJarDetectorResult.getArchiveFile() != null && + isLayeredJar(fatJarDetectorResult.getArchiveFile())) { + return new LayeredJarGenerator(generatorContext, generatorConfig, fatJarDetectorResult); + } return new FatJarGenerator(generatorContext, generatorConfig); } } diff --git a/jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/SpringBootLayeredJarExecUtilsTest.java b/jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/SpringBootLayeredJarExecUtilsTest.java new file mode 100644 index 0000000000..be5ce75744 --- /dev/null +++ b/jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/SpringBootLayeredJarExecUtilsTest.java @@ -0,0 +1,57 @@ +/* + * 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; + +import org.eclipse.jkube.kit.common.KitLogger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +class SpringBootLayeredJarExecUtilsTest { + @TempDir + private File temporaryFolder; + + private KitLogger kitLogger; + + @BeforeEach + void setup() { + kitLogger = new KitLogger.SilentLogger(); + } + + @Test + void listLayers_whenJarInvalid_thenThrowException() { + // Given + File layeredJar = new File(temporaryFolder, "sample.jar"); + + // When + Then + assertThatIllegalStateException() + .isThrownBy(() -> SpringBootLayeredJarExecUtils.listLayers(kitLogger, layeredJar)) + .withMessage("Failure in getting spring boot jar layers information"); + } + + @Test + void extractLayers_whenJarInvalid_thenThrowException() { + // Given + File layeredJar = new File(temporaryFolder, "sample.jar"); + + // When + Then + assertThatIllegalStateException() + .isThrownBy(() -> SpringBootLayeredJarExecUtils.extractLayers(kitLogger, temporaryFolder, layeredJar)) + .withMessage("Failure in extracting spring boot jar layers"); + } +} 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..3a2f297412 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 @@ -19,30 +19,40 @@ import org.eclipse.jkube.generator.javaexec.FatJarDetector; 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.Dependency; import org.eclipse.jkube.kit.common.JavaProject; import org.eclipse.jkube.kit.common.KitLogger; 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.springboot.SpringBootLayeredJarExecUtils; 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.mockito.MockedConstruction; +import org.mockito.MockedStatic; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Properties; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; class SpringBootGeneratorIntegrationTest { @@ -185,7 +195,7 @@ void customize_inKubernetesAndJarArtifact_shouldCreateAssembly() throws IOExcept when(fatJarDetectorResult.getArchiveFile()).thenReturn(targetDir.toPath().resolve("sample.jar").toFile()); })) { // Given - Files.createFile(targetDir.toPath().resolve("sample.jar")); + createDummyFatJar(targetDir.toPath().resolve("sample.jar").toFile()); // When final List resultImages = new SpringBootGenerator(context).customize(new ArrayList<>(), false); @@ -209,6 +219,144 @@ void customize_inKubernetesAndJarArtifact_shouldCreateAssembly() throws IOExcept } } + @Test + @DisplayName("customize, in Kubernetes and layered jar artifact, should create assembly layers") + void customize_inKubernetesAndLayeredJarArtifact_shouldCreateAssemblyLayers() throws IOException { + File layeredJar = targetDir.toPath().resolve("layered.jar").toFile(); + try ( + MockedStatic springBootLayeredJarExecUtilsMockedStatic = mockStatic(SpringBootLayeredJarExecUtils.class); + MockedConstruction ignore = mockConstruction(FatJarDetector.class, (mock, ctx) -> { + FatJarDetector.Result fatJarDetectorResult = mock(FatJarDetector.Result.class); + when(mock.scan()).thenReturn(fatJarDetectorResult); + when(fatJarDetectorResult.getArchiveFile()).thenReturn(layeredJar); + })) { + // Given + createDummyLayeredJar(layeredJar); + springBootLayeredJarExecUtilsMockedStatic.when(() -> SpringBootLayeredJarExecUtils.listLayers(any(), any(File.class))) + .thenReturn(Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies", "application")); + createExtractedLayers(targetDir); + + // When + final List resultImages = new SpringBootGenerator(context).customize(new ArrayList<>(), false); + // Then + assertThat(resultImages) + .isNotNull() + .singleElement() + .extracting(ImageConfiguration::getBuild) + .satisfies(b -> assertThat(b.getEnv()) + .containsEntry("JAVA_MAIN_CLASS", "org.springframework.boot.loader.JarLauncher")) + .extracting(BuildConfiguration::getAssembly) + .hasFieldOrPropertyWithValue("targetDir", "/deployments") + .hasFieldOrPropertyWithValue("excludeFinalOutputArtifact", true) + .extracting(AssemblyConfiguration::getLayers) + .asList() + .hasSize(5) + .contains( + Assembly.builder() + .id("jkube-includes") + .fileSet(AssemblyFileSet.builder() + .directory(new File("src/main/jkube-includes/bin")) + .outputDirectory(new File("bin")) + .fileMode("0755") + .build()) + .fileSet(AssemblyFileSet.builder() + .directory(new File("src/main/jkube-includes")) + .outputDirectory(new File(".")) + .fileMode("0644") + .build()) + .build(), + Assembly.builder() + .id("dependencies") + .fileSet(AssemblyFileSet.builder() + .outputDirectory(new File(".")) + .directory(new File("target/dependencies")) + .exclude("*") + .fileMode("0640") + .build()) + .build(), + Assembly.builder() + .id("spring-boot-loader") + .fileSet(AssemblyFileSet.builder() + .outputDirectory(new File(".")) + .directory(new File("target/spring-boot-loader")) + .exclude("*") + .fileMode("0640") + .build()) + .build(), + Assembly.builder() + .id("snapshot-dependencies") + .fileSet(AssemblyFileSet.builder() + .outputDirectory(new File(".")) + .directory(new File("target/snapshot-dependencies")) + .exclude("*") + .fileMode("0640") + .build()) + .build(), + Assembly.builder() + .id("application") + .fileSet(AssemblyFileSet.builder() + .outputDirectory(new File(".")) + .directory(new File("target/application")) + .exclude("*") + .fileMode("0640") + .build()) + .build() + ); + } + } + + private void createExtractedLayers(File targetDir) throws IOException { + File applicationLayer = new File(targetDir, "application"); + File dependencies = new File(targetDir, "dependencies"); + File snapshotDependencies = new File(targetDir, "snapshot-dependencies"); + File springBootLoader = new File(targetDir, "spring-boot-loader"); + Files.createDirectories(new File(applicationLayer, "BOOT-INF/classes").toPath()); + Files.createDirectory(applicationLayer.toPath().resolve("META-INF")); + Files.createFile(applicationLayer.toPath().resolve("BOOT-INF").resolve("classes").resolve("application.properties")); + Files.createDirectories(dependencies.toPath().resolve("BOOT-INF").resolve("lib")); + Files.createFile(dependencies.toPath().resolve("BOOT-INF").resolve("lib").resolve("spring-core.jar")); + Files.createDirectories(snapshotDependencies.toPath().resolve("BOOT-INF").resolve("lib")); + Files.createFile(snapshotDependencies.toPath().resolve("BOOT-INF").resolve("lib").resolve("test-SNAPSHOT.jar")); + Files.createDirectories(springBootLoader.toPath().resolve("org").resolve("springframework").resolve("boot").resolve("loader")); + Files.createFile(springBootLoader.toPath().resolve("org").resolve("springframework").resolve("boot").resolve("loader").resolve("Launcher.class")); + } + + private void createDummyLayeredJar(File layeredJar) throws IOException { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, "org.springframework.boot.loader.JarLauncher"); + try (JarOutputStream jarOutputStream = new JarOutputStream(Files.newOutputStream(layeredJar.toPath()), manifest)) { + jarOutputStream.putNextEntry(new JarEntry("META-INF/")); + jarOutputStream.putNextEntry(new JarEntry("org/")); + jarOutputStream.putNextEntry(new JarEntry("org/springframework/")); + jarOutputStream.putNextEntry(new JarEntry("org/springframework/boot/")); + jarOutputStream.putNextEntry(new JarEntry("org/springframework/boot/loader/")); + jarOutputStream.putNextEntry(new JarEntry("org/springframework/boot/loader/ClassPathIndexFile.class")); + jarOutputStream.putNextEntry(new JarEntry("org/springframework/boot/loader/JarLauncher.class")); + jarOutputStream.putNextEntry(new JarEntry("BOOT-INF/")); + jarOutputStream.putNextEntry(new JarEntry("BOOT-INF/classes/")); + jarOutputStream.putNextEntry(new JarEntry("BOOT-INF/layers.idx")); + } + } + + private void createDummyFatJar(File layeredJar) throws IOException { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, "org.springframework.boot.loader.JarLauncher"); + try (JarOutputStream jarOutputStream = new JarOutputStream(Files.newOutputStream(layeredJar.toPath()), manifest)) { + jarOutputStream.putNextEntry(new JarEntry("META-INF/")); + jarOutputStream.putNextEntry(new JarEntry("org/")); + jarOutputStream.putNextEntry(new JarEntry("org/springframework/")); + jarOutputStream.putNextEntry(new JarEntry("org/springframework/boot/")); + jarOutputStream.putNextEntry(new JarEntry("org/springframework/boot/loader/")); + jarOutputStream.putNextEntry(new JarEntry("org/springframework/boot/loader/ClassPathIndexFile.class")); + jarOutputStream.putNextEntry(new JarEntry("org/springframework/boot/loader/JarLauncher.class")); + jarOutputStream.putNextEntry(new JarEntry("BOOT-INF/")); + jarOutputStream.putNextEntry(new JarEntry("BOOT-INF/classes/")); + jarOutputStream.putNextEntry(new JarEntry("BOOT-INF/classpath.idx")); + } + } + @Test @DisplayName("customize, with standard packaging, has java environment variables") void customize_withStandardPackaging_thenImageHasJavaMainClassAndJavaAppDirEnvVars() {