Skip to content

Commit

Permalink
feat (jkube-kit/spring-boot) : SpringBootGenerator utilizes layered j…
Browse files Browse the repository at this point in the history
…ar if present and use it as Docker layers (eclipse-jkube#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 <rohaan@redhat.com>
  • Loading branch information
rohanKanojia committed Aug 25, 2023
1 parent f7b25ad commit 26df5d2
Show file tree
Hide file tree
Showing 12 changed files with 466 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
Expand Up @@ -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();
}
Expand Down
Expand Up @@ -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;

Expand Down Expand Up @@ -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");
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -119,5 +122,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);
}
}
}

Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
@@ -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<String> 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<String> 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<String> getLayers() {
return layers;
}
}
}
Expand Up @@ -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 {

Expand All @@ -42,4 +43,8 @@ public String getBuildWorkdir() {
public String getTargetDir() {
return generatorConfig.get(JavaExecGenerator.Config.TARGET_DIR);
}

protected KitLogger getLogger() {
return generatorContext.getLogger();
}
}
@@ -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<String, String> getEnv() {
return Collections.singletonMap("JAVA_MAIN_CLASS", "org.springframework.boot.loader.JarLauncher");
}

@Override
public AssemblyConfiguration createAssemblyConfiguration(List<AssemblyFileSet> defaultFileSets) {
getLogger().info("Spring Boot layered jar detected");

List<String> layerNames = listLayers(getLogger(), fatJarDetectorResult.getArchiveFile());
List<Assembly> 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();
}
}
Expand Up @@ -57,7 +57,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
Expand Down Expand Up @@ -95,6 +95,7 @@ protected Map<String, String> getEnv(boolean prePackagePhase) {
res.put(SpringBootUtil.DEV_TOOLS_REMOTE_SECRET_ENV, secret);
}
}
res.putAll(nestedGenerator.getEnv());
return res;
}

Expand Down Expand Up @@ -124,7 +125,7 @@ protected String getDefaultWebPort() {

@Override
protected AssemblyConfiguration createAssembly() {
return Optional.ofNullable(nestedGenerator.createAssemblyConfiguration())
return Optional.ofNullable(nestedGenerator.createAssemblyConfiguration(addAdditionalFiles()))
.orElse(super.createAssembly());
}

Expand Down
Expand Up @@ -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<AssemblyFileSet> defaultFileSets) {
return null;
}

Expand All @@ -49,7 +56,15 @@ default Arguments getBuildEntryPoint() {

String getTargetDir();

static SpringBootNestedGenerator from(GeneratorContext generatorContext, GeneratorConfig generatorConfig) {
default Map<String, String> 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);
}
}

0 comments on commit 26df5d2

Please sign in to comment.