Skip to content

Commit

Permalink
Merge pull request #68 from paketo-buildpacks/native-image-classpath
Browse files Browse the repository at this point in the history
Contributes expanded classpath during native image builds
  • Loading branch information
ekcasey authored Mar 10, 2021
2 parents 0b90e58 + 272d054 commit eb416ed
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 92 deletions.
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

0 comments on commit eb416ed

Please sign in to comment.