Skip to content

Commit 86c1fd9

Browse files
committed
Preserve Tomcat META-INF/context.xml configurations in ROOT.xml
Fix issue where Tomcat realm and resource configurations defined in META-INF/context.xml were lost because ROOT.xml (created by buildpack) completely overrides it per Tomcat's context precedence rules. Changes: - Add injectDocBase() function to merge META-INF/context.xml content into ROOT.xml while injecting required docBase attribute - Update Finalize() to read META-INF/context.xml if present and merge configurations (realms, resources, etc.) into ROOT.xml - Add 3 new test cases to verify merging behavior and edge cases - Uses simple string manipulation approach (no XML parser dependencies) - Handles malformed XML defensively with bounds checking All 296 unit tests passing.
1 parent bc844be commit 86c1fd9

2 files changed

Lines changed: 131 additions & 44 deletions

File tree

src/java/containers/container_test.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,14 +662,76 @@ var _ = Describe("Container Registry", func() {
662662
container.Detect()
663663
})
664664

665-
It("finalizes successfully", func() {
665+
It("finalizes successfully without META-INF/context.xml", func() {
666+
err := container.Finalize()
667+
Expect(err).NotTo(HaveOccurred())
668+
669+
tomcatDir := filepath.Join(depsDir, "0", "tomcat")
670+
contextFile := filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "ROOT.xml")
671+
Expect(contextFile).To(BeAnExistingFile())
672+
673+
content, err := os.ReadFile(contextFile)
674+
Expect(err).NotTo(HaveOccurred())
675+
Expect(string(content)).To(ContainSubstring("docBase=\"${user.home}/app\""))
676+
Expect(string(content)).To(ContainSubstring("reloadable=\"false\""))
677+
})
678+
679+
It("merges META-INF/context.xml with realm configuration", func() {
680+
metaInfDir := filepath.Join(buildDir, "META-INF")
681+
os.MkdirAll(metaInfDir, 0755)
682+
683+
contextXML := `<?xml version="1.0" encoding="UTF-8"?>
684+
<Context>
685+
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
686+
resourceName="UserDatabase"/>
687+
<Resource name="jdbc/TestDB"
688+
auth="Container"
689+
type="javax.sql.DataSource"/>
690+
</Context>`
691+
os.WriteFile(filepath.Join(metaInfDir, "context.xml"), []byte(contextXML), 0644)
692+
666693
err := container.Finalize()
667694
Expect(err).NotTo(HaveOccurred())
668695

669-
// Verify context configuration was created
670696
tomcatDir := filepath.Join(depsDir, "0", "tomcat")
671697
contextFile := filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "ROOT.xml")
672698
Expect(contextFile).To(BeAnExistingFile())
699+
700+
content, err := os.ReadFile(contextFile)
701+
Expect(err).NotTo(HaveOccurred())
702+
contentStr := string(content)
703+
704+
Expect(contentStr).To(ContainSubstring("docBase=\"${user.home}/app\""))
705+
Expect(contentStr).To(ContainSubstring("org.apache.catalina.realm.UserDatabaseRealm"))
706+
Expect(contentStr).To(ContainSubstring("resourceName=\"UserDatabase\""))
707+
Expect(contentStr).To(ContainSubstring("jdbc/TestDB"))
708+
Expect(contentStr).To(ContainSubstring("javax.sql.DataSource"))
709+
})
710+
711+
It("handles META-INF/context.xml with existing docBase attribute", func() {
712+
metaInfDir := filepath.Join(buildDir, "META-INF")
713+
os.MkdirAll(metaInfDir, 0755)
714+
715+
contextXML := `<?xml version="1.0" encoding="UTF-8"?>
716+
<Context docBase="/old/path" reloadable="true">
717+
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
718+
resourceName="UserDatabase"/>
719+
</Context>`
720+
os.WriteFile(filepath.Join(metaInfDir, "context.xml"), []byte(contextXML), 0644)
721+
722+
err := container.Finalize()
723+
Expect(err).NotTo(HaveOccurred())
724+
725+
tomcatDir := filepath.Join(depsDir, "0", "tomcat")
726+
contextFile := filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "ROOT.xml")
727+
728+
content, err := os.ReadFile(contextFile)
729+
Expect(err).NotTo(HaveOccurred())
730+
contentStr := string(content)
731+
732+
Expect(contentStr).To(ContainSubstring("docBase=\"${user.home}/app\""))
733+
Expect(contentStr).NotTo(ContainSubstring("/old/path"))
734+
Expect(contentStr).To(ContainSubstring("org.apache.catalina.realm.UserDatabaseRealm"))
673735
})
674736
})
675737

src/java/containers/tomcat.go

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -555,68 +555,93 @@ func extractVersion(config string) string {
555555
return ""
556556
}
557557

