From 43990c683d96d9f52af95d115855543a535a7b63 Mon Sep 17 00:00:00 2001 From: Mihail Shlyukarski Date: Fri, 6 Mar 2026 11:54:56 +0200 Subject: [PATCH 1/2] allow tomcat selection by manifest value --- src/integration/tomcat_test.go | 62 ++++++++++++++++++++++++++++++++++ src/java/common/context.go | 12 +++++++ src/java/containers/tomcat.go | 20 +++++++---- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/integration/tomcat_test.go b/src/integration/tomcat_test.go index e0a00e5ed..8090a97cd 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,33 @@ 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("deploys with Java 11 (Tomcat 10 + jakarta.servlet)", func() { deployment, logs, err := platform.Deploy. WithEnv(map[string]string{ diff --git a/src/java/common/context.go b/src/java/common/context.go index d9290d2e8..7c39a5bdf 100644 --- a/src/java/common/context.go +++ b/src/java/common/context.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "regexp" "strconv" "strings" ) @@ -50,6 +51,17 @@ type Context struct { Command Command } +// DetermineTomcatVersion determines the version of the tomcat +// based on the JBP_CONFIG_TOMCAT field from manifest +func DetermineTomcatVersion(raw string) (string, error) { + re := regexp.MustCompile(`(\d+)`) + match := re.FindStringSubmatch(raw) + if len(match) < 2 { + return "", fmt.Errorf("unable to extract Tomcat version from %q", raw) + } + return match[1] + ".x", nil +} + // DetermineJavaVersion determines the major Java version from a Java installation // by reading the JAVA_VERSION field from the release file. // diff --git a/src/java/containers/tomcat.go b/src/java/containers/tomcat.go index be53ad148..60c01c736 100644 --- a/src/java/containers/tomcat.go +++ b/src/java/containers/tomcat.go @@ -60,18 +60,24 @@ func (t *TomcatContainer) Supply() error { if javaHome != "" { javaMajorVersion, versionErr := common.DetermineJavaVersion(javaHome) if versionErr == nil { + tomcatVersion, err := common.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) } // Resolve the version pattern to actual version using libbuildpack From 21cca68d399935f092d840901f29cfbc6af340a5 Mon Sep 17 00:00:00 2001 From: Mihail Shlyukarski Date: Tue, 10 Mar 2026 11:09:09 +0200 Subject: [PATCH 2/2] comments addressed --- src/integration/tomcat_test.go | 17 +++++++++ src/java/common/context.go | 12 ------- src/java/containers/tomcat.go | 55 ++++++++++++++++++++++++++---- src/java/containers/tomcat_test.go | 31 +++++++++++++++++ 4 files changed, 96 insertions(+), 19 deletions(-) diff --git a/src/integration/tomcat_test.go b/src/integration/tomcat_test.go index 8090a97cd..7598a578d 100644 --- a/src/integration/tomcat_test.go +++ b/src/integration/tomcat_test.go @@ -202,6 +202,23 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T, 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/common/context.go b/src/java/common/context.go index 7c39a5bdf..d9290d2e8 100644 --- a/src/java/common/context.go +++ b/src/java/common/context.go @@ -7,7 +7,6 @@ import ( "io" "os" "path/filepath" - "regexp" "strconv" "strings" ) @@ -51,17 +50,6 @@ type Context struct { Command Command } -// DetermineTomcatVersion determines the version of the tomcat -// based on the JBP_CONFIG_TOMCAT field from manifest -func DetermineTomcatVersion(raw string) (string, error) { - re := regexp.MustCompile(`(\d+)`) - match := re.FindStringSubmatch(raw) - if len(match) < 2 { - return "", fmt.Errorf("unable to extract Tomcat version from %q", raw) - } - return match[1] + ".x", nil -} - // DetermineJavaVersion determines the major Java version from a Java installation // by reading the JAVA_VERSION field from the release file. // diff --git a/src/java/containers/tomcat.go b/src/java/containers/tomcat.go index 60c01c736..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,7 +61,7 @@ func (t *TomcatContainer) Supply() error { if javaHome != "" { javaMajorVersion, versionErr := common.DetermineJavaVersion(javaHome) if versionErr == nil { - tomcatVersion, err := common.DetermineTomcatVersion(os.Getenv("JBP_CONFIG_TOMCAT")) + 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 @@ -80,16 +81,20 @@ func (t *TomcatContainer) Supply() error { 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()) } @@ -448,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("")) + }) + }) })