Skip to content

feat: rewrite java-buildpack in Go using libbuildpack#1217

Merged
ramonskie merged 354 commits intomainfrom
feature/go-migration
Mar 20, 2026
Merged

feat: rewrite java-buildpack in Go using libbuildpack#1217
ramonskie merged 354 commits intomainfrom
feature/go-migration

Conversation

@ramonskie
Copy link
Contributor

Summary

This PR is a complete rewrite of the java-buildpack from Ruby to Go, aligning it with
the rest of the Cloud Foundry buildpack ecosystem. The buildpack now uses the standard
libbuildpack shared library and follows
the same architecture, conventions, and lifecycle contracts as the go-buildpack,
python-buildpack, ruby-buildpack, and all other modern CF buildpacks.

What Changed

Architecture

  • Rewritten in Go — replaces the previous Ruby implementation entirely
  • Uses libbuildpack — leverages the shared CF buildpack library for manifest parsing,
    dependency installation (3-tier caching), staging directory management, and structured logging
  • Uses switchblade — integration tests now use the standard CF buildpack testing framework
  • Standard lifecycle scriptsbin/detect, bin/supply, bin/finalize, bin/release
    all follow the standard bash-wrapper → Go binary pattern used across CF buildpacks
  • Standard manifest.yml — dependency declarations follow the same format as all other
    CF buildpacks, with full cf_stacks filtering and SHA256 checksums

Source Structure

src/java/
├── supply/       # bin/supply — install JRE, frameworks, agents
├── finalize/     # bin/finalize — final app configuration
└── ...
src/internal/
├── jres/         # JRE providers (OpenJDK, Zulu, SapMachine, etc.)
├── containers/   # App containers (Tomcat, Spring Boot, Groovy, etc.)
├── frameworks/   # Agent frameworks (AppDynamics, Dynatrace, New Relic, etc.)
└── ...

Preserved Functionality

All existing Java buildpack functionality has been preserved and ported:

  • JRE support: OpenJDK, Zulu (Azul), SapMachine, BellSoft Liberica
  • Containers: Tomcat, Spring Boot, Groovy, DistZip, Java Main, Play
  • Frameworks/Agents: AppDynamics, Dynatrace, New Relic, Contrast Security, Elastic APM,
    JaCoCo, JMX, jvmkill, memory-calculator, auto-reconfiguration, and more
  • Offline/airgapped buildpack support via libbuildpack's Tier 1 cache
  • Multi-buildpack support (DEPS_IDX isolation)

Why This Rewrite

The previous Ruby implementation was the last remaining non-Go buildpack in the CF ecosystem.
This rewrite:

  • Eliminates the Ruby runtime dependency during staging
  • Aligns with the actively maintained libbuildpack ecosystem
  • Enables consistent CI/CD pipelines shared across all CF buildpacks
  • Improves staging performance and maintainability

Backup

The previous Ruby-based main is preserved at backup/main-before-go-migration for reference.

During the finalize phase, frameworks like ContainerSecurityProvider and
LunaSecurityProvider need to detect the Java version by reading JAVA_HOME
from the process environment. However, only OpenJDK was setting JAVA_HOME
via os.Setenv() in its Finalize() method.

This caused warnings when using other JREs:
  **WARNING** Unable to detect Java version, assuming Java 8: JAVA_HOME not set

Note: profile.d scripts (which export JAVA_HOME) are only sourced at runtime,
not during the staging/finalize phase, so frameworks cannot rely on them.

Changes:
- Added os.Setenv("JAVA_HOME", javaHome) to Finalize() methods of:
  * SapMachine JRE
  * Zulu JRE
  * GraalVM JRE
  * IBM JRE
  * Oracle JRE
  * Zing JRE

All JREs now follow the same pattern established by OpenJDK, setting
JAVA_HOME in the process environment during finalize so that frameworks
can detect Java version correctly.

Fixes: Container Security Provider warning when using non-OpenJDK JREs
This commit activates 5 fully-implemented but unregistered frameworks that were
previously unreachable dead code, and eliminates significant code duplication
by centralizing component registration logic.

Activated Frameworks:
- ContainerCustomizer: Tomcat configuration customization for Spring Boot WAR apps
- JavaMemoryAssistant: Memory leak detection and heap dump management
- MetricWriter: Micrometer metrics export for Spring Boot applications
- ProtectAppSecurityProvider: Key management and security certificates
- SeekerSecurityProvider: Synopsys Seeker IAST agent

Code Duplication Eliminated:
- Created RegisterStandardFrameworks() method in framework.go to centralize
  43 framework registrations previously duplicated in supply.go and finalize.go
- Created RegisterStandardJREs() method in jre.go to centralize 7 JRE
  registrations previously duplicated in supply.go and finalize.go
