Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/framework-java-cfenv.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ The framework is implemented in `src/java/frameworks/java_cf_env.go`:
3. **Finalize** — appends the installed jar to `CLASSPATH` via a `.profile.d/java_cf_env.sh` script, so it is on the application's runtime classpath.
4. **Runtime** — Spring Boot reads the jar's `META-INF/spring.factories`: the `EnvironmentPostProcessor`s map `VCAP_SERVICES` to Spring properties, and `CloudProfileApplicationListener` (in the `java-cfenv-all` module) activates the `cloud` profile when running in Cloud Foundry.

## VCAP_SERVICES auto-configuration

Spring Boot has built-in support for flattening `VCAP_SERVICES` into `vcap.services.<name>.credentials.*` properties (`CloudFoundryVcapEnvironmentPostProcessor`). This works without java-cfenv but does **not** set `spring.datasource.url` or activate the `cloud` profile.

`java-cfenv-all` adds service-aware auto-configuration on top: it detects bound services by tag, label, or URI scheme and maps them to the appropriate Spring Boot properties. For example, a bound PostgreSQL service with URI `postgres://host:5432/db` is automatically mapped to `spring.datasource.url=jdbc:postgresql://host:5432/db` (plus username, password, and driver class). For reactive apps, `spring.r2dbc.*` properties are set as well. With a single bound database service, no `application.yml` DataSource configuration is needed.

This behavior is identical across java-cfenv 3.x and 4.x (the only difference is a Spring Boot 4 package relocation of `EnvironmentPostProcessor`).

### JDBC services (`CfDataSourceEnvironmentPostProcessor`)

| Service | Matching criteria | JDBC prefix |
|---------|-------------------|-------------|
| PostgreSQL | tag `postgresql`/`postgres`, label prefix `postgresql`, URI scheme `postgres://`/`postgresql://` | `jdbc:postgresql://` |
| MySQL / MariaDB | tag `mysql`/`mariadb`, label prefix `mysql`/`mariadb`, URI scheme `mysql://`/`mariadb://` | `jdbc:mysql://` or `jdbc:mariadb://` |
| SQL Server | label prefix `sqlserver`, URI scheme `sqlserver://` | `jdbc:sqlserver://` |
| Oracle | label prefix `oracle`, URI scheme `oracle://` | `jdbc:oracle:thin:@` |
| DB2 | tag `db2`/`sqldb`/`dashDB`, label prefix `db2`, URI scheme `db2://` | `jdbc:db2://` |

### Other services (`CfEnvProcessor` implementations)

| Service | Matching criteria | Properties set |
|---------|-------------------|----------------|
| MongoDB | tag `mongodb`, label prefix `mongolab`/`mongodb` | `spring.data.mongodb.uri` |
| Redis | tag `redis`, URI scheme `redis://`/`rediss://` | `spring.data.redis.*` |
| RabbitMQ | tag `rabbitmq`/`amqp`, URI scheme `amqp://`/`amqps://` | `spring.rabbitmq.*` |

## Configuration

The framework can be disabled via the `JBP_CONFIG_JAVA_CF_ENV` environment variable:
Expand Down
39 changes: 32 additions & 7 deletions src/integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,41 @@ You can also run the tests directly using Go:
cd src/integration

# Run all tests
BUILDPACK_FILE=/path/to/buildpack.zip go test -v -timeout 30m
BUILDPACK_FILE=/path/to/buildpack.zip go test -mod vendor -v -timeout 30m ./

# Run specific test suite
BUILDPACK_FILE=/path/to/buildpack.zip go test -v -run TestIntegration/Tomcat

# Run on Docker
BUILDPACK_FILE=/path/to/buildpack.zip go test -v -platform=docker
# Run on Docker (requires GitHub token)
BUILDPACK_FILE=/path/to/buildpack.zip go test -mod vendor -v -timeout 30m ./ \
-platform=docker -github-token=<token>

