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

✨ Camel Spring Boot on Tomcat App/Product #981

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import software.tnb.common.config.OpenshiftConfiguration;
import software.tnb.common.config.TestConfiguration;
import software.tnb.product.csb.TomcatCamelSpringBoot;
import software.tnb.product.csb.configuration.SpringBootConfiguration;

import java.util.Optional;
import java.util.ServiceLoader;
Expand All @@ -28,6 +30,7 @@ public static Product create() {
public static <P extends Product> P create(Class<P> clazz) {
final Optional<Product> product = StreamSupport.stream(ServiceLoader.load(Product.class).spliterator(), false)
.filter(p -> p.getClass().getSimpleName().toLowerCase().contains(TestConfiguration.product().getValue()))
.filter(p -> p instanceof TomcatCamelSpringBoot == SpringBootConfiguration.isTomcat())
.filter(p -> p instanceof OpenshiftProduct == OpenshiftConfiguration.isOpenshift())
.findFirst();
if (product.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import software.tnb.common.config.TestConfiguration;
import software.tnb.common.product.ProductType;
import software.tnb.product.cq.configuration.QuarkusConfiguration;
import software.tnb.product.csb.configuration.SpringBootConfiguration;

public enum ProductConfiguration {
ALL,
QUARKUS, QUARKUS_JVM, QUARKUS_JVM_LOCAL, QUARKUS_JVM_OPENSHIFT, QUARKUS_NATIVE, QUARKUS_NATIVE_LOCAL, QUARKUS_NATIVE_OPENSHIFT,
CAMEL_K,
SPRINGBOOT, SPRINGBOOT_JVM_LOCAL, SPRINGBOOT_JVM_OPENSHIFT;
SPRINGBOOT, SPRINGBOOT_JVM_LOCAL, SPRINGBOOT_JVM_OPENSHIFT, SPRINGBOOT_TOMCAT;

public boolean isCurrentEnv() {
return switch (this) {
Expand All @@ -33,6 +34,8 @@ public boolean isCurrentEnv() {
&& !OpenshiftConfiguration.isOpenshift();
case SPRINGBOOT_JVM_OPENSHIFT -> TestConfiguration.product() == ProductType.CAMEL_SPRINGBOOT
&& OpenshiftConfiguration.isOpenshift();
case SPRINGBOOT_TOMCAT -> TestConfiguration.product() == ProductType.CAMEL_SPRINGBOOT
&& SpringBootConfiguration.isTomcat();
default -> true;
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package software.tnb.product.csb;

import org.apache.maven.model.Dependency;

import software.tnb.product.LocalProduct;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck> reported by reviewdog 🐶
Wrong order for 'software.tnb.product.LocalProduct' import.

import software.tnb.product.Product;
import software.tnb.product.application.App;
import software.tnb.product.csb.application.TomcatSpringBootApp;
import software.tnb.product.customizer.Customizer;
import software.tnb.product.customizer.component.rest.RestCustomizer;
import software.tnb.product.integration.builder.AbstractIntegrationBuilder;

import com.google.auto.service.AutoService;

import software.tnb.product.util.maven.Maven;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck> reported by reviewdog 🐶
Wrong order for 'software.tnb.product.util.maven.Maven' import.


import java.util.List;
import java.util.stream.Collectors;

@AutoService(Product.class)
public class TomcatCamelSpringBoot extends LocalProduct {

private TomcatSpringBootApp app;

@Override
public App createIntegrationApp(AbstractIntegrationBuilder<?> integrationBuilder) {
// Let's remove restcustomizer, since it exclude tomcat
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say restcustomizer (and all customizers) should be changed to handle the tomcat as well, rather than changing it here

List<Customizer> customizers = integrationBuilder.getCustomizers()
.stream().filter(customizer -> customizer instanceof RestCustomizer)
.collect(Collectors.toList());
integrationBuilder.getCustomizers().remove(customizers);

// spring web is needed and tomcat is provided
Dependency providedTomcatDependency = new Dependency();
providedTomcatDependency.setArtifactId("spring-boot-starter-tomcat");
providedTomcatDependency.setGroupId("org.springframework.boot");
providedTomcatDependency.setScope("provided");

integrationBuilder.dependencies(
Maven.createDependency("org.springframework.boot:spring-boot-starter-web"),
providedTomcatDependency);

app = new TomcatSpringBootApp(integrationBuilder);
return app;
}

@Override
public void setupProduct() {
super.setupProduct();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessary here since it only calls super


@Override
public void teardownProduct() {
app.stop();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apps are stopped via Product#removeIntegrations

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package software.tnb.product.csb.application;

import software.tnb.common.config.TestConfiguration;
import software.tnb.product.application.App;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck> reported by reviewdog 🐶
Unused import - software.tnb.product.application.App.

import software.tnb.product.application.Phase;
import software.tnb.product.csb.configuration.SpringBootConfiguration;
import software.tnb.product.customizer.Customizer;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck> reported by reviewdog 🐶
Unused import - software.tnb.product.customizer.Customizer.

import software.tnb.product.customizer.component.rest.RestCustomizer;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck> reported by reviewdog 🐶
Unused import - software.tnb.product.customizer.component.rest.RestCustomizer.

import software.tnb.product.integration.builder.AbstractIntegrationBuilder;
import software.tnb.product.log.FileLog;
import software.tnb.product.log.Log;
import software.tnb.product.log.stream.LogStream;
import software.tnb.product.util.ZipUtils;
import software.tnb.product.util.maven.BuildRequest;
import software.tnb.product.util.maven.Maven;

import org.apache.commons.io.FileUtils;
import org.apache.maven.model.Dependency;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck> reported by reviewdog 🐶
Unused import - org.apache.maven.model.Dependency.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck> reported by reviewdog 🐶
Unused import - java.util.List.

import java.util.Map;
import java.util.stream.Collectors;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck> reported by reviewdog 🐶
Unused import - java.util.stream.Collectors.


public class TomcatSpringBootApp extends SpringBootApp {
private static final Logger LOG = LoggerFactory.getLogger(TomcatSpringBootApp.class);

private AbstractIntegrationBuilder<?> integrationBuilder;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used for anything

private Path tomcatTmpDirectory;
private Path tomcatHome = null;
private Process tomcatProcess = null;
private static final String TOMCAT_PARENT_DIRECTORY = "tomcat";
private static final String TOMCAT_ARCHIVE_NAME = "tomcat-archive.zip";

@Override
public Log getLog() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set the log variable after the app is started, instead of overriding this method

return new FileLog(getLogPath());
}

public TomcatSpringBootApp(AbstractIntegrationBuilder<?> integrationBuilder) {
super(integrationBuilder);

downloadTomcat();

this.integrationBuilder = integrationBuilder;
}

private void startTomcat() {
File[] file = tomcatTmpDirectory.resolve(TOMCAT_PARENT_DIRECTORY).toFile().listFiles();
for (File f : file) {
if (f.isDirectory()) {
if (f.getName().contains("apache-tomcat")) {
tomcatHome = f.toPath();
} else if (f.getName().contains("jws")) {
tomcatHome = f.toPath().resolve("tomcat"); // Case JWS
}
}
}

if (tomcatHome == null) {
throw new RuntimeException("Could not find Tomcat home in " + tomcatTmpDirectory.resolve(TOMCAT_PARENT_DIRECTORY));
}

Path logFile = getLogPath();
// startup.sh starts on another process, let's use catalina run so that we can control the lifecyle
ProcessBuilder processBuilder = new ProcessBuilder(tomcatHome + File.separator + "bin" + File.separator + "catalina.sh", "run")
.redirectError(logFile.toFile())
.redirectOutput(logFile.toFile());

try {
tomcatProcess = processBuilder.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (tomcatProcess != null) {
tomcatProcess.destroyForcibly();
}
}));
} catch (IOException e) {
throw new RuntimeException(e);
}

LOG.info("Starting tomcat in {}", tomcatHome);
}

private void downloadTomcat() {
try {
tomcatTmpDirectory = Files.createTempDirectory("tnb-tomcat");
LOG.info("Downloading tomcat in {}", tomcatTmpDirectory);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info -> debug

FileUtils.copyURLToFile(new URL(SpringBootConfiguration.tomcatZipUrl()), tomcatTmpDirectory.resolve(TOMCAT_ARCHIVE_NAME).toFile());

ZipUtils.unzip(tomcatTmpDirectory.resolve(TOMCAT_ARCHIVE_NAME), tomcatTmpDirectory.resolve(TOMCAT_PARENT_DIRECTORY));
} catch (IOException e) {
throw new RuntimeException(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw new RuntimeException("Unable to download tomcat", e);

}
}

@Override
public void start() {
startTomcat();

// replace packaging and extend SpringBootServletInitializer
try {
Path pomPath = TestConfiguration.appLocation().resolve(name).resolve("pom.xml");
String pom = Files.readString(pomPath);
pom = pom.replace("<artifactId>" + name + "</artifactId>",
"<artifactId>" + name + "</artifactId><packaging>war</packaging>");
Files.write(pomPath, pom.getBytes(StandardCharsets.UTF_8));

Path mainPath = TestConfiguration.appLocation().resolve(name)
.resolve("src")
.resolve("main")
.resolve("java")
.resolve("com")
.resolve("test")
.resolve("MySpringBootApplication.java");
String main = Files.readString(mainPath);

main = main.replace("class MySpringBootApplication {",
"""
class MySpringBootApplication extends org.springframework.boot.web.servlet.support.SpringBootServletInitializer {

@Override
protected org.springframework.boot.builder.SpringApplicationBuilder
configure(org.springframework.boot.builder.SpringApplicationBuilder application) {
return application.sources(MySpringBootApplication.class);
}
""");
Comment on lines +109 to +133
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is something that should be done for all tomcat apps, it should be done by a TomcatCustomizer

Files.write(mainPath, main.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new RuntimeException(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw new RuntimeException("Unable to start application", e);

}

// package application
BuildRequest.Builder requestBuilder = new BuildRequest.Builder()
.withBaseDirectory(TestConfiguration.appLocation().resolve(name))
.withGoals("clean", "package")
.withProperties(Map.of(
"skipTests", "true"
))
.withLogFile(getLogPath(Phase.BUILD))
.withLogMarker(LogStream.marker(name, Phase.BUILD));

LOG.info("Building {} application project for tomcat", name);
Maven.invoke(requestBuilder.build());

// copy generated WAR in tomcat
String warName = name + "-1.0.0-SNAPSHOT.war";
try {
Files.copy(TestConfiguration.appLocation().resolve(name).resolve("target").resolve(warName),
tomcatHome.resolve("webapps").resolve(warName));
} catch (IOException e) {
throw new RuntimeException(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw new RuntimeException("Unable to copy file", e);

}
}

@Override
public void stop() {
if (logStream != null) {
logStream.stop();
}

if (log != null) {
log.save();
}

if (tomcatProcess != null) {
try {
Process shutdown = new ProcessBuilder(tomcatHome + File.separator + "bin" + File.separator + "shutdown.sh")
.start();
shutdown.waitFor();

tomcatProcess.destroyForcibly();
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}

@Override
public boolean isReady() {
return tomcatProcess != null && tomcatProcess.isAlive();
}

@Override
public boolean isFailed() {
return tomcatProcess != null && !tomcatProcess.isAlive();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public class SpringBootConfiguration extends CamelConfiguration {

public static final String OPENSHIFT_SB_RESULT_IMAGE_REPOSITORY = "openshift-sb.result-image.repo";

public static final String TOMCAT_ZIP_DOWNLOAD_URL = "tomcat.zip.download.url";
public static final String USE_TOMCAT = "test.use.tomcat";

public static String springBootVersion() {
return getProperty(SPRINGBOOT_VERSION, "2.6.1");
}
Expand Down Expand Up @@ -104,4 +107,12 @@ public static String openshiftBaseImage() {
public static String openshiftResultImageRepository() {
return getProperty(OPENSHIFT_SB_RESULT_IMAGE_REPOSITORY);
}

public static String tomcatZipUrl() {
return getProperty(TOMCAT_ZIP_DOWNLOAD_URL, "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.23/bin/apache-tomcat-10.1.23.zip");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the version be configurable?

}

public static boolean isTomcat() {
return getBoolean(USE_TOMCAT, false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package software.tnb.product.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class ZipUtils {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.design.HideUtilityClassConstructorCheck> reported by reviewdog 🐶
Utility classes should not have a public or default constructor.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.design.FinalClassCheck> reported by reviewdog 🐶
Class ZipUtils should be declared as final.


private ZipUtils() {
}

public static void unzip(Path source, Path target) throws IOException {
Set<PosixFilePermission> executePermissions = new HashSet<>();
executePermissions.add(PosixFilePermission.OTHERS_EXECUTE);
executePermissions.add(PosixFilePermission.OTHERS_WRITE);
executePermissions.add(PosixFilePermission.OTHERS_READ);
executePermissions.add(PosixFilePermission.GROUP_EXECUTE);
executePermissions.add(PosixFilePermission.GROUP_WRITE);
executePermissions.add(PosixFilePermission.GROUP_READ);
executePermissions.add(PosixFilePermission.OWNER_EXECUTE);
executePermissions.add(PosixFilePermission.OWNER_WRITE);
executePermissions.add(PosixFilePermission.OWNER_READ);

try (ZipInputStream zis = new ZipInputStream(new FileInputStream(source.toFile()))) {

// list files in zip
ZipEntry zipEntry = zis.getNextEntry();

while (zipEntry != null) {

boolean isDirectory = zipEntry.getName().endsWith(File.separator);

Path newPath = zipSlipProtect(zipEntry, target);

if (isDirectory) {
Files.createDirectories(newPath);
} else {
if (newPath.getParent() != null) {
if (Files.notExists(newPath.getParent())) {
Files.createDirectories(newPath.getParent());
}
}

Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING);
if (newPath.getFileName().toString().endsWith(".sh")) {
Files.setPosixFilePermissions(newPath, executePermissions);
}
}

zipEntry = zis.getNextEntry();
}
zis.closeEntry();

}

}

private static Path zipSlipProtect(ZipEntry zipEntry, Path targetDir)
throws IOException {
Path targetDirResolved = targetDir.resolve(zipEntry.getName());

Path normalizePath = targetDirResolved.normalize();
if (!normalizePath.startsWith(targetDir)) {
throw new IOException("Bad zip entry: " + zipEntry.getName());
}

return normalizePath;
}
}
Loading
Loading