Skip to content

Commit 22e272d

Browse files
committed
fix: emit task process type in release YAML for cf run-task support
Ruby buildpack v4 declares both web and task in default_process_types. Go buildpack only declared web, causing cf run-task to fail with "command presence FAILED" when no --command is given. - Add task process type with same command as web, matching Ruby behaviour - Escape single quotes in command (YAML single-quoted scalar requires '') - Add tests: web+task both present, single-quote YAML round-trip (TDD) - Add docs: CF task usage for Spring Boot and Java Main containers, including PropertiesLauncher/-Dloader.main pattern for per-task class Closes #1323
1 parent 4787943 commit 22e272d

4 files changed

Lines changed: 84 additions & 4 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

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: 49 additions & 3 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() {
@@ -99,7 +101,7 @@ dependencies: []
99101

100102
// Create META-INF/MANIFEST.MF with corresponding content of a Spring Boot app
101103
manifestFile := filepath.Join(buildDir, "META-INF", "MANIFEST.MF")
102-
Expect(os.WriteFile(manifestFile, []byte("Spring-Boot-Version: 3.x.x"), 0644)).To(Succeed())
104+
Expect(os.WriteFile(manifestFile, []byte("Spring-Boot-Version: 4.x.x"), 0644)).To(Succeed())
103105

104106
finalizer.JREName = "OpenJDK"
105107
finalizer.ContainerName = "Spring Boot"
@@ -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.

0 commit comments

Comments
 (0)