From 14516757dda078e286e94634726df5e63ff36e29 Mon Sep 17 00:00:00 2001 From: Anthony Dahanne Date: Sun, 11 Feb 2024 21:13:09 -0500 Subject: [PATCH] Spring Boot App CDS * can now unpack,with BP_APP_CDS_ENABLED * uses BP_APP_CDS_AOT_ENABLED and BPL_APP_CDS_AOT_ENABLED to enabled CDS --- boot/build.go | 20 +- boot/spring_app_cds.go | 267 ++++++++++++++++++ boot/spring_class_data_sharing.go | 105 ------- cmd/helper/main.go | 4 +- cmd/main/unpack_test.go | 24 ++ ...lass_data_sharing.go => spring_app_cds.go} | 51 +--- 6 files changed, 318 insertions(+), 153 deletions(-) create mode 100644 boot/spring_app_cds.go delete mode 100644 boot/spring_class_data_sharing.go rename helper/{spring_class_data_sharing.go => spring_app_cds.go} (58%) diff --git a/boot/build.go b/boot/build.go index 66ad859..389f70a 100644 --- a/boot/build.go +++ b/boot/build.go @@ -21,7 +21,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/paketo-buildpacks/libpak/sherpa" "io/fs" "os" "path/filepath" @@ -57,13 +56,19 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { return libcnb.BuildResult{}, fmt.Errorf("unable to read manifest in %s\n%w", context.Application.Path, err) } - cds, _ := sherpa.FileExists("run-app.jar") + cr, err := libpak.NewConfigurationResolver(context.Buildpack, &b.Logger) + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) + } + + //cds, _ := sherpa.FileExists("run-app.jar") result := libcnb.NewBuildResult() - if cds { + if cr.ResolveBool("BP_APP_CDS_ENABLED") { + //if cds { // cds specific b.Logger.Title(context.Buildpack) - h, be := libpak.NewHelperLayer(context.Buildpack, "spring-class-data-sharing") + h, be := libpak.NewHelperLayer(context.Buildpack, "spring-app-cds") h.Logger = b.Logger // add labels @@ -80,7 +85,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { return libcnb.BuildResult{}, fmt.Errorf("unable to create dependency cache\n%w", err) } dc.Logger = b.Logger - bindingsLayer := NewSpringClassDataSharing(dc, context.Application.Path) + bindingsLayer := NewSpringAppCDS(dc, context.Application.Path, manifest, cr.ResolveBool("BP_APP_CDS_AOT_ENABLED")) bindingsLayer.Logger = b.Logger result.Layers = append(result.Layers, bindingsLayer) result.BOM.Entries = append(result.BOM.Entries, be) @@ -103,11 +108,6 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { return libcnb.BuildResult{}, fmt.Errorf("unable to create dependency resolver\n%w", err) } - cr, err := libpak.NewConfigurationResolver(context.Buildpack, &b.Logger) - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) - } - dc, err := libpak.NewDependencyCache(context) if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to create dependency cache\n%w", err) diff --git a/boot/spring_app_cds.go b/boot/spring_app_cds.go new file mode 100644 index 0000000..0a4ca25 --- /dev/null +++ b/boot/spring_app_cds.go @@ -0,0 +1,267 @@ +/* + * Copyright 2018-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package boot + +import ( + "fmt" + "github.com/magiconair/properties" + "github.com/paketo-buildpacks/libjvm" + "github.com/paketo-buildpacks/libpak/sherpa" + "github.com/paketo-buildpacks/spring-boot/v5/helper" + "gopkg.in/yaml.v3" + "os" + "path/filepath" + "strings" + "text/template" + "time" + + "github.com/buildpacks/libcnb" + "github.com/paketo-buildpacks/libpak" + "github.com/paketo-buildpacks/libpak/bard" + "github.com/paketo-buildpacks/libpak/effect" +) + +type SpringAppCDS struct { + Dependency libpak.BuildpackDependency + LayerContributor libpak.LayerContributor + Logger bard.Logger + Executor effect.Executor + AppPath string + Manifest *properties.Properties + AotEnabled bool +} + +func NewSpringAppCDS(cache libpak.DependencyCache, appPath string, manifest *properties.Properties, aotEnabled bool) SpringAppCDS { + contributor := libpak.NewLayerContributor("spring-app-cds", cache, libcnb.LayerTypes{ + Build: true, + }) + return SpringAppCDS{ + LayerContributor: contributor, + Executor: effect.NewExecutor(), + AppPath: appPath, + Manifest: manifest, + AotEnabled: aotEnabled, + } +} + +func (s SpringAppCDS) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { + s.LayerContributor.Logger = s.Logger + layer, err := s.LayerContributor.Contribute(layer, func() (libcnb.Layer, error) { + + s.Logger.Bodyf("This is the value of AppPath: %s", s.AppPath) + s.Logger.Body("Those are the files we have in the workspace") + helper.StartOSCommand("", "ls", "-al", s.AppPath) + + // we extract the vital information from the Spring Boot app manifest + implementationTitle, okIT := s.Manifest.Get("Implementation-Title") + implementationValue, okIV := s.Manifest.Get("Implementation-Version") + startClassValue, okSC := s.Manifest.Get("Start-Class") + classpathIndex, okSI := s.Manifest.Get("Spring-Boot-Classpath-Index") + if !(okIT && okIV && okSC && okSI) { + return layer, fmt.Errorf("unable to contribute spring-app-cds layer - " + + "missing Spring Boot Manifest entries, Implementation-Title or Implementation-Version" + + "or Start-Class or Spring-Boot-Classpath-Index\n") + } + + // the spring boot jar is already unzipped + originalJarExplodedDirectory := s.AppPath + + // we prepare a location for the unpacked version + targetUnpackedDirectory := os.TempDir() + "/" + fmt.Sprint(time.Now().UnixMilli()) + "/unpacked" + os.MkdirAll(targetUnpackedDirectory, 0755) + os.MkdirAll(targetUnpackedDirectory+"/application", 0755) + os.MkdirAll(targetUnpackedDirectory+"/dependencies", 0755) + + // we create the application jar: the one that contains the user classes + jarName := implementationTitle + "-" + implementationValue + ".jar" + helper.StartOSCommand("", "jar", "cf", targetUnpackedDirectory+"/application/"+jarName, "-C", originalJarExplodedDirectory+"/BOOT-INF/classes/", ".") + + s.Logger.Bodyf("Those are the files we have in the target folder %s", targetUnpackedDirectory) + helper.StartOSCommand("", "ls", "-al", targetUnpackedDirectory) + + // we prepare and create the MANIFEST.MF of the runner-app.jar + tempDirectory := os.TempDir() + "/" + fmt.Sprint(time.Now().UnixMilli()) + "/" + os.MkdirAll(tempDirectory+"/META-INF/", 0755) + runAppJarManifest, _ := os.Create(tempDirectory + "/META-INF/MANIFEST.MF") + // TODO: it should be possible to rather use the JDK Created-by + const createdBy = "17.9.9 (Spring Boot Paketo Buildpack)" + writeRunAppJarManifest(originalJarExplodedDirectory, runAppJarManifest, "application/"+jarName, createdBy, startClassValue, classpathIndex) + + // we create the runner-app.jar that will contain just its MANIFEST + helper.StartOSCommand("", "jar", "cfm", targetUnpackedDirectory+"/run-app.jar", runAppJarManifest.Name()) + + // we copy all the dependencies libs from the original jar to the dependencies/folder + sherpa.CopyDir(originalJarExplodedDirectory+"/BOOT-INF/lib/", targetUnpackedDirectory+"/dependencies/") + + // we discard the original Spring Boot app jar + os.RemoveAll(s.AppPath) + + // we copy the unpack folder to the app path, so that it'll be kept in the layer + sherpa.CopyDir(targetUnpackedDirectory, s.AppPath) + + // we set the creation date to the buildpack default 1980/01/01 date; so that cds will be fine + if err := s.Executor.Execute(effect.Execution{ + Command: "find", + Env: []string{"TZ=UTC"}, + Args: []string{"./", "-exec", "touch", "-t", "198001010000.01", "{}", ";"}, + Dir: s.AppPath, + Stdout: s.Logger.InfoWriter(), + Stderr: s.Logger.InfoWriter(), + }); err != nil { + return libcnb.Layer{}, fmt.Errorf("error running build\n%w", err) + } + + // prepare the training run JVM opts + var trainingRunArgs []string + if s.AotEnabled { + trainingRunArgs = append(trainingRunArgs, "-Dspring.aot.enabled=true") + } + trainingRunArgs = append(trainingRunArgs, + "-Dspring.context.exit=onRefresh", + "-XX:ArchiveClassesAtExit=application.jsa", + "-jar", "run-app.jar") + + // perform the training run, application.dsa, the cache file, will be created + if err := s.Executor.Execute(effect.Execution{ + Command: "java", + Args: trainingRunArgs, + Dir: s.AppPath, + Stdout: s.Logger.InfoWriter(), + Stderr: s.Logger.InfoWriter(), + }); err != nil { + return libcnb.Layer{}, fmt.Errorf("error running build\n%w", err) + } + return layer, nil + }) + + if err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to contribute spring-app-cds layer\n%w", err) + } + return layer, nil +} + +// OldContribute TODO: this function could still be interesting when an unpack'ed Spring Boot app was provided (run-app.jar was found) +func (s SpringAppCDS) OldContribute(layer libcnb.Layer) (libcnb.Layer, error) { + s.LayerContributor.Logger = s.Logger + layer, err := s.LayerContributor.Contribute(layer, func() (libcnb.Layer, error) { + s.Logger.Body("Those are the files we have in the workspace BEFORE the training run", layer.Path) + if err := s.Executor.Execute(effect.Execution{ + Command: "find", + Env: []string{"TZ=UTC"}, + Args: []string{"./", "-exec", "touch", "-t", "198001010000.01", "{}", ";"}, + Dir: s.AppPath, + Stdout: s.Logger.InfoWriter(), + Stderr: s.Logger.InfoWriter(), + }); err != nil { + return libcnb.Layer{}, fmt.Errorf("error running build\n%w", err) + } + + if err := s.Executor.Execute(effect.Execution{ + Command: "java", + Args: []string{"-Dspring.aot.enabled=true", + "-Dspring.context.exit=onRefresh", + "-XX:ArchiveClassesAtExit=application.jsa", + "-jar", "run-app.jar"}, + Dir: s.AppPath, + Stdout: s.Logger.InfoWriter(), + Stderr: s.Logger.InfoWriter(), + }); err != nil { + return libcnb.Layer{}, fmt.Errorf("error running build\n%w", err) + } + return layer, nil + }) + if err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to contribute spring-app-cds layer\n%w", err) + } + return layer, nil +} + +func (s SpringAppCDS) Name() string { + return s.LayerContributor.Name +} + +func writeRunAppJarManifest(originalJarExplodedDirectory string, runAppJarManifest *os.File, relocatedOriginalJar string, createdBy string, startClassValue string, classpathIdx string) { + originalManifest, _ := libjvm.NewManifest(originalJarExplodedDirectory) + classPathValue, _ := retrieveClasspathFromIdx(originalManifest, originalJarExplodedDirectory, "dependencies/", relocatedOriginalJar, classpathIdx) + + type Manifest struct { + MainClass string + ClassPath string + CreatedBy string + } + + manifestValues := Manifest{startClassValue, rewriteWithMaxLineLength("Class-Path: "+classPathValue, 72), createdBy} + tmpl, err := template.New("manifest").Parse("Manifest-Version: 1.0\n" + + "Main-Class: {{.MainClass}}\n" + + "{{.ClassPath}}\n" + + "Created-By: {{.CreatedBy}}\n" + + " ") + if err != nil { + panic(err) + } + fmt.Printf("that's my manifest: %s", manifestValues) + //buf := &bytes.Buffer{} + err = tmpl.Execute(runAppJarManifest, manifestValues) + if err != nil { + panic(err) + } +} + +func rewriteWithMaxLineLength(s string, length int) string { + result := "" + currentLine := "" + indent := 0 + remainder := "" + + for i, r := range s { + currentLine = currentLine + string(r) + remainder = remainder + string(r) + j := i + 1 + if indent > 0 { + j = i + 1 + indent + } + if i > 0 && j%length == 0 { + result = result + currentLine + "\n" + currentLine = " " + indent = indent + 1 + remainder = " " + } + } + result = result + remainder + return result +} +func retrieveClasspathFromIdx(manifest *properties.Properties, dir string, relocatedDir string, relocatedOriginalJar string, classpathIdx string) (string, error) { + file := filepath.Join(dir, classpathIdx) + in, err := os.Open(filepath.Join(dir, classpathIdx)) + if err != nil { + return "", fmt.Errorf("unable to open %s\n%w", file, err) + } + defer in.Close() + + var libs []string + if err := yaml.NewDecoder(in).Decode(&libs); err != nil { + return "", fmt.Errorf("unable to decode %s\n%w", file, err) + } + + var relocatedLibs []string + relocatedLibs = append(relocatedLibs, relocatedOriginalJar) + for _, lib := range libs { + relocatedLibs = append(relocatedLibs, strings.ReplaceAll(lib, "BOOT-INF/lib/", relocatedDir)) + } + + return strings.Join(relocatedLibs, " "), nil +} diff --git a/boot/spring_class_data_sharing.go b/boot/spring_class_data_sharing.go deleted file mode 100644 index 846a8a5..0000000 --- a/boot/spring_class_data_sharing.go +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2018-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package boot - -import ( - "fmt" - "log" - "os" - "path/filepath" - "time" - - "github.com/buildpacks/libcnb" - "github.com/paketo-buildpacks/libpak" - "github.com/paketo-buildpacks/libpak/bard" - "github.com/paketo-buildpacks/libpak/effect" -) - -type SpringClassDataSharing struct { - Dependency libpak.BuildpackDependency - LayerContributor libpak.LayerContributor - Logger bard.Logger - Executor effect.Executor - AppPath string -} - -func NewSpringClassDataSharing(cache libpak.DependencyCache, appPath string) SpringClassDataSharing { - contributor := libpak.NewLayerContributor("spring-class-data-sharing", cache, libcnb.LayerTypes{ - Build: true, - }) - return SpringClassDataSharing{ - LayerContributor: contributor, - Executor: effect.NewExecutor(), - AppPath: appPath, - } -} - -func (s SpringClassDataSharing) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { - s.LayerContributor.Logger = s.Logger - layer, err := s.LayerContributor.Contribute(layer, func() (libcnb.Layer, error) { - s.Logger.Body("Those are the files we have in the workspace BEFORE the training run", layer.Path) - if err := s.Executor.Execute(effect.Execution{ - Command: "find", - Env: []string{"TZ=UTC"}, - Args: []string{"./", "-exec", "touch", "-t", "198001010000.01", "{}", ";"}, - Dir: s.AppPath, - Stdout: s.Logger.InfoWriter(), - Stderr: s.Logger.InfoWriter(), - }); err != nil { - return libcnb.Layer{}, fmt.Errorf("error running build\n%w", err) - } - - if err := s.Executor.Execute(effect.Execution{ - Command: "java", - Args: []string{"-Dspring.aot.enabled=true", - "-Dspring.context.exit=onRefresh", - "-XX:ArchiveClassesAtExit=application.jsa", - "-jar", "run-app.jar"}, - Dir: s.AppPath, - Stdout: s.Logger.InfoWriter(), - Stderr: s.Logger.InfoWriter(), - }); err != nil { - return libcnb.Layer{}, fmt.Errorf("error running build\n%w", err) - } - return layer, nil - }) - if err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to contribute spring-class-data-sharing layer\n%w", err) - } - return layer, nil -} - -func (s SpringClassDataSharing) Name() string { - return s.LayerContributor.Name -} - -func resetAllFilesMtimeAndATime(root string, date time.Time) ([]string, error) { - var files []string - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if !info.IsDir() { - println(path) - mtime := date - atime := date - if err := os.Chtimes(path, atime, mtime); err != nil { - log.Printf("Could not update atime and mtime for %s\n", path) - } - files = append(files, path) - } - return nil - }) - return files, err -} diff --git a/cmd/helper/main.go b/cmd/helper/main.go index e9a1393..eafe25b 100644 --- a/cmd/helper/main.go +++ b/cmd/helper/main.go @@ -28,8 +28,8 @@ import ( func main() { sherpa.Execute(func() error { return sherpa.Helpers(map[string]sherpa.ExecD{ - "spring-cloud-bindings": helper.SpringCloudBindings{Logger: bard.NewLogger(os.Stdout)}, - "spring-class-data-sharing": helper.SpringClassDataSharing{Logger: bard.NewLogger(os.Stdout)}, + "spring-cloud-bindings": helper.SpringCloudBindings{Logger: bard.NewLogger(os.Stdout)}, + "spring-app-cds": helper.SpringAppCds{Logger: bard.NewLogger(os.Stdout)}, }) }) } diff --git a/cmd/main/unpack_test.go b/cmd/main/unpack_test.go index 97b9dc9..edb9243 100644 --- a/cmd/main/unpack_test.go +++ b/cmd/main/unpack_test.go @@ -292,3 +292,27 @@ func CloseOrPanic(f io.Closer) func() { } } } + +func resetAllFilesMtimeAndATime(root string, date time.Time) ([]string, error) { + println("Entering resetAllFIles") + var files []string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + println(path) + file, err := os.Open(path) + if err != nil { + log.Printf("Could not open file: %s", path) + } + sherpa.CopyFile(file, fmt.Sprintf("%s.bak", path)) + + if err := os.Chtimes(path, date, date); err != nil { + log.Printf("Could not update atime and mtime for %s\n", fmt.Sprintf("%s.bak", path)) + } + os.Remove(path) + os.Rename(fmt.Sprintf("%s.bak", path), path) + files = append(files, path) + } + return nil + }) + return files, err +} diff --git a/helper/spring_class_data_sharing.go b/helper/spring_app_cds.go similarity index 58% rename from helper/spring_class_data_sharing.go rename to helper/spring_app_cds.go index 8d3b53f..6f411fa 100644 --- a/helper/spring_class_data_sharing.go +++ b/helper/spring_app_cds.go @@ -19,40 +19,43 @@ package helper import ( "bytes" "fmt" - "github.com/paketo-buildpacks/libpak/sherpa" - "log" + "github.com/paketo-buildpacks/libpak/bard" "os" "os/exec" - "path/filepath" + "strconv" "strings" - "time" - - "github.com/paketo-buildpacks/libpak/bard" ) -type SpringClassDataSharing struct { +type SpringAppCds struct { Logger bard.Logger } -func (s SpringClassDataSharing) Execute() (map[string]string, error) { +func (s SpringAppCds) Execute() (map[string]string, error) { - s.Logger.Info("Spring Class Data Sharing Enabled, contributing -Dspring.aot.enabled=true and -XX:SharedArchiveFile=application.jsa to JAVA_OPTS") + s.Logger.Info("Spring App CDS Enabled, contributing -XX:SharedArchiveFile=application.jsa to JAVA_OPTS") s.Logger.Body("Those are the files we have in the workspace") StartOSCommand("", "ls", "-al", "./") - + StartOSCommand("", "env") var values []string if s, ok := os.LookupEnv("JAVA_TOOL_OPTIONS"); ok { values = append(values, s) } - - values = append(values, "-Dspring.aot.enabled=true") + if val, ok := os.LookupEnv("BPL_APP_CDS_AOT_ENABLED"); ok { + enabled, err := strconv.ParseBool(val) + if enabled && err == nil { + s.Logger.Info("Spring AOT Enabled, contributing -Dspring.aot.enabled=true to JAVA_OPTS") + values = append(values, "-Dspring.aot.enabled=true") + } + } values = append(values, "-XX:SharedArchiveFile=application.jsa") return map[string]string{"JAVA_TOOL_OPTIONS": strings.Join(values, " ")}, nil } func StartOSCommand(envVariable string, command string, arguments ...string) { + fmt.Println("StartOSCommand") + fmt.Println(command, arguments) cmd := exec.Command(command, arguments...) cmd.Env = os.Environ() if envVariable != "" { @@ -68,27 +71,3 @@ func StartOSCommand(envVariable string, command string, arguments ...string) { } fmt.Println("Result: " + out.String()) } - -func resetAllFilesMtimeAndATime(root string, date time.Time) ([]string, error) { - println("Entering resetAllFIles") - var files []string - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if !info.IsDir() { - println(path) - file, err := os.Open(path) - if err != nil { - log.Printf("Could not open file: %s", path) - } - sherpa.CopyFile(file, fmt.Sprintf("%s.bak", path)) - - if err := os.Chtimes(path, date, date); err != nil { - log.Printf("Could not update atime and mtime for %s\n", fmt.Sprintf("%s.bak", path)) - } - os.Remove(path) - os.Rename(fmt.Sprintf("%s.bak", path), path) - files = append(files, path) - } - return nil - }) - return files, err -}