558+
func injectDocBase(xmlContent string, docBase string) string {
559+
idx := strings.Index(xmlContent, "<Context")
560+
if idx == -1 {
561+
return xmlContent
562+
}
563+
564+
endIdx := strings.Index(xmlContent[idx:], ">")
565+
if endIdx == -1 {
566+
return xmlContent
567+
}
568+
endIdx += idx
569+
570+
contextTag := xmlContent[idx:endIdx]
571+
572+
for strings.Contains(contextTag, "docBase=") {
573+
docBaseIdx := strings.Index(contextTag, "docBase=")
574+
575+
if docBaseIdx+8 >= len(contextTag) {
576+
break
577+
}
578+
quote := contextTag[docBaseIdx+8]
579+
if quote != '"' && quote != '\'' {
580+
break
581+
}
582+
583+
endQuoteIdx := strings.Index(contextTag[docBaseIdx+9:], string(quote))
584+
if endQuoteIdx == -1 {
585+
break
586+
}
587+
endQuoteIdx += docBaseIdx + 9
588+
589+
before := strings.TrimSpace(contextTag[:docBaseIdx])
590+
after := strings.TrimSpace(contextTag[endQuoteIdx+1:])
591+
if before != "" && after != "" {
592+
contextTag = before + " " + after
593+
} else {
594+
contextTag = before + after
595+
}
596+
}
597+
598+
newContextTag := strings.Replace(contextTag, "<Context", `<Context docBase="`+docBase+`"`, 1)
599+
600+
return xmlContent[:idx] + newContextTag + xmlContent[endIdx:]
601+
}
602+
558603
// Finalize performs final Tomcat configuration
559604
func (t *TomcatContainer) Finalize() error {
560605
t.context.Log.BeginStep("Finalizing Tomcat")
561606

562607
buildDir := t.context.Stager.BuildDir()
563-
tomcatDir := filepath.Join(t.context.Stager.DepDir(), "tomcat")
608+
contextXMLPath := filepath.Join(t.context.Stager.DepDir(), "tomcat", "conf", "Catalina", "localhost", "ROOT.xml")
564609

565-
// Check if we have an exploded WAR (WEB-INF directory in BuildDir)
566610
webInf := filepath.Join(buildDir, "WEB-INF")
567611
if _, err := os.Stat(webInf); err == nil {
568-
// Configure Tomcat to serve the application from BuildDir
569-
// This follows the immutable BuildDir pattern: application stays where deployed
570-
t.context.Log.Info("Configuring Tomcat to serve exploded WAR from BuildDir")
571-
572-
// Create a custom context.xml file that points to BuildDir
573-
// At runtime, $HOME will resolve to the application directory
574-
if err := t.configureContextDocBase(tomcatDir); err != nil {
575-
return fmt.Errorf("failed to configure Tomcat context: %w", err)
612+
contextXMLDir := filepath.Dir(contextXMLPath)
613+
if err := os.MkdirAll(contextXMLDir, 0755); err != nil {
614+
return fmt.Errorf("failed to create context directory: %w", err)
576615
}
577616

578-
t.context.Log.Info("Tomcat configured to serve application from $HOME (BuildDir)")
579-
}
580-
581-
// Tomcat support JARs are already installed directly to tomcat/lib during Supply phase
582-
// No additional configuration needed in Finalize phase
583-
584-
// JVMKill agent is configured by JRE component in JAVA_OPTS
617+
appContextXML := filepath.Join(buildDir, "META-INF", "context.xml")
618+
var contextContent string
585619

586-
return nil
587-
}
620+
if _, err := os.Stat(appContextXML); err == nil {
621+
xmlBytes, err := os.ReadFile(appContextXML)
622+
if err != nil {
623+
return fmt.Errorf("failed to read META-INF/context.xml: %w", err)
624+
}
588625

589-
// configureContextDocBase creates a context configuration that points to BuildDir
590-
func (t *TomcatContainer) configureContextDocBase(tomcatHome string) error {
591-
// Create conf/Catalina/localhost directory if it doesn't exist
592-
contextDir := filepath.Join(tomcatHome, "conf", "Catalina", "localhost")
593-
if err := os.MkdirAll(contextDir, 0755); err != nil {
594-
return fmt.Errorf("failed to create context directory: %w", err)
595-
}
626+
xmlStr := string(xmlBytes)
627+
xmlStr = strings.TrimSpace(xmlStr)
596628

597-
// Create ROOT.xml context file
598-
// This tells Tomcat to serve the ROOT webapp from BuildDir (the application directory)
599-
// Tomcat supports ${propertyName} syntax for system properties in context.xml
600-
contextFile := filepath.Join(contextDir, "ROOT.xml")
601-
contextXML := `<?xml version="1.0" encoding="UTF-8"?>
602-
<Context docBase="${user.home}/app" reloadable="false">
603-
<!-- Application served from BuildDir (/home/vcap/app), not moved to DepDir -->
604-
<!-- At runtime: user.home system property = /home/vcap, so we use ${user.home}/app -->
605-
</Context>
606-
`
629+
contextContent = injectDocBase(xmlStr, "${user.home}/app")
630+
t.context.Log.Info("Merged META-INF/context.xml with ROOT.xml - realm and resource configurations preserved")
631+
} else {
632+
contextContent = fmt.Sprintf("<Context docBase=\"${user.home}/app\" reloadable=\"false\">\n</Context>\n")
633+
t.context.Log.Info("Created ROOT.xml with docBase pointing to application directory")
634+
}
607635

608-
if err := os.WriteFile(contextFile, []byte(contextXML), 0644); err != nil {
609-
return fmt.Errorf("failed to write context file: %w", err)
636+
if err := os.WriteFile(contextXMLPath, []byte(contextContent), 0644); err != nil {
637+
return fmt.Errorf("failed to write ROOT.xml: %w", err)
638+
}
610639
}
611640

612-
t.context.Log.Debug("Created Tomcat context configuration: %s", contextFile)
613641
return nil
614642
}
615643

616-
// configureTomcatSupport copies Tomcat lifecycle support JAR to Tomcat's lib directory
617-
// This ensures the JAR is loaded early enough for logging initialization
618644
// Release returns the Tomcat startup command
619-
// Uses $CATALINA_HOME which is set by profile.d/tomcat.sh at runtime
620645
func (t *TomcatContainer) Release() (string, error) {
621646
// Use $CATALINA_HOME environment variable set by profile.d script
622647
// Profile.d scripts run BEFORE the release command at runtime (same as $JAVA_HOME)

0 commit comments

Comments
 (0)