Skip to content

Commit c845362

Browse files
[go-migration] Refactoring and code cleanup for Tomcat container and some frameworks (#1225)
refactor(tomcat): replace regex heuristics with proper YAML parsing Switch JBP_CONFIG_TOMCAT parsing from regex/string-contains heuristics to structured YAML parsing via the YamlHandler wrapper around yaml.v3. Migrate JavaCfEnvFramework and JavaOptsFramework from yaml.v2 to yaml.v3. - Tomcat container: parse JBP_CONFIG_TOMCAT using YamlHandler instead of regex; simplify DetermineTomcatVersion to use strings.ReplaceAll; fix access logging enable/disable logic - JavaCfEnvFramework: migrate YAML map type from map[interface{}]interface{} to map[string]interface{} for yaml.v3 compatibility - JavaOptsFramework: same yaml.v3 migration; add additional unit tests - Fix malformed YAML in integration tests (missing closing brace, unquoted version strings) - Move yaml.v2 to indirect dependency in go.mod Note: malformed JBP_CONFIG_TOMCAT values will now fail the build instead of silently falling back to defaults.
1 parent d051cb9 commit c845362

File tree

7 files changed

+144
-153
lines changed

7 files changed

+144
-153
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ require (
1111
github.com/onsi/gomega v1.38.2
1212
github.com/sclevine/spec v1.4.0
1313
go.yaml.in/yaml/v3 v3.0.4
14-
gopkg.in/yaml.v2 v2.4.0
1514
)
1615

1716
require (
@@ -50,6 +49,7 @@ require (
5049
golang.org/x/sys v0.38.0 // indirect
5150
golang.org/x/text v0.31.0 // indirect
5251
golang.org/x/tools v0.39.0 // indirect
52+
gopkg.in/yaml.v2 v2.4.0 // indirect
5353
)
5454

5555
// Replace directives to fix OpenTelemetry dependency conflicts from docker/docker test dependencies

src/integration/tomcat_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T,
4040
deployment, logs, err := platform.Deploy.
4141
WithEnv(map[string]string{
4242
"BP_JAVA_VERSION": "11",
43-
"JBP_CONFIG_TOMCAT": "{tomcat: { version: \"9.+\" }, access_logging_support: {access_logging: enabled}}",
43+
"JBP_CONFIG_TOMCAT": "{ tomcat: { version: \"9.+\" }, access_logging_support: { access_logging: enabled } }",
4444
}).
4545
Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax"))
4646

@@ -75,7 +75,7 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T,
7575
deployment, logs, err := platform.Deploy.
7676
WithEnv(map[string]string{
7777
"BP_JAVA_VERSION": "11",
78-
"JBP_CONFIG_TOMCAT": "{access_logging_support: {access_logging: enabled}}",
78+
"JBP_CONFIG_TOMCAT": "{ access_logging_support: { access_logging: enabled } }",
7979
}).
8080
Execute(name, filepath.Join(fixtures, "containers", "tomcat_jakarta"))
8181

@@ -139,7 +139,7 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T,
139139
deployment, logs, err := platform.Deploy.
140140
WithEnv(map[string]string{
141141
"BP_JAVA_VERSION": "11",
142-
"JBP_CONFIG_TOMCAT": "{access_logging_support: {access_logging: enabled}}",
142+
"JBP_CONFIG_TOMCAT": "{ access_logging_support: { access_logging: enabled } }",
143143
}).
144144
Execute(name, filepath.Join(fixtures, "containers", "tomcat_jakarta"))
145145

@@ -179,7 +179,7 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T,
179179
deployment, logs, err := platform.Deploy.
180180
WithEnv(map[string]string{
181181
"BP_JAVA_VERSION": "11",
182-
"JBP_CONFIG_TOMCAT": "{tomcat: { version: \"9.+\" }",
182+
"JBP_CONFIG_TOMCAT": "{ tomcat: { version: \"9.+\" } }",
183183
}).
184184
Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax"))
185185
Expect(err).NotTo(HaveOccurred(), logs.String)
@@ -192,7 +192,7 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T,
192192
it("deploys with default Java (Tomcat 9 + javax.servlet)", func() {
193193
deployment, logs, err := platform.Deploy.
194194
WithEnv(map[string]string{
195-
"JBP_CONFIG_TOMCAT": "{ tomcat: { version: 9.+ } }",
195+
"JBP_CONFIG_TOMCAT": "{ tomcat: { version: \"9.+\" } }",
196196
}).
197197
Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax"))
198198
Expect(err).NotTo(HaveOccurred(), logs.String)
@@ -251,7 +251,7 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T,
251251
deployment, logs, err := platform.Deploy.
252252
WithEnv(map[string]string{
253253
"JBP_CONFIG_OPEN_JDK_JRE": "{ jre: { version: 17.+ } }",
254-
"JBP_CONFIG_TOMCAT": "{tomcat: { version: 10.1.+ }}",
254+
"JBP_CONFIG_TOMCAT": "{ tomcat: { version: 10.1.+ } }",
255255
}).
256256
Execute(name, filepath.Join(fixtures, "containers", "tomcat_jakarta"))
257257

src/java/containers/tomcat.go

Lines changed: 74 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,17 @@ import (
66
"net/http"
77
"os"
88
"path/filepath"
9-
"regexp"
109
"strings"
1110

1211
"github.com/cloudfoundry/java-buildpack/src/java/common"
1312
"github.com/cloudfoundry/java-buildpack/src/java/resources"
1413
"github.com/cloudfoundry/libbuildpack"
15-
yaml "gopkg.in/yaml.v2"
1614
)
1715

1816
// TomcatContainer handles servlet/WAR applications
1917
type TomcatContainer struct {
2018
context *common.Context
19+
config *tomcatConfig
2120
}
2221

2322
// NewTomcatContainer creates a new Tomcat container
@@ -58,15 +57,21 @@ func (t *TomcatContainer) Supply() error {
5857
var dep libbuildpack.Dependency
5958
var err error
6059

60+
t.config, err = t.loadConfig()
61+
if err != nil {
62+
return fmt.Errorf("failed to load tomcat config: %w", err)
63+
}
64+
6165
if javaHome != "" {
6266
javaMajorVersion, versionErr := common.DetermineJavaVersion(javaHome)
6367
if versionErr == nil {
64-
tomcatVersion := determineTomcatVersion(os.Getenv("JBP_CONFIG_TOMCAT"))
68+
tomcatVersion := DetermineTomcatVersion(t.config.Tomcat.Version)
6569
t.context.Log.Debug("Detected Java major version: %d", javaMajorVersion)
6670

6771
// Select Tomcat version pattern based on Java version
6872
var versionPattern string
6973
if tomcatVersion == "" {
74+
t.context.Log.Info("Tomcat version not specified")
7075
if javaMajorVersion >= 11 {
7176
// Java 11+: Use Tomcat 10.x (Jakarta EE 9+)
7277
versionPattern = "10.x"
@@ -110,7 +115,7 @@ func (t *TomcatContainer) Supply() error {
110115

111116
// Install Tomcat with strip components to remove the top-level directory
112117
// Apache Tomcat tarballs extract to apache-tomcat-X.Y.Z/ subdirectory
113-
tomcatDir := filepath.Join(t.context.Stager.DepDir(), "tomcat")
118+
tomcatDir := t.tomcatDir()
114119
if err := t.context.Installer.InstallDependencyWithStrip(dep, tomcatDir, 1); err != nil {
115120
return fmt.Errorf("failed to install Tomcat: %w", err)
116121
}
@@ -186,7 +191,7 @@ func (t *TomcatContainer) installTomcatLifecycleSupport() error {
186191

187192
// InstallDependency for JAR files (non-archives) copies the file to the target directory
188193
// The JAR will be placed in tomcat/lib/ as tomcat/lib/tomcat-lifecycle-support-X.Y.Z.RELEASE.jar
189-
tomcatDir := filepath.Join(t.context.Stager.DepDir(), "tomcat")
194+
tomcatDir := filepath.Join(t.tomcatDir())
190195
libDir := filepath.Join(tomcatDir, "lib")
191196

192197
// Ensure lib directory exists
@@ -211,7 +216,7 @@ func (t *TomcatContainer) installTomcatAccessLoggingSupport() error {
211216

212217
// InstallDependency for JAR files (non-archives) copies the file to the target directory
213218
// The JAR will be placed in tomcat/lib/ as tomcat/lib/tomcat-access-logging-support-X.Y.Z.RELEASE.jar
214-
tomcatDir := filepath.Join(t.context.Stager.DepDir(), "tomcat")
219+
tomcatDir := filepath.Join(t.tomcatDir())
215220
libDir := filepath.Join(tomcatDir, "lib")
216221

217222
// Ensure lib directory exists
@@ -238,7 +243,7 @@ func (t *TomcatContainer) installTomcatLoggingSupport() (string, error) {
238243

239244
// InstallDependency for JAR files (non-archives) copies the file to the target directory
240245
// The JAR will be placed in tomcat/bin/ as tomcat/bin/tomcat-logging-support-X.Y.Z.RELEASE.jar
241-
tomcatDir := filepath.Join(t.context.Stager.DepDir(), "tomcat")
246+
tomcatDir := filepath.Join(t.tomcatDir())
242247
binDir := filepath.Join(tomcatDir, "bin")
243248

244249
// Ensure bin directory exists
@@ -356,7 +361,8 @@ func (t *TomcatContainer) downloadExternalConfiguration(repositoryRoot, version,
356361

357362
// Parse YAML as map[string]string (version -> URL)
358363
var index map[string]string
359-
if err := yaml.Unmarshal(indexData, &index); err != nil {
364+
yamlHandler := common.YamlHandler{}
365+
if err := yamlHandler.Unmarshal(indexData, &index); err != nil {
360366
return fmt.Errorf("failed to parse index.yml: %w", err)
361367
}
362368

@@ -456,33 +462,15 @@ func getKeys(m map[string]string) []string {
456462
return keys
457463
}
458464

459-
// DetermineTomcatVersion is an exported wrapper around determineTomcatVersion.
460-
// It exists primarily to allow unit tests in the containers_test package to
461-
// verify Tomcat version parsing behavior without changing production semantics.
462-
func DetermineTomcatVersion(raw string) string {
463-
return determineTomcatVersion(raw)
464-
}
465-
466-
// determineTomcatVersion determines the version of the tomcat
465+
// DetermineTomcatVersion determines the version of the tomcat
467466
// based on the JBP_CONFIG_TOMCAT field from manifest.
468467
// It looks for a tomcat block with a version of the form "<major>.+" (e.g. "9.+", "10.+", "10.1.+").
469468
// Returns the pattern with "+" replaced by "*" (e.g. "9.*", "10.*", "10.1.*") so libbuildpack can resolve it.
470469
// Masterminds/semver treats x, X, and * as equivalent wildcards.
471-
func determineTomcatVersion(raw string) string {
472-
raw = strings.TrimSpace(raw)
473-
if raw == "" {
474-
return ""
475-
}
476-
477-
re := regexp.MustCompile(`(?i)tomcat\s*:\s*\{[\s\S]*?version\s*:\s*["']?([\d.]+\.\+)`)
478-
match := re.FindStringSubmatch(raw)
479-
if len(match) < 2 {
480-
return ""
481-
}
482-
470+
func DetermineTomcatVersion(version string) string {
483471
// Replace "+" with "*" so libbuildpack's FindMatchingVersion can resolve it.
484472
// e.g. "9.+" -> "9.*", "10.+" -> "10.*", "10.1.+" -> "10.1.*"
485-
return strings.ReplaceAll(match[1], "+", "*")
473+
return strings.ReplaceAll(version, "+", "*")
486474
}
487475

488476
// isAccessLoggingEnabled checks if access logging is enabled in configuration
@@ -491,108 +479,28 @@ func determineTomcatVersion(raw string) string {
491479
// Can be enabled via: JBP_CONFIG_TOMCAT='{access_logging_support: {access_logging: enabled}}'
492480
func (t *TomcatContainer) isAccessLoggingEnabled() string {
493481
// Check for JBP_CONFIG_TOMCAT environment variable
494-
configEnv := os.Getenv("JBP_CONFIG_TOMCAT")
495-
if configEnv != "" {
496-
t.context.Log.Debug("Checking access logging configuration in JBP_CONFIG_TOMCAT")
497-
498-
// Look for access_logging_support section with access_logging: enabled
499-
// Format: {access_logging_support: {access_logging: enabled}}
500-
if strings.Contains(configEnv, "access_logging_support") {
501-
// Check if access_logging is set to enabled
502-
if strings.Contains(configEnv, "access_logging") &&
503-
(strings.Contains(configEnv, "enabled") || strings.Contains(configEnv, "true")) {
504-
t.context.Log.Info("Access logging enabled via JBP_CONFIG_TOMCAT")
505-
return "true"
506-
}
507-
// Check if explicitly disabled
508-
if strings.Contains(configEnv, "access_logging") &&
509-
(strings.Contains(configEnv, "disabled") || strings.Contains(configEnv, "false")) {
510-
t.context.Log.Debug("Access logging explicitly disabled via JBP_CONFIG_TOMCAT")
511-
return "false"
512-
}
513-
}
482+
if t.config.AccessLoggingSupport.AccessLogging == "enabled" || t.config.AccessLoggingSupport.AccessLogging == "true" {
483+
t.context.Log.Info("Access logging enabled via JBP_CONFIG_TOMCAT")
484+
return "true"
514485
}
515486

516-
// Default to disabled (matches Ruby buildpack default)
517487
t.context.Log.Info("Access logging disabled by default (use JBP_CONFIG_TOMCAT to enable)")
518488
return "false"
519489
}
520490

521491
// isExternalConfigurationEnabled checks if external configuration is enabled in config
522492
// Returns: (enabled bool, repositoryRoot string, version string)
523493
func (t *TomcatContainer) isExternalConfigurationEnabled() (bool, string, string) {
524-
// Read buildpack configuration from environment or config file
525-
// The libbuildpack Stager provides access to buildpack config
526-
527-
// Check for JBP_CONFIG_TOMCAT environment variable
528-
configEnv := os.Getenv("JBP_CONFIG_TOMCAT")
529-
if configEnv != "" {
530-
// Parse the configuration to check external_configuration_enabled
531-
// For now, we'll do a simple string check
532-
// A full implementation would parse the YAML/JSON
533-
t.context.Log.Debug("JBP_CONFIG_TOMCAT: %s", configEnv)
534-
535-
// Simple check for external_configuration_enabled: true
536-
if strings.Contains(configEnv, "external_configuration_enabled") &&
537-
(strings.Contains(configEnv, "true") || strings.Contains(configEnv, "True")) {
538-
539-
// Extract repository_root and version if present
540-
repositoryRoot := extractRepositoryRoot(configEnv)
541-
version := extractVersion(configEnv)
542-
return true, repositoryRoot, version
543-
}
494+
if t.config.Tomcat.ExternalConfigurationEnabled {
495+
repositoryRoot := t.config.ExternalConfiguration.RepositoryRoot
496+
version := t.config.ExternalConfiguration.Version
497+
return true, repositoryRoot, version
544498
}
545499

546500
// Default to false (disabled)
547501
return false, "", ""
548502
}
549503

550-
// extractRepositoryRoot extracts the repository_root value from config string
551-
func extractRepositoryRoot(config string) string {
552-
// Simple extraction - look for repository_root: "value"
553-
// This is a basic implementation; a full parser would use YAML/JSON libraries
554-
555-
// Look for repository_root: "..."
556-
if idx := strings.Index(config, "repository_root"); idx != -1 {
557-
remaining := config[idx:]
558-
// Find the opening quote
559-
if startQuote := strings.Index(remaining, "\""); startQuote != -1 {
560-
remaining = remaining[startQuote+1:]
561-
// Find the closing quote
562-
if endQuote := strings.Index(remaining, "\""); endQuote != -1 {
563-
return remaining[:endQuote]
564-
}
565-
}
566-
}
567-
568-
return ""
569-
}
570-
571-
// extractVersion extracts the version value from config string
572-
func extractVersion(config string) string {
573-
// Look for version: "value" in the external_configuration section
574-
// This is a basic implementation; a full parser would use YAML/JSON libraries
575-
576-
// Find external_configuration section first
577-
if idx := strings.Index(config, "external_configuration"); idx != -1 {
578-
remaining := config[idx:]
579-
// Look for version: "..."
580-
if versionIdx := strings.Index(remaining, "version"); versionIdx != -1 {
581-
remaining = remaining[versionIdx:]
582-
// Find the opening quote
583-
if startQuote := strings.Index(remaining, "\""); startQuote != -1 {
584-
remaining = remaining[startQuote+1:]
585-
// Find the closing quote
586-
if endQuote := strings.Index(remaining, "\""); endQuote != -1 {
587-
return remaining[:endQuote]
588-
}
589-
}
590-
}
591-
}
592-
593-
return ""
594-
}
595-
596504
func injectDocBase(xmlContent string, docBase string) string {
597505
idx := strings.Index(xmlContent, "<Context")
598506
if idx == -1 {
@@ -643,7 +551,7 @@ func (t *TomcatContainer) Finalize() error {
643551
t.context.Log.BeginStep("Finalizing Tomcat")
644552

645553
buildDir := t.context.Stager.BuildDir()
646-
contextXMLPath := filepath.Join(t.context.Stager.DepDir(), "tomcat", "conf", "Catalina", "localhost", "ROOT.xml")
554+
contextXMLPath := filepath.Join(t.tomcatDir(), "conf", "Catalina", "localhost", "ROOT.xml")
647555

648556
webInf := filepath.Join(buildDir, "WEB-INF")
649557
if _, err := os.Stat(webInf); err == nil {
@@ -693,3 +601,52 @@ func (t *TomcatContainer) Release() (string, error) {
693601

694602
return cmd, nil
695603
}
604+
605+
func (t *TomcatContainer) tomcatDir() string {
606+
return filepath.Join(t.context.Stager.DepDir(), "tomcat")
607+
}
608+
609+
func (t *TomcatContainer) loadConfig() (*tomcatConfig, error) {
610+
tConfig := tomcatConfig{
611+
Tomcat: Tomcat{
612+
Version: "",
613+
ExternalConfigurationEnabled: false,
614+
},
615+
ExternalConfiguration: ExternalConfiguration{
616+
Version: "",
617+
RepositoryRoot: "",
618+
},
619+
AccessLoggingSupport: AccessLoggingSupport{
620+
AccessLogging: "disabled",
621+
},
622+
}
623+
config := os.Getenv("JBP_CONFIG_TOMCAT")
624+
if config != "" {
625+
yamlHandler := common.YamlHandler{}
626+
// overlay JBP_CONFIG_TOMCAT over default values
627+
if err := yamlHandler.Unmarshal([]byte(config), &tConfig); err != nil {
628+
return nil, fmt.Errorf("failed to parse JBP_CONFIG_TOMCAT: %w", err)
629+
}
630+
}
631+
return &tConfig, nil
632+
}
633+
634+
type tomcatConfig struct {
635+
Tomcat Tomcat `yaml:"tomcat"`
636+
ExternalConfiguration ExternalConfiguration `yaml:"external_configuration"`
637+
AccessLoggingSupport AccessLoggingSupport `yaml:"access_logging_support"`
638+
}
639+
640+
type Tomcat struct {
641+
Version string `yaml:"version"`
642+
ExternalConfigurationEnabled bool `yaml:"external_configuration_enabled"`
643+
}
644+
645+
type ExternalConfiguration struct {
646+
Version string `yaml:"version"`
647+
RepositoryRoot string `yaml:"repository_root"`
648+
}
649+
650+
type AccessLoggingSupport struct {
651+
AccessLogging string `yaml:"access_logging"`
652+
}

0 commit comments

Comments
 (0)