Skip to content

Commit 9d886b0

Browse files
committed
feat(tomcat): handle packaged WAR and fix external config ordering for context_path
- Finalize() now writes context XML for packaged .war apps (docBase points to the WAR file), so context_path works for both exploded and packaged WARs - Skip writing context XML if the file already exists (external config via external_configuration_enabled provides its own context descriptor) - Unit tests (TDD): 4 new tests covering WAR path, external-config guard - Integration test: verify context_path routes /my/app correctly and / returns 404
1 parent 267ada6 commit 9d886b0

3 files changed

Lines changed: 138 additions & 21 deletions

File tree

src/integration/tomcat_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package integration_test
22

33
import (
4+
"net/http"
45
"path/filepath"
56
"testing"
67

78
"github.com/cloudfoundry/switchblade"
89
"github.com/cloudfoundry/switchblade/matchers"
10+
"github.com/onsi/gomega"
911
"github.com/sclevine/spec"
1012

1113
. "github.com/onsi/gomega"
@@ -441,5 +443,20 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T,
441443
Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK")))
442444
})
443445
})
446+
447+
context("with context_path configured", func() {
448+
it("serves app at configured path and returns 404 at root", func() {
449+
deployment, logs, err := platform.Deploy.
450+
WithEnv(map[string]string{
451+
"BP_JAVA_VERSION": "11",
452+
"JBP_CONFIG_TOMCAT": `{tomcat: {context_path: /my/app}}`,
453+
}).
454+
Execute(name, filepath.Join(fixtures, "containers", "tomcat_jakarta"))
455+
Expect(err).NotTo(HaveOccurred(), logs.String())
456+
457+
Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK")).WithEndpoint("/my/app"))
458+
Eventually(deployment).Should(matchers.Serve(gomega.Anything()).WithEndpoint("/").WithExpectedStatusCode(http.StatusNotFound))
459+
})
460+
})
444461
}
445462
}

src/java/containers/tomcat.go

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -593,34 +593,67 @@ func (t *TomcatContainer) Finalize() error {
593593
return fmt.Errorf("failed to create context directory: %w", err)
594594
}
595595

