Skip to content

Commit d3b395b

Browse files
[go-migration] Add dependencies to application classpath after staging (#1208)
Framework JAR dependencies (e.g. MariaDB JDBC, PostgreSQL JDBC, Spring Auto-reconfiguration, Java CF Env, Client Certificate Mapper) were written to deps/index/env/CLASSPATH during staging but that file is never sourced at runtime, so the dependencies were silently dropped from the application classpath. - Replace WriteEnvFile("CLASSPATH", ...) with WriteProfileD() scripts in all framework finalizers so CLASSPATH is assembled correctly at runtime when profile.d scripts are sourced - Extract container_security_provider JAR path into a CONTAINER_SECURITY_PROVIDER env var (set via profile.d) and thread it through tomcat (setenv.sh CLASSPATH) and spring-boot (via -cp / -Dloader.path flags) instead of using -Xbootclasspath/a - Add a zzz_classpath_symlinks.sh profile.d script for tomcat (WEB-INF/lib) and spring-boot (BOOT-INF/lib) containers that symlinks every entry on CLASSPATH into the container's lib directory so deps are subject to application class-loading - Allow symlinked resources in Tomcat context.xml (allowLinking='true') - Remove redundant context struct construction in finalizeFrameworks; reuse the ctx already built in Run()
1 parent a430897 commit d3b395b

11 files changed

Lines changed: 92 additions & 56 deletions

File tree

src/java/containers/container.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,23 @@ func (r *Registry) RegisterStandardContainers() {
107107
r.Register(NewDistZipContainer(r.context))
108108
r.Register(NewJavaMainContainer(r.context))
109109
}
110+
111+
// This script is used to process the CLASSPATH assembled from various framework scripts sourced from profile.d
112+
// to further create symlinks to the corresponding framework dependencies in WEB-INF/lib, BOOT-INF/lib and where ever
113+
// needed thus they are available for application classloading
114+
var symlinkScript = `#!/bin/bash
115+
set -euo pipefail
116+
TARGET_DIR="$PWD/%s"
117+
CLASSPATH=${CLASSPATH:-}
118+
mkdir -p "$TARGET_DIR"
119+
# Split CLASSPATH on :
120+
IFS=':' read -ra PATHS <<< "$CLASSPATH"
121+
for p in "${PATHS[@]}"; do
122+
# Skip empty entries
123+
[[ -z "$p" ]] && continue
124+
name=$(basename "$p")
125+
link="$TARGET_DIR/$name"
126+
ln -sf "$p" "$link"
127+
echo "Created symlink: $link -> $p"
128+
done
129+
`

src/java/containers/spring_boot.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package containers
22

33
import (
4-
"github.com/cloudfoundry/java-buildpack/src/java/common"
54
"fmt"
5+
"github.com/cloudfoundry/java-buildpack/src/java/common"
66
"os"
77
"path/filepath"
88
"strings"
@@ -218,6 +218,16 @@ func (s *SpringBootContainer) Finalize() error {
218218
finalOpts = strings.Join(additionalOpts, " ")
219219
}
220220

221+
buildDir := s.context.Stager.BuildDir()
222+
bootInf := filepath.Join(buildDir, "BOOT-INF")
223+
if _, err := os.Stat(bootInf); err == nil {
224+
// the script name is prefixed with 'zzz' as it is important to be the last script sourced from profile.d
225+
// so that the previous scripts assembling the CLASSPATH variable(left from frameworks) are sourced previous to it.
226+
if err := s.context.Stager.WriteProfileD("zzz_classpath_symlinks.sh", fmt.Sprintf(symlinkScript, filepath.Join("BOOT-INF", "lib"))); err != nil {
227+
return fmt.Errorf("failed to write zzz_classpath_symlinks.sh: %w", err)
228+
}
229+
}
230+
221231
// Write combined JAVA_OPTS
222232
if err := s.context.Stager.WriteEnvFile("JAVA_OPTS", finalOpts); err != nil {
223233
return fmt.Errorf("failed to write JAVA_OPTS: %w", err)
@@ -234,20 +244,21 @@ func (s *SpringBootContainer) Release() (string, error) {
234244
bootInf := filepath.Join(buildDir, "BOOT-INF")
235245
if _, err := os.Stat(bootInf); err == nil {
236246
// Verify this is actually a Spring Boot application
247+
237248
if s.isSpringBootExplodedJar(buildDir) {
238249
// True Spring Boot exploded JAR - use JarLauncher
239250
// Determine the correct JarLauncher class name based on Spring Boot version
240251
jarLauncherClass := s.getJarLauncherClass(buildDir)
241252
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
242-
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp . %s", jarLauncherClass), nil
253+
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $PWD/.${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", jarLauncherClass), nil
243254
}
244255

245256
// Exploded JAR but NOT Spring Boot - use Main-Class from MANIFEST.MF
246257
mainClass := s.readMainClassFromManifest(buildDir)
247258
if mainClass != "" {
248259
// Use classpath from BOOT-INF/classes and BOOT-INF/lib
249260
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
250-
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $HOME:$HOME/BOOT-INF/classes:$HOME/BOOT-INF/lib/* %s", mainClass), nil
261+
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $HOME${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER}:$HOME/BOOT-INF/classes:$HOME/BOOT-INF/lib/* %s", mainClass), nil
251262
}
252263

253264
return "", fmt.Errorf("exploded JAR found but no Main-Class in MANIFEST.MF")
@@ -270,7 +281,7 @@ func (s *SpringBootContainer) Release() (string, error) {
270281
}
271282

272283
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
273-
cmd := fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -jar %s", jarFile)
284+
cmd := fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS ${CONTAINER_SECURITY_PROVIDER:+-Dloader.path=$CONTAINER_SECURITY_PROVIDER} -jar %s", jarFile)
274285
return cmd, nil
275286
}
276287

src/java/containers/tomcat.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,12 @@ func (t *TomcatContainer) createSetenvScript(tomcatDir, loggingSupportJar string
256256
setenvPath := filepath.Join(binDir, "setenv.sh")
257257

258258
jarPath := "$CATALINA_HOME/bin/" + loggingSupportJar
259-
259+
// Note that Tomcat builds its own CLASSPATH env before starting. It ensures that any user defined CLASSPATH variables
260+
// are not used on startup, as can be seen in the catalina.sh script. That is why even we have something already
261+
// sourced in CLASSPATH env from profile.d scripts it is disregarded on Tomcat startup and fresh CLASSPATH env is
262+
// built here in the setenv.sh script.
260263
setenvContent := fmt.Sprintf(`#!/bin/sh
261-
JAVA_OPTS="$JAVA_OPTS -Xbootclasspath/a:%s"
264+
CLASSPATH="%s${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER}"
262265
`, jarPath)
263266

264267
if err := os.WriteFile(setenvPath, []byte(setenvContent), 0755); err != nil {
@@ -604,6 +607,12 @@ func (t *TomcatContainer) Finalize() error {
604607

605608
webInf := filepath.Join(buildDir, "WEB-INF")
606609
if _, err := os.Stat(webInf); err == nil {
610+
// the script name is prefixed with 'zzz' as it is important to be the last script sourced from profile.d
611+
// so that the previous scripts assembling the CLASSPATH variable(left from frameworks) are sourced previous to it.
612+
if err := t.context.Stager.WriteProfileD("zzz_classpath_symlinks.sh", fmt.Sprintf(symlinkScript, filepath.Join("WEB-INF", "lib"))); err != nil {
613+
return fmt.Errorf("failed to write zzz_classpath_symlinks.sh: %w", err)
614+
}
615+
607616
contextXMLDir := filepath.Dir(contextXMLPath)
608617
if err := os.MkdirAll(contextXMLDir, 0755); err != nil {
609618
return fmt.Errorf("failed to create context directory: %w", err)

src/java/finalize/finalize.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func Run(f *Finalizer) error {
102102
}
103103

104104
// Finalize frameworks (APM agents, etc.)
105-
if err := f.finalizeFrameworks(); err != nil {
105+
if err := f.finalizeFrameworks(ctx); err != nil {
106106
f.Log.Error("Failed to finalize frameworks: %s", err.Error())
107107
return err
108108
}
@@ -158,17 +158,9 @@ func (f *Finalizer) finalizeJRE() error {
158158
}
159159

160160
// finalizeFrameworks finalizes framework components (APM agents, etc.)
161-
func (f *Finalizer) finalizeFrameworks() error {
161+
func (f *Finalizer) finalizeFrameworks(ctx *common.Context) error {
162162
f.Log.BeginStep("Finalizing frameworks")
163163

164-
ctx := &common.Context{
165-
Stager: f.Stager,
166-
Manifest: f.Manifest,
167-
Installer: f.Installer,
168-
Log: f.Log,
169-
Command: f.Command,
170-
}
171-
172164
registry := frameworks.NewRegistry(ctx)
173165
registry.RegisterStandardFrameworks()
174166

src/java/frameworks/client_certificate_mapper.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,17 @@ func (c *ClientCertificateMapperFramework) Finalize() error {
6969
return nil
7070
}
7171

72-
// Add to classpath via CLASSPATH environment variable
73-
classpath := os.Getenv("CLASSPATH")
74-
if classpath != "" {
75-
classpath += ":"
76-
}
77-
classpath += matches[0]
72+
depsIdx := c.context.Stager.DepsIdx()
73+
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/client_certificate_mapper/%s", depsIdx, filepath.Base(matches[0]))
74+
75+
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", runtimePath)
7876

79-
if err := c.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
80-
return fmt.Errorf("failed to set CLASSPATH for Client Certificate Mapper: %w", err)
77+
if err := c.context.Stager.WriteProfileD("client_certificate_mapper.sh", profileScript); err != nil {
78+
return fmt.Errorf("failed to write client_certificate_mapper.sh profile.d script: %w", err)
8179
}
82-
80+
81+
c.context.Log.Debug("Client Certificate Mapper JAR will be added to classpath at runtime: %s", runtimePath)
82+
8383
return nil
8484
}
8585

src/java/frameworks/container_security_provider.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,13 @@ func (c *ContainerSecurityProviderFramework) Finalize() error {
7575
// Build JAVA_OPTS with runtime paths using $DEPS_DIR
7676
var javaOpts string
7777
if javaVersion >= 9 {
78-
// Java 9+: Add to bootstrap classpath via -Xbootclasspath/a
7978
runtimeJarPath := fmt.Sprintf("$DEPS_DIR/%s/container_security_provider/%s", depsIdx, jarFilename)
80-
javaOpts = fmt.Sprintf("-Xbootclasspath/a:%s", runtimeJarPath)
79+
80+
profileScript := fmt.Sprintf("export CONTAINER_SECURITY_PROVIDER=\"%s\"\n", runtimeJarPath)
81+
82+
if err := c.context.Stager.WriteProfileD("container_security_provider.sh", profileScript); err != nil {
83+
return fmt.Errorf("failed to write container_security_provider.sh profile.d script: %w", err)
84+
}
8185
} else {
8286
// Java 8: Use extension directory
8387
runtimeProviderDir := fmt.Sprintf("$DEPS_DIR/%s/container_security_provider", depsIdx)

src/java/frameworks/java_cf_env.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package frameworks
22

33
import (
4-
"github.com/cloudfoundry/java-buildpack/src/java/common"
54
"fmt"
5+
"github.com/cloudfoundry/java-buildpack/src/java/common"
66
"os"
77
"path/filepath"
88
"strings"
@@ -81,17 +81,16 @@ func (j *JavaCfEnvFramework) Finalize() error {
8181
return nil
8282
}
8383

84-
// Add to classpath via CLASSPATH environment variable
85-
classpath := os.Getenv("CLASSPATH")
86-
if classpath != "" {
87-
classpath += ":"
88-
}
89-
classpath += matches[0]
84+
depsIdx := j.context.Stager.DepsIdx()
85+
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/java_cf_env/%s", depsIdx, filepath.Base(matches[0]))
9086

91-
if err := j.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
92-
return fmt.Errorf("failed to set CLASSPATH for Java CF Env: %w", err)
87+
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", runtimePath)
88+
if err := j.context.Stager.WriteProfileD("java_cf_env.sh", profileScript); err != nil {
89+
return fmt.Errorf("failed to write java_cf_env.sh profile.d script: %w", err)
9390
}
9491

92+
j.context.Log.Debug("Java CF Env JAR will be added to classpath at runtime: %s", runtimePath)
93+
9594
return nil
9695
}
9796

src/java/frameworks/maria_db_jdbc.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,16 @@ func (f *MariaDBJDBCFramework) Finalize() error {
8888

8989
f.context.Log.BeginStep("Configuring MariaDB JDBC driver")
9090

91-
// Add to CLASSPATH environment variable
92-
if err := f.context.Stager.WriteEnvFile("CLASSPATH", f.jarPath); err != nil {
91+
depsIdx := f.context.Stager.DepsIdx()
92+
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/mariadb_jdbc/%s", depsIdx, filepath.Base(f.jarPath))
93+
94+
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", runtimePath)
95+
if err := f.context.Stager.WriteProfileD("mariadb_jdbc.sh", profileScript); err != nil {
9396
f.context.Log.Warning("Failed to add MariaDB JDBC to CLASSPATH: %s", err)
9497
return nil // Non-blocking
9598
}
9699

97-
f.context.Log.Info("MariaDB JDBC driver added to CLASSPATH")
100+
f.context.Log.Debug("Maria JDBC will be added to classpath at runtime: %s", runtimePath)
98101
return nil
99102
}
100103

src/java/frameworks/postgresql_jdbc.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package frameworks
33
import (
44
"fmt"
55
"github.com/cloudfoundry/java-buildpack/src/java/common"
6-
"os"
76
"path/filepath"
87
"strings"
98

@@ -73,17 +72,16 @@ func (p *PostgresqlJdbcFramework) Finalize() error {
7372
return nil
7473
}
7574

76-
// Add to classpath via CLASSPATH environment variable
77-
classpath := os.Getenv("CLASSPATH")
78-
if classpath != "" {
79-
classpath += ":"
80-
}
81-
classpath += matches[0]
75+
depsIdx := p.context.Stager.DepsIdx()
76+
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/postgresql_jdbc/%s", depsIdx, filepath.Base(matches[0]))
8277

83-
if err := p.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
84-
return fmt.Errorf("failed to set CLASSPATH for PostgreSQL JDBC: %w", err)
78+
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", runtimePath)
79+
if err := p.context.Stager.WriteProfileD("postgresql_jdbc.sh", profileScript); err != nil {
80+
return fmt.Errorf("failed to write postgresql_jdbc.sh profile.d script: %w", err)
8581
}
8682

83+
p.context.Log.Debug("PostgreSQL JDBC JAR will be added to classpath at runtime: %s", runtimePath)
84+
8785
return nil
8886
}
8987

src/java/frameworks/spring_auto_reconfiguration.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,16 @@ func (s *SpringAutoReconfigurationFramework) Finalize() error {
101101
return nil
102102
}
103103

104-
// Add to classpath via CLASSPATH environment variable
105-
classpath := os.Getenv("CLASSPATH")
106-
if classpath != "" {
107-
classpath += ":"
108-
}
109-
classpath += matches[0]
104+
depsIdx := s.context.Stager.DepsIdx()
105+
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/spring_auto_reconfiguration/%s", depsIdx, filepath.Base(matches[0]))
110106

111-
if err := s.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
112-
return fmt.Errorf("failed to set CLASSPATH for Spring Auto-reconfiguration: %w", err)
107+
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", runtimePath)
108+
if err := s.context.Stager.WriteProfileD("spring_auto_reconfiguration.sh", profileScript); err != nil {
109+
return fmt.Errorf("failed to write spring_auto_reconfiguration.sh profile.d script: %w", err)
113110
}
114111

112+
s.context.Log.Debug("Spring Auto-reconfiguration JAR will be added to classpath at runtime: %s", runtimePath)
113+
115114
return nil
116115
}
117116

0 commit comments

Comments
 (0)