Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contributes expanded classpath during native image builds #68

Merged
merged 2 commits into from
Mar 10, 2021
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
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ The buildpack will do the following:

* Contributes Spring Boot version to `org.springframework.boot.version` image label
* Contributes Spring Boot configuration metadata to `org.springframework.boot.spring-configuration-metadata.json` image label
* If `<APPLICATION_ROOT>/META-INF/dataflow-configuration-metadata.properties` exists
* Contributes Spring Cloud Data Flow configuration metadata to `org.springframework.cloud.dataflow.spring-configuration-metadata.json` image label
* Contributes `Implementation-Title` manifest entry to `org.opencontainers.image.title` image label
* Contributes `Implementation-version` manifest entry to `org.opencontainers.image.version` image label
* Contributes dependency information extracted from Maven naming conventions to the image's BOM
* Contributes [Spring Cloud Bindings][b] as an application dependency
* This enables bindings-aware Spring Boot auto-configuration when [CNB bindings][c] are present during launch
* If `<APPLICATION_ROOT>/META-INF/dataflow-configuration-metadata.properties` exists
* Contributes Spring Cloud Data Flow configuration metadata to `org.springframework.cloud.dataflow.spring-configuration-metadata.json` image label
* If `<APPLICATION_ROOT>/META-INF/MANIFEST.MF` contains a `Spring-Boot-Layers-Index` entry
* Contributes application slices as defined by the layer's index
* If the application is a reactive web application
* Configures `$BPL_JVM_THREAD_COUNT` to 50
* When contributing to a JVM application:
* Contributes [Spring Cloud Bindings][b] as an application dependency
* This enables bindings-aware Spring Boot auto-configuration when [CNB bindings][c] are present during launch
* If `<APPLICATION_ROOT>/META-INF/MANIFEST.MF` contains a `Spring-Boot-Layers-Index` entry
* Contributes application slices as defined by the layer's index
* If the application is a reactive web application
* Configures `$BPL_JVM_THREAD_COUNT` to 50
* When contributing to a native image application:
* Adds classes from the executable JAR and entries from `classpath.idx` to the build-time class path, so they are available to `native-image`

[b]: https://github.com/spring-cloud/spring-cloud-bindings
[c]: https://github.com/buildpacks/spec/blob/main/extensions/bindings.md
Expand Down
208 changes: 129 additions & 79 deletions boot/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,28 @@ package boot
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/buildpacks/libcnb"
"github.com/magiconair/properties"
"github.com/paketo-buildpacks/libjvm"
"github.com/paketo-buildpacks/libpak"
"github.com/paketo-buildpacks/libpak/bard"
"gopkg.in/yaml.v3"
)

const (
LabelSpringBootVersion = "org.springframework.boot.version"
LabelImageTitle = "org.opencontainers.image.title"
LabelImageVersion = "org.opencontainers.image.version"
LabelBootConfigurationMetadata = "org.springframework.boot.spring-configuration-metadata.json"
LabelDataFlowConfigurationMetadata = "org.springframework.cloud.dataflow.spring-configuration-metadata.json"
)

type Build struct {
Logger bard.Logger
}
Expand All @@ -43,6 +53,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {

version, ok := manifest.Get("Spring-Boot-Version")
if !ok {
// this isn't a boot app, return without printing title
return libcnb.BuildResult{}, nil
}

Expand All @@ -62,71 +73,17 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
}
dc.Logger = b.Logger

lib, ok := manifest.Get("Spring-Boot-Lib")
if !ok {
return libcnb.BuildResult{}, fmt.Errorf("manifest does not container Spring-Boot-Lib")
}

result.Labels = append(result.Labels, libcnb.Label{Key: "org.springframework.boot.version", Value: version})

if s, ok := manifest.Get("Implementation-Title"); ok {
result.Labels = append(result.Labels, libcnb.Label{Key: "org.opencontainers.image.title", Value: s})
}

if s, ok := manifest.Get("Implementation-Version"); ok {
result.Labels = append(result.Labels, libcnb.Label{Key: "org.opencontainers.image.version", Value: s})
}

c, err := NewConfigurationMetadataFromPath(context.Application.Path)
// add labels
result.Labels, err = labels(context, manifest)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to read configuration metadata from %s\n%w", context.Application.Path, err)
return libcnb.BuildResult{}, err
}

file := filepath.Join(lib, "*.jar")
files, err := filepath.Glob(file)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to glob %s\n%w", file, err)
}

for _, file := range files {
d, err := NewConfigurationMetadataFromJAR(file)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to read configuration metadata from %s\n%w", file, err)
}

c.Groups = append(c.Groups, d.Groups...)
c.Properties = append(c.Properties, d.Properties...)
c.Hints = append(c.Hints, d.Hints...)
}