- Removed ~146 lines of duplicate registration code across both phases

JRE Detection Logic Fixed:
- Fixed Registry.Detect() to return error when no JRE found (not nil)
- Fixed OpenJDK.Detect() to only detect when explicitly configured via env vars
- Added test coverage for explicit JRE selection (SapMachine over OpenJDK default)
- Updated tests to properly use SetDefault() mechanism

Impact:
- Net reduction: ~66 lines of code
- All 28 JRE unit tests pass
- All Java buildpack unit tests pass (containers, frameworks, supply, finalize)
- 81/87 integration tests pass (5 pre-existing Groovy failures unrelated to changes)
- No new dead code introduced
Add comprehensive integration tests to verify that multiple frameworks can
coexist without overwriting each other's JAVA_OPTS at runtime. This ensures
the centralized .opts file system works correctly.
Key test scenarios:
- Verify configured JAVA_OPTS are applied at runtime with from_environment=false
- Verify user JAVA_OPTS are preserved with from_environment=true
- Verify 4 simultaneous frameworks (Java Opts, Container Security Provider,
  Debug, JRebel) all contribute their options without conflicts
- Verify runtime paths use $DEPS_DIR variables, not staging paths like
  /tmp/contents
Test fixtures:
- spring_boot_multi_framework: New fixture with rebel-remote.xml to auto-trigger
  JRebel agent, plus Debug and Container Security Provider
- spring_boot_staged: Enhanced with /jvm-args endpoint to expose actual JVM
  arguments and system properties at runtime for verification
Add dynamic JAR detection to locate javaagent.jar which may be extracted
into versioned subdirectories (e.g., ver4.5.18/javaagent.jar) rather than
directly in the agent root directory.
Problem:
AppDynamics agent downloads extract into versioned directory structures,
but the buildpack was hardcoded to look for javaagent.jar at the root of
the extraction directory, causing agent initialization to fail.
Solution:
- Add findAppDynamicsAgent() helper that searches multiple paths:
  1. Direct path: app_dynamics_agent/javaagent.jar
  2. Glob pattern: app_dynamics_agent/ver*/javaagent.jar
  3. Recursive walk as fallback for any directory structure
- Move agent path resolution from Supply() to Finalize() to ensure the
  correct path is used at runtime
- Convert absolute staging paths to runtime paths using $DEPS_DIR/0/...
  for proper path resolution when the application starts
This ensures AppDynamics works regardless of the archive structure used
by different agent versions.
…iders

Preserve existing JVM security providers (SUN, SunRsaSign, SunEC, etc.)
instead of replacing them entirely with CloudFoundryContainerProvider.
This fixes SSL/TLS and cryptographic operations that depend on default
providers.
Problem:
The buildpack was writing a java.security file with ONLY the
CloudFoundryContainerProvider, removing all default JVM security providers.
This broke SSL/TLS connections and cryptographic operations because essential
providers like sun.security.provider.Sun and com.sun.crypto.provider.SunJCE
were no longer available.
Solution:
- Add readExistingSecurityProviders() to read providers from the JRE's
  java.security file (supports both Java 8 and Java 9+ locations)
- Add parseSecurityProviders() to extract security.provider.N entries
- Add getDefaultSecurityProviders() as fallback for OpenJDK/HotSpot defaults
- Insert CloudFoundryContainerProvider at position 1, then append all
  existing providers starting at position 2
This ensures CloudFoundryContainerProvider is prioritized while maintaining
full JVM security functionality.
Runtime path changes:
- Convert staging paths to runtime paths using $DEPS_DIR/0/... for proper
  path resolution when the application starts
Change JAR detection to specifically search for sl-test-listener*.jar
instead of any sl-*.jar, ensuring the correct agent is loaded at runtime.
Problem:
Sealights packages contain multiple JARs (sl-build-scanner.jar,
sl-test-listener.jar, etc.). The buildpack was using a glob pattern
"sl-*.jar" which would match the wrong JAR (typically sl-build-scanner.jar)
instead of the runtime agent (sl-test-listener.jar), causing the agent
to fail at application startup.
Solution:
- Change glob pattern from "sl-*.jar" to "sl-test-listener*.jar" to match
  both exact (sl-test-listener.jar) and versioned (sl-test-listener-4.0.jar)
  filenames
- Add recursive fallback search that specifically looks for files starting
  with "sl-test-listener" prefix
- Improve error messages to indicate we're looking for test-listener
  specifically
Runtime path changes:
- Convert staging paths to runtime paths using $DEPS_DIR/0/... for proper
  path resolution when the application starts
