This guide explains how to implement new framework support in the Cloud Foundry Java Buildpack. Frameworks provide additional capabilities to Java applications, such as APM agents, security providers, profilers, and runtime enhancements.
- Overview
- Framework Interface
- Framework Types
- Implementation Steps
- Complete Examples
- Common Patterns
- Testing Frameworks
- Configuration
- Best Practices
- Troubleshooting
A framework is a buildpack component that adds functionality to Java applications at runtime. Examples include:
- APM Agents: New Relic, AppDynamics, Dynatrace, DataDog
- Security Providers: Luna HSM, Seeker IAST, Container Security Provider
- Profilers: JProfiler, YourKit
- Debugging Tools: Java Debug Wire Protocol (JDWP)
- Database Drivers: PostgreSQL JDBC, MariaDB JDBC
- Utilities: JMX, Java Options, Logging configuration
Frameworks participate in three phases of the buildpack lifecycle:
- Detect Phase - Determine if the framework should be included
- Supply Phase - Download and install framework dependencies (during staging)
- Finalize Phase - Configure the framework for runtime (write environment variables, profile.d scripts)
To implement a new framework, you need:
- Implementation:
src/java/frameworks/my_framework.go - Tests:
src/java/frameworks/my_framework_test.go - Configuration:
config/my_framework.yml - Documentation:
docs/framework-my_framework.md - Registration: Add to
config/components.yml
All frameworks must implement this interface:
// src/java/frameworks/framework.go
type Framework interface {
Detect() (string, error) // Returns detection tag if included, empty string if not
Supply() error // Install dependencies during staging
Finalize() error // Configure for runtime
}Frameworks receive a Context struct with access to buildpack services:
type Context struct {
Stager *libbuildpack.Stager // Build directory, deps directory access
Manifest *libbuildpack.Manifest // Buildpack manifest with dependency versions
Installer *libbuildpack.Installer // Download and install dependencies
Log *libbuildpack.Logger // Logging
Command *libbuildpack.Command // Execute shell commands
}Key Context Methods:
// Get build directory (staging directory during supply, /home/vcap/app at runtime)
buildDir := ctx.Stager.BuildDir()
// Get deps directory (where framework dependencies are installed)
depsDir := ctx.Stager.DepDir()
// Write environment variable to .profile.d/
ctx.Stager.WriteEnvFile("MY_VAR", "value")
// Write profile.d script (executed before app starts)
ctx.Stager.WriteProfileD("my_framework.sh", "export MY_VAR=value")
// Log messages
ctx.Log.BeginStep("Installing My Framework")
ctx.Log.Info("Installed version %s", version)
ctx.Log.Warning("Optional feature not available")
ctx.Log.Debug("Debug information")
// Get dependency version from manifest
dep, err := ctx.Manifest.DefaultVersion("my-framework")
// Install dependency
err := ctx.Installer.InstallDependency(dep, targetDir)Detect when a specific Cloud Foundry service is bound via VCAP_SERVICES.
Examples: New Relic, AppDynamics, Seeker Security Provider
Detection: Looks for service name/label/tags in VCAP_SERVICES
Enable/disable via environment variable or configuration.
Examples: Debug, JMX, Java Memory Assistant
Detection: Checks JBP_CONFIG_* or BPL_* environment variables
Detect based on files present in the application.
Examples: Container Customizer (detects Spring Boot WARs)
Detection: Checks for specific files/directories in build directory
Always available or conditionally enabled by configuration.
Examples: PostgreSQL JDBC, Java Options
Detection: Usually enabled if configuration allows
Create src/java/frameworks/my_framework.go:
package frameworks
import (
"fmt"
"os"
"path/filepath"
)
// MyFramework implements ...
type MyFramework struct {
context *Context
}
// NewMyFramework creates a new instance
func NewMyFramework(ctx *Context) *MyFramework {
return &MyFramework{context: ctx}
}
// Detect checks if framework should be included
func (m *MyFramework) Detect() (string, error) {
// TODO: Implement detection logic
return "", nil
}
// Supply installs framework dependencies
func (m *MyFramework) Supply() error {
// TODO: Implement supply phase
return nil
}
// Finalize configures framework for runtime
func (m *MyFramework) Finalize() error {
// TODO: Implement finalize phase
return nil
}Choose the appropriate detection pattern based on your framework type:
Service-Bound Detection:
func (m *MyFramework) Detect() (string, error) {
vcapServices, err := GetVCAPServices()
if err != nil {
return "", nil
}
if !vcapServices.HasService("my-service") {
return "", nil
}
// Verify required credentials
service := vcapServices.GetService("my-service")
if service == nil {
return "", nil
}
apiKey, ok := service.Credentials["api_key"].(string)
if !ok || apiKey == "" {
return "", nil
}
return "my-framework", nil
}Configuration-Based Detection:
func (m *MyFramework) Detect() (string, error) {
enabled := os.Getenv("JBP_CONFIG_MY_FRAMEWORK")
if enabled == "" {
return "", nil // Not configured
}
// Parse config to check enabled flag
if contains(enabled, "enabled: true") {
return "my-framework", nil
}
return "", nil
}File-Based Detection:
func (m *MyFramework) Detect() (string, error) {
buildDir := m.context.Stager.BuildDir()
markerFile := filepath.Join(buildDir, "META-INF", "my-marker.xml")
if _, err := os.Stat(markerFile); err == nil {
return "my-framework", nil
}
return "", nil
}Download and install framework dependencies:
func (m *MyFramework) Supply() error {
m.context.Log.BeginStep("Installing My Framework")
// Get version from manifest
dep, err := m.context.Manifest.DefaultVersion("my-framework")
if err != nil {
return fmt.Errorf("unable to determine version: %w", err)
}
// Create target directory in deps
targetDir := filepath.Join(m.context.Stager.DepDir(), "my_framework")
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Download and extract dependency
if err := m.context.Installer.InstallDependency(dep, targetDir); err != nil {
return fmt.Errorf("failed to install: %w", err)
}
m.context.Log.Info("Installed My Framework version %s", dep.Version)
return nil
}Configure the framework for runtime execution:
func (m *MyFramework) Finalize() error {
// Find installed agent JAR
frameworkDir := filepath.Join(m.context.Stager.DepDir(), "my_framework")
jarPath := filepath.Join(frameworkDir, "my-agent.jar")
// Write profile.d script to configure at runtime
depsIdx := m.context.Stager.DepsIdx()
profileScript := fmt.Sprintf(`#!/bin/bash
# My Framework Configuration
export MY_FRAMEWORK_HOME="$DEPS_DIR/%s/my_framework"
export JAVA_OPTS="${JAVA_OPTS} -javaagent:%s"
`, depsIdx, jarPath)
if err := m.context.Stager.WriteProfileD("my_framework.sh", profileScript); err != nil {
return fmt.Errorf("failed to write profile.d script: %w", err)
}
m.context.Log.Info("Configured My Framework")
return nil
}Add to config/components.yml:
frameworks:
- "JavaBuildpack::Framework::AppDynamicsAgent"
- "JavaBuildpack::Framework::MyFramework" # Add your framework
- "JavaBuildpack::Framework::NewRelicAgent"Note: The component names still use Ruby-style class names for compatibility. The Go implementation maps these to the corresponding Go constructors.
Create config/my_framework.yml:
# Cloud Foundry Java Buildpack config for My Framework
---
enabled: true
version: 1.+
repository_root: "{default.repository.root}/my-framework"Create src/java/frameworks/my_framework_test.go:
package frameworks_test
import (
"os"
"testing"
"github.com/cloudfoundry/java-buildpack/src/java/frameworks"
"github.com/cloudfoundry/libbuildpack"
)
func TestMyFrameworkDetect(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test-*")
defer os.RemoveAll(tmpDir)
logger := libbuildpack.NewLogger(os.Stdout)
stager := libbuildpack.NewStager([]string{tmpDir, "", "0"}, logger, &libbuildpack.Manifest{})
ctx := &frameworks.Context{
Stager: stager,
Log: logger,
}
framework := frameworks.NewMyFramework(ctx)
// Test without service
name, err := framework.Detect()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if name != "" {
t.Errorf("Expected no detection, got: %s", name)
}
// Test with service
vcapJSON := `{
"my-service": [{
"name": "my-service-instance",
"credentials": {"api_key": "test-key"}
}]
}`
os.Setenv("VCAP_SERVICES", vcapJSON)
defer os.Unsetenv("VCAP_SERVICES")
name, err = framework.Detect()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if name != "my-framework" {
t.Errorf("Expected 'my-framework', got: %s", name)
}
}Create docs/framework-my_framework.md with usage instructions, configuration options, and examples.
The Debug framework is the simplest example - it enables Java debugging based on configuration.
File: src/java/frameworks/debug.go:1
package frameworks
import (
"fmt"
"os"
"strconv"
)
type DebugFramework struct {
context *Context
}
func NewDebugFramework(ctx *Context) *DebugFramework {
return &DebugFramework{context: ctx}
}
// Detect: Check if debug is enabled in configuration
func (d *DebugFramework) Detect() (string, error) {
if !d.isEnabled() {
return "", nil
}
port := d.getPort()
return fmt.Sprintf("debug=%d", port), nil
}
// Supply: Log that debugging will be enabled
func (d *DebugFramework) Supply() error {
if !d.isEnabled() {
return nil
}
port := d.getPort()
suspend := d.getSuspend()
suspendMsg := ""
if suspend {
suspendMsg = ", suspended on start"
}
d.context.Log.BeginStep("Debugging enabled on port %d%s", port, suspendMsg)
return nil
}
// Finalize: Add JDWP agent options to JAVA_OPTS
func (d *DebugFramework) Finalize() error {
if !d.isEnabled() {
return nil
}
port := d.getPort()
suspend := d.getSuspend()
suspendValue := "n"
if suspend {
suspendValue = "y"
}
debugOpts := fmt.Sprintf(
"-agentlib:jdwp=transport=dt_socket,server=y,address=%d,suspend=%s",
port, suspendValue,
)
// Add to JAVA_OPTS
javaOpts := os.Getenv("JAVA_OPTS")
if javaOpts != "" {
javaOpts += " "
}
javaOpts += debugOpts
if err := d.context.Stager.WriteEnvFile("JAVA_OPTS", javaOpts); err != nil {
return fmt.Errorf("failed to set JAVA_OPTS: %w", err)
}
return nil
}
// Helper: Check if debugging is enabled
func (d *DebugFramework) isEnabled() bool {
// Check BPL_DEBUG_ENABLED (Cloud Native Buildpacks convention)
bplEnabled := os.Getenv("BPL_DEBUG_ENABLED")
if bplEnabled == "true" || bplEnabled == "1" {
return true
}
// Check JBP_CONFIG_DEBUG (Java Buildpack convention)
config := os.Getenv("JBP_CONFIG_DEBUG")
if contains(config, "enabled: true") {
return true
}
return false
}
// Helper: Get debug port (default 8000)
func (d *DebugFramework) getPort() int {
if port := os.Getenv("BPL_DEBUG_PORT"); port != "" {
if p, err := strconv.Atoi(port); err == nil && p > 0 {
return p
}
}
return 8000
}
// Helper: Check if JVM should suspend on start
func (d *DebugFramework) getSuspend() bool {
config := os.Getenv("JBP_CONFIG_DEBUG")
return contains(config, "suspend: true")
}Key Points:
- ✅ Simple configuration-based detection
- ✅ No dependencies to download (Supply does minimal work)
- ✅ Finalize adds JVM options to enable debugging
- ✅ Respects multiple configuration conventions (BPL_, JBP_CONFIG_)
The Container Customizer detects Spring Boot WAR applications and adds Tomcat customization support.
File: src/java/frameworks/container_customizer.go:1
package frameworks
import (
"fmt"
"os"
"path/filepath"
)
type ContainerCustomizerFramework struct {
context *Context
}
func NewContainerCustomizerFramework(ctx *Context) *ContainerCustomizerFramework {
return &ContainerCustomizerFramework{context: ctx}
}
// Detect: Check for Spring Boot WAR structure
func (c *ContainerCustomizerFramework) Detect() (string, error) {
buildDir := c.context.Stager.BuildDir()
// Spring Boot WARs have both WEB-INF and BOOT-INF directories
webInfPath := filepath.Join(buildDir, "WEB-INF")
bootInfPath := filepath.Join(buildDir, "BOOT-INF")
webInfStat, webInfErr := os.Stat(webInfPath)
bootInfStat, bootInfErr := os.Stat(bootInfPath)
if webInfErr == nil && webInfStat.IsDir() &&
bootInfErr == nil && bootInfStat.IsDir() {
// Verify it's actually Spring Boot
if c.hasSpringBootJars(buildDir) {
return "Container Customizer", nil
}
}
return "", nil
}
// Helper: Check for spring-boot-*.jar files
func (c *ContainerCustomizerFramework) hasSpringBootJars(buildDir string) bool {
libDirs := []string{
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 {
if filepath.Ext(entry.Name()) == ".jar" &&
strings.Contains(entry.Name(), "spring-boot-") {
return true
}
}
}
return false
}
// Supply: Download Container Customizer JAR
func (c *ContainerCustomizerFramework) Supply() error {
c.context.Log.BeginStep("Installing Container Customizer")
// Get version from manifest
dep, err := c.context.Manifest.DefaultVersion("container-customizer")
if err != nil {
return fmt.Errorf("unable to determine version: %w", err)
}
// Install to deps directory
customizerDir := filepath.Join(c.context.Stager.DepDir(), "container_customizer")
if err := c.context.Installer.InstallDependency(dep, customizerDir); err != nil {
return fmt.Errorf("failed to install: %w", err)
}
c.context.Log.Info("Installed Container Customizer version %s", dep.Version)
return nil
}
// Finalize: Add Container Customizer JAR to classpath
func (c *ContainerCustomizerFramework) Finalize() error {
// Find installed JAR
customizerDir := filepath.Join(c.context.Stager.DepDir(), "container_customizer")
jarPattern := filepath.Join(customizerDir, "container-customizer-*.jar")
matches, err := filepath.Glob(jarPattern)
if err != nil || len(matches) == 0 {
c.context.Log.Warning("Container Customizer JAR not found")
return nil
}
// Create runtime path (using $DEPS_DIR variable)
depsIdx := c.context.Stager.DepsIdx()
relPath := filepath.Base(matches[0])
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/container_customizer/%s", depsIdx, relPath)
// Write profile.d script to add to classpath
profileScript := fmt.Sprintf(`# Container Customizer Framework
export CLASSPATH="%s:${CLASSPATH:-}"
`, runtimePath)
if err := c.context.Stager.WriteProfileD("container_customizer.sh", profileScript); err != nil {
return fmt.Errorf("failed to write profile.d script: %w", err)
}
c.context.Log.Info("Configured Container Customizer for embedded Tomcat")
return nil
}Key Points:
- ✅ File-based detection (checks for WEB-INF and BOOT-INF)
- ✅ Downloads dependency JAR during Supply
- ✅ Adds JAR to classpath via profile.d script
- ✅ Uses
$DEPS_DIRvariable for runtime paths
The Seeker Security Provider detects a bound Seeker service and downloads the agent.
File: src/java/frameworks/seeker_security_provider.go:1
package frameworks
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type SeekerSecurityProviderFramework struct {
context *Context
}
func NewSeekerSecurityProviderFramework(ctx *Context) *SeekerSecurityProviderFramework {
return &SeekerSecurityProviderFramework{context: ctx}
}
// Detect: Check for bound Seeker service
func (s *SeekerSecurityProviderFramework) Detect() (string, error) {
seekerService, err := s.findSeekerService()
if err != nil {
return "", nil
}
// Verify required credentials
credentials, ok := seekerService["credentials"].(map[string]interface{})
if !ok {
return "", nil
}
serverURL, ok := credentials["seeker_server_url"].(string)
if !ok || serverURL == "" {
return "", nil
}
return "seeker-security-provider", nil
}
// Supply: Download Seeker agent from server
func (s *SeekerSecurityProviderFramework) Supply() error {
s.context.Log.BeginStep("Installing Synopsys Seeker Security Provider")
seekerService, err := s.findSeekerService()
if err != nil {
return fmt.Errorf("Seeker service not found: %w", err)
}
credentials, ok := seekerService["credentials"].(map[string]interface{})
if !ok {
return fmt.Errorf("credentials not found")
}
serverURL, ok := credentials["seeker_server_url"].(string)
if !ok {
return fmt.Errorf("seeker_server_url not found")
}
// Download agent from Seeker server
// Agent URL: {serverURL}/rest/api/latest/installers/agents/binaries/JAVA
seekerDir := filepath.Join(s.context.Stager.DepDir(), "seeker_security_provider")
if err := os.MkdirAll(seekerDir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Download and extract agent ZIP
// (Implementation would use http.Get and archive/zip)
s.context.Log.Info("Installed Synopsys Seeker from %s", serverURL)
return nil
}
// Finalize: Configure Seeker agent
func (s *SeekerSecurityProviderFramework) Finalize() error {
seekerService, err := s.findSeekerService()
if err != nil {
return err
}
credentials := seekerService["credentials"].(map[string]interface{})
serverURL := credentials["seeker_server_url"].(string)
// Find agent JAR
seekerDir := filepath.Join(s.context.Stager.DepDir(), "seeker_security_provider")
agentJar := filepath.Join(seekerDir, "seeker-agent.jar")
// Write profile.d script
profileScript := fmt.Sprintf(`#!/bin/bash
# Synopsys Seeker Security Provider
export SEEKER_SERVER_URL="%s"
export JAVA_OPTS="${JAVA_OPTS} -javaagent:%s"
`, serverURL, agentJar)
if err := s.context.Stager.WriteProfileD("seeker_security_provider.sh", profileScript); err != nil {
return fmt.Errorf("failed to write profile.d script: %w", err)
}
s.context.Log.Info("Configured Synopsys Seeker Security Provider")
return nil
}
// Helper: Find Seeker service in VCAP_SERVICES
func (s *SeekerSecurityProviderFramework) findSeekerService() (map[string]interface{}, error) {
vcapServices := os.Getenv("VCAP_SERVICES")
if vcapServices == "" {
return nil, fmt.Errorf("VCAP_SERVICES not set")
}
var services map[string][]map[string]interface{}
if err := json.Unmarshal([]byte(vcapServices), &services); err != nil {
return nil, err
}
// Search for service with "seeker" in name/label/tags
for serviceType, serviceList := range services {
if strings.Contains(strings.ToLower(serviceType), "seeker") {
if len(serviceList) > 0 {
return serviceList[0], nil
}
}
for _, service := range serviceList {
// Check service name
if name, ok := service["name"].(string); ok {
if strings.Contains(strings.ToLower(name), "seeker") {
return service, nil
}
}
// Check tags
if tags, ok := service["tags"].([]interface{}); ok {
for _, tag := range tags {
if tagStr, ok := tag.(string); ok {
if strings.Contains(strings.ToLower(tagStr), "seeker") {
return service, nil
}
}
}
}
}
}
return nil, fmt.Errorf("Seeker service not found")
}Key Points:
- ✅ Service-bound detection (parses VCAP_SERVICES)
- ✅ Flexible service matching (name, label, or tags)
- ✅ Downloads agent from service-provided URL
- ✅ Configures agent with service credentials
- ✅ Adds javaagent to JAVA_OPTS
Many frameworks add a -javaagent option. Use this pattern:
func (f *MyFramework) Finalize() error {
agentPath := filepath.Join(f.context.Stager.DepDir(), "my_framework", "agent.jar")
javaOpts := os.Getenv("JAVA_OPTS")
if javaOpts != "" {
javaOpts += " "
}
javaOpts += fmt.Sprintf("-javaagent:%s", agentPath)
return f.context.Stager.WriteEnvFile("JAVA_OPTS", javaOpts)
}To find a bound service:
func (f *MyFramework) findService() (*VCAPService, error) {
vcapServices, err := GetVCAPServices()
if err != nil {
return nil, err
}
// Check by service type
if vcapServices.HasService("my-service") {
return vcapServices.GetService("my-service"), nil
}
// Check by tag
if vcapServices.HasTag("my-tag") {
return vcapServices.GetServiceByTag("my-tag"), nil
}
return nil, fmt.Errorf("service not found")
}Profile.d scripts run before the application starts:
func (f *MyFramework) Finalize() error {
depsIdx := f.context.Stager.DepsIdx()
script := fmt.Sprintf(`#!/bin/bash
# My Framework Configuration
# Set environment variables
export MY_VAR="value"
# Add to JAVA_OPTS
export JAVA_OPTS="${JAVA_OPTS} -Dmy.property=value"
# Add to classpath
export CLASSPATH="$DEPS_DIR/%s/my_framework/lib/*:${CLASSPATH}"
`, depsIdx)
return f.context.Stager.WriteProfileD("my_framework.sh", script)
}Allow users to disable via configuration:
func (f *MyFramework) isEnabled() bool {
// Check explicit enable/disable
config := os.Getenv("JBP_CONFIG_MY_FRAMEWORK")
if contains(config, "enabled: false") {
return false
}
if contains(config, "enabled: true") {
return true
}
// Check if service is bound (auto-enable)
vcapServices, _ := GetVCAPServices()
return vcapServices.HasService("my-service")
}Download files from URLs in service credentials:
func (f *MyFramework) Supply() error {
service := f.getService()
downloadURL := service.Credentials["download_url"].(string)
targetDir := filepath.Join(f.context.Stager.DepDir(), "my_framework")
os.MkdirAll(targetDir, 0755)
// Use installer to download
// (Implementation would use http.Get or libbuildpack downloader)
return nil
}Convert staging paths to runtime paths using environment variables:
// During Finalize, use runtime path variables
depsIdx := f.context.Stager.DepsIdx()
stagingPath := "/tmp/staging/deps/<idx>/my_framework/lib.jar"
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/my_framework/lib.jar", depsIdx)
// Use runtimePath in profile.d scripts
profileScript := fmt.Sprintf("export CLASSPATH=%s:$CLASSPATH", runtimePath)package frameworks_test
import (
"os"
"testing"
"github.com/cloudfoundry/java-buildpack/src/java/frameworks"
"github.com/cloudfoundry/libbuildpack"
)
func TestMyFrameworkDetect(t *testing.T) {
// Create temp directory for testing
tmpDir, err := os.MkdirTemp("", "test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create test context
logger := libbuildpack.NewLogger(os.Stdout)
stager := libbuildpack.NewStager([]string{tmpDir, "", "0"}, logger, &libbuildpack.Manifest{})
ctx := &frameworks.Context{
Stager: stager,
Log: logger,
}
framework := frameworks.NewMyFramework(ctx)
// Test detection
name, err := framework.Detect()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if name != "my-framework" {
t.Errorf("Expected 'my-framework', got: %s", name)
}
}func TestServiceBoundFramework(t *testing.T) {
vcapJSON := `{
"my-service": [{
"name": "my-service-instance",
"label": "my-service",
"credentials": {
"api_key": "test-key-123"
}
}]
}`
os.Setenv("VCAP_SERVICES", vcapJSON)
defer os.Unsetenv("VCAP_SERVICES")
// Test framework detection
// ...
}func TestFileBasedDetection(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test-*")
defer os.RemoveAll(tmpDir)
// Create marker file
markerFile := filepath.Join(tmpDir, "META-INF", "marker.xml")
os.MkdirAll(filepath.Dir(markerFile), 0755)
os.WriteFile(markerFile, []byte("<marker/>"), 0644)
// Test framework detection
// ...
}# Run all framework tests
cd src/java
ginkgo frameworks/
# Run specific test
ginkgo frameworks/my_framework_test.go
# Run with verbose output
ginkgo -v frameworks/
# Watch and re-run on changes
ginkgo watch frameworks/config/my_framework.yml:
# Cloud Foundry Java Buildpack config for My Framework
---
# Enable/disable framework (default: true)
enabled: true
# Version to install (supports version ranges)
version: 1.+
# Repository location for downloading artifacts
repository_root: "{default.repository.root}/my-framework"
# Framework-specific options
options:
debug: false
timeout: 30The buildpack supports semantic version ranges:
1.+- Latest 1.x version1.2.+- Latest 1.2.x version1.2.3- Exact version[1.2.0,2.0.0)- Range from 1.2.0 to 2.0.0 (exclusive)
Users can override configuration via environment variables:
# Override entire config file
cf set-env my-app JBP_CONFIG_MY_FRAMEWORK '{ enabled: true, version: 2.0.0 }'
# Specific property
cf set-env my-app JBP_CONFIG_MY_FRAMEWORK '{ options: { debug: true } }'Always return meaningful errors with context:
// BAD
if err != nil {
return err
}
// GOOD
if err != nil {
return fmt.Errorf("failed to install My Framework: %w", err)
}Use appropriate log levels:
ctx.Log.BeginStep("Installing My Framework") // Major steps
ctx.Log.Info("Installed version %s", version) // Important info
ctx.Log.Warning("Optional feature disabled") // Warnings
ctx.Log.Debug("Config value: %+v", config) // Debug detailsDon't fail if optional features are unavailable:
dep, err := ctx.Manifest.DefaultVersion("optional-component")
if err != nil {
ctx.Log.Warning("Optional component not available, skipping")
return nil // Continue without failing
}Detection should be fast and have no side effects:
// BAD - Don't download or modify files in Detect
func (f *MyFramework) Detect() (string, error) {
ctx.Installer.InstallDependency(...) // NO!
return "my-framework", nil
}
// GOOD - Only check conditions
func (f *MyFramework) Detect() (string, error) {
if !f.isServiceBound() {
return "", nil
}
return "my-framework", nil
}Supply and Finalize should be idempotent (safe to run multiple times):
func (f *MyFramework) Supply() error {
targetDir := filepath.Join(ctx.Stager.DepDir(), "my_framework")
// Check if already installed
if _, err := os.Stat(filepath.Join(targetDir, "agent.jar")); err == nil {
ctx.Log.Debug("Already installed, skipping")
return nil
}
// Install...
}Always use filepath.Join for cross-platform compatibility:
// BAD
path := ctx.Stager.DepDir() + "/my_framework/agent.jar"
// GOOD
path := filepath.Join(ctx.Stager.DepDir(), "my_framework", "agent.jar")Never log sensitive information (API keys, passwords, tokens):
// BAD
ctx.Log.Info("API Key: %s", apiKey)
// GOOD
ctx.Log.Info("API Key configured")Check:
- Is the service bound?
cf services - Is VCAP_SERVICES set?
cf env my-app - Is detection logic correct? Add debug logging
- Is framework registered in
config/components.yml?
Check:
- Is dependency in buildpack manifest?
- Is download URL accessible?
- Are permissions correct (0755 for directories)?
- Check logs:
cf logs my-app --recent
Check:
- Are paths using
$DEPS_DIRvariable (not hardcoded)? - Are profile.d scripts executable?
- Are JAR files actually installed during Supply?
- Test profile.d scripts:
cf ssh my-appthencat .profile.d/my_framework.sh
Check:
- View environment:
cf ssh my-appthenenv - Check JAVA_OPTS:
cf ssh my-appthenecho $JAVA_OPTS - Verify files exist:
cf ssh my-appthenls $DEPS_DIR/<idx>/my_framework/(where is the buildpack index) - Check application logs:
cf logs my-app
# Rebuild before testing
./scripts/build.sh
# Run tests with verbose output
cd src/java
ginkgo -v frameworks/my_framework_test.go
# Check for Go errors
go vet ./frameworks/...
gofmt -d frameworks/- Testing Guide - Comprehensive testing patterns and strategies
- Implementing Containers - Learn how to add new container types
- Implementing JREs - Learn how to add new JRE providers
- Architecture Overview - Understand buildpack architecture
- Contributing - Contribution guidelines and code standards
Study these existing frameworks for examples:
Simple Frameworks:
debug.go- Configuration-based, no dependenciesjmx.go- Configuration-based, JMX enablement
Service-Bound Frameworks:
new_relic.go- New Relic APM agentapp_dynamics_agent.go- AppDynamics agentseeker_security_provider.go- IAST agent
Complex Frameworks:
luna_security_provider.go- HSM integration with certificatesprotect_app_security_provider.go- Key managementcontainer_customizer.go- File-based detection
All framework implementations: src/java/frameworks/