Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat (jkube-kit/spring-boot) : SpringBootGenerator utilizes layered jar if present and use it as Docker layers (#1674) #2309

Merged
merged 2 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Usage:
./scripts/extract-changelog-for-version.sh 1.3.37 5
```
### 1.14-SNAPSHOT
* Fix #2353: Add condition and alias to HelmDependency model
* 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 All @@ -39,6 +39,7 @@ Usage:
* Fix #2302 Bump Kubernetes Client version to 6.8.0
* Fix #2324: Update SpringBootConfigurationHelper for Spring Boot 3.x
* Fix #2350: Enrichers with NAME configuration override fragments with default names
* Fix #2353: Add condition and alias to HelmDependency model

### 1.13.1 (2023-06-16)
* Fix #2212: Bump Kubernetes Client version to 6.7.2 (fixes issues when trace-logging OpenShift builds -regression in 6.7.1-)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
*/
package org.eclipse.jkube.gradle.plugin.tests;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

import org.eclipse.jkube.kit.common.ResourceVerify;

Expand All @@ -26,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 {
Expand Down Expand Up @@ -66,4 +91,5 @@ 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");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@

/**
* @author roland
* @since 14/09/16
*/
public abstract class ExternalCommand {
protected final KitLogger log;
Expand Down
Original file line number Diff line number Diff line change
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);
}
}
}

Original file line number Diff line number Diff line change
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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 lombok.Getter;
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 SpringBootLayeredJar {

private final File layeredJar;
private final KitLogger kitLogger;

public SpringBootLayeredJar(File layeredJar, KitLogger kitLogger) {
this.layeredJar = layeredJar;
this.kitLogger = kitLogger;
}

public List<String> listLayers() {
final 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 void extractLayers(File extractionDir) {
try {
new LayerExtractorCommand(kitLogger, extractionDir, layeredJar).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 File layeredJar;
@Getter
private final List<String> layers;

protected LayerListCommand(KitLogger log, File layeredJar) {
super(log);
this.layeredJar = layeredJar;
layers = new ArrayList<>();
}

@Override
protected String[] getArgs() {
return new String[] { "java", "-Djarmode=layertools", "-jar", layeredJar.getAbsolutePath(), "list"};
}

@Override
protected void processLine(String line) {
layers.add(line);
}

}
}
Original file line number Diff line number Diff line change
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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.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;
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;

public class LayeredJarGenerator extends AbstractSpringBootNestedGenerator {

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);
springBootLayeredJar = new SpringBootLayeredJar(layeredJar, getLogger());
}

@Override
public Arguments getBuildEntryPoint() {
return Arguments.builder()
.exec(Arrays.asList("java", MAIN_CLASS))
.build();
}

@Override
public Map<String, String> getEnv() {
return Collections.singletonMap("JAVA_MAIN_CLASS", MAIN_CLASS);
}

@Override
public AssemblyConfiguration createAssemblyConfiguration(List<AssemblyFileSet> defaultFileSets) {
getLogger().info("Spring Boot layered jar detected");
final List<Assembly> layerAssemblies = new ArrayList<>();
layerAssemblies.add(Assembly.builder().id("jkube-includes").fileSets(defaultFileSets).build());
springBootLayeredJar.extractLayers(getProject().getBuildPackageDirectory());

for (String springBootLayer : springBootLayeredJar.listLayers()) {
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();
}
}
Original file line number Diff line number Diff line change
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