if len(c.Groups) > 0 || len(c.Properties) > 0 || len(c.Hints) > 0 {
b := &bytes.Buffer{}
if err := json.NewEncoder(b).Encode(c); err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to encode configuration metadata\n%w", err)
}

result.Labels = append(result.Labels, libcnb.Label{
Key: "org.springframework.boot.spring-configuration-metadata.json",
Value: strings.TrimSpace(b.String()),
})
}

c, err = NewDataFlowConfigurationMetadata(context.Application.Path, c)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to generate data flow configuration metadata\n%w", err)
}
if len(c.Groups) > 0 || len(c.Properties) > 0 || len(c.Hints) > 0 {
b := &bytes.Buffer{}
if err := json.NewEncoder(b).Encode(c); err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to encode configuration metadata\n%w", err)
}

result.Labels = append(result.Labels, libcnb.Label{
Key: "org.springframework.cloud.dataflow.spring-configuration-metadata.json",
Value: strings.TrimSpace(b.String()),
})
// add dependencies to BOM
lib, ok := manifest.Get("Spring-Boot-Lib")
if !ok {
return libcnb.BuildResult{}, fmt.Errorf("manifest does not container Spring-Boot-Lib")
}

d, err := libjvm.NewMavenJARListing(filepath.Join(context.Application.Path, lib))
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to generate dependencies from %s\n%w", context.Application.Path, err)
Expand All @@ -137,6 +94,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
Launch: true,
})

// validate generations
gv, err := NewGenerationValidator(filepath.Join(context.Buildpack.Path, "spring-generations.toml"))
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to create generation validator\n%w", err)
Expand All @@ -147,38 +105,30 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
return libcnb.BuildResult{}, fmt.Errorf("unable to validate spring-boot version\n%w", err)
}

ni := false
buildNativeImage := false
if n, ok, err := pr.Resolve("spring-boot"); err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to resolve spring-boot plan entry\n%w", err)
} else if ok {
if v, ok := n.Metadata["native-image"].(bool); ok {
ni = v
buildNativeImage = v
}
}

if !ni {
classes, ok := manifest.Get("Spring-Boot-Classes")
if !ok {
return libcnb.BuildResult{}, fmt.Errorf("manifest does not contain Spring-Boot-Classes")
}

wr, err := NewWebApplicationResolver(classes, lib)
if buildNativeImage {
// set CLASSPATH for native image build
classpathLayer, err := NewNativeImageClasspath(context.Application.Path, manifest)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to create WebApplicationTypeResolver\n%w", err)
return libcnb.BuildResult{}, fmt.Errorf("unable to create NativeImageClasspath\n%w", err)
}

classpathLayer.Logger = b.Logger
result.Layers = append(result.Layers, classpathLayer)
} else {
// contribute Spring Cloud Bindings
h, be := libpak.NewHelperLayer(context.Buildpack, "spring-cloud-bindings")
h.Logger = b.Logger
result.Layers = append(result.Layers, h)
result.BOM.Entries = append(result.BOM.Entries, be)

at, err := NewWebApplicationType(context.Application.Path, wr)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to create WebApplicationType\n%w", err)
}
at.Logger = b.Logger
result.Layers = append(result.Layers, at)

dep, err := dr.Resolve("spring-cloud-bindings", "")
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to find dependency\n%w", err)
Expand All @@ -189,6 +139,25 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
result.Layers = append(result.Layers, bindingsLayer)
result.BOM.Entries = append(result.BOM.Entries, be)

// configure JVM for application type
classes, ok := manifest.Get("Spring-Boot-Classes")
if !ok {
return libcnb.BuildResult{}, fmt.Errorf("manifest does not contain Spring-Boot-Classes")
}

wr, err := NewWebApplicationResolver(classes, lib)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to create WebApplicationTypeResolver\n%w", err)
}

at, err := NewWebApplicationType(context.Application.Path, wr)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to create WebApplicationType\n%w", err)
}
at.Logger = b.Logger
result.Layers = append(result.Layers, at)

