Skip to content

Commit 12b80a9

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/tomcat-context-path
2 parents 9d886b0 + dab9179 commit 12b80a9

9 files changed

Lines changed: 134 additions & 26 deletions

File tree

docs/container-java_main.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ If the application uses Spring, [Spring profiles][] can be specified by setting
2525

2626
If `java_main_class` is set to one of Spring Boot's launchers (`JarLauncher`, `PropertiesLauncher` or `WarLauncher`), the Java Main Container sets `SERVER_PORT=$PORT` so that the application binds to the CF-assigned port.
2727

28+
## CF Tasks
29+
30+
The buildpack emits both `web` and `task` process types with the same command so `cf run-task` works without `--command`.
31+
32+
To run a task with a different main class (batch job, migration, etc.), set `java_main_class` to Spring Boot's `PropertiesLauncher` at staging time:
33+
34+
```yaml
35+
env:
36+
JBP_CONFIG_JAVA_MAIN: '{java_main_class: "org.springframework.boot.loader.launch.PropertiesLauncher"}'
37+
```
38+
39+
Then override the main class per task at run time (requires CF CLI v7+):
40+
41+
```bash
42+
cf run-task my-app --env JAVA_OPTS="-Dloader.main=com.example.BatchJob"
43+
```
44+
45+
`-Dloader.main` is a Spring Boot `PropertiesLauncher` system property -- the buildpack passes it through to the JVM unchanged. `JBP_CONFIG_JAVA_MAIN` is a staging-time setting that applies to both `web` and `task`; `-Dloader.main` is a per-task runtime override.
46+
2847
## Configuration
2948
For general information on configuring the buildpack, including how to specify configuration values through environment variables, refer to [Configuration and Extension][].
3049

docs/container-spring_boot.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ The container expects to run the application creating by running [`gradle distZi
1717

1818
If the application uses Spring, [Spring profiles][] can be specified by setting the [`SPRING_PROFILES_ACTIVE`][] environment variable. This is automatically detected and used by Spring. The Spring Auto-reconfiguration Framework will specify the `cloud` profile in addition to any others.
1919

20+
## CF Tasks
21+
22+
The buildpack includes a `task` process type in the release output using the same command as `web`, so `cf run-task` works without an explicit `--command`.
23+
24+
```bash
25+
cf run-task my-app # uses the task process type command
26+
cf run-task my-app --command "..." # explicit override
27+
```
28+
29+
To run a task with a different main class (batch job, migration, etc.), see [Java Main Container - CF Tasks][java-main-tasks].
30+
31+
[java-main-tasks]: container-java_main.md#cf-tasks
32+
2033
## Configuration
2134
The Spring Boot Container cannot be configured.
2235

manifest.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -295,22 +295,22 @@ dependencies:
295295
source: https://repo1.maven.org/maven2/org/cloudfoundry/container-security-provider/1.20.0/container-security-provider-1.20.0-RELEASE.jar
296296
source_sha256: e791ccfcfee9c0d299d07474d9bfcbfcbebf1181323be601220c8a823062ab99
297297
- name: contrast-security
298-
version: 6.30.1
299-
uri: https://buildpacks.cloudfoundry.org/dependencies/contrast-security/contrast-security_6.30.1_linux_noarch_any-stack_52b78e02.jar
300-
sha256: 52b78e029439d8ce9d3bfde0e93f224948d4bed1dcf1a7e4d22d35cc80dc386a
298+
version: 6.31.0
299+
uri: https://buildpacks.cloudfoundry.org/dependencies/contrast-security/contrast-security_6.31.0_linux_noarch_any-stack_0c176385.jar
300+
sha256: 0c1763850f31734e3514713cfe78dc5d95ea276561b4dc42c6f1272c112c1a86
301301
cf_stacks:
302302
- cflinuxfs4
303303
- cflinuxfs5
304-
source: https://repo1.maven.org/maven2/com/contrastsecurity/contrast-agent/6.30.1/contrast-agent-6.30.1.jar
304+
source: https://repo1.maven.org/maven2/com/contrastsecurity/contrast-agent/6.31.0/contrast-agent-6.31.0.jar
305305
source_sha256: ''
306306
- name: datadog-javaagent
307-
version: 1.63.0
308-
uri: https://buildpacks.cloudfoundry.org/dependencies/datadog-javaagent/datadog-javaagent_1.63.0_linux_noarch_any-stack_6a8df6bb.jar
309-
sha256: 6a8df6bb668bb2ca4318112a9c39e99737663fe1eb887cab02f9f46b59f7418a
307+
version: 1.63.1
308+
uri: https://buildpacks.cloudfoundry.org/dependencies/datadog-javaagent/datadog-javaagent_1.63.1_linux_noarch_any-stack_b1266db2.jar
309+
sha256: b1266db2aa6f5dda9673eb6b8c23e21f2cd262dffbee19ee55972f13cbfab98d
310310
cf_stacks:
311311
- cflinuxfs4
312312
- cflinuxfs5
313-
source: https://repo1.maven.org/maven2/com/datadoghq/dd-java-agent/1.63.0/dd-java-agent-1.63.0.jar
313+
source: https://repo1.maven.org/maven2/com/datadoghq/dd-java-agent/1.63.1/dd-java-agent-1.63.1.jar
314314
source_sha256: ''
315315
- name: elastic-apm-agent
316316
version: 1.56.0

scripts/unit.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ function main() {
1414
local src
1515
src="$(find "${ROOTDIR}/src" -mindepth 1 -maxdepth 1 -type d )"
1616

17+
echo "bash: ${BASH_VERSION}"
18+
1719
util::tools::ginkgo::install --directory "${ROOTDIR}/.bin"
1820
util::tools::buildpack-packager::install --directory "${ROOTDIR}/.bin"
1921

src/java/finalize/finalize.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,12 @@ func (f *Finalizer) writeReleaseYaml(container containers.Container) error {
280280
}
281281

282282
releaseYamlPath := filepath.Join(tmpDir, "java-buildpack-release-step.yml")
283+
escapedCommand := strings.ReplaceAll(fullCommand, "'", "''")
283284
yamlContent := fmt.Sprintf(`---
284285
default_process_types:
285286
web: '%s'
286-
`, fullCommand)
287+
task: '%s'
288+
`, escapedCommand, escapedCommand)
287289

