This guide explains how to implement new container support in the Cloud Foundry Java Buildpack. Containers are responsible for detecting application types and configuring their runtime execution environment.
- Overview
- Container Interface
- Container Types
- Implementation Steps
- Complete Examples
- Common Patterns
- Release Command Generation
- Testing Containers
- Best Practices
- Troubleshooting
A container is a buildpack component that:
- Detects the application type (Spring Boot JAR, Tomcat WAR, Java Main, etc.)
- Supplies necessary runtime dependencies during staging
- Finalizes the application for execution (classpath, launch command, environment)
The buildpack currently supports these container types:
| Container | Detection | Application Type |
|---|---|---|
| Spring Boot | BOOT-INF directory, spring-boot-*.jar | Spring Boot JARs and exploded JARs |
| Tomcat | WEB-INF directory, *.war files | Servlet applications and WARs |
| Java Main | Main-Class manifest, *.jar files | Standalone JAR applications |
| DistZip | bin/ + lib/ directories | Gradle/Maven distributions |
| Groovy | *.groovy files | Groovy scripts |
| Play Framework | start script + playVersion file | Play Framework apps |
| Ratpack | Ratpack.class | Ratpack applications |
| Spring Boot CLI | *.groovy + Spring annotations | Spring Boot CLI apps |
Containers participate in three phases:
- Detect Phase - First container to successfully detect wins
- Supply Phase - Install runtime dependencies (Tomcat, support libraries, etc.)
- Finalize Phase - Generate launch command, set environment variables
All containers must implement this interface:
// src/java/containers/container.go
type Container interface {
Detect() (string, error) // Returns container name if detected
Supply() error // Install dependencies
Finalize() error // Configure runtime
Release() (string, error) // Generate startup command
}Containers receive a Context struct:
type Context struct {
Stager *libbuildpack.Stager // Build directory access
Manifest *libbuildpack.Manifest // Dependency versions
Installer *libbuildpack.Installer // Install dependencies
Log *libbuildpack.Logger // Logging
Command *libbuildpack.Command // Execute commands
}Key Context Methods:
// Build and deps directories
buildDir := ctx.Stager.BuildDir() // /tmp/staging
depsDir := ctx.Stager.DepDir() // /tmp/staging/deps/0
depsIdx := ctx.Stager.DepsIdx() // "0"
// Environment and profile.d scripts
ctx.Stager.WriteEnvFile("VAR", "value")
ctx.Stager.WriteProfileD("script.sh", "export VAR=value")
// Logging
ctx.Log.BeginStep("Installing Container")
ctx.Log.Info("Installed version %s", version)Run standalone JAR applications.
Examples: Spring Boot, Java Main
Detection:
- JAR files in root directory
- MANIFEST.MF with Main-Class or Spring-Boot-Version
- BOOT-INF directory (Spring Boot)
Launch: java -jar application.jar
Install and configure application servers.
Examples: Tomcat, Play Framework
Detection:
- WEB-INF directory (Tomcat)
- server/conf/ structure (Play)
Launch: Server-specific startup script or command
Execute applications via startup scripts.
Examples: DistZip, Groovy, Spring Boot CLI
Detection:
- bin/ directory with executable scripts
- Script files (*.groovy)
Launch: Execute startup script
Create src/java/containers/my_container.go:
package containers
import (
"fmt"
"os"
"path/filepath"
)
// MyContainer implements support for My application type
type MyContainer struct {
context *Context
}
// NewMyContainer creates a new instance
func NewMyContainer(ctx *Context) *MyContainer {
return &MyContainer{context: ctx}
}
// Detect checks if this is a My application
func (m *MyContainer) Detect() (string, error) {
// TODO: Implement detection
return "", nil
}
// Supply installs container dependencies
func (m *MyContainer) Supply() error {
// TODO: Implement supply
return nil
}
// Finalize configures runtime
func (m *MyContainer) Finalize() error {
// TODO: Implement finalize
return nil
}
// Release generates the command to start the application
func (m *MyContainer) Release() (string, error) {
// TODO: Implement launch command
return "", nil
}Detection determines if the application matches this container type:
File-Based Detection:
func (m *MyContainer) Detect() (string, error) {
buildDir := m.context.Stager.BuildDir()
// Check for marker file/directory
markerPath := filepath.Join(buildDir, "WEB-INF")
if _, err := os.Stat(markerPath); err == nil {
m.context.Log.Debug("Detected My application via WEB-INF directory")
return "My Container", nil
}
return "", nil
}Pattern-Based Detection:
func (m *MyContainer) Detect() (string, error) {
buildDir := m.context.Stager.BuildDir()
// Check for specific file patterns
matches, err := filepath.Glob(filepath.Join(buildDir, "*.myapp"))
if err == nil && len(matches) > 0 {
m.context.Log.Debug("Detected My application: %s", matches[0])
return "My Container", nil
}
return "", nil
}Manifest-Based Detection:
func (m *MyContainer) Detect() (string, error) {
buildDir := m.context.Stager.BuildDir()
// Read MANIFEST.MF
manifestPath := filepath.Join(buildDir, "META-INF", "MANIFEST.MF")
data, err := os.ReadFile(manifestPath)
if err != nil {
return "", nil
}
// Check for specific manifest entry
if strings.Contains(string(data), "My-Container-Version:") {
return "My Container", nil
}
return "", nil
}Install dependencies needed at runtime:
func (m *MyContainer) Supply() error {
m.context.Log.BeginStep("Supplying My Container")
// Get dependency version from manifest
dep, err := m.context.Manifest.DefaultVersion("my-server")
if err != nil {
return fmt.Errorf("unable to determine version: %w", err)
}
// Install to deps directory
serverDir := filepath.Join(m.context.Stager.DepDir(), "my_server")
if err := m.context.Installer.InstallDependency(dep, serverDir); err != nil {
return fmt.Errorf("failed to install server: %w", err)
}
m.context.Log.Info("Installed My Server version %s", dep.Version)
// Write profile.d script for runtime environment
depsIdx := m.context.Stager.DepsIdx()
envScript := fmt.Sprintf(`export MY_SERVER_HOME="$DEPS_DIR/%s/my_server"
export PATH="$MY_SERVER_HOME/bin:$PATH"
`, depsIdx)
if err := m.context.Stager.WriteProfileD("my_server.sh", envScript); err != nil {
return fmt.Errorf("failed to write profile.d script: %w", err)
}
return nil
}Configure the application for execution:
func (m *MyContainer) Finalize() error {
m.context.Log.BeginStep("Finalizing My Container")
// Build classpath
classpath, err := m.buildClasspath()
if err != nil {
return fmt.Errorf("failed to build classpath: %w", err)
}
// Write environment variables
if err := m.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
return fmt.Errorf("failed to write CLASSPATH: %w", err)
}
return nil
}
func (m *MyContainer) buildClasspath() (string, error) {
buildDir := m.context.Stager.BuildDir()
var entries []string
// Add lib directory
libDir := filepath.Join(buildDir, "lib")
if _, err := os.Stat(libDir); err == nil {
entries = append(entries, "$HOME/lib/*")
}
return strings.Join(entries, ":"), nil
}Generate the command to start the application:
func (m *MyContainer) Release() (string, error) {
buildDir := m.context.Stager.BuildDir()
// Find main JAR or script
jarFile := filepath.Join("$HOME", "application.jar")
// Build java command with options
javaOpts := os.Getenv("JAVA_OPTS")
command := fmt.Sprintf("java %s -jar %s", javaOpts, jarFile)
m.context.Log.Debug("Launch command: %s", command)
return command, nil
}Add to src/java/containers/registry.go:
func (r *Registry) RegisterAll() {
r.Register(NewSpringBootContainer(r.context))
r.Register(NewTomcatContainer(r.context))
r.Register(NewMyContainer(r.context)) // Add your container
r.Register(NewJavaMainContainer(r.context))
// ...
}Note: Container order matters! Place more specific containers before generic ones.
Create src/java/containers/my_container_test.go:
package containers_test
import (
"os"
"path/filepath"
"testing"
"github.com/cloudfoundry/java-buildpack/src/java/containers"
"github.com/cloudfoundry/libbuildpack"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("MyContainer", func() {
var (
ctx *containers.Context
buildDir string
)
BeforeEach(func() {
var err error
buildDir, err = os.MkdirTemp("", "build")
Expect(err).NotTo(HaveOccurred())
logger := libbuildpack.NewLogger(os.Stdout)
stager := libbuildpack.NewStager(
[]string{buildDir, "", "0"},
logger,
&libbuildpack.Manifest{},
)
ctx = &containers.Context{
Stager: stager,
Log: logger,
}
})
AfterEach(func() {
os.RemoveAll(buildDir)
})
Context("detection", func() {
Context("with marker file", func() {
BeforeEach(func() {
os.MkdirAll(filepath.Join(buildDir, "MY-APP"), 0755)
})
It("detects the container", func() {
container := containers.NewMyContainer(ctx)
name, err := container.Detect()
Expect(err).NotTo(HaveOccurred())
Expect(name).To(Equal("My Container"))
})
})
Context("without marker", func() {
It("does not detect", func() {
container := containers.NewMyContainer(ctx)
name, err := container.Detect()
Expect(err).NotTo(HaveOccurred())
Expect(name).To(BeEmpty())
})
})
})
})A minimal container for standalone JAR applications.
File: src/java/containers/java_main.go:1
package containers
import (
"fmt"
"os"
"path/filepath"
"strings"
)
type JavaMainContainer struct {
context *Context
mainClass string
jarFile string
}
func NewJavaMainContainer(ctx *Context) *JavaMainContainer {
return &JavaMainContainer{context: ctx}
}
// Detect: Look for JAR files or Main-Class manifest
func (j *JavaMainContainer) Detect() (string, error) {
buildDir := j.context.Stager.BuildDir()
// Look for JAR files
mainClass, jarFile := j.findMainClass(buildDir)
if mainClass != "" {
j.mainClass = mainClass
j.jarFile = jarFile
j.context.Log.Debug("Detected Java Main: %s (main: %s)", jarFile, mainClass)
return "Java Main", nil
}
// Check for META-INF/MANIFEST.MF with Main-Class
manifestPath := filepath.Join(buildDir, "META-INF", "MANIFEST.MF")
if _, err := os.Stat(manifestPath); err == nil {
if mainClass := j.readMainClassFromManifest(manifestPath); mainClass != "" {
j.mainClass = mainClass
return "Java Main", nil
}
}
// Check for compiled .class files
classFiles, _ := filepath.Glob(filepath.Join(buildDir, "*.class"))
if len(classFiles) > 0 {
return "Java Main", nil
}
return "", nil
}
func (j *JavaMainContainer) findMainClass(buildDir string) (string, string) {
entries, err := os.ReadDir(buildDir)
if err != nil {
return "", ""
}
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".jar") {
// In full implementation: extract and read MANIFEST.MF
return "Main", filepath.Join("$HOME", entry.Name())
}
}
return "", ""
}
func (j *JavaMainContainer) readMainClassFromManifest(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "Main-Class:") {
return strings.TrimSpace(strings.TrimPrefix(line, "Main-Class:"))
}
}
return ""
}
// Supply: No dependencies needed for Java Main
func (j *JavaMainContainer) Supply() error {
j.context.Log.BeginStep("Supplying Java Main")
return nil
}
// Finalize: Set up classpath
func (j *JavaMainContainer) Finalize() error {
j.context.Log.BeginStep("Finalizing Java Main")
classpath, err := j.buildClasspath()
if err != nil {
return fmt.Errorf("failed to build classpath: %w", err)
}
if err := j.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
return fmt.Errorf("failed to write CLASSPATH: %w", err)
}
return nil
}
func (j *JavaMainContainer) buildClasspath() (string, error) {
var entries []string
// Add current directory
entries = append(entries, ".")
// Add all JARs in lib/
entries = append(entries, "$HOME/lib/*")
return strings.Join(entries, ":"), nil
}
// Release: Generate java -jar or java -cp command
func (j *JavaMainContainer) Release() (string, error) {
javaOpts := os.Getenv("JAVA_OPTS")
if j.jarFile != "" {
// JAR file execution
return fmt.Sprintf("java %s -jar %s", javaOpts, j.jarFile), nil
}
if j.mainClass != "" {
// Class file execution
return fmt.Sprintf("java %s -cp $CLASSPATH %s", javaOpts, j.mainClass), nil
}
return "", fmt.Errorf("no main class or JAR file found")
}Key Points:
- ✅ Simple detection (JAR files or Main-Class)
- ✅ Minimal supply phase (no dependencies)
- ✅ Classpath configuration
- ✅ Flexible launch command (JAR or class)
Installs Tomcat server and deploys WARs.
File: src/java/containers/tomcat.go:1
package containers
import (
"fmt"
"os"
"path/filepath"
"github.com/cloudfoundry/java-buildpack/src/java/jres"
"github.com/cloudfoundry/libbuildpack"
)
type TomcatContainer struct {
context *Context
}
func NewTomcatContainer(ctx *Context) *TomcatContainer {
return &TomcatContainer{context: ctx}
}
// Detect: Look for WEB-INF or WAR files
func (t *TomcatContainer) Detect() (string, error) {
buildDir := t.context.Stager.BuildDir()
// Check for WEB-INF directory (exploded WAR)
webInf := filepath.Join(buildDir, "WEB-INF")
if _, err := os.Stat(webInf); err == nil {
t.context.Log.Debug("Detected WAR via WEB-INF directory")
return "Tomcat", nil
}
// Check for WAR files
matches, _ := filepath.Glob(filepath.Join(buildDir, "*.war"))
if len(matches) > 0 {
t.context.Log.Debug("Detected WAR file: %s", matches[0])
return "Tomcat", nil
}
return "", nil
}
// Supply: Install Tomcat server
func (t *TomcatContainer) Supply() error {
t.context.Log.BeginStep("Supplying Tomcat")
// Select Tomcat version based on Java version
javaHome := os.Getenv("JAVA_HOME")
var dep libbuildpack.Dependency
var err error
if javaHome != "" {
javaMajorVersion, _ := jres.DetermineJavaVersion(javaHome)
// Tomcat 10.x for Java 11+, Tomcat 9.x for Java 8-10
versionPattern := "9.x"
if javaMajorVersion >= 11 {
versionPattern = "10.x"
t.context.Log.Info("Using Tomcat 10.x for Java %d", javaMajorVersion)
} else {
t.context.Log.Info("Using Tomcat 9.x for Java %d", javaMajorVersion)
}
// Resolve version pattern
allVersions := t.context.Manifest.AllDependencyVersions("tomcat")
resolvedVersion, err := libbuildpack.FindMatchingVersion(versionPattern, allVersions)
if err == nil {
dep.Name = "tomcat"
dep.Version = resolvedVersion
}
}
// Fallback to default version
if dep.Version == "" {
dep, err = t.context.Manifest.DefaultVersion("tomcat")
if err != nil {
return fmt.Errorf("unable to determine Tomcat version: %w", err)
}
}
// Install Tomcat (strip top-level directory from tarball)
tomcatDir := filepath.Join(t.context.Stager.DepDir(), "tomcat")
if err := t.context.Installer.InstallDependencyWithStrip(dep, tomcatDir, 1); err != nil {
return fmt.Errorf("failed to install Tomcat: %w", err)
}
t.context.Log.Info("Installed Tomcat version %s", dep.Version)
// Write profile.d script
depsIdx := t.context.Stager.DepsIdx()
tomcatPath := fmt.Sprintf("$DEPS_DIR/%s/tomcat", depsIdx)
envScript := fmt.Sprintf(`export CATALINA_HOME=%s
export CATALINA_BASE=%s
`, tomcatPath, tomcatPath)
if err := t.context.Stager.WriteProfileD("tomcat.sh", envScript); err != nil {
return fmt.Errorf("failed to write tomcat.sh: %w", err)
}
// Install Tomcat support libraries
t.installTomcatSupport()
return nil
}
func (t *TomcatContainer) installTomcatSupport() error {
dep, err := t.context.Manifest.DefaultVersion("tomcat-lifecycle-support")
if err != nil {
return err
}
supportDir := filepath.Join(t.context.Stager.DepDir(), "tomcat-lifecycle-support")
if err := t.context.Installer.InstallDependency(dep, supportDir); err != nil {
return fmt.Errorf("failed to install Tomcat support: %w", err)
}
t.context.Log.Info("Installed Tomcat Lifecycle Support %s", dep.Version)
return nil
}
// Finalize: Configure Tomcat for application
func (t *TomcatContainer) Finalize() error {
t.context.Log.BeginStep("Finalizing Tomcat")
// Deploy application to Tomcat webapps
if err := t.deployApplication(); err != nil {
return err
}
return nil
}
func (t *TomcatContainer) deployApplication() error {
buildDir := t.context.Stager.BuildDir()
tomcatDir := filepath.Join(t.context.Stager.DepDir(), "tomcat")
webappsDir := filepath.Join(tomcatDir, "webapps", "ROOT")
// Copy application to webapps/ROOT
if err := os.MkdirAll(webappsDir, 0755); err != nil {
return fmt.Errorf("failed to create webapps directory: %w", err)
}
// Copy WEB-INF and other files
// (Implementation would recursively copy files)
t.context.Log.Debug("Deployed application to Tomcat webapps/ROOT")
return nil
}
// Release: Start Tomcat
func (t *TomcatContainer) Release() (string, error) {
depsIdx := t.context.Stager.DepsIdx()
catalinaHome := fmt.Sprintf("$DEPS_DIR/%s/tomcat", depsIdx)
command := fmt.Sprintf("%s/bin/catalina.sh run", catalinaHome)
return command, nil
}Key Points:
- ✅ Version selection based on Java version
- ✅ Installs Tomcat server during Supply
- ✅ Deploys application to webapps/ROOT
- ✅ Launches Tomcat with catalina.sh
Handles Spring Boot executable JARs.
File: src/java/containers/spring_boot.go:1
package containers
import (
"fmt"
"os"
"path/filepath"
"strings"
)
type SpringBootContainer struct {
context *Context
jarFile string
startScript string
}
func NewSpringBootContainer(ctx *Context) *SpringBootContainer {
return &SpringBootContainer{context: ctx}
}
// Detect: Multiple detection strategies
func (s *SpringBootContainer) Detect() (string, error) {
buildDir := s.context.Stager.BuildDir()
// Strategy 1: BOOT-INF directory (exploded Spring Boot JAR)
bootInf := filepath.Join(buildDir, "BOOT-INF")
if _, err := os.Stat(bootInf); err == nil {
if s.isSpringBootExplodedJar(buildDir) {
s.context.Log.Debug("Detected Spring Boot via BOOT-INF")
return "Spring Boot", nil
}
}
// Strategy 2: Spring Boot JAR in root
jarFile, err := s.findSpringBootJar(buildDir)
if err == nil && jarFile != "" {
s.jarFile = jarFile
s.context.Log.Debug("Detected Spring Boot JAR: %s", jarFile)
return "Spring Boot", nil
}
// Strategy 3: Staged application (bin/ + lib/ with spring-boot-*.jar)
if s.hasSpringBootInLib(buildDir) {
startScript, _ := s.findStartupScript(buildDir)
if startScript != "" {
s.startScript = startScript
s.context.Log.Debug("Detected staged Spring Boot app: %s", startScript)
return "Spring Boot", nil
}
}
return "", nil
}
func (s *SpringBootContainer) isSpringBootExplodedJar(buildDir string) bool {
manifestPath := filepath.Join(buildDir, "META-INF", "MANIFEST.MF")
data, err := os.ReadFile(manifestPath)
if err != nil {
return false
}
content := string(data)
return strings.Contains(content, "Spring-Boot-Version:") ||
strings.Contains(content, "Start-Class:")
}
func (s *SpringBootContainer) findSpringBootJar(buildDir string) (string, error) {
entries, err := os.ReadDir(buildDir)
if err != nil {
return "", err
}
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".jar") {
jarPath := filepath.Join(buildDir, entry.Name())
if s.isSpringBootJar(jarPath) {
return filepath.Join("$HOME", entry.Name()), nil
}
}
}
return "", nil
}
func (s *SpringBootContainer) isSpringBootJar(jarPath string) bool {
// Check file name patterns
name := filepath.Base(jarPath)
return strings.Contains(strings.ToLower(name), "spring") ||
strings.Contains(strings.ToLower(name), "boot")
}
func (s *SpringBootContainer) hasSpringBootInLib(buildDir string) bool {
libDirs := []string{
filepath.Join(buildDir, "lib"),
filepath.Join(buildDir, "WEB-INF", "lib"),
filepath.Join(buildDir, "BOOT-INF", "lib"),
}
for _, libDir := range libDirs {
entries, err := os.ReadDir(libDir)
if err != nil {
continue
}
for _, entry := range entries {
name := entry.Name()
if strings.HasPrefix(name, "spring-boot-") && strings.HasSuffix(name, ".jar") {
return true
}
}
}
return false
}
func (s *SpringBootContainer) findStartupScript(buildDir string) (string, error) {
binDir := filepath.Join(buildDir, "bin")
entries, err := os.ReadDir(binDir)
if err != nil {
return "", err
}
for _, entry := range entries {
if !entry.IsDir() && filepath.Ext(entry.Name()) != ".bat" {
return entry.Name(), nil
}
}
return "", fmt.Errorf("no startup script found")
}
// Supply: No dependencies needed
func (s *SpringBootContainer) Supply() error {
s.context.Log.BeginStep("Supplying Spring Boot")
return nil
}
// Finalize: Minimal configuration
func (s *SpringBootContainer) Finalize() error {
s.context.Log.BeginStep("Finalizing Spring Boot")
// Spring Boot apps are self-contained
// No additional configuration needed
return nil
}
// Release: Execute Spring Boot JAR or script
func (s *SpringBootContainer) Release() (string, error) {
javaOpts := os.Getenv("JAVA_OPTS")
// JAR file execution
if s.jarFile != "" {
return fmt.Sprintf("java %s -jar %s", javaOpts, s.jarFile), nil
}
// Staged app execution (via bin/ script)
if s.startScript != "" {
return fmt.Sprintf("$HOME/bin/%s", s.startScript), nil
}
// Exploded JAR execution
return fmt.Sprintf("java %s org.springframework.boot.loader.JarLauncher", javaOpts), nil
}Key Points:
- ✅ Multiple detection strategies (BOOT-INF, JAR, staged)
- ✅ Self-contained (no dependencies to install)
- ✅ Flexible launch (JAR, script, or JarLauncher)
- ✅ Handles various Spring Boot packaging formats
func (c *MyContainer) Detect() (string, error) {
buildDir := c.context.Stager.BuildDir()
// Check for specific directory
markerDir := filepath.Join(buildDir, "WEB-INF")
if _, err := os.Stat(markerDir); err == nil {
return "My Container", nil
}
return "", nil
}func (c *MyContainer) Supply() error {
// Get version from manifest
dep, err := c.context.Manifest.DefaultVersion("my-server")
if err != nil {
return fmt.Errorf("unable to determine version: %w", err)
}
// Install with strip (removes top-level directory from tarball)
serverDir := filepath.Join(c.context.Stager.DepDir(), "my_server")
if err := c.context.Installer.InstallDependencyWithStrip(dep, serverDir, 1); err != nil {
return fmt.Errorf("failed to install: %w", err)
}
return nil
}func (c *MyContainer) Supply() error {
depsIdx := c.context.Stager.DepsIdx()
script := fmt.Sprintf(`export MY_HOME="$DEPS_DIR/%s/my_server"
export PATH="$MY_HOME/bin:$PATH"
`, depsIdx)
return c.context.Stager.WriteProfileD("my_container.sh", script)
}func (c *MyContainer) buildClasspath() (string, error) {
var entries []string
// Add current directory
entries = append(entries, ".")
// Add lib directory
entries = append(entries, "$HOME/lib/*")
// Add BOOT-INF directories (if present)
entries = append(entries, "$HOME/BOOT-INF/classes")
entries = append(entries, "$HOME/BOOT-INF/lib/*")
return strings.Join(entries, ":"), nil
}func (c *MyContainer) readManifest(manifestPath string) map[string]string {
data, err := os.ReadFile(manifestPath)
if err != nil {
return nil
}
manifest := make(map[string]string)
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if strings.Contains(line, ":") {
parts := strings.SplitN(line, ":", 2)
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
manifest[key] = value
}
}
return manifest
}func (c *MyContainer) Release() (string, error) {
javaOpts := os.Getenv("JAVA_OPTS")
jarFile := "$HOME/application.jar"
return fmt.Sprintf("java %s -jar %s", javaOpts, jarFile), nil
}func (c *MyContainer) Release() (string, error) {
depsIdx := c.context.Stager.DepsIdx()
serverHome := fmt.Sprintf("$DEPS_DIR/%s/server", depsIdx)
return fmt.Sprintf("%s/bin/start.sh", serverHome), nil
}func (c *MyContainer) Release() (string, error) {
javaOpts := os.Getenv("JAVA_OPTS")
mainClass := c.mainClass
return fmt.Sprintf("java %s -cp $CLASSPATH %s", javaOpts, mainClass), nil
}func (c *MyContainer) Release() (string, error) {
scriptName := c.findScript()
return fmt.Sprintf("$HOME/bin/%s", scriptName), nil
}package containers_test
import (
"os"
"path/filepath"
"testing"
"github.com/cloudfoundry/java-buildpack/src/java/containers"
"github.com/cloudfoundry/libbuildpack"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestContainers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Containers Suite")
}
var _ = Describe("MyContainer", func() {
var (
ctx *containers.Context
buildDir string
depsDir string
)
BeforeEach(func() {
var err error
buildDir, err = os.MkdirTemp("", "build")
Expect(err).NotTo(HaveOccurred())
depsDir, err = os.MkdirTemp("", "deps")
Expect(err).NotTo(HaveOccurred())
logger := libbuildpack.NewLogger(os.Stdout)
stager := libbuildpack.NewStager(
[]string{buildDir, "", depsDir, "0"},
logger,
&libbuildpack.Manifest{},
)
ctx = &containers.Context{
Stager: stager,
Log: logger,
}
})
AfterEach(func() {
os.RemoveAll(buildDir)
os.RemoveAll(depsDir)
})
Describe("Detection", func() {
Context("with valid application", func() {
BeforeEach(func() {
// Create application structure
os.MkdirAll(filepath.Join(buildDir, "MY-APP"), 0755)
})
It("detects the container", func() {
container := containers.NewMyContainer(ctx)
name, err := container.Detect()
Expect(err).NotTo(HaveOccurred())
Expect(name).To(Equal("My Container"))
})
})
Context("without markers", func() {
It("does not detect", func() {
container := containers.NewMyContainer(ctx)
name, err := container.Detect()
Expect(err).NotTo(HaveOccurred())
Expect(name).To(BeEmpty())
})
})
})
Describe("Release Command", func() {
It("generates correct command", func() {
container := containers.NewMyContainer(ctx)
command, err := container.Release()
Expect(err).NotTo(HaveOccurred())
Expect(command).To(ContainSubstring("java"))
})
})
})Integration tests deploy real applications. See docs/TESTING.md for details.
Make detection as specific as possible to avoid false positives:
// GOOD - Multiple checks
func (c *MyContainer) Detect() (string, error) {
hasMarkerDir := c.hasMarkerDir()
hasRequiredJar := c.hasRequiredJar()
if hasMarkerDir && hasRequiredJar {
return "My Container", nil
}
return "", nil
}
// BAD - Too generic
func (c *MyContainer) Detect() (string, error) {
// Detects any JAR file
matches, _ := filepath.Glob("*.jar")
if len(matches) > 0 {
return "My Container", nil
}
return "", nil
}Register more specific containers before generic ones:
// GOOD order
r.Register(NewSpringBootContainer(r.context)) // Specific
r.Register(NewTomcatContainer(r.context)) // Specific
r.Register(NewJavaMainContainer(r.context)) // Generic (fallback)
// BAD order - JavaMain would detect everything
r.Register(NewJavaMainContainer(r.context)) // Too generic, runs first
r.Register(NewSpringBootContainer(r.context)) // Never reached!Use $DEPS_DIR and $HOME variables for paths:
// GOOD - Uses runtime variables
tomcatPath := fmt.Sprintf("$DEPS_DIR/%s/tomcat", depsIdx)
// BAD - Hardcoded staging paths
tomcatPath := "/tmp/staging/deps/0/tomcat" // Won't work at runtime!Only install what's necessary:
// GOOD - Only installs if needed
func (c *MyContainer) Supply() error {
if c.needsServer() {
return c.installServer()
}
return nil
}
// BAD - Installs everything
func (c *MyContainer) Supply() error {
c.installServer()
c.installSupport()
c.installUtilities()
// ... too much
}Log what's happening at each phase:
c.context.Log.BeginStep("Installing Tomcat") // Major steps
c.context.Log.Info("Installed version %s", ver) // Important info
c.context.Log.Debug("Found file: %s", path) // Debug details
c.context.Log.Warning("Feature disabled") // WarningsCheck:
- Is detection logic correct? Add debug logging
- Are required files present? Check with
cf files - Is container registered in registry?
- Is another container detecting first? Check order
Check:
- Is dependency in manifest? Check
manifest.yml - Is download URL accessible?
- Are permissions correct (0755 for directories)?
- Check logs:
cf logs my-app --recent
Check:
- Are paths using runtime variables (
$DEPS_DIR,$HOME)? - Is classpath correct? Check
CLASSPATHenv var - Is
JAVA_OPTSset correctly? - Test command:
cf ssh my-appthen manually run command
Problem: Generic container detecting before specific one
Solution: Reorder container registration - specific before generic
- Implementing JREs - Add new JRE providers
- Implementing Frameworks - Add framework integrations
- Testing Guide - Comprehensive testing strategies
- Architecture - Understand buildpack design
- Contributing - Contribution guidelines
Study these existing containers:
Simple Containers:
java_main.go- Standalone JAR applicationsgroovy.go- Groovy script execution
Server Containers:
tomcat.go- Servlet container with server installationplay.go- Play Framework with native packager
Complex Containers:
spring_boot.go- Multiple detection strategiesdist_zip.go- Gradle/Maven distribution handling
All container implementations: src/java/containers/