diff --git a/docs/container-java_main.md b/docs/container-java_main.md index f2bcfc92b..fa0a94346 100644 --- a/docs/container-java_main.md +++ b/docs/container-java_main.md @@ -25,6 +25,25 @@ If the application uses Spring, [Spring profiles][] can be specified by setting 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. +## CF Tasks + +The buildpack emits both `web` and `task` process types with the same command so `cf run-task` works without `--command`. + +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: + +```yaml +env: + JBP_CONFIG_JAVA_MAIN: '{java_main_class: "org.springframework.boot.loader.launch.PropertiesLauncher"}' +``` + +Then override the main class per task at run time (requires CF CLI v7+): + +```bash +cf run-task my-app --env JAVA_OPTS="-Dloader.main=com.example.BatchJob" +``` + +`-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. + ## Configuration For general information on configuring the buildpack, including how to specify configuration values through environment variables, refer to [Configuration and Extension][]. diff --git a/docs/container-spring_boot.md b/docs/container-spring_boot.md index 25d019bb6..17c8833ac 100644 --- a/docs/container-spring_boot.md +++ b/docs/container-spring_boot.md @@ -17,6 +17,19 @@ The container expects to run the application creating by running [`gradle distZi 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. +## CF Tasks + +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`. + +```bash +cf run-task my-app # uses the task process type command +cf run-task my-app --command "..." # explicit override +``` + +To run a task with a different main class (batch job, migration, etc.), see [Java Main Container - CF Tasks][java-main-tasks]. + +[java-main-tasks]: container-java_main.md#cf-tasks + ## Configuration The Spring Boot Container cannot be configured. diff --git a/docs/debugging-the-buildpack.md b/docs/debugging-the-buildpack.md index 425386f99..93ccd7618 100644 --- a/docs/debugging-the-buildpack.md +++ b/docs/debugging-the-buildpack.md @@ -86,7 +86,7 @@ Sometimes logging just isn't going to cut it for debugging. There are times when ### Requirements -The buildpack API consists of three bash scripts. This means that if you've got a Unix like environment with Ruby installed at an appropriate level, a filesystem that looks like what Cloud Foundry will present to the buildpack and a local copy of your buildpack, you can run the bash scripts locally. +The buildpack API consists of bash scripts. This means that if you've got a Unix like environment with a filesystem that looks like what Cloud Foundry will present to the buildpack and a local copy of your buildpack, you can run the bash scripts locally. ### Example invocation @@ -110,7 +110,7 @@ $ $BUILDPACK_ROOT/bin/compile . $TMPDIR -----> Downloading Play Framework Auto Reconfiguration 1.4.0_RELEASE from https://java-buildpack.cloudfoundry.org/auto-reconfiguration/auto-reconfiguration-1.4.0_RELEASE.jar (found in cache) -----> Downloading Spring Auto Reconfiguration 1.4.0_RELEASE from https://java-buildpack.cloudfoundry.org/auto-reconfiguration/auto-reconfiguration-1.4.0_RELEASE.jar (found in cache) -$ $BUILDPACK_ROOT/bin/release . | ruby -e "require \"yaml\"; puts YAML.load(STDIN.read)[\"default_process_types\"][\"web\"]" +$ $BUILDPACK_ROOT/bin/release . | sed -n "s/^ web: '\\(.*\\)'$/\\1/p" | sed "s/''/'/g; s/exec //" PATH=$PWD/.java-buildpack/open_jdk_jre/bin:$PATH JAVA_HOME=$PWD/.java-buildpack/open_jdk_jre $PWD/play-application-1.0.0.BUILD-SNAPSHOT/bin/play-application -J-Djava.io.tmpdir=$TMPDIR -J-XX:MaxPermSize=64M -J-XX:PermSize=64M -J-javaagent:$PWD/.java-buildpack/app_dynamics_agent/javaagent.jar -J-Dappdynamics.agent.applicationName='' -J-Dappdynamics.agent.tierName='Cloud Foundry' -J-Dappdynamics.agent.nodeName=$(expr "$VCAP_APPLICATION" : '.*instance_id[": ]*"\([a-z0-9]\+\)".*') -J-Dappdynamics.agent.accountAccessKey=[REDACTED] -J-Dappdynamics.agent.accountName=[REDACTED] -J-Dappdynamics.controller.hostName=[REDACTED] -J-Dappdynamics.controller.port=443 -J-Dappdynamics.controller.ssl.enabled=true -J-Dhttp.port=$PORT ``` @@ -131,7 +131,7 @@ Running the different stages of the buildpack lifecycle can be made simpler with ```bash $ alias detect='$BUILDPACK_ROOT/bin/detect .' $ alias compile='$BUILDPACK_ROOT/bin/compile . $TMPDIR' -$ alias release='$BUILDPACK_ROOT/bin/release . | ruby -e "require \"yaml\"; puts YAML.load(STDIN.read)[\"default_process_types\"][\"web\"]" | sed "s| exec||"' +$ release() { $BUILDPACK_ROOT/bin/release . | sed -n "s/^ web: '\\(.*\\)'$/\\1/p" | sed "s/''/'/g; s/exec //"; } ``` [d]: extending-logging.md#configuration diff --git a/src/integration/README.md b/src/integration/README.md index e4580ba09..c9945cefc 100644 --- a/src/integration/README.md +++ b/src/integration/README.md @@ -24,10 +24,10 @@ Switchblade is a Go-based integration testing framework that supports both Cloud First, create a buildpack zip file: ```bash -bundle exec rake package +./scripts/package.sh ``` -This will create a file like `java-buildpack-v4.x.x.zip` in the project root. +This will create `build/buildpack.zip` in the project root. ### Run Integration Tests diff --git a/src/java/finalize/finalize.go b/src/java/finalize/finalize.go index 16b50598f..264f5aaba 100644 --- a/src/java/finalize/finalize.go +++ b/src/java/finalize/finalize.go @@ -280,10 +280,12 @@ func (f *Finalizer) writeReleaseYaml(container containers.Container) error { } releaseYamlPath := filepath.Join(tmpDir, "java-buildpack-release-step.yml") + escapedCommand := strings.ReplaceAll(fullCommand, "'", "''") yamlContent := fmt.Sprintf(`--- default_process_types: web: '%s' -`, fullCommand) + task: '%s' +`, escapedCommand, escapedCommand) if err := os.WriteFile(releaseYamlPath, []byte(yamlContent), 0644); err != nil { return fmt.Errorf("failed to write release YAML: %w", err) diff --git a/src/java/finalize/finalize_test.go b/src/java/finalize/finalize_test.go index d756f2c8d..56eb34e5b 100644 --- a/src/java/finalize/finalize_test.go +++ b/src/java/finalize/finalize_test.go @@ -1,16 +1,18 @@ package finalize_test import ( - "github.com/cloudfoundry/java-buildpack/src/internal/mocks" - "github.com/golang/mock/gomock" "os" "path/filepath" + "regexp" "time" + "github.com/cloudfoundry/java-buildpack/src/internal/mocks" "github.com/cloudfoundry/java-buildpack/src/java/finalize" "github.com/cloudfoundry/libbuildpack" + "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "gopkg.in/yaml.v2" ) var _ = Describe("Finalize", func() { @@ -99,7 +101,7 @@ dependencies: [] // Create META-INF/MANIFEST.MF with corresponding content of a Spring Boot app manifestFile := filepath.Join(buildDir, "META-INF", "MANIFEST.MF") - Expect(os.WriteFile(manifestFile, []byte("Spring-Boot-Version: 3.x.x"), 0644)).To(Succeed()) + Expect(os.WriteFile(manifestFile, []byte("Spring-Boot-Version: 4.x.x"), 0644)).To(Succeed()) finalizer.JREName = "OpenJDK" finalizer.ContainerName = "Spring Boot" @@ -243,6 +245,50 @@ dependencies: [] }) }) + Describe("Release YAML", func() { + BeforeEach(func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", "{java_main_class: com.example.App}") + finalizer.JREName = "OpenJDK" + finalizer.ContainerName = "Java Main" + }) + + AfterEach(func() { + os.Unsetenv("JBP_CONFIG_JAVA_MAIN") + }) + + It("emits web and task process types with identical commands", func() { + Expect(finalize.Run(finalizer)).To(Succeed()) + content, err := os.ReadFile(filepath.Join(buildDir, "tmp", "java-buildpack-release-step.yml")) + Expect(err).NotTo(HaveOccurred()) + + re := regexp.MustCompile(`(?m)^\s+(web|task):\s+'(.+)'$`) + matches := re.FindAllStringSubmatch(string(content), -1) + Expect(matches).To(HaveLen(2), "expected both web and task process types") + + commands := map[string]string{} + for _, m := range matches { + commands[m[1]] = m[2] + } + Expect(commands).To(HaveKey("web")) + Expect(commands).To(HaveKey("task")) + Expect(commands["web"]).To(Equal(commands["task"])) + }) + + It("escapes single quotes in commands so release YAML is valid", func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", `{java_main_class: com.example.App, arguments: "--message=it's alive"}`) + Expect(finalize.Run(finalizer)).To(Succeed()) + content, err := os.ReadFile(filepath.Join(buildDir, "tmp", "java-buildpack-release-step.yml")) + Expect(err).NotTo(HaveOccurred()) + + var parsed struct { + DefaultProcessTypes map[string]string `yaml:"default_process_types"` + } + Expect(yaml.Unmarshal(content, &parsed)).To(Succeed(), "release YAML must be valid") + Expect(parsed.DefaultProcessTypes["web"]).To(ContainSubstring("it's alive")) + Expect(parsed.DefaultProcessTypes["task"]).To(ContainSubstring("it's alive")) + }) + }) + Describe("javaexec launcher installation", func() { It("installs launcher from buildpack bin/ for packaged buildpack usage", func() { // Default path: /bin/javaexec already written in BeforeEach.