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 fd395ec1be..d4916c8b7d 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,10 +86,6 @@ public File resolveDefaultOpenShiftResourceDir() { return resolveFile("build", "classes", "java", "main", "META-INF", "jkube", "openshift"); } - public File resolveDefaultDockerfile(String imageNamespace, String imageName, String imageTag) { - return resolveFile("build", "docker", 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 1b96f0c4f6..833070790f 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 @@ -13,6 +13,7 @@ */ package org.eclipse.jkube.gradle.plugin.tests; +import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -27,7 +28,30 @@ class SpringBootIT { @RegisterExtension - private final ITGradleRunnerExtension gradleRunner = new ITGradleRunnerExtension(); + protected final ITGradleRunnerExtension gradleRunner = new ITGradleRunnerExtension(); + + @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 + final File dockerFile = gradleRunner.resolveFile("build", "docker", "gradle", "spring-boot", "latest", "build", "Dockerfile"); + assertThat(new String(Files.readAllBytes(dockerFile.toPath()))) + .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") + .contains("JIB image build started"); + } @Test void k8sResource_whenRun_generatesK8sManifests() throws IOException, ParseException { @@ -68,25 +92,4 @@ void ocResource_whenRun_generatesOpenShiftManifests() throws IOException, ParseE .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( "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/ExternalCommand.java b/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/ExternalCommand.java index 8bb2033c09..74fafbf077 100644 --- a/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/ExternalCommand.java +++ b/jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/ExternalCommand.java @@ -32,7 +32,6 @@ /** * @author roland - * @since 14/09/16 */ public abstract class ExternalCommand { protected final KitLogger log; 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/SpringBootLayeredJar.java similarity index 77% rename from jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/SpringBootLayeredJarExecUtils.java rename to jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/SpringBootLayeredJar.java index 4bf89e8bbe..40025985cb 100644 --- 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/SpringBootLayeredJar.java @@ -13,6 +13,7 @@ */ package org.eclipse.jkube.springboot; +import lombok.Getter; import org.eclipse.jkube.kit.common.ExternalCommand; import org.eclipse.jkube.kit.common.KitLogger; @@ -21,11 +22,18 @@ import java.util.ArrayList; import java.util.List; -public class SpringBootLayeredJarExecUtils { - private SpringBootLayeredJarExecUtils() { } +public class SpringBootLayeredJar { - public static List listLayers(KitLogger kitLogger, File layeredJar) { - LayerListCommand layerListCommand = new LayerListCommand(kitLogger, layeredJar); + private final File layeredJar; + private final KitLogger kitLogger; + + public SpringBootLayeredJar(File layeredJar, KitLogger kitLogger) { + this.layeredJar = layeredJar; + this.kitLogger = kitLogger; + } + + public List listLayers() { + final LayerListCommand layerListCommand = new LayerListCommand(kitLogger, layeredJar); try { layerListCommand.execute(); return layerListCommand.getLayers(); @@ -34,10 +42,9 @@ public static List listLayers(KitLogger kitLogger, File layeredJar) { } } - public static void extractLayers(KitLogger kitLogger, File extractionDir, File layeredJar) { - LayerExtractorCommand layerExtractorCommand = new LayerExtractorCommand(kitLogger, extractionDir, layeredJar); + public void extractLayers(File extractionDir) { try { - layerExtractorCommand.execute(); + new LayerExtractorCommand(kitLogger, extractionDir, layeredJar).execute(); } catch (IOException ioException) { throw new IllegalStateException("Failure in extracting spring boot jar layers", ioException); } @@ -57,12 +64,14 @@ protected String[] getArgs() { } private static class LayerListCommand extends ExternalCommand { - private final List layers; private final File layeredJar; + @Getter + private final List layers; + protected LayerListCommand(KitLogger log, File layeredJar) { super(log); - layers = new ArrayList<>(); this.layeredJar = layeredJar; + layers = new ArrayList<>(); } @Override @@ -75,8 +84,5 @@ 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/LayeredJarGenerator.java b/jkube-kit/jkube-kit-spring-boot/src/main/java/org/eclipse/jkube/springboot/generator/LayeredJarGenerator.java index 23ac72bcb0..6015deacdd 100644 --- 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 @@ -15,11 +15,11 @@ 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 org.eclipse.jkube.springboot.SpringBootLayeredJar; import java.io.File; import java.util.ArrayList; @@ -29,38 +29,37 @@ 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) { + + private static final String MAIN_CLASS = "org.springframework.boot.loader.JarLauncher"; + private final SpringBootLayeredJar springBootLayeredJar; + + public LayeredJarGenerator(GeneratorContext generatorContext, GeneratorConfig generatorConfig, File layeredJar) { super(generatorContext, generatorConfig); - fatJarDetectorResult = result; + springBootLayeredJar = new SpringBootLayeredJar(layeredJar, getLogger()); } @Override public Arguments getBuildEntryPoint() { return Arguments.builder() - .exec(Arrays.asList("java", "org.springframework.boot.loader.JarLauncher")) + .exec(Arrays.asList("java", MAIN_CLASS)) .build(); } @Override public Map getEnv() { - return Collections.singletonMap("JAVA_MAIN_CLASS", "org.springframework.boot.loader.JarLauncher"); + return Collections.singletonMap("JAVA_MAIN_CLASS", MAIN_CLASS); } @Override public AssemblyConfiguration createAssemblyConfiguration(List defaultFileSets) { getLogger().info("Spring Boot layered jar detected"); - - List layerNames = listLayers(getLogger(), fatJarDetectorResult.getArchiveFile()); - List layerAssemblies = new ArrayList<>(); + final List layerAssemblies = new ArrayList<>(); layerAssemblies.add(Assembly.builder().id("jkube-includes").fileSets(defaultFileSets).build()); - extractLayers(getLogger(), getProject().getBuildPackageDirectory(), fatJarDetectorResult.getArchiveFile()); + springBootLayeredJar.extractLayers(getProject().getBuildPackageDirectory()); - for (String springBootLayer : layerNames) { + for (String springBootLayer : springBootLayeredJar.listLayers()) { File layerDir = new File(getProject().getBuildPackageDirectory(), springBootLayer); layerAssemblies.add(Assembly.builder() .id(springBootLayer) 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 68af5316fe..a9c31231bf 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 @@ -63,7 +63,7 @@ default Map getEnv() { 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 LayeredJarGenerator(generatorContext, generatorConfig, fatJarDetectorResult.getArchiveFile()); } 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 deleted file mode 100644 index be5ce75744..0000000000 --- a/jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/SpringBootLayeredJarExecUtilsTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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/SpringBootLayeredJarTest.java b/jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/SpringBootLayeredJarTest.java new file mode 100644 index 0000000000..42df0da0d7 --- /dev/null +++ b/jkube-kit/jkube-kit-spring-boot/src/test/java/org/eclipse/jkube/springboot/SpringBootLayeredJarTest.java @@ -0,0 +1,96 @@ +/* + * 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.eclipse.jkube.kit.common.assertj.FileAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +class SpringBootLayeredJarTest { + + @TempDir + private File projectDir; + + private SpringBootLayeredJar springBootLayeredJar; + + @Nested + @DisplayName("with invalid jar") + class InvalidJar { + @BeforeEach + void setup() { + springBootLayeredJar = new SpringBootLayeredJar(new File(projectDir, "invalid.jar"), new KitLogger.SilentLogger()); + } + + @Test + void listLayers_whenJarInvalid_thenThrowException() { + assertThatIllegalStateException() + .isThrownBy(() -> springBootLayeredJar.listLayers()) + .withMessage("Failure in getting spring boot jar layers information"); + } + + @Test + void extractLayers_whenJarInvalid_thenThrowException() { + assertThatIllegalStateException() + .isThrownBy(() -> springBootLayeredJar.extractLayers(projectDir)) + .withMessage("Failure in extracting spring boot jar layers"); + } + } + @Nested + @DisplayName("with valid jar") + class ValidJar { + @BeforeEach + void setup() throws IOException { + final File layeredJar = new File(projectDir, "layered.jar"); + Files.copy( + Objects.requireNonNull(SpringBootLayeredJarTest.class.getResourceAsStream("/generator-integration-test/layered-jar.jar")), + new File(projectDir, "layered.jar").toPath() + ); + springBootLayeredJar = new SpringBootLayeredJar(layeredJar, new KitLogger.SilentLogger()); + } + + @Test + void listLayers_whenJarInvalid_thenThrowException() { + // When + final List result = springBootLayeredJar.listLayers(); + // Then + assertThat(result) + .containsExactly("dependencies", "spring-boot-loader", "snapshot-dependencies", "application"); + } + + @Test + void extractLayers_whenJarInvalid_thenThrowException() throws IOException { + // Given + final File extractionDir = Files.createDirectory(new File(projectDir, "extracted").toPath()).toFile(); + // When + springBootLayeredJar.extractLayers(extractionDir); + // Then + FileAssertions.assertThat(extractionDir) + .fileTree() + .contains("dependencies", "spring-boot-loader", "snapshot-dependencies", "application"); + } + } +} 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 3a2f297412..9419ba76a9 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 @@ -16,7 +16,6 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.eclipse.jkube.generator.api.GeneratorContext; import org.eclipse.jkube.generator.api.GeneratorMode; -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; @@ -26,20 +25,17 @@ 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.Nested; 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; @@ -49,25 +45,19 @@ 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 { + + private static final String SPRING_VERSION = "2.7.2"; private File targetDir; private Properties properties; - @TempDir - Path temporaryFolder; - private GeneratorContext context; @BeforeEach - void setUp() throws IOException { + void setUp(@TempDir Path temporaryFolder) throws IOException { properties = new Properties(); targetDir = Files.createDirectory(temporaryFolder.resolve("target")).toFile(); - JavaProject javaProject = JavaProject.builder() + final JavaProject javaProject = JavaProject.builder() .baseDirectory(temporaryFolder.toFile()) .buildDirectory(targetDir.getAbsoluteFile()) .buildPackageDirectory(targetDir.getAbsoluteFile()) @@ -77,12 +67,12 @@ void setUp() throws IOException { .dependency(Dependency.builder() .groupId("org.springframework.boot") .artifactId("spring-boot-web") - .version("2.7.2") + .version(SPRING_VERSION) .build()) .plugin(Plugin.builder() .groupId("org.springframework.boot") .artifactId("spring-boot-maven-plugin") - .version("2.7.2") + .version(SPRING_VERSION) .build()) .buildFinalName("sample") .build(); @@ -92,363 +82,312 @@ void setUp() throws IOException { .build(); } - @Test - @DisplayName("customize, with standard packaging, has standard image name") - void customize_withStandardPackaging_thenImageNameContainsGroupArtifactAndLatestTag() { - // Given - withCustomMainClass(); - SpringBootGenerator springBootGenerator = new SpringBootGenerator(context); - List images = new ArrayList<>(); - - // When - images = springBootGenerator.customize(images, false); + @Nested + @DisplayName("With fat Jar packaging") + class StandardPackaging { + + @BeforeEach + void standardFatJar() { + 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( + targetDir.toPath().resolve("fat.jar")), 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")); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } - // Then - assertThat(images) + @Test + @DisplayName("has image name (group/artifact:latest)") + void imageNameContainsGroupArtifactAndLatestTag() { + // When + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); + // Then + assertThat(images) .singleElement(InstanceOfAssertFactories.type(ImageConfiguration.class)) .hasFieldOrPropertyWithValue("name", "%g/%a:%l"); - } - - @Test - @DisplayName("customize, with standard packaging, has 'spring-boot' image alias") - void customize_withStandardPackaging_thenImageAliasSpringBoot() { - // Given - withCustomMainClass(); - SpringBootGenerator springBootGenerator = new SpringBootGenerator(context); - List images = new ArrayList<>(); - - // When - images = springBootGenerator.customize(images, false); + } - // Then - assertThat(images) + @Test + @DisplayName("has 'spring-boot' image alias") + void imageAliasSpringBoot() { + // When + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); + // Then + assertThat(images) .singleElement(InstanceOfAssertFactories.type(ImageConfiguration.class)) .hasFieldOrPropertyWithValue("alias", "spring-boot"); - } - - @Test - @DisplayName("customize, with standard packaging, has image from based on standard Java Exec generator image") - void customize_withStandardPackaging_hasFrom() { - // Given - withCustomMainClass(); - SpringBootGenerator springBootGenerator = new SpringBootGenerator(context); - List images = new ArrayList<>(); - - // When - images = springBootGenerator.customize(images, false); + } - // Then - assertThat(images) + @Test + @DisplayName("has image from based on standard Java Exec generator image") + void hasFrom() { + // When + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); + // Then + assertThat(images) .singleElement(InstanceOfAssertFactories.type(ImageConfiguration.class)) .extracting(ImageConfiguration::getBuild) .extracting(BuildConfiguration::getFrom) .asString() .startsWith("quay.io/jkube/jkube-java"); - } - - @Test - @DisplayName("customize, with standard packaging, has '8080' web port") - void customize_withStandardPackaging_thenImageHasDefaultWebPort() { - // Given - withCustomMainClass(); - SpringBootGenerator springBootGenerator = new SpringBootGenerator(context); - List images = new ArrayList<>(); - - // When - images = springBootGenerator.customize(images, false); + } - // Then - assertThat(images) + @Test + @DisplayName("has '8080' web port") + void imageHasDefaultWebPort() { + // When + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); + // Then + assertThat(images) .singleElement() .extracting("buildConfiguration.ports").asList() .contains("8080"); - } - - @Test - @DisplayName("customize, with standard packaging, has Jolokia port") - void customize_withStandardPackaging_hasJolokiaPort() { - // When - final List result = new SpringBootGenerator(context).customize(new ArrayList<>(), true); - // Then - assertThat(result).singleElement() + } + @Test + @DisplayName("has Jolokia port") + void hasJolokiaPort() { + // When + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); + // Then + assertThat(images).singleElement() .extracting("buildConfiguration.ports").asList() .contains("8778"); - } + } - @Test - @DisplayName("customize, with standard packaging, has Prometheus port") - void customize_withStandardPackaging_hasPrometheusPort() { - // When - final List result = new SpringBootGenerator(context).customize(new ArrayList<>(), true); - // Then - assertThat(result).singleElement() + @Test + @DisplayName("has Prometheus port") + void hasPrometheusPort() { + // When + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); + // Then + assertThat(images).singleElement() .extracting("buildConfiguration.ports").asList() .contains("9779"); - } - - @Test - @DisplayName("customize, in Kubernetes and jar artifact, should create assembly") - void customize_inKubernetesAndJarArtifact_shouldCreateAssembly() throws IOException { - try (MockedConstruction ignore = mockConstruction(FatJarDetector.class, (mock, ctx) -> { - FatJarDetector.Result fatJarDetectorResult = mock(FatJarDetector.Result.class); - when(mock.scan()).thenReturn(fatJarDetectorResult); - when(fatJarDetectorResult.getArchiveFile()).thenReturn(targetDir.toPath().resolve("sample.jar").toFile()); - })) { - // Given - createDummyFatJar(targetDir.toPath().resolve("sample.jar").toFile()); + } + @Test + @DisplayName("has java environment variable for app dir") + void hasJavaJavaAppDirEnvVar() { // When - final List resultImages = new SpringBootGenerator(context).customize(new ArrayList<>(), false); - + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); // Then - assertThat(resultImages) - .isNotNull() - .singleElement() - .extracting(ImageConfiguration::getBuild) - .extracting(BuildConfiguration::getAssembly) - .hasFieldOrPropertyWithValue("targetDir", "/deployments") - .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(".")) - .extracting("includes").asList() - .containsExactly("sample.jar")); + assertThat(images) + .singleElement(InstanceOfAssertFactories.type(ImageConfiguration.class)) + .extracting(ImageConfiguration::getBuild) + .extracting(BuildConfiguration::getEnv) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .containsEntry("JAVA_APP_DIR", "/deployments"); } - } - - @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); + @Test + @DisplayName("creates assembly with Fat Jar") + void createAssembly() { // When - final List resultImages = new SpringBootGenerator(context).customize(new ArrayList<>(), false); + final List images = 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")); + assertThat(images) + .isNotNull() + .singleElement() + .extracting(ImageConfiguration::getBuild) + .extracting(BuildConfiguration::getAssembly) + .hasFieldOrPropertyWithValue("targetDir", "/deployments") + .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(".")) + .extracting("includes").asList() + .containsExactly("fat.jar")); } - } - @Test - @DisplayName("customize, with standard packaging, has java environment variables") - void customize_withStandardPackaging_thenImageHasJavaMainClassAndJavaAppDirEnvVars() { - // Given - withCustomMainClass(); - SpringBootGenerator springBootGenerator = new SpringBootGenerator(context); - List images = new ArrayList<>(); - - // When - images = springBootGenerator.customize(images, false); - - // Then - assertThat(images) + @Test + @DisplayName("with custom main class, has java environment for main class") + void withCustomMainClass_hasJavaMainClassEnvVar() { + // Given + properties.put("jkube.generator.spring-boot.mainClass", "org.example.Foo"); + // When + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); + // Then + assertThat(images) .singleElement(InstanceOfAssertFactories.type(ImageConfiguration.class)) .extracting(ImageConfiguration::getBuild) .extracting(BuildConfiguration::getEnv) .asInstanceOf(InstanceOfAssertFactories.MAP) .containsEntry("JAVA_MAIN_CLASS", "org.example.Foo") .containsEntry("JAVA_APP_DIR", "/deployments"); - } + } - @Test - @DisplayName("customize, with custom port in application.properties, has overridden web port in image") - void customize_whenApplicationPortOverridden_shouldUseOverriddenWebPort() { - // Given - withCustomMainClass(); - context = context.toBuilder() + @Test + @DisplayName("with custom port in application.properties, has overridden web port in image") + void withApplicationPortOverridden_shouldUseOverriddenWebPort() { + // Given + context = context.toBuilder() .project(context.getProject().toBuilder() - .compileClassPathElement(Objects.requireNonNull(getClass().getResource("/port-override-application-properties")).getPath()) - .build()) + .compileClassPathElement(Objects.requireNonNull(getClass().getResource("/port-override-application-properties")).getPath()) + .build()) .build(); - SpringBootGenerator springBootGenerator = new SpringBootGenerator(context); - List images = new ArrayList<>(); - - // When - images = springBootGenerator.customize(images, false); - - // Then - assertThat(images) + // When + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); + // Then + assertThat(images) .singleElement() .extracting("buildConfiguration.ports").asList() .contains("8081"); - } + } + + @Test + @DisplayName("with color configuration provided, enables ANSI color output") + void withColorConfiguration_shouldAddAnsiEnabledPropertyToJavaOptions() { + // Given + properties.put("jkube.generator.spring-boot.color", "always"); + // When + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); + // Then + assertThat(images) + .singleElement(InstanceOfAssertFactories.type(ImageConfiguration.class)) + .extracting(ImageConfiguration::getBuild) + .extracting(BuildConfiguration::getEnv) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .containsEntry("JAVA_OPTIONS", "-Dspring.output.ansi.enabled=always"); + } - @Test - @DisplayName("customize, when generator mode WATCH, then add Spring Boot Devtools environment variable to image") - void customize_whenGeneratorModeWatch_shouldAddSpringBootDevtoolsSecretEnvVar() { - // Given - withCustomMainClass(); - context = context.toBuilder() + @Test + @DisplayName("with generator mode WATCH, then add Spring Boot Devtools environment variable to image") + void withGeneratorModeWatch_shouldAddSpringBootDevtoolsSecretEnvVar() throws IOException { + // Given + final Path applicationProperties = Files.createFile( + Files.createDirectory(targetDir.toPath().resolve("classes")) + .resolve("application.properties")); + context = context.toBuilder() .generatorMode(GeneratorMode.WATCH) .project(context.getProject().toBuilder() - .compileClassPathElement(Objects.requireNonNull(getClass().getResource("/devtools-application-properties")).getPath()) + .compileClassPathElement(Objects.requireNonNull(getClass().getResource("/devtools-application-properties")).getPath()) + .dependency(Dependency.builder() + .groupId("org.springframework.boot") + .artifactId("spring-boot-devtools") + .version(SPRING_VERSION) + .type("jar") + .file(Files.createFile(targetDir.toPath().resolve("spring-boot-devtools.zip")).toFile()) .build()) + .build()) .build(); - SpringBootGenerator springBootGenerator = new SpringBootGenerator(context); - List images = new ArrayList<>(); - - // When - images = springBootGenerator.customize(images, false); - - // Then - assertThat(images) + // When + final List images = new SpringBootGenerator(context) + .customize(new ArrayList<>(), false); + // Then + assertThat(images) .singleElement(InstanceOfAssertFactories.type(ImageConfiguration.class)) .extracting(ImageConfiguration::getBuild) .extracting(BuildConfiguration::getEnv) .asInstanceOf(InstanceOfAssertFactories.MAP) .containsEntry("SPRING_DEVTOOLS_REMOTE_SECRET", "some-secret"); + } } - @Test - @DisplayName("customize, when color configuration provided, enables ANSI color output") - void customize_withColorConfiguration_shouldAddAnsiEnabledPropertyToJavaOptions() { - // Given - properties.put("jkube.generator.spring-boot.color", "always"); - withCustomMainClass(); - List images = new ArrayList<>(); - SpringBootGenerator springBootGenerator = new SpringBootGenerator(context); - - // When - images = springBootGenerator.customize(images, false); + @Nested + @DisplayName("With layered jar") + class LayeredJar { - // Then - assertThat(images) - .singleElement(InstanceOfAssertFactories.type(ImageConfiguration.class)) + @Test + @DisplayName("should create assembly layers matching layered jar layers") + void shouldCreateAssemblyLayers() throws IOException { + // Given + Files.copy( + Objects.requireNonNull(SpringBootGeneratorIntegrationTest.class.getResourceAsStream("/generator-integration-test/layered-jar.jar")), + targetDir.toPath().resolve("layered.jar") + ); + // When + final List images = new SpringBootGenerator(context).customize(new ArrayList<>(), false); + // Then + assertThat(images) + .isNotNull() + .singleElement() .extracting(ImageConfiguration::getBuild) - .extracting(BuildConfiguration::getEnv) - .asInstanceOf(InstanceOfAssertFactories.MAP) - .containsEntry("JAVA_OPTIONS", "-Dspring.output.ansi.enabled=always"); + .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 withCustomMainClass() { - properties.put("jkube.generator.spring-boot.mainClass", "org.example.Foo"); - } } diff --git a/jkube-kit/jkube-kit-spring-boot/src/test/resources/generator-integration-test/layered-jar.jar b/jkube-kit/jkube-kit-spring-boot/src/test/resources/generator-integration-test/layered-jar.jar new file mode 100644 index 0000000000..17025649b1 Binary files /dev/null and b/jkube-kit/jkube-kit-spring-boot/src/test/resources/generator-integration-test/layered-jar.jar differ