Add dynamic JAR detection to locate newrelic.jar which may be extracted
into nested subdirectories (e.g., newrelic/newrelic.jar) rather than
directly in the agent root directory.
Problem:
New Relic agent downloads can extract into nested directory structures,
but the buildpack was hardcoded to look for newrelic.jar at the root of
the extraction directory, causing agent initialization to fail.
Solution:
- Add findNewRelicAgent() helper that searches multiple paths:
  1. Direct path: new_relic_agent/newrelic.jar
  2. Nested path: new_relic_agent/newrelic/newrelic.jar
  3. Recursive walk as fallback for any directory structure
- Move agent path resolution from Supply() to Finalize() to ensure the
  correct path is used at runtime
- Convert absolute staging paths to runtime paths using $DEPS_DIR/0/...
  for proper path resolution when the application starts
This ensures New Relic works regardless of the archive structure used
by different agent versions.
Change JaCoCo agent property format from concatenated equals signs to
proper comma-separated key=value pairs as required by the JaCoCo agent.
Problem:
JaCoCo agent properties were being formatted as:
  -javaagent:jacocoagent.jar=key1=val1=key2=val2=key3=val3
This format is invalid. JaCoCo expects comma-separated properties:
  -javaagent:jacocoagent.jar=key1=val1,key2=val2,key3=val3
This caused the agent to fail parsing configuration, breaking code
coverage collection in Cloud Foundry deployments.
Solution:
- Add first/else logic to properly format properties with commas
- First property: =key=value (no leading comma)
- Subsequent properties: ,key=value (comma separator)
- Example result: -javaagent:path.jar=address=localhost,port=6300,output=file
Runtime path changes:
- Convert staging paths to runtime paths using $DEPS_DIR/0/... for proper
  path resolution when the application starts
Minor cleanup:
- Remove redundant BeginStep log call from Finalize() (already logged
  during detection/supply phases)
Switch from InstallOnlyVersion to InstallDependency and add dynamic JAR
detection to properly locate the agent after extraction.
Problem:
The buildpack was using InstallOnlyVersion() which downloads the dependency
but doesn't extract it. This left a .tar.gz or .zip file instead of the
actual JAR, causing the agent to fail at runtime with "file not found" errors.
Additionally, the code assumed a specific JAR filename pattern
(contrast-security-{version}.jar) which might not match the actual extracted
filename.
Solution:
- Change from InstallOnlyVersion to InstallDependency for proper extraction
- Add findContrastAgent() helper that searches multiple patterns:
  1. Exact match if version known: contrast-security-{version}.jar
  2. Glob pattern: contrast-security-*.jar
  3. Broader pattern: contrast*.jar
  4. Recursive walk as fallback for any JAR containing "contrast"
- Update both Supply() and Finalize() to use dynamic detection
- Add debug logging to track which JAR was found
This ensures Contrast Security works regardless of the archive structure
or filename variations across different agent versions.
Add support for nested jrebel/ directory structure in extracted archives,
checking multiple path variations to locate libjrebel64.so.
Problem:
JRebel agent downloads extract with a nested directory structure where
the agent library is located at jrebel/lib/libjrebel64.so, but the
buildpack was only checking the flat lib/libjrebel64.so path, causing
agent initialization to fail with "library not found" errors.
Solution:
- Check nested path first: jrebel/jrebel/lib/libjrebel64.so (current versions)
- Fall back to flat path: jrebel/lib/libjrebel64.so (older versions)
- Final fallback: jrebel/libjrebel64.so (legacy layout)
- Apply same logic in both Supply() and Finalize() phases to handle
  separate buildpack instances
Runtime path preparation:
- Compute relative path from framework directory to agent library
- Convert to runtime path using $DEPS_DIR/0/jrebel/... format for
  use in subsequent configuration steps
This ensures JRebel works across different archive structures used by
various agent versions.
Replace per-framework env/JAVA_OPTS file writes with a centralized .opts
file system that allows multiple frameworks to coexist without overwriting
each other's options. This completes the migration started with the
infrastructure commit.

Architecture:
- Build time (Finalize phase): Each framework writes JAVA_OPTS to a
  numbered .opts file in $DEPS_DIR/0/java_opts/ (e.g., 17_container_security.opts)
- Runtime: Single profile.d/00_java_opts.sh script reads ALL .opts files
  in numerical order and assembles them into one JAVA_OPTS variable

