diff --git a/src/integration/tomcat_test.go b/src/integration/tomcat_test.go index e0a00e5ed..7598a578d 100644 --- a/src/integration/tomcat_test.go +++ b/src/integration/tomcat_test.go @@ -36,6 +36,41 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T, }) context("with a simple servlet app", func() { + it("successfully deploys and runs with Java 11 (Javax)", func() { + deployment, logs, err := platform.Deploy. + WithEnv(map[string]string{ + "BP_JAVA_VERSION": "11", + "JBP_CONFIG_TOMCAT": "{tomcat: { version: \"9.+\" }, access_logging_support: {access_logging: enabled}}", + }). + Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax")) + + Expect(err).NotTo(HaveOccurred(), logs.String) + + // Verify embedded Cloud Foundry-optimized Tomcat configuration was installed + Expect(logs.String()).To(ContainSubstring("Installing Cloud Foundry-optimized Tomcat configuration defaults")) + Expect(logs.String()).To(ContainSubstring("Dynamic port binding (${http.port} from $PORT)")) + Expect(logs.String()).To(ContainSubstring("HTTP/2 support enabled")) + Expect(logs.String()).To(ContainSubstring("RemoteIpValve for X-Forwarded-* headers")) + Expect(logs.String()).To(ContainSubstring("CloudFoundryAccessLoggingValve with vcap_request_id")) + Expect(logs.String()).To(ContainSubstring("Stdout logging via CloudFoundryConsoleHandler")) + + Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK"))) + + // Verify runtime logs contain CloudFoundry-specific Tomcat features + // Use Eventually to wait for logs to be flushed, as they may not appear immediately + + // Check for HTTP/2 support in runtime logs (Tomcat startup messages) + // These should appear quickly during Tomcat startup + Eventually(func() string { + logs, _ := deployment.RuntimeLogs() + return logs + }, "10s", "1s").Should(Or( + ContainSubstring("Http11NioProtocol"), + ContainSubstring("Starting ProtocolHandler"), + ContainSubstring("HTTP/1.1"), + )) + }) + it("successfully deploys and runs with Java 11 (Jakarta EE)", func() { deployment, logs, err := platform.Deploy. WithEnv(map[string]string{ @@ -140,6 +175,50 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T, Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK"))) }) + it("deploys with Java 11 (Tomcat 9 + javax.servlet)", func() { + deployment, logs, err := platform.Deploy. + WithEnv(map[string]string{ + "BP_JAVA_VERSION": "11", + "JBP_CONFIG_TOMCAT": "{tomcat: { version: \"9.+\" }", + }). + Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax")) + Expect(err).NotTo(HaveOccurred(), logs.String) + + Expect(logs.String()).To(ContainSubstring("Installing OpenJDK 11.")) + Expect(logs.String()).To(ContainSubstring("Tomcat 9")) + Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK"))) + }) + + it("deploys with default Java (Tomcat 9 + javax.servlet)", func() { + deployment, logs, err := platform.Deploy. + WithEnv(map[string]string{ + "JBP_CONFIG_TOMCAT": "{ tomcat: { version: 9.+ } }", + }). + Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax")) + Expect(err).NotTo(HaveOccurred(), logs.String) + + Expect(logs.String()).To(ContainSubstring("Installing OpenJDK 17.")) + Expect(logs.String()).To(ContainSubstring("Tomcat 9")) + Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK"))) + }) + + it("fails staging with a compatibility error for Tomcat 10 with Java 8 (javax)", func() { + if settings.Platform == "docker" { + t.Skip("Tomcat 10 + Java 8 compatibility enforcement is only guaranteed on CF platform") + } + + _, logs, err := platform.Deploy. + WithEnv(map[string]string{ + "BP_JAVA_VERSION": "8", + "JBP_CONFIG_TOMCAT": "{ tomcat: { version: \"10.+\" } }", + }). + Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax")) + + // Now we expect staging to fail + Expect(err).To(HaveOccurred()) + Expect(logs.String()).To(ContainSubstring("Tomcat 10.x requires Java 11+, but Java 8 detected")) + }) + it("deploys with Java 11 (Tomcat 10 + jakarta.servlet)", func() { deployment, logs, err := platform.Deploy. WithEnv(map[string]string{ diff --git a/src/java/containers/tomcat.go b/src/java/containers/tomcat.go index be53ad148..ad3e81d11 100644 --- a/src/java/containers/tomcat.go +++ b/src/java/containers/tomcat.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "path/filepath" + "regexp" "strings" "github.com/cloudfoundry/java-buildpack/src/java/common" @@ -60,30 +61,40 @@ func (t *TomcatContainer) Supply() error { if javaHome != "" { javaMajorVersion, versionErr := common.DetermineJavaVersion(javaHome) if versionErr == nil { + tomcatVersion := determineTomcatVersion(os.Getenv("JBP_CONFIG_TOMCAT")) t.context.Log.Debug("Detected Java major version: %d", javaMajorVersion) // Select Tomcat version pattern based on Java version var versionPattern string - if javaMajorVersion >= 11 { - // Java 11+: Use Tomcat 10.x (Jakarta EE 9+) - versionPattern = "10.x" - t.context.Log.Info("Using Tomcat 10.x for Java %d", javaMajorVersion) + if tomcatVersion == "" { + if javaMajorVersion >= 11 { + // Java 11+: Use Tomcat 10.x (Jakarta EE 9+) + versionPattern = "10.x" + t.context.Log.Info("Using Tomcat 10.x for Java %d", javaMajorVersion) + } else { + // Java 8-10: Use Tomcat 9.x (Java EE 8) + versionPattern = "9.x" + t.context.Log.Info("Using Tomcat 9.x for Java %d", javaMajorVersion) + } } else { - // Java 8-10: Use Tomcat 9.x (Java EE 8) - versionPattern = "9.x" - t.context.Log.Info("Using Tomcat 9.x for Java %d", javaMajorVersion) + versionPattern = tomcatVersion + t.context.Log.Info("Using Tomcat %s for Java %d", versionPattern, javaMajorVersion) + } + + if strings.HasPrefix(versionPattern, "10.") && javaMajorVersion < 11 { + t.context.Log.Warning("Tomcat %s requires Java 11+, but Java %d detected. Tomcat may fail to start.", versionPattern, javaMajorVersion) } // Resolve the version pattern to actual version using libbuildpack allVersions := t.context.Manifest.AllDependencyVersions("tomcat") resolvedVersion, err := libbuildpack.FindMatchingVersion(versionPattern, allVersions) - if err == nil { - dep.Name = "tomcat" - dep.Version = resolvedVersion - t.context.Log.Debug("Resolved Tomcat version pattern '%s' to %s", versionPattern, resolvedVersion) - } else { - t.context.Log.Warning("Unable to resolve Tomcat version pattern '%s': %s", versionPattern, err.Error()) + if err != nil { + return fmt.Errorf("tomcat version resolution error for pattern %q: %w", versionPattern, err) } + + dep.Name = "tomcat" + dep.Version = resolvedVersion + t.context.Log.Debug("Resolved Tomcat version pattern '%s' to %s", versionPattern, resolvedVersion) } else { t.context.Log.Warning("Unable to determine Java version: %s", versionErr.Error()) } @@ -442,6 +453,42 @@ func getKeys(m map[string]string) []string { return keys } +// DetermineTomcatVersion is an exported wrapper around determineTomcatVersion. +// It exists primarily to allow unit tests in the containers_test package to +// verify Tomcat version parsing behavior without changing production semantics. +func DetermineTomcatVersion(raw string) string { + return determineTomcatVersion(raw) +} + +// determineTomcatVersion determines the version of the tomcat +// based on the JBP_CONFIG_TOMCAT field from manifest. +// It looks for a tomcat block with a version of the form ".+" (e.g. "9.+", "10.+"). +// Returns ".x" (e.g. "9.x", "10.x") so libbuildpack can resolve it, +func determineTomcatVersion(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + + re := regexp.MustCompile(`(?i)tomcat\s*:\s*\{[\s\S]*?version\s*:\s*["']?([\d.]+\.\+)`) + match := re.FindStringSubmatch(raw) + if len(match) < 2 { + return "" + } + + pattern := match[1] // e.g. "9.+", "10.+", "10.23.+" + + // If it's just ".+" (no additional dot), convert to ".x" + if !strings.Contains(strings.TrimSuffix(pattern, ".+"), ".") { + // "9.+" -> "9.x" + major := strings.TrimSuffix(pattern, ".+") + return major + ".x" + } + + // Otherwise, it's something like "10.23.+": pass it through unchanged + return pattern +} + // isAccessLoggingEnabled checks if access logging is enabled in configuration // Returns: "true" or "false" as a string (for use in JAVA_OPTS) // Default: "false" (disabled, matching Ruby buildpack behavior) diff --git a/src/java/containers/tomcat_test.go b/src/java/containers/tomcat_test.go index 7c568f9e3..51e0c6718 100644 --- a/src/java/containers/tomcat_test.go +++ b/src/java/containers/tomcat_test.go @@ -196,4 +196,35 @@ var _ = Describe("Tomcat Container", func() { Expect(contentStr).To(ContainSubstring("org.apache.catalina.realm.UserDatabaseRealm")) }) }) + + Describe("determineTomcatVersion", func() { + It("returns empty string when JBP_CONFIG_TOMCAT is empty", func() { + v := containers.DetermineTomcatVersion("") + Expect(v).To(Equal("")) + }) + + It("returns 9.x for tomcat version 9.+", func() { + raw := `{ tomcat: { version: "9.+" } }` + v := containers.DetermineTomcatVersion(raw) + Expect(v).To(Equal("9.x")) + }) + + It("returns 10.x for tomcat version 10.+", func() { + raw := `{ tomcat: { version: "10.+" } }` + v := containers.DetermineTomcatVersion(raw) + Expect(v).To(Equal("10.x")) + }) + + It("returns 10.23.+ for tomcat version 10.23.+", func() { + raw := `{ tomcat: { version: "10.23.+" } }` + v := containers.DetermineTomcatVersion(raw) + Expect(v).To(Equal("10.23.+")) + }) + + It("returns empty string when only access logging is configured", func() { + raw := `{access_logging_support: {access_logging: enabled}}` + v := containers.DetermineTomcatVersion(raw) + Expect(v).To(Equal("")) + }) + }) })