596-
appContextXML := filepath.Join(buildDir, "META-INF", "context.xml")
597-
var contextContent string
598-
599-
if _, err := os.Stat(appContextXML); err == nil {
600-
xmlBytes, err := os.ReadFile(appContextXML)
601-
if err != nil {
602-
return fmt.Errorf("failed to read META-INF/context.xml: %w", err)
596+
if _, statErr := os.Stat(contextXMLPath); os.IsNotExist(statErr) {
597+
appContextXML := filepath.Join(buildDir, "META-INF", "context.xml")
598+
var contextContent string
599+
600+
if _, err := os.Stat(appContextXML); err == nil {
601+
xmlBytes, err := os.ReadFile(appContextXML)
602+
if err != nil {
603+
return fmt.Errorf("failed to read META-INF/context.xml: %w", err)
604+
}
605+
606+
xmlStr := string(xmlBytes)
607+
xmlStr = strings.TrimSpace(xmlStr)
608+
609+
contextContent = injectDocBase(xmlStr, "${user.home}/app")
610+
t.context.Log.Info("Merged META-INF/context.xml with %s - realm and resource configurations preserved", contextXMLName)
611+
} else {
612+
contextContent = fmt.Sprintf("<Context docBase=\"${user.home}/app\" reloadable=\"false\">\n</Context>\n")
613+
t.context.Log.Info("Created %s with docBase pointing to application directory", contextXMLName)
603614
}
604615

605-
xmlStr := string(xmlBytes)
606-
xmlStr = strings.TrimSpace(xmlStr)
616+
if err := os.WriteFile(contextXMLPath, []byte(contextContent), 0644); err != nil {
617+
return fmt.Errorf("failed to write %s: %w", contextXMLName, err)
618+
}
607619

608-
contextContent = injectDocBase(xmlStr, "${user.home}/app")
609-
t.context.Log.Info("Merged META-INF/context.xml with %s - realm and resource configurations preserved", contextXMLName)
620+
if contextXMLName != "ROOT.xml" {
621+
rootXMLPath := filepath.Join(t.tomcatDir(), "conf", "Catalina", "localhost", "ROOT.xml")
622+
if err := os.Remove(rootXMLPath); err != nil && !os.IsNotExist(err) {
623+
return fmt.Errorf("failed to remove ROOT.xml: %w", err)
624+
}
625+
}
610626
} else {
611-
contextContent = fmt.Sprintf("<Context docBase=\"${user.home}/app\" reloadable=\"false\">\n</Context>\n")
612-
t.context.Log.Info("Created %s with docBase pointing to application directory", contextXMLName)
613-
}
614-
615-
if err := os.WriteFile(contextXMLPath, []byte(contextContent), 0644); err != nil {
616-
return fmt.Errorf("failed to write %s: %w", contextXMLName, err)
627+
t.context.Log.Info("Context XML %s already exists (e.g. from external config), skipping generation", contextXMLName)
617628
}
629+
} else {
630+
warMatches, err := filepath.Glob(filepath.Join(buildDir, "*.war"))
631+
if err == nil && len(warMatches) == 1 {
632+
warFilename := filepath.Base(warMatches[0])
633+
contextContent := fmt.Sprintf("<Context docBase=\"${user.home}/app/%s\" reloadable=\"false\">\n</Context>\n", warFilename)
634+
635+
contextXMLDir := filepath.Dir(contextXMLPath)
636+
if err := os.MkdirAll(contextXMLDir, 0755); err != nil {
637+
return fmt.Errorf("failed to create context directory: %w", err)
638+
}
618639

619-
if contextXMLName != "ROOT.xml" {
620-
rootXMLPath := filepath.Join(t.tomcatDir(), "conf", "Catalina", "localhost", "ROOT.xml")
621-
if err := os.Remove(rootXMLPath); err != nil && !os.IsNotExist(err) {
622-
return fmt.Errorf("failed to remove ROOT.xml: %w", err)
640+
if _, statErr := os.Stat(contextXMLPath); os.IsNotExist(statErr) {
641+
if err := os.WriteFile(contextXMLPath, []byte(contextContent), 0644); err != nil {
642+
return fmt.Errorf("failed to write %s: %w", contextXMLName, err)
643+
}
644+
645+
if contextXMLName != "ROOT.xml" {
646+
rootXMLPath := filepath.Join(t.tomcatDir(), "conf", "Catalina", "localhost", "ROOT.xml")
647+
if err := os.Remove(rootXMLPath); err != nil && !os.IsNotExist(err) {
648+
return fmt.Errorf("failed to remove ROOT.xml: %w", err)
649+
}
650+
}
651+
t.context.Log.Info("Created %s with docBase pointing to %s", contextXMLName, warFilename)
652+
} else {
653+
t.context.Log.Info("Context XML %s already exists (e.g. from external config), skipping generation", contextXMLName)
623654
}
655+
} else if len(warMatches) > 1 {
656+
t.context.Log.Warning("Multiple WAR files found in build directory; context_path not applied")
624657
}
625658
}
626659

src/java/containers/tomcat_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,73 @@ var _ = Describe("Tomcat Container", func() {
262262
Expect(rootXML).NotTo(BeAnExistingFile())
263263
Expect(filepath.Join(contextDir, "the#intended#path.xml")).To(BeAnExistingFile())
264264
})
265+
266+
It("skips writing context XML when file already exists (external config)", func() {
267+
tomcatDir := filepath.Join(depsDir, "0", "tomcat")
268+
contextDir := filepath.Join(tomcatDir, "conf", "Catalina", "localhost")
269+
Expect(os.MkdirAll(contextDir, 0755)).To(Succeed())
270+
preExistingContent := "<Context docBase=\"/external/path\"/>"
271+
rootXML := filepath.Join(contextDir, "ROOT.xml")
272+
Expect(os.WriteFile(rootXML, []byte(preExistingContent), 0644)).To(Succeed())
273+
274+
err := container.Finalize()
275+
Expect(err).NotTo(HaveOccurred())
276+
277+
content, readErr := os.ReadFile(rootXML)
278+
Expect(readErr).NotTo(HaveOccurred())
279+
Expect(string(content)).To(Equal(preExistingContent))
280+
})
281+
282+
Context("with packaged WAR (no WEB-INF)", func() {
283+
BeforeEach(func() {
284+
Expect(os.RemoveAll(filepath.Join(buildDir, "WEB-INF"))).To(Succeed())
285+
})
286+
287+
It("creates context XML pointing to WAR file when context_path is set", func() {
288+
Expect(os.WriteFile(filepath.Join(buildDir, "myapp.war"), []byte("fakewar"), 0644)).To(Succeed())
289+
os.Setenv("JBP_CONFIG_TOMCAT", `{tomcat: {context_path: /my/path}}`)
290+
defer os.Unsetenv("JBP_CONFIG_TOMCAT")
291+
292+
err := container.Finalize()
293+
Expect(err).NotTo(HaveOccurred())
294+
295+
tomcatDir := filepath.Join(depsDir, "0", "tomcat")
296+
contextFile := filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "my#path.xml")
297+
Expect(contextFile).To(BeAnExistingFile())
298+
content, _ := os.ReadFile(contextFile)
299+
Expect(string(content)).To(ContainSubstring(`docBase="${user.home}/app/myapp.war"`))
300+
})
301+
302+
It("creates ROOT.xml pointing to WAR file when no context_path set", func() {
303+
Expect(os.WriteFile(filepath.Join(buildDir, "myapp.war"), []byte("fakewar"), 0644)).To(Succeed())
304+
305+
err := container.Finalize()
306+
Expect(err).NotTo(HaveOccurred())
307+
308+
tomcatDir := filepath.Join(depsDir, "0", "tomcat")
309+
contextFile := filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "ROOT.xml")
310+
Expect(contextFile).To(BeAnExistingFile())
311+
content, _ := os.ReadFile(contextFile)
312+
Expect(string(content)).To(ContainSubstring(`docBase="${user.home}/app/myapp.war"`))
313+
})
314+
315+
It("removes ROOT.xml when packaged WAR uses non-root context_path", func() {
316+
Expect(os.WriteFile(filepath.Join(buildDir, "myapp.war"), []byte("fakewar"), 0644)).To(Succeed())
317+
os.Setenv("JBP_CONFIG_TOMCAT", `{tomcat: {context_path: /my/path}}`)
318+
defer os.Unsetenv("JBP_CONFIG_TOMCAT")
319+
320+
tomcatDir := filepath.Join(depsDir, "0", "tomcat")
321+
contextDir := filepath.Join(tomcatDir, "conf", "Catalina", "localhost")
322+
Expect(os.MkdirAll(contextDir, 0755)).To(Succeed())
323+
rootXML := filepath.Join(contextDir, "ROOT.xml")
324+
Expect(os.WriteFile(rootXML, []byte("<Context/>"), 0644)).To(Succeed())
325+
326+
err := container.Finalize()
327+
Expect(err).NotTo(HaveOccurred())
328+
Expect(rootXML).NotTo(BeAnExistingFile())
329+
Expect(filepath.Join(contextDir, "my#path.xml")).To(BeAnExistingFile())
330+
})
331+
})
265332
})
266333

267334
Describe("SelectTomcatVersionPattern", func() {

0 commit comments

Comments
 (0)