# Run offline tests
BUILDPACK_FILE=/path/to/buildpack.zip go test -v -cached
BUILDPACK_FILE=/path/to/buildpack.zip go test -mod vendor -v -timeout 30m ./ -cached
```

### Running a Subset of Tests

The spec framework creates subtests 5 levels deep:
`TestIntegration/integration/<Suite>/<context>/<it>`

Use `-run` with a regex; Go does substring matching at each `/`-separated level:

```bash
# All SpringBoot real-jar tests
-run "TestIntegration/integration/SpringBoot/with_real_Spring_Boot_fat_jars"

# Single test — SB3 only
-run "TestIntegration/integration/SpringBoot/with_real_Spring_Boot_fat_jars/SB3"

# Single test — SB4 cfenv + loaded-jars
-run "TestIntegration/integration/SpringBoot/with_real_Spring_Boot_fat_jars/SB4.*cloud_profile"

# Single test — SB4 combined (cfenv + csp + metrics)
-run "TestIntegration/integration/SpringBoot/with_real_Spring_Boot_fat_jars/SB4.*container-security"

# All Tomcat tests
-run "TestIntegration/integration/Tomcat"

# All Frameworks tests
-run "TestIntegration/integration/Frameworks"
```

## Test Organization
Expand Down
148 changes: 148 additions & 0 deletions src/integration/fixtures_helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package integration_test

import (
"archive/zip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)

const (
javaTestAppsReleaseTag = "v1.0.0"
javaTestAppsBaseURL = "https://github.com/cloudfoundry/java-test-applications/releases/download/" + javaTestAppsReleaseTag
sb3JarName = "java-main-application-boot3-1.0.0.jar"
sb4JarName = "java-main-application-1.0.0.jar"
)

// downloadJavaTestAppsJars downloads the pinned SB3 and SB4 fat jars.
// For Docker-mode tests, jars are extracted (exploded) into fixture directories
// to simulate what real CF staging does: CF treats the pushed artifact as a zip
// and extracts it before running the buildpack, so BOOT-INF/ and META-INF/
// appear as flat files on disk.
// For CF-mode tests, the raw jar is returned as-is — `cf push -p` handles
// zip extraction natively.
//
// Pre-exploding is the correct pattern for Docker-mode tests: switchblade's
// Docker mode archives the fixture directory as-is (TGZArchiver), while CF mode
// delegates to `cf push -p` which handles zip extraction natively.
// See https://github.com/cloudfoundry/switchblade/issues/134 for details.
//
// Returns (sb3FixturePath, sb4FixturePath, cleanup func, error).
func downloadJavaTestAppsJars(platform string) (string, string, func(), error) {
dir, err := os.MkdirTemp("", "java-test-apps-*")
if err != nil {
return "", "", nil, fmt.Errorf("create temp dir: %w", err)
}

cleanup := func() { os.RemoveAll(dir) }

type jarEntry struct {
jarName string
path string // set after download/extract
}
entries := []jarEntry{
{jarName: sb3JarName},
{jarName: sb4JarName},
}

for i := range entries {
jarPath := filepath.Join(dir, entries[i].jarName)
if err := downloadFile(javaTestAppsBaseURL+"/"+entries[i].jarName, jarPath); err != nil {
cleanup()
return "", "", nil, fmt.Errorf("download %s: %w", entries[i].jarName, err)
}

if platform == "docker" {
// Docker mode: explode jar into directory (simulates CF staging zip extraction).
// Switchblade Docker archives the fixture dir as-is via TGZArchiver.
explodedDir := filepath.Join(dir, fmt.Sprintf("exploded-%d", i))
if err := extractZip(jarPath, explodedDir); err != nil {
cleanup()
return "", "", nil, fmt.Errorf("extract %s: %w", entries[i].jarName, err)
}
_ = os.Remove(jarPath)
entries[i].path = explodedDir
} else {
// CF mode: return raw jar path — `cf push -p` handles zip extraction natively.
entries[i].path = jarPath
}
}

return entries[0].path, entries[1].path, cleanup, nil
}

// extractZip extracts a zip/jar file into destDir, replicating the flat-file
// layout that CF staging creates when it unpacks the pushed artifact.
func extractZip(src, destDir string) error {
r, err := zip.OpenReader(src)
if err != nil {
return fmt.Errorf("open zip: %w", err)
}
defer r.Close()

for _, f := range r.File {
target := filepath.Join(destDir, f.Name)

// Guard against zip-slip
if !strings.HasPrefix(filepath.Clean(target)+string(os.PathSeparator),
filepath.Clean(destDir)+string(os.PathSeparator)) {
return fmt.Errorf("zip entry %q escapes destination", f.Name)
}

if f.FileInfo().IsDir() {
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
continue
}

if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}

out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode())
if err != nil {
return err
}

rc, err := f.Open()
if err != nil {
out.Close()
return err
}

_, copyErr := io.Copy(out, rc)
rc.Close()
out.Close()
if copyErr != nil {
return copyErr
}
}
return nil
}

func downloadFile(url, dest string) error {
client := &http.Client{Timeout: 2 * time.Minute}
resp, err := client.Get(url) //nolint:noctx
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP %d for %s", resp.StatusCode, url)
}

f, err := os.Create(dest)
if err != nil {
return err
}
defer f.Close()

_, err = io.Copy(f, resp.Body)
return err
}
2 changes: 1 addition & 1 deletion src/integration/frameworks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ func testFrameworks(platform switchblade.Platform, fixtures string) func(*testin
deployment, logs, err := platform.Deploy.
WithServices(map[string]switchblade.Service{
"checkmarx-iast": {
"url": "https://github.com/cloudfoundry/java-test-applications/raw/main/java-main-application/java-main-application.jar",
"url": "https://github.com/cloudfoundry/java-test-applications/releases/download/v1.0.0/java-main-application-1.0.0.jar",
"manager_url": "https://checkmarx.example.com",
"api_key": "test-api-key-12345",
},
Expand Down
18 changes: 17 additions & 1 deletion src/integration/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ var settings struct {
GitHubToken string
Platform string
Stack string

// Paths to fixture directories containing real Spring Boot fat jars from java-test-applications release.
// Each directory holds a single fat jar; passed to switchblade Deploy.Execute as the app source.
// Populated in TestIntegration before the suite runs.
SB3JarPath string
SB4JarPath string
}

func init() {
Expand Down Expand Up @@ -66,6 +72,16 @@ func TestIntegration(t *testing.T) {
)
Expect(err).NotTo(HaveOccurred())

// Download real Spring Boot fat jars for java-cfenv integration tests.
// Skip in cached/offline mode — tests self-skip when paths are empty.
if !settings.Cached {
sb3Jar, sb4Jar, cleanupJars, err := downloadJavaTestAppsJars(settings.Platform)
Expect(err).NotTo(HaveOccurred(), "failed to download java-test-applications jars for integration tests")
defer cleanupJars()
settings.SB3JarPath = sb3Jar
settings.SB4JarPath = sb4Jar
}

var suite spec.Suite
if settings.Serial {
suite = spec.New("integration", spec.Report(report.Terminal{}), spec.Sequential())
Expand All @@ -75,7 +91,7 @@ func TestIntegration(t *testing.T) {

// Core container tests
suite("Tomcat", testTomcat(platform, fixtures))
suite("SpringBoot", testSpringBoot(platform, fixtures))
suite("SpringBoot", testSpringBoot(platform, fixtures, settings.SB3JarPath, settings.SB4JarPath))
suite("JavaMain", testJavaMain(platform, fixtures))
suite("DistZip", testDistZip(platform, fixtures))

Expand Down
Loading