Priority ordering (based on Ruby buildpack precedence):
  05 - JRE (memory calculator, JVMKill agent)
  11 - AppDynamics Agent
  12 - AspectJ Weaver Agent
  13 - Azure Application Insights Agent
  14 - Checkmarx IAST Agent
  15 - Contrast Security Agent
  17 - Container Security Provider
  18 - Datadog Javaagent
  19 - Elastic APM Agent
  20 - Debug (JDWP)
  21 - Google Stackdriver Debugger
  22 - Google Stackdriver Profiler
  26 - JaCoCo Agent
  27 - Introscope Agent
  28 - Java Memory Assistant
  29 - JMX
  30 - JProfiler Profiler
  31 - JRebel Agent
  32 - Luna Security Provider
  35 - New Relic Agent
  36 - OpenTelemetry Javaagent
  37 - Riverbed AppInternals Agent
  38 - ProtectApp Security Provider
  39 - Sealights Agent
  40 - Seeker Security Provider
  41 - SkyWalking Agent
  42 - Splunk OTEL Java Agent
  45 - YourKit Profiler
  46 - Takipi Agent
  99 - User JAVA_OPTS (always last)

Migration pattern applied to 30 frameworks + JRE:
1. Convert staging paths to runtime paths using $DEPS_DIR/0/... or $HOME/...
2. Build all JAVA_OPTS options (agents, system properties, etc.)
3. Call writeJavaOptsFile(ctx, PRIORITY, "name", javaOpts)
4. Remove old AppendToJavaOpts() or WriteProfileD() calls

Key changes by component:

Core infrastructure:
- finalize.go: Call CreateJavaOptsAssemblyScript() after all frameworks
- java_opts_writer.go: Already committed in infrastructure commit

JRE (priority 05):
- jre.go: Write memory calculator and JVMKill opts to 05_jre.opts
- Use $DEPS_DIR for buildpack-installed components

App-provided frameworks:
- AspectJ: Use $HOME for app-provided JARs (not buildpack-installed)

Buildpack-installed frameworks (all others):
- Convert staging paths like /tmp/contents/deps/0/... to $DEPS_DIR/0/...
- Replace AppendToJavaOpts() with writeJavaOptsFile()
- Maintain priority order from Ruby buildpack

Special cases:
- java_opts.go: Priority 99 ensures user opts always applied last
- Container Security Provider: Preserves from_environment flag behavior
- JProfiler: Simplified nested directory handling for linux-x64 only

Runtime script features:
- Expands $DEPS_DIR, $HOME, and $JAVA_OPTS variables using sed
- Preserves user-provided JAVA_OPTS when from_environment: true
- Processes .opts files in numerical order for predictable precedence

Documentation:
- docs/framework-ordering.md: Documents all framework priorities and
  rationale for ordering decisions

Tests updated:
- framework_test.go: Update expected JAVA_OPTS behavior
- container_test.go: Verify runtime path conversion
- jre_test.go: Test JRE .opts file generation

This migration enables multiple frameworks (e.g., New Relic + AppDynamics +
Debug + Container Security) to all contribute their JAVA_OPTS without
conflicts. Previously, only the last framework's options would survive.
Change external Tomcat configuration download to fetch index.yml first and
look up the download URL, instead of directly constructing the URL. This
matches Ruby buildpack behavior and allows flexible URL patterns.

Problem: The buildpack was constructing download URLs as:
  {repository_root}/tomcat-external-configuration-{version}.tar.gz

This hardcoded pattern caused 404 errors when users' repositories used
different naming conventions. The buildpack should fetch an index.yml
file that maps versions to their actual download URLs.

Solution:
- Modify downloadExternalConfiguration() to:
  1. Download {repository_root}/index.yml
  2. Parse YAML as map[string]string (version -> URL mapping)
  3. Look up requested version in index
  4. Download configuration from the URL specified in index.yml
- Add getKeys() helper to list available versions in error messages
- Update documentation with index.yml format and examples
- Update integration test to verify index.yml fetch behavior

Expected index.yml format:
  1.0.0: https://example.com/tomcat-config/tomcat-config-1.0.0.tar.gz
  1.1.0: https://example.com/tomcat-config/tomcat-config-1.1.0.tar.gz
  1.4.0: https://example.com/tomcat-config/tomcat-config-1.4.0.tar.gz

This provides flexibility in archive naming and hosting while maintaining
backward compatibility with forked buildpacks that include external
configuration in their manifest.yml.
- Add shared utility functions to framework.go:
  * GetApplicationName(includeSpace) - parse VCAP_APPLICATION with optional space prefix
  * GetJavaMajorVersion() - detect Java version from JAVA_HOME/release
  * FindFileInDirectory() - smart file finding with common paths + recursive fallback
  * FindFileInDirectoryWithArchFilter() - architecture-aware file finding for native libraries
  * FindFileByPattern() - glob pattern-based file finding

- Remove duplicate helper methods across frameworks:
  * getApplicationName from Datadog (20 lines)
  * getApplicationNameWithSpace from SkyWalking (27 lines)
  * getJavaMajorVersion from Container Security Provider (78 lines)
  * getJavaMajorVersion from Luna Security Provider (23 lines)
  * Simplified findAgent methods in 6 frameworks (AppDynamics, JaCoCo, NewRelic, JProfiler, YourKit, Contrast)