// slice app dir
if index, ok := manifest.Get("Spring-Boot-Layers-Index"); ok {
b.Logger.Header("Creating slices from layers index")

Expand All @@ -215,3 +184,84 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {

return result, nil
}

func labels(context libcnb.BuildContext, manifest *properties.Properties) ([]libcnb.Label, error) {
var labels []libcnb.Label

if s, ok := manifest.Get("Spring-Boot-Version"); ok {
labels = append(labels, libcnb.Label{Key: LabelSpringBootVersion, Value: s})
}

if s, ok := manifest.Get("Implementation-Title"); ok {
labels = append(labels, libcnb.Label{Key: LabelImageTitle, Value: s})
}

if s, ok := manifest.Get("Implementation-Version"); ok {
labels = append(labels, libcnb.Label{Key: LabelImageVersion, Value: s})
}

mdLabels, err := configurationMetadataLabels(context.Application.Path, manifest)
if err != nil {
return nil, fmt.Errorf("unable to generate data flow configuration metadata\n%w", err)
}
labels = append(labels, mdLabels...)

return labels, nil
}

func configurationMetadataLabels(appDir string, manifest *properties.Properties) ([]libcnb.Label, error) {
var labels []libcnb.Label

md, err := NewConfigurationMetadataFromPath(appDir)
if err != nil {
return nil, fmt.Errorf("unable to read configuration metadata from %s\n%w", appDir, err)
}

lib, ok := manifest.Get("Spring-Boot-Lib")
if !ok {
return nil, errors.New("manifest does not container Spring-Boot-Lib")
}
file := filepath.Join(lib, "*.jar")
files, err := filepath.Glob(file)
if err != nil {
return nil, fmt.Errorf("unable to glob %s\n%w", file, err)
}

for _, file := range files {
jarMD, err := NewConfigurationMetadataFromJAR(file)
if err != nil {
return nil, fmt.Errorf("unable to read configuration metadata from %s\n%w", file, err)
}

md.Groups = append(md.Groups, jarMD.Groups...)
md.Properties = append(md.Properties, jarMD.Properties...)
md.Hints = append(md.Hints, jarMD.Hints...)
}
if len(md.Groups) > 0 || len(md.Properties) > 0 || len(md.Hints) > 0 {
b := &bytes.Buffer{}
if err := json.NewEncoder(b).Encode(md); err != nil {
return nil, fmt.Errorf("unable to encode configuration metadata\n%w", err)
}
labels = append(labels, libcnb.Label{
Key: LabelBootConfigurationMetadata,
Value: strings.TrimSpace(b.String()),
})
}

md, err = NewDataFlowConfigurationMetadata(appDir, md)
if err != nil {
return nil, fmt.Errorf("unable to generate data flow configuration metadata\n%w", err)
}
if len(md.Groups) > 0 || len(md.Properties) > 0 || len(md.Hints) > 0 {
b := &bytes.Buffer{}
if err := json.NewEncoder(b).Encode(md); err != nil {
return nil, fmt.Errorf("unable to encode configuration metadata\n%w", err)
}
labels = append(labels, libcnb.Label{
Key: LabelDataFlowConfigurationMetadata,
Value: strings.TrimSpace(b.String()),
})
}

return labels, err
}
10 changes: 5 additions & 5 deletions boot/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ Spring-Boot-Lib: BOOT-INF/lib
Expect(result.Layers).To(HaveLen(3))
Expect(result.Layers[0].Name()).To(Equal("helper"))
Expect(result.Layers[0].(libpak.HelperLayerContributor).Names).To(Equal([]string{"spring-cloud-bindings"}))
Expect(result.Layers[1].Name()).To(Equal("web-application-type"))
Expect(result.Layers[2].Name()).To(Equal("spring-cloud-bindings"))
Expect(result.Layers[1].Name()).To(Equal("spring-cloud-bindings"))
Expect(result.Layers[2].Name()).To(Equal("web-application-type"))

Expect(result.BOM.Entries).To(HaveLen(3))
Expect(result.BOM.Entries[0].Name).To(Equal("dependencies"))
Expand Down Expand Up @@ -248,7 +248,7 @@ Spring-Boot-Layers-Index: layers.idx
})
})

it("adds no layers to the result", func() {
it("sets the CLASSPATH for the native image build", func() {
Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(`
Spring-Boot-Version: 1.1.1
Spring-Boot-Classes: BOOT-INF/classes
Expand All @@ -258,7 +258,8 @@ Spring-Boot-Lib: BOOT-INF/lib
result, err := build.Build(ctx)
Expect(err).NotTo(HaveOccurred())

Expect(result.Layers).To(HaveLen(0))
Expect(result.Layers).To(HaveLen(1))
Expect(result.Layers[0].Name()).To(Equal("Class Path"))
})

it("adds no slices to the result", func() {
Expand All @@ -273,6 +274,5 @@ Spring-Boot-Lib: BOOT-INF/lib

Expect(result.Slices).To(HaveLen(0))
})

})
}
5 changes: 5 additions & 0 deletions boot/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import (
"github.com/buildpacks/libcnb"
)

const (
PlanEntrySpringBoot = "spring-boot"
PlanEntryJVMApplication = "jvm-application"
)

type Detect struct{}

func (Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) {
Expand Down
1 change: 1 addition & 0 deletions boot/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ func TestUnit(t *testing.T) {
suite("SpringCloudBindings", testSpringCloudBindings)
suite("WebApplicationType", testWebApplicationType)
suite("WebApplicationTypeResolver", testWebApplicationTypeResolver)
suite("NativeImage", testNativeImage)
suite.Run(t)
}
Loading