diff --git a/fuse-products/src/main/java/software/tnb/product/ProductFactory.java b/fuse-products/src/main/java/software/tnb/product/ProductFactory.java index 533d48e16..72f724195 100644 --- a/fuse-products/src/main/java/software/tnb/product/ProductFactory.java +++ b/fuse-products/src/main/java/software/tnb/product/ProductFactory.java @@ -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; @@ -28,6 +30,7 @@ public static Product create() { public static

P create(Class

clazz) { final Optional 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()) { diff --git a/fuse-products/src/main/java/software/tnb/product/configuration/ProductConfiguration.java b/fuse-products/src/main/java/software/tnb/product/configuration/ProductConfiguration.java index 1adc770e4..dc2d6a730 100644 --- a/fuse-products/src/main/java/software/tnb/product/configuration/ProductConfiguration.java +++ b/fuse-products/src/main/java/software/tnb/product/configuration/ProductConfiguration.java @@ -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) { @@ -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; }; } diff --git a/fuse-products/src/main/java/software/tnb/product/csb/TomcatCamelSpringBoot.java b/fuse-products/src/main/java/software/tnb/product/csb/TomcatCamelSpringBoot.java new file mode 100644 index 000000000..d906463bc --- /dev/null +++ b/fuse-products/src/main/java/software/tnb/product/csb/TomcatCamelSpringBoot.java @@ -0,0 +1,31 @@ +package software.tnb.product.csb; + +import software.tnb.product.LocalProduct; +import software.tnb.product.Product; +import software.tnb.product.application.App; +import software.tnb.product.csb.application.TomcatSpringBootApp; +import software.tnb.product.integration.builder.AbstractIntegrationBuilder; + +import com.google.auto.service.AutoService; + +@AutoService(Product.class) +public class TomcatCamelSpringBoot extends LocalProduct { + + private TomcatSpringBootApp app; + + @Override + public App createIntegrationApp(AbstractIntegrationBuilder integrationBuilder) { + app = new TomcatSpringBootApp(integrationBuilder); + return app; + } + + @Override + public void setupProduct() { + super.setupProduct(); + } + + @Override + public void teardownProduct() { + app.stop(); + } +} diff --git a/fuse-products/src/main/java/software/tnb/product/csb/application/TomcatSpringBootApp.java b/fuse-products/src/main/java/software/tnb/product/csb/application/TomcatSpringBootApp.java new file mode 100644 index 000000000..5a666c0b7 --- /dev/null +++ b/fuse-products/src/main/java/software/tnb/product/csb/application/TomcatSpringBootApp.java @@ -0,0 +1,233 @@ +package software.tnb.product.csb.application; + +import software.tnb.common.config.TestConfiguration; +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; +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; +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; +import java.util.Map; +import java.util.stream.Collectors; + +public class TomcatSpringBootApp extends App { + private static final Logger LOG = LoggerFactory.getLogger(TomcatSpringBootApp.class); + + private AbstractIntegrationBuilder integrationBuilder; + 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() { + return new FileLog(getLogPath()); + } + + public TomcatSpringBootApp(AbstractIntegrationBuilder integrationBuilder) { + super(integrationBuilder.getIntegrationName()); + + 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); + 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); + } + } + + @Override + public void start() { + startTomcat(); + + // Let's remove restcustomizer, since it exclude tomcat + List 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); + + // Create the integration + new SpringBootApp(integrationBuilder) { + @Override + public void start() { + // We just need the application to be generated, not run + } + + @Override + public void stop() { + // do nothing + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public boolean isFailed() { + return false; + } + }.start(); + + // replace packaging and extend SpringBootServletInitializer + try { + Path pomPath = TestConfiguration.appLocation().resolve(name).resolve("pom.xml"); + String pom = Files.readString(pomPath); + pom = pom.replace("" + name + "", + "" + name + "war"); + 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); + } + """); + Files.write(mainPath, main.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(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); + } + } + + @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(); + } +} diff --git a/fuse-products/src/main/java/software/tnb/product/csb/configuration/SpringBootConfiguration.java b/fuse-products/src/main/java/software/tnb/product/csb/configuration/SpringBootConfiguration.java index 00f5b533a..621be0c6e 100644 --- a/fuse-products/src/main/java/software/tnb/product/csb/configuration/SpringBootConfiguration.java +++ b/fuse-products/src/main/java/software/tnb/product/csb/configuration/SpringBootConfiguration.java @@ -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"); } @@ -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"); + } + + public static boolean isTomcat() { + return getBoolean(USE_TOMCAT, false); + } } diff --git a/fuse-products/src/main/java/software/tnb/product/util/ZipUtils.java b/fuse-products/src/main/java/software/tnb/product/util/ZipUtils.java new file mode 100644 index 000000000..ebb57339f --- /dev/null +++ b/fuse-products/src/main/java/software/tnb/product/util/ZipUtils.java @@ -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 { + + private ZipUtils() { + } + + public static void unzip(Path source, Path target) throws IOException { + Set 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; + } +} diff --git a/fuse-products/src/test/java/software/tnb/product/TomcatSpringBootTest.java b/fuse-products/src/test/java/software/tnb/product/TomcatSpringBootTest.java new file mode 100644 index 000000000..ccc23fb92 --- /dev/null +++ b/fuse-products/src/test/java/software/tnb/product/TomcatSpringBootTest.java @@ -0,0 +1,53 @@ +package software.tnb.product; + +import software.tnb.common.utils.WaitUtils; +import software.tnb.product.application.App; +import software.tnb.product.csb.integration.builder.SpringBootIntegrationBuilder; +import software.tnb.product.parent.TestParent; +import software.tnb.util.maven.TestMaven; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +@Tag("integration") +public class TomcatSpringBootTest extends TestParent { + + @Test + public void test() throws Exception { + String productName = "camelspringboot"; + + System.setProperty("test.product", productName); + System.setProperty("test.use.tomcat", "true"); + TestMaven.setupDefaultMaven(); + Product product = ProductFactory.create(); + + String appName = "xml-timer-app-" + productName; + + App application = product.createIntegration(new SpringBootIntegrationBuilder(appName) + .fromSpringBootXmlCamelContext( + SpringBootXmlGeneratorTest.class.getPackageName().replace(".", File.separator) + File.separator + + "camel-context.xml") + .dependencies("cron", "log") + ); + + Path app = Paths.get("target", appName); + + Optional routeBuilderFile = Files.walk(app).filter(file -> "camel-context.xml".equals(file.getFileName().toString())) + .findAny(); + + Assertions.assertTrue(routeBuilderFile.isPresent(), "camel context xml not present"); + + WaitUtils.sleep(3000L); + + Assertions.assertTrue(application.getLog().contains("The message contains I was fired at")); + + application.stop(); + } +}