- Fix priority collision: Datadog changed from priority 18 to 19 (Contrast Security already uses 18)

- Fix architecture detection bug in JProfiler and YourKit:
  * Now correctly finds x86-64 native libraries, avoiding ARM64 versions (linux-aarch64)
  * Prevents "cannot load AARCH64-bit .so on a AMD 64-bit platform" errors

- Add comprehensive priority documentation to java_opts_writer.go

Net change: -103 lines while adding powerful reusable functionality
12 files changed, 277 insertions(+), 380 deletions(-)
This commit fixes Java option parsing by implementing proper shell escaping
that matches the Ruby buildpack's behavior.

Key changes:
- Add shellSplit() to parse quoted strings (like Ruby's Shellwords.shellsplit)
- Add rubyStyleEscape() to escape Java options by splitting on first '='
  and escaping only the VALUE part (matches Ruby's escape_value method)
- Add comprehensive tests for shell parsing and escaping edge cases
- Update integration tests to verify correct JAVA_OPTS handling

Implementation details:
- Split on FIRST '=' only to separate key from value
- Escape only the value part using Ruby's character set: A-Za-z0-9_-.,:/@$\
- All other characters (including = in values) are backslash-escaped
- Special handling for newlines and empty values

Examples:
  -Dkey=value with spaces           → -Dkey=value\ with\ spaces
  -XX:OnOutOfMemoryError=kill -9 %p → -XX:OnOutOfMemoryError=kill\ -9\ \%p
  -Dtest=(value)                    → -Dtest=\(value\)

This fixes the original issue where parentheses and special characters
in JAVA_OPTS values were breaking shell execution.

Note: Currently using custom implementation to match Ruby exactly.
      Future consideration: investigate if go-shellquote or similar
      libraries could provide equivalent functionality with less code,
      though exact Ruby compatibility must be maintained.
Consolidate duplicate Context definitions from containers, frameworks,
and jres packages into a single shared common.Context type. This
eliminates type conversion issues and establishes common package as
the location for shared types and utilities.
Changes:
- Create src/java/common/context.go with shared Context definition
- Remove duplicate Context structs from containers/container.go,
  frameworks/framework.go, and jres/jre.go
- Update all packages to use *common.Context instead of package-specific
  Context types
- Update 60 files across containers (9), frameworks (39), jres (10),
  and build phases (2)
Benefits:
- Single source of truth for Context structure
- No type conversions needed when passing context between packages
- Cleaner architecture with explicit common dependencies
- Establishes pattern for future shared types
All three Context definitions were identical before consolidation,
ensuring this is a pure refactoring with no behavior changes.
Consolidate duplicate Java version detection functions from jres and
frameworks packages into common package. This follows the same
consolidation pattern established for Context.

Changes:
- Add DetermineJavaVersion(javaHome string) to common package
  Takes explicit JAVA_HOME path, used by JREs and containers
- Add GetJavaMajorVersion() to common package
  Reads JAVA_HOME from environment, used by frameworks
- Remove duplicate DetermineJavaVersion from src/java/jres/jre.go
- Remove duplicate GetJavaMajorVersion from src/java/frameworks/framework.go
- Update 7 JRE implementations to use common.DetermineJavaVersion():
  graalvm, ibm, openjdk, oracle, sapmachine, zulu
- Update Tomcat container to use common.DetermineJavaVersion()
- Update 2 frameworks to use common.GetJavaMajorVersion():
  container_security_provider, luna_security_provider
- Update test files to use common.DetermineJavaVersion()

Benefits:
- Single source of truth for Java version detection logic
- Consistent behavior across all packages
- Common package as centralized utilities location
- Follows idiomatic Go pattern (explicit vs environment-reading functions)
AppendToJavaOpts is dead code that is never called anywhere in the
codebase. It was replaced by the centralized JAVA_OPTS assembly system
using WriteJavaOpts and .opts files.

The current approach (WriteJavaOpts) writes numbered .opts files that
are assembled at runtime, which is more robust than the old approach
of accumulating JAVA_OPTS in environment variables during build.

This removes 44 lines of unused code including documentation.
Move duplicate VCAP_SERVICES types and parsing logic from frameworks and jres packages into common package, establishing it as the single source of truth for Cloud Foundry service binding utilities.

Changes:
- Add VCAPServices and VCAPService types to src/java/common/context.go
- Add GetVCAPServices() and helper methods (HasService, GetService, HasTag, etc.) to common
- Add ContainsIgnoreCase() utility for case-insensitive string matching
- Remove duplicate VCAPServices/VCAPService types from frameworks/framework.go
- Remove incomplete GetVCAPServices() stub from jres/jvmkill.go
- Replace duplicate 'Service' type in jres with common.VCAPService
- Update frameworks/framework.go to use type aliases for backward compatibility
- Update 3 framework files to use common.ContainsIgnoreCase():
  - contrast_security_agent.go
  - jprofiler_profiler.go
  - your_kit_profiler.go
- Update jres/jvmkill.go to call common.GetVCAPServices()

Benefits:
- Removes ~130 lines of duplicate VCAP parsing code
- Fixes incomplete jvmkill.go implementation that was returning empty results
- Single source of truth for service binding queries
- Consistent case-insensitive pattern matching across all frameworks
- Type aliases in frameworks package maintain backward compatibility

Net change: +102 lines in common, -177 lines elsewhere (53 line reduction)
Implement Ruby buildpack parity by invoking memory calculator at container
runtime to calculate optimal JVM memory settings based on available memory.

Key changes:
- Add MemoryCalculatorCommand() interface method to all JRE implementations
  to return shell command snippet for runtime memory calculation
- Update memory calculator to use v4.x double-dash flag format:
  --total-memory, --loaded-class-count, --thread-count, --head-room,
  --jvm-options (replacing v3.x single-dash format)
- Remove --pool-type flag (not used in v4.x)
- Prepend memory calculator command to container startup in release.yml
- Add base JAVA_OPTS: -Djava.io.tmpdir, -XX:ActiveProcessorCount,
  -Djava.ext.dirs (Ruby buildpack parity)
- Escape startup command with single quotes in YAML to preserve shell
  special characters ($, quotes, etc.)
- Use default class count (6,300) when class counting fails or returns 0
  (v4 calculator requires this parameter)

Integration test updates:
- Reduce memory settings to fit within 1G container limit used by tests
- Memory calculator v4 has stricter defaults (240M code cache, 250 threads)
  compared to v3.x used by Ruby buildpack
- Updated 8 integration tests with reduced heap (-Xmx384m or -Xmx256m),
  smaller code cache (-XX:ReservedCodeCacheSize=120M), and reduced thread
  stack (-Xss512k)

The memory calculator now runs inline in startup command (after profile.d
scripts assemble JAVA_OPTS) to read runtime $MEMORY_LIMIT and calculate
optimal memory flags, matching Ruby buildpack behavior.

All 99 integration tests pass.
…pport libraries

This commit adds full support for Tomcat external configuration in the Go buildpack,
matching the Ruby buildpack behavior. External configurations can now reference Cloud
Foundry-specific Tomcat classes for enhanced logging and lifecycle management.

Changes:
- Install tomcat-lifecycle-support JAR to tomcat/lib/ for ApplicationStartupFailureDetectingLifecycleListener
- Install tomcat-access-logging-support JAR to tomcat/lib/ for CloudFoundryAccessLoggingValve
- Install tomcat-logging-support JAR to tomcat/bin/ for CloudFoundryConsoleHandler
- Create setenv.sh in tomcat/bin/ to add logging JAR to CLASSPATH before Tomcat starts
- Add -Dhttp.port=$PORT to JAVA_OPTS so Tomcat uses the Cloud Foundry assigned port
- Add tomcat-access-logging-support to manifest.yml default_versions
- Enable external configuration when JBP_CONFIG_TOMCAT includes external_configuration_enabled: true
- Download and extract external configuration from repository_root URL
- Update integration tests to verify external configuration with real repository
- Update documentation with external configuration usage

Technical Details:
- Tomcat's catalina.sh automatically sources setenv.sh if present, allowing early CLASSPATH setup
- Support JARs are installed directly using InstallDependency (non-archive files are copied as-is)
- External configuration archives extract to tomcat/ directory with structure: ./conf/...
- HTTP port configuration uses system property http.port, expected by external server.xml configs

Fixes: Tomcat external configuration support
Tested: Integration test passes with real external repository at tomcat-config.cfapps.eu12.hana.ondemand.com
Implement Go's embed directive to include Cloud Foundry-optimized Tomcat
configuration files and framework resources directly in the buildpack binary,
replacing the Ruby buildpack's approach of bundled resource files.

Changes:

1. New resources package with Go embed:
   - src/java/resources/embed.go: Core embed package with helper functions
     * GetResource(path): Read embedded files
     * ExtractToDir(dir): Extract all resources
     * ListResources(): List embedded resources
     * Exists(path): Check resource existence
   - Embeds 8 resource files from Ruby buildpack using //go:embed directive

2. Embedded Tomcat configurations (from Ruby buildpack):
   - tomcat/conf/server.xml: CF-optimized with:
     * Dynamic port binding via ${http.port} variable
     * HTTP/2 support (UpgradeProtocol)
     * RemoteIpValve for X-Forwarded-* headers
     * CloudFoundryAccessLoggingValve with vcap_request_id tracking
     * ApplicationStartupFailureDetectingLifecycleListener
     * Security-hardened ErrorReportValve
   - tomcat/conf/logging.properties: CloudFoundryConsoleHandler for stdout
   - tomcat/conf/context.xml: Minimal context configuration

3. Embedded framework resources (from Ruby buildpack):
   - luna_security_provider/Chrystoki.conf
   - protect_app_security_provider/IngrianNAE.properties
   - new_relic_agent/newrelic.yml
   - app_dynamics_agent/defaults/conf/app-agent-config.xml
   - azure_application_insights_agent/AI-Agent.xml

4. Tomcat container updates:
   - Add installDefaultConfiguration() to install embedded configs
   - Install configs before external configuration (external overrides defaults)
   - Add -Daccess.logging.enabled=true to JAVA_OPTS for CloudFoundryAccessLoggingValve
   - Comprehensive logging of installed features

5. Integration test enhancements:
   - Verify embedded configs are installed during staging
   - Use Eventually() pattern to avoid flaky access log checks
   - Verify HTTP/2 support in runtime logs
   - Verify CloudFoundryAccessLoggingValve with [ACCESS] prefix and vcap_request_id
   - Verified test stability with 3 consecutive successful runs

Benefits:
- Eliminates need for separate resource distribution
- Configs embedded at compile time, guaranteed availability
- Matches Ruby buildpack functionality exactly
- Simpler deployment (single binary includes all resources)
- Maintains backward compatibility with external configuration

Verification:
- Embedded configs match Ruby buildpack exactly (byte-for-byte comparison)
- Integration tests confirm all CF features work:
  * Dynamic port binding via $PORT
  * HTTP/2 enabled
  * Access logging with vcap_request_id
  * X-Forwarded-* header support
  * CloudFoundry console logging
- Tested with intentional config breakage to prove embed is being used
- Compared with stock Tomcat to verify 8 CF-specific optimizations
- Standardize agent naming: 'new-relic' -> 'newrelic', 'app-dynamics' -> 'appdynamics'
- Add newrelic to default_versions for caching
- Remove dependencies from default_versions that lack manifest entries:
  - appdynamics: requires authentication to AppDynamics repository
  - dynatrace: downloaded from customer-specific environment
  - azure-application-insights-agent: duplicate of azure-application-insights
  - protect-app-security-provider: requires proprietary infrastructure
  - introscope-agent: requires customer CA APM server
  - riverbed-appinternals-agent: downloaded from service broker
  - sky-walking-agent: duplicate of skywalking-agent
  - splunk-otel-java-agent: duplicate of splunk-otel-javaagent
  - google-stackdriver-debugger: requires GCP credentials
  - container-customizer: deprecated download source
  - metric-writer: on-demand download only
  - java-memory-assistant: disabled by default
  - java-memory-assistant-cleanup: bundled with java-memory-assistant

These agents will be downloaded on-demand during staging when service
bindings are detected, matching the original Ruby buildpack behavior.

This enables successful buildpack packaging with --cached flag.
Implement installDefaultConfiguration() methods for five frameworks:

1. AppDynamics Agent
   - Installs embedded app-agent-config.xml to defaults/conf/
   - Provides sensible agent settings and filters

2. New Relic Agent
   - Installs embedded newrelic.yml with ERB template processing
   - Replaces placeholders for generated_for_user and license_key
   - License key configured via JAVA_OPTS at runtime

3. Azure Application Insights Agent
   - Installs embedded AI-Agent.xml
   - Provides instrumentation settings

4. Luna Security Provider
   - Installs Luna HSM configuration from embedded resources

5. ProtectApp Security Provider
   - Installs ProtectApp configuration from embedded resources

Common behavior across all frameworks:
- Check if configuration exists before installing (allows user override)
- Load configuration from embedded resources via resources.GetResource()
- Log warnings on failure (non-fatal, continues build)
- Add proper imports: os, resources, strings (where needed)

This matches the Ruby buildpack pattern of providing working defaults
while allowing users to supply custom configurations.
Add comprehensive unit tests for five frameworks:

- app_dynamics_test.go: Tests AppDynamics config file installation
- new_relic_test.go: Tests New Relic config with ERB template processing
- azure_application_insights_agent_test.go: Tests Azure AI config
- luna_security_provider_test.go: Tests Luna HSM configuration
- protect_app_security_provider_test.go: Tests ProtectApp configuration

Each test suite verifies:
- Embedded configuration files exist in resources
- Configuration content is valid
- Files are created in correct locations
- Existing configurations are not overwritten
- Error handling works correctly

These tests ensure default configurations are properly installed
during the supply phase.
Change framework installation error handling from warnings to fatal errors:
- Framework installation failures now abort the build
- Previously logged warning and continued with other frameworks
- New behavior matches Ruby buildpack (fail fast on framework errors)

Rationale:
Framework installation failures indicate serious issues (missing
dependencies, configuration problems, resource constraints) that should
not be silently ignored. Applications deployed with failed framework
installations may lack critical APM monitoring, security features, or
container customizations.

This ensures build failures are visible and actionable rather than
resulting in partially configured applications.
- Remove .Focus() from Tomcat test to run all integration tests
- Update test comments for accuracy: 'detected and installed' -> 'detected'
  (installation verification is implicit in the framework supply phase)

This ensures the full integration test suite runs during CI/CD rather
than only focused tests.
Changes:
- Make access logging disabled by default (was hardcoded to enabled)
- Add isAccessLoggingEnabled() method to parse JBP_CONFIG_TOMCAT
- Support configuration via: JBP_CONFIG_TOMCAT='{access_logging_support: {access_logging: enabled}}'
- Default: -Daccess.logging.enabled=false (Ruby buildpack parity)
- Enabled: -Daccess.logging.enabled=true (when explicitly configured)

This matches the Ruby buildpack default behavior where access logging
is disabled by default to reduce log volume and I/O overhead. Users
can opt-in to access logs when needed for debugging or compliance.

The CloudFoundryAccessLoggingValve in server.xml uses the system
property to enable/disable access logging at runtime.

Integration tests:
- Add test for default behavior (disabled, no [ACCESS] logs)
- Add test for enabled behavior (via JBP_CONFIG_TOMCAT)
- Update existing test to explicitly enable access logging
- Remove test focus from init_test.go

Resolves Ruby buildpack parity issue for -Daccess.logging.enabled
ari-wg-gitbot and others added 27 commits March 9, 2026 15:50
for stack(s) cflinuxfs4, cflinuxfs5
for stack(s) cflinuxfs4, cflinuxfs5
for stack(s) cflinuxfs4, cflinuxfs5
for stack(s) cflinuxfs4, cflinuxfs5
for stack(s) cflinuxfs4, cflinuxfs5
for stack(s) cflinuxfs4, cflinuxfs5
for stack(s) cflinuxfs4, cflinuxfs5
for stack(s) cflinuxfs4, cflinuxfs5
Updating version for sapmachine for 21.X.X
Updating version for sapmachine for 25.X.X
for stack(s) cflinuxfs4, cflinuxfs5
Updating version for sapmachine for 17.X.X
#1208)

Framework JAR dependencies (e.g. MariaDB JDBC, PostgreSQL JDBC,
Spring Auto-reconfiguration, Java CF Env, Client Certificate Mapper)
were written to deps/index/env/CLASSPATH during staging but that
file is never sourced at runtime, so the dependencies were silently
dropped from the application classpath.
- Replace WriteEnvFile("CLASSPATH", ...) with WriteProfileD() scripts
  in all framework finalizers so CLASSPATH is assembled correctly at
  runtime when profile.d scripts are sourced
- Extract container_security_provider JAR path into a
  CONTAINER_SECURITY_PROVIDER env var (set via profile.d) and thread
  it through tomcat (setenv.sh CLASSPATH) and spring-boot (via
  -cp / -Dloader.path flags) instead of using -Xbootclasspath/a
- Add a zzz_classpath_symlinks.sh profile.d script for tomcat
  (WEB-INF/lib) and spring-boot (BOOT-INF/lib) containers that
  symlinks every entry on CLASSPATH into the container's lib
  directory so deps are subject to application class-loading
- Allow symlinked resources in Tomcat context.xml
  (allowLinking='true')
- Remove redundant context struct construction in finalizeFrameworks;
  reuse the ctx already built in Run()
…refactor spring-boot container (#1209)

* Refactor Spring Boot launcher class handling

* Fix tests
[go-migration] Updated distZip container functionality.
…lasspath (#1215)

* Refactor Groovy command construction with classpath

* Add container security provider

* Adjust profile.d for additional classpath

* Consider CLASSPATH

* Refactoring + comment

* Adjust groovy container

* Add unit tests

* Add integration test
* Just show JMX name for consistent framework naming, not jmx=<port>. Note: "JMX enabled on port %d" shows port already.

* Consistent frameworks logging, with brackets and commas.
# Conflicts:
#	bin/run
#	config/jprofiler_profiler.yml
#	config/ruby.yml
@ramonskie ramonskie merged commit 2d234ff into main Mar 20, 2026
2 checks passed
@ramonskie ramonskie deleted the feature/go-migration branch March 20, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants