diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index d6016d52b..ac6119c1c 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -233,6 +233,16 @@ func (s *SpringBootContainer) Finalize() error { return fmt.Errorf("failed to write JAVA_OPTS: %w", err) } + // Ensure the app binds to CF's assigned port, overriding any server.port set in + // application.yml or other Spring config. Without this, apps with a hardcoded + // server.port will either bind to the wrong port (health check fails) or crash + // with java.net.BindException: Permission denied for privileged ports (< 1024). + // Uses WriteProfileD (not WriteEnvFile) so that $PORT is shell-expanded at runtime. + // Mirrors Ruby buildpack: lib/java_buildpack/container/spring_boot.rb release() + if err := s.context.Stager.WriteProfileD("spring_boot_server_port.sh", "export SERVER_PORT=$PORT\n"); err != nil { + return fmt.Errorf("failed to write SERVER_PORT profile.d script: %w", err) + } + return nil } diff --git a/src/java/containers/spring_boot_cli.go b/src/java/containers/spring_boot_cli.go index ffeefaf4d..ff0c80211 100644 --- a/src/java/containers/spring_boot_cli.go +++ b/src/java/containers/spring_boot_cli.go @@ -107,15 +107,13 @@ func (s *SpringBootCLIContainer) Finalize() error { s.context.Log.BeginStep("Finalizing Spring Boot CLI") // Set environment variables for Spring Boot CLI - envVars := map[string]string{ - "JAVA_OPTS": "$JAVA_OPTS", - "SERVER_PORT": "$PORT", + if err := s.context.Stager.WriteEnvFile("JAVA_OPTS", "$JAVA_OPTS"); err != nil { + s.context.Log.Warning("Failed to set JAVA_OPTS: %s", err.Error()) } - for key, value := range envVars { - if err := s.context.Stager.WriteEnvFile(key, value); err != nil { - s.context.Log.Warning("Failed to set %s: %s", key, err.Error()) - } + // Use WriteProfileD so $PORT is shell-expanded at runtime (WriteEnvFile writes plain text, no expansion). + if err := s.context.Stager.WriteProfileD("spring_boot_cli_server_port.sh", "export SERVER_PORT=$PORT\n"); err != nil { + return fmt.Errorf("failed to write SERVER_PORT profile.d script: %w", err) } return nil diff --git a/src/java/containers/spring_boot_cli_test.go b/src/java/containers/spring_boot_cli_test.go new file mode 100644 index 000000000..cf68dd395 --- /dev/null +++ b/src/java/containers/spring_boot_cli_test.go @@ -0,0 +1,82 @@ +package containers_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/cloudfoundry/java-buildpack/src/java/common" + "github.com/cloudfoundry/java-buildpack/src/java/containers" + "github.com/cloudfoundry/libbuildpack" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Spring Boot CLI Container", func() { + var ( + ctx *common.Context + container *containers.SpringBootCLIContainer + buildDir string + depsDir string + cacheDir string + ) + + BeforeEach(func() { + var err error + buildDir, err = os.MkdirTemp("", "build") + Expect(err).NotTo(HaveOccurred()) + + depsDir, err = os.MkdirTemp("", "deps") + Expect(err).NotTo(HaveOccurred()) + + cacheDir, err = os.MkdirTemp("", "cache") + Expect(err).NotTo(HaveOccurred()) + + err = os.MkdirAll(filepath.Join(depsDir, "0"), 0755) + Expect(err).NotTo(HaveOccurred()) + + logger := libbuildpack.NewLogger(os.Stdout) + manifest := &libbuildpack.Manifest{} + installer := &libbuildpack.Installer{} + stager := libbuildpack.NewStager([]string{buildDir, cacheDir, depsDir, "0"}, logger, manifest) + command := &libbuildpack.Command{} + + ctx = &common.Context{ + Stager: stager, + Manifest: manifest, + Installer: installer, + Log: logger, + Command: command, + } + + container = containers.NewSpringBootCLIContainer(ctx) + }) + + AfterEach(func() { + os.RemoveAll(buildDir) + os.RemoveAll(depsDir) + os.RemoveAll(cacheDir) + }) + + Describe("Finalize", func() { + It("writes a profile.d script that exports SERVER_PORT=$PORT so the variable is shell-expanded at runtime", func() { + err := container.Finalize() + Expect(err).NotTo(HaveOccurred()) + + profileScript := filepath.Join(depsDir, "0", "profile.d", "spring_boot_cli_server_port.sh") + data, err := os.ReadFile(profileScript) + Expect(err).NotTo(HaveOccurred()) + Expect(string(data)).To(Equal("export SERVER_PORT=$PORT\n")) + + // Verify $PORT is actually shell-expanded at runtime (not left as literal "$PORT"). + // Simulates what CF's launcher does: source the profile.d script with PORT set in env. + cmd := exec.Command("bash", "-c", fmt.Sprintf("PORT=8080 . %s && echo $SERVER_PORT", profileScript)) + out, bashErr := cmd.Output() + Expect(bashErr).NotTo(HaveOccurred()) + Expect(strings.TrimSpace(string(out))).To(Equal("8080"), + "SERVER_PORT should be the expanded value of $PORT, not the literal string \"$PORT\"") + }) + }) +}) diff --git a/src/java/containers/spring_boot_test.go b/src/java/containers/spring_boot_test.go index f5897c827..752dd1ec7 100644 --- a/src/java/containers/spring_boot_test.go +++ b/src/java/containers/spring_boot_test.go @@ -1,8 +1,11 @@ package containers_test import ( + "fmt" "os" + "os/exec" "path/filepath" + "strings" "github.com/cloudfoundry/java-buildpack/src/java/common" "github.com/cloudfoundry/java-buildpack/src/java/containers" @@ -169,5 +172,23 @@ var _ = Describe("Spring Boot Container", func() { err := container.Finalize() Expect(err).NotTo(HaveOccurred()) }) + + It("writes a profile.d script that exports SERVER_PORT=$PORT so the variable is shell-expanded at runtime", func() { + err := container.Finalize() + Expect(err).NotTo(HaveOccurred()) + + profileScript := filepath.Join(depsDir, "0", "profile.d", "spring_boot_server_port.sh") + data, err := os.ReadFile(profileScript) + Expect(err).NotTo(HaveOccurred()) + Expect(string(data)).To(Equal("export SERVER_PORT=$PORT\n")) + + // Verify $PORT is actually shell-expanded at runtime (not left as literal "$PORT"). + // Simulates what CF's launcher does: source the profile.d script with PORT set in env. + cmd := exec.Command("bash", "-c", fmt.Sprintf("PORT=8080 . %s && echo $SERVER_PORT", profileScript)) + out, bashErr := cmd.Output() + Expect(bashErr).NotTo(HaveOccurred()) + Expect(strings.TrimSpace(string(out))).To(Equal("8080"), + "SERVER_PORT should be the expanded value of $PORT, not the literal string \"$PORT\"") + }) }) })