288290
if err := os.WriteFile(releaseYamlPath, []byte(yamlContent), 0644); err != nil {
289291
return fmt.Errorf("failed to write release YAML: %w", err)

src/java/finalize/finalize_test.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
package finalize_test
22

33
import (
4-
"github.com/cloudfoundry/java-buildpack/src/internal/mocks"
5-
"github.com/golang/mock/gomock"
64
"os"
75
"path/filepath"
6+
"regexp"
87
"time"
98

9+
"github.com/cloudfoundry/java-buildpack/src/internal/mocks"
1010
"github.com/cloudfoundry/java-buildpack/src/java/finalize"
1111
"github.com/cloudfoundry/libbuildpack"
12+
"github.com/golang/mock/gomock"
1213
. "github.com/onsi/ginkgo/v2"
1314
. "github.com/onsi/gomega"
15+
"gopkg.in/yaml.v2"
1416
)
1517

1618
var _ = Describe("Finalize", func() {
@@ -243,6 +245,50 @@ dependencies: []
243245
})
244246
})
245247

248+
Describe("Release YAML", func() {
249+
BeforeEach(func() {
250+
os.Setenv("JBP_CONFIG_JAVA_MAIN", "{java_main_class: com.example.App}")
251+
finalizer.JREName = "OpenJDK"
252+
finalizer.ContainerName = "Java Main"
253+
})
254+
255+
AfterEach(func() {
256+
os.Unsetenv("JBP_CONFIG_JAVA_MAIN")
257+
})
258+
259+
It("emits web and task process types with identical commands", func() {
260+
Expect(finalize.Run(finalizer)).To(Succeed())
261+
content, err := os.ReadFile(filepath.Join(buildDir, "tmp", "java-buildpack-release-step.yml"))
262+
Expect(err).NotTo(HaveOccurred())
263+
264+
re := regexp.MustCompile(`(?m)^\s+(web|task):\s+'(.+)'$`)
265+
matches := re.FindAllStringSubmatch(string(content), -1)
266+
Expect(matches).To(HaveLen(2), "expected both web and task process types")
267+
268+
commands := map[string]string{}
269+
for _, m := range matches {
270+
commands[m[1]] = m[2]
271+
}
272+
Expect(commands).To(HaveKey("web"))
273+
Expect(commands).To(HaveKey("task"))
274+
Expect(commands["web"]).To(Equal(commands["task"]))
275+
})
276+
277+
It("escapes single quotes in commands so release YAML is valid", func() {
278+
os.Setenv("JBP_CONFIG_JAVA_MAIN", `{java_main_class: com.example.App, arguments: "--message=it's alive"}`)
279+
Expect(finalize.Run(finalizer)).To(Succeed())
280+
content, err := os.ReadFile(filepath.Join(buildDir, "tmp", "java-buildpack-release-step.yml"))
281+
Expect(err).NotTo(HaveOccurred())
282+
283+
var parsed struct {
284+
DefaultProcessTypes map[string]string `yaml:"default_process_types"`
285+
}
286+
Expect(yaml.Unmarshal(content, &parsed)).To(Succeed(), "release YAML must be valid")
287+
Expect(parsed.DefaultProcessTypes["web"]).To(ContainSubstring("it's alive"))
288+
Expect(parsed.DefaultProcessTypes["task"]).To(ContainSubstring("it's alive"))
289+
})
290+
})
291+
246292
Describe("javaexec launcher installation", func() {
247293
It("installs launcher from buildpack bin/ for packaged buildpack usage", func() {
248294
// Default path: <buildpackDir>/bin/javaexec already written in BeforeEach.

src/java/frameworks/java_opts_writer.go

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,7 @@ case "$USER_JAVA_OPTS" in
140140
;;
141141
esac
142142
143-
# Escape replacement-special chars once; these values are loop-invariant.
144-
_escaped_deps_dir="${DEPS_DIR//\\/\\\\}"
145-
_escaped_deps_dir="${_escaped_deps_dir//&/\\&}"
146-
_escaped_home="${HOME//\\/\\\\}"
147-
_escaped_home="${_escaped_home//&/\\&}"
148143
_user_java_opts_placeholder='__JAVA_OPTS_BUILDPACK_PLACEHOLDER__'
149-
_escaped_user_java_opts="${USER_JAVA_OPTS//\\/\\\\}"
150-
_escaped_user_java_opts="${_escaped_user_java_opts//&/\\&}"
151144
152145
if [ -d "$DEPS_DIR/%s/java_opts" ]; then
153146
for opts_file in "$DEPS_DIR/%s/java_opts"/*.opts; do
@@ -159,11 +152,22 @@ if [ -d "$DEPS_DIR/%s/java_opts" ]; then
159152
# a literal $VAR to the JVM (Ruby buildpack parity).
160153
opts_content="${opts_content//\\\$/$_escaped_dollar_placeholder}"
161154
162-
# Expand $DEPS_DIR and $HOME using bash parameter expansion.
163-
# In ${var//pattern/repl}, '&' and '\' are special in replacement strings,
164-
# so escape them first to preserve literal path contents.
165-
opts_content="${opts_content//\$DEPS_DIR/$_escaped_deps_dir}"
166-
opts_content="${opts_content//\$HOME/$_escaped_home}"
155+
# Replace $DEPS_DIR and $HOME using string-split, not ${//}: bash 4.x, 5.1.x
156+
# and 5.2.x differ in how '&' and '\' in replacement variables are handled,
157+
# causing literal \& and \\ in output on bash 5.1 (cflinuxfs4).
158+
_rest="$opts_content"; opts_content=""
159+
while [[ "$_rest" == *'$DEPS_DIR'* ]]; do
160+
opts_content+="${_rest%%%%'$DEPS_DIR'*}${DEPS_DIR}"
161+
_rest="${_rest#*'$DEPS_DIR'}"
162+
done
163+
opts_content+="$_rest"
164+
165+
_rest="$opts_content"; opts_content=""
166+
while [[ "$_rest" == *'$HOME'* ]]; do
167+
opts_content+="${_rest%%%%'$HOME'*}${HOME}"
168+
_rest="${_rest#*'$HOME'}"
169+
done
170+
opts_content+="$_rest"
167171
168172
# Resolve the trusted $(nproc) token to the computed processor count.
169173
opts_content="${opts_content//\$\(nproc\)/$_nproc_count}"
@@ -194,8 +198,21 @@ if [ -d "$DEPS_DIR/%s/java_opts" ]; then
194198
;;
195199
esac
196200
197-
# Now safely substitute JAVA_OPTS after expansion (preserves quotes, backslashes, and ampersands)
198-
opts_content="${opts_content//$_user_java_opts_placeholder/$_escaped_user_java_opts}"
201+
# Restore USER_JAVA_OPTS via string-split, not ${//}: bash 4.x and 5.x
202+
# treat '\\' in replacement strings differently, corrupting backslashes.
203+
# %%%% / # are pure string ops — no special-char interpretation, any bash.
204+
# Pre-count occurrences so the loop is bounded even if USER_JAVA_OPTS
205+
# itself contained the placeholder string (infinite-loop defence).
206+
# Counting uses empty replacement — no backslash in replacement, safe.
207+
# Handles multiple occurrences (e.g. user config "$JAVA_OPTS -Xmx512m"
208+
# with from_environment:true produces "$JAVA_OPTS -Xmx512m $JAVA_OPTS").
209+
_stripped="${opts_content//"$_user_java_opts_placeholder"/}"
210+
_placeholder_count=$(( (${#opts_content} - ${#_stripped}) / ${#_user_java_opts_placeholder} ))
211+
for (( _i=0; _i<_placeholder_count; _i++ )); do
212+
_before="${opts_content%%%%"$_user_java_opts_placeholder"*}"
213+
_after="${opts_content#*"$_user_java_opts_placeholder"}"
214+
opts_content="${_before}${USER_JAVA_OPTS}${_after}"
215+
done
199216
200217
if [ -n "$opts_content" ]; then
201218
JAVA_OPTS="$JAVA_OPTS $opts_content"

src/java/frameworks/java_opts_writer_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,15 @@ var _ = Describe("Java Opts Writer", func() {
370370
Expect(output).To(ContainSubstring(`-Dpath=C:\\double`))
371371
})
372372

373+
It("replaces all occurrences of $JAVA_OPTS when opts file contains it twice", func() {
374+
// Scenario: JBP_CONFIG_JAVA_OPTS: '{java_opts: "$JAVA_OPTS -Xmx512m", from_environment: true}'
375+
// produces opts file: "$JAVA_OPTS -Xmx512m $JAVA_OPTS" — two placeholders
376+
output, err := runScript(`-Dfoo=bar`, "$JAVA_OPTS -Xmx512m $JAVA_OPTS")
377+
Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output)
378+
Expect(strings.Count(output, "-Dfoo=bar")).To(Equal(2))
379+
Expect(output).NotTo(ContainSubstring("__JAVA_OPTS_BUILDPACK_PLACEHOLDER__"))
380+
})
381+
373382
// Full invocation cycle tests for issue #1301:
374383
// Verify that javaexec tokenizes $JAVA_OPTS without a shell, so glob chars,
375384
// pipes, and shell metacharacters are never expanded or executed.

src/java/resources/files/tomcat/conf/server.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
<Service name='Catalina'>
2222
<Connector port='${http.port}' bindOnInit='false' connectionTimeout='20000' keepAliveTimeout='120000'>
23-
<UpgradeProtocol className='org.apache.coyote.http2.Http2Protocol' />
23+
<UpgradeProtocol className='org.apache.coyote.http2.Http2Protocol' allowSchemeMismatch='true'/>
2424
</Connector>
2525

2626
<Engine defaultHost='localhost' name='Catalina'>

0 commit comments

Comments
 (0)