Skip to content
Merged
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
5 changes: 4 additions & 1 deletion src/java/frameworks/java_cf_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ func (j *JavaCfEnvFramework) Supply() error {
func (j *JavaCfEnvFramework) Finalize() error {
// Add the JAR to additional libraries (classpath)
javaCfEnvDir := filepath.Join(j.context.Stager.DepDir(), "java_cf_env")
jarPattern := filepath.Join(javaCfEnvDir, "java-cfenv-*.jar")
// The CF mirror installs the jar as java-cfenv_<ver>_<stack>_<sha>.jar (underscores).
// Glob java-cfenv*.jar so this — and a hyphenated Maven name, if the uri is ever
// pointed directly at Maven Central — both match.
jarPattern := filepath.Join(javaCfEnvDir, "java-cfenv*.jar")

matches, err := filepath.Glob(jarPattern)
if err != nil || len(matches) == 0 {
Expand Down
165 changes: 165 additions & 0 deletions src/java/frameworks/java_cf_env_artifact_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//go:build cfenv_artifact

// This test verifies a *necessary condition* for the buildpack's automatic "cloud"
// Spring profile behaviour: the java-cfenv artifact the manifest ships must register
// io.pivotal.cfenv.profile.CloudProfileApplicationListener as a Spring ApplicationListener
// (via META-INF/spring.factories). That listener lives only in the java-cfenv-all module;
// the bare java-cfenv core module does not carry it, which silently drops the "cloud"
// profile (see cloudfoundry/java-buildpack#1349).
//
// It downloads each shipped jar, verifies its SHA-256 against the manifest (which also
// guards against duplicate/mismatched mirror entries), then inspects spring.factories.
//
// It is network-bound, so it is excluded from the default unit suite by the cfenv_artifact
// build tag. Run explicitly:
//
// go test -tags cfenv_artifact ./src/java/frameworks/
package frameworks_test

import (
"archive/zip"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"gopkg.in/yaml.v2"
)

const cloudProfileListener = "io.pivotal.cfenv.profile.CloudProfileApplicationListener"

type manifestDependency struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
URI string `yaml:"uri"`
SHA256 string `yaml:"sha256"`
}

type manifestFile struct {
Dependencies []manifestDependency `yaml:"dependencies"`
}

var _ = Describe("Java CF Env artifact", func() {
var deps []manifestDependency

BeforeEach(func() {
deps = nil

manifestPath, err := filepath.Abs(filepath.Join("..", "..", "..", "manifest.yml"))
Expect(err).NotTo(HaveOccurred())

raw, err := os.ReadFile(manifestPath)
Expect(err).NotTo(HaveOccurred())

var m manifestFile
Expect(yaml.Unmarshal(raw, &m)).To(Succeed())

for _, d := range m.Dependencies {
if d.Name == "java-cfenv" && d.URI != "" {
deps = append(deps, d)
}
}
Expect(deps).NotTo(BeEmpty(), "no java-cfenv dependency entries with a uri found in manifest.yml")
})

It("ships a jar that registers the cloud-profile ApplicationListener", func() {
for _, dep := range deps {
By("checking java-cfenv " + dep.Version)

jar := downloadToTemp(dep.URI)
defer os.Remove(jar)

Expect(fileSHA256(jar)).To(Equal(dep.SHA256),
"java-cfenv %s: downloaded bytes do not match manifest sha256 (%s)", dep.Version, dep.URI)

factories, ok := readZipEntry(jar, "META-INF/spring.factories")
Expect(ok).To(BeTrue(),
"java-cfenv %s: jar has no META-INF/spring.factories — this is the bare core module, "+
"which cannot auto-activate the 'cloud' profile; ship java-cfenv-all instead", dep.Version)

Expect(factories).To(ContainSubstring(cloudProfileListener),
"java-cfenv %s: spring.factories does not register %s — the 'cloud' profile will not be "+
"activated automatically; ship java-cfenv-all instead", dep.Version, cloudProfileListener)

Expect(registersAsApplicationListener(factories, cloudProfileListener)).To(BeTrue(),
"java-cfenv %s: %s is present but not registered under "+
"org.springframework.context.ApplicationListener", dep.Version, cloudProfileListener)
}
})
})

// registersAsApplicationListener reports whether class is listed under the
// org.springframework.context.ApplicationListener key, accounting for backslash
// line continuations used in spring.factories files.
func registersAsApplicationListener(factories, class string) bool {
const key = "org.springframework.context.ApplicationListener"
joined := strings.ReplaceAll(factories, "\\\n", "")
for _, line := range strings.Split(joined, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, key+"=") {
return strings.Contains(line, class)
}
}
return false
}

func downloadToTemp(uri string) string {
GinkgoHelper()

client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Get(uri)
Expect(err).NotTo(HaveOccurred(), "download %s", uri)
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(http.StatusOK), "download %s returned %d", uri, resp.StatusCode)

f, err := os.CreateTemp("", "java-cfenv-*.jar")
Expect(err).NotTo(HaveOccurred())
defer f.Close()

_, err = io.Copy(f, resp.Body)
Expect(err).NotTo(HaveOccurred())

return f.Name()
}

func fileSHA256(path string) string {
GinkgoHelper()

f, err := os.Open(path)
Expect(err).NotTo(HaveOccurred())
defer f.Close()

h := sha256.New()
_, err = io.Copy(h, f)
Expect(err).NotTo(HaveOccurred())

return hex.EncodeToString(h.Sum(nil))
}

func readZipEntry(jarPath, entry string) (string, bool) {
GinkgoHelper()

r, err := zip.OpenReader(jarPath)
Expect(err).NotTo(HaveOccurred())
defer r.Close()

for _, f := range r.File {
if f.Name == entry {
rc, err := f.Open()
Expect(err).NotTo(HaveOccurred())
defer rc.Close()

data, err := io.ReadAll(rc)
Expect(err).NotTo(HaveOccurred())
return string(data), true
}
}
return "", false
}
24 changes: 24 additions & 0 deletions src/java/frameworks/java_cf_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,5 +272,29 @@ var _ = Describe("Java CF Env", func() {
Expect(string(content)).To(ContainSubstring("java-cfenv-2.5.0.jar"))
})
})

Context("when the mirrored, underscore-named JAR is installed", func() {
// The CF dependency mirror names the artifact
// java-cfenv_<version>_<stack>_<sha>.jar (underscores), not the
// hyphenated Maven name. Finalize must still add it to the classpath.
BeforeEach(func() {
javaCfEnvDir := filepath.Join(depsDir, "0", "java_cf_env")
Expect(os.MkdirAll(javaCfEnvDir, 0755)).To(Succeed())
Expect(os.WriteFile(
filepath.Join(javaCfEnvDir, "java-cfenv_3.5.1_linux_noarch_any-stack_696225e3.jar"),
[]byte("fake jar"),
0644,
)).To(Succeed())
})

It("adds the mirrored JAR to the profile.d CLASSPATH", func() {
Expect(fw.Finalize()).To(Succeed())
scriptPath := filepath.Join(depsDir, "0", "profile.d", "java_cf_env.sh")
Expect(scriptPath).To(BeAnExistingFile())
content, err := os.ReadFile(scriptPath)
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(ContainSubstring("java-cfenv_3.5.1_linux_noarch_any-stack_696225e3.jar"))
})
})
})
})