diff --git a/tomcat/base.go b/tomcat/base.go index a9ec130..ad3eada 100644 --- a/tomcat/base.go +++ b/tomcat/base.go @@ -21,6 +21,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "github.com/paketo-buildpacks/libpak/sbom" @@ -44,6 +45,7 @@ type Base struct { LifecycleDependency libpak.BuildpackDependency LoggingDependency libpak.BuildpackDependency Logger bard.Logger + WarFilesExist bool } func NewBase( @@ -56,6 +58,7 @@ func NewBase( lifecycleDependency libpak.BuildpackDependency, loggingDependency libpak.BuildpackDependency, cache libpak.DependencyCache, + warFilesExist bool, ) (Base, []libcnb.BOMEntry) { dependencies := []libpak.BuildpackDependency{accessLoggingDependency, lifecycleDependency, loggingDependency} @@ -79,6 +82,7 @@ func NewBase( }), LifecycleDependency: lifecycleDependency, LoggingDependency: loggingDependency, + WarFilesExist: warFilesExist, } var bomEntries []libcnb.BOMEntry @@ -163,14 +167,23 @@ func (b Base) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { } file = filepath.Join(layer.Path, "webapps") - if err := os.MkdirAll(file, 0755); err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to create directory %s\n%w", file, err) - } + if b.WarFilesExist { + if err := os.Symlink(b.ApplicationPath, file); err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to create symlink from %s to %s\n%w", b.ApplicationPath, file, err) + } + if err := b.explodeWarFiles(); err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to explode war files in %s\n%w", b.ApplicationPath, err) + } + } else { + if err := os.MkdirAll(file, 0755); err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to create directory %s\n%w", file, err) + } - file = filepath.Join(layer.Path, "webapps", b.ContextPath) - b.Logger.Headerf("Mounting application at %s", b.ContextPath) - if err := os.Symlink(b.ApplicationPath, file); err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to create symlink from %s to %s\n%w", b.ApplicationPath, file, err) + file = filepath.Join(layer.Path, "webapps", b.ContextPath) + b.Logger.Headerf("Mounting application at %s", b.ContextPath) + if err := os.Symlink(b.ApplicationPath, file); err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to create symlink from %s to %s\n%w", b.ApplicationPath, file, err) + } } environmentPropertySourceDisabled := b.ConfigurationResolver.ResolveBool("BP_TOMCAT_ENV_PROPERTY_SOURCE_DISABLED") @@ -359,6 +372,40 @@ func (b Base) writeDependencySBOM(layer libcnb.Layer, syftArtifacts []sbom.SyftA return nil } +func (b Base) explodeWarFiles() error { + warFiles, err := filepath.Glob(filepath.Join(b.ApplicationPath, "*.war")) + if err != nil { + return err + } + + for _, warFilePath := range warFiles { + b.Logger.Debugf("Extracting: %s\n", warFilePath) + + if _, err := os.Stat(warFilePath); err == nil { + in, err := os.Open(warFilePath) + if err != nil { + return fmt.Errorf("An error occurred while extracting %s: %s\n", warFilePath, err) + } + defer in.Close() + + targetDir := strings.TrimSuffix(warFilePath, filepath.Ext(warFilePath)) + if err := os.MkdirAll(targetDir, 0755); err != nil { + return fmt.Errorf("An error occurred while extracting %s: %s\n", warFilePath, err) + } + + if err := crush.Extract(in, targetDir, 0); err != nil { + return fmt.Errorf("An error occurred while extracting %s: %s\n", warFilePath, err) + } + + err = os.Remove(warFilePath) + if err != nil { + return fmt.Errorf("An error occurred while removing the .war file: %s\n", err) + } + } + } + return nil +} + func (Base) Name() string { return "catalina-base" } diff --git a/tomcat/base_test.go b/tomcat/base_test.go index 46637c5..5e70a33 100644 --- a/tomcat/base_test.go +++ b/tomcat/base_test.go @@ -18,8 +18,10 @@ package tomcat_test import ( "fmt" + "io" "os" "path/filepath" + "strings" "testing" "github.com/buildpacks/libcnb" @@ -101,6 +103,7 @@ func testBase(t *testing.T, context spec.G, it spec.S) { lifecycleDep, loggingDep, dc, + false, ) Expect(entries).To(HaveLen(3)) @@ -175,7 +178,18 @@ func testBase(t *testing.T, context spec.G, it spec.S) { dc := libpak.DependencyCache{CachePath: "testdata"} - contrib, entries := tomcat.NewBase(ctx.Application.Path, ctx.Buildpack.Path, libpak.ConfigurationResolver{}, "test-context-path", accessLoggingDep, &externalConfigurationDep, lifecycleDep, loggingDep, dc) + contrib, entries := tomcat.NewBase( + ctx.Application.Path, + ctx.Buildpack.Path, + libpak.ConfigurationResolver{}, + "test-context-path", + accessLoggingDep, + &externalConfigurationDep, + lifecycleDep, + loggingDep, + dc, + false, + ) layer, err := ctx.Layers.Layer("test-layer") Expect(err).NotTo(HaveOccurred()) @@ -240,7 +254,18 @@ func testBase(t *testing.T, context spec.G, it spec.S) { dc := libpak.DependencyCache{CachePath: "testdata"} - contrib, entries := tomcat.NewBase(ctx.Application.Path, ctx.Buildpack.Path, libpak.ConfigurationResolver{}, "test-context-path", accessLoggingDep, &externalConfigurationDep, lifecycleDep, loggingDep, dc) + contrib, entries := tomcat.NewBase( + ctx.Application.Path, + ctx.Buildpack.Path, + libpak.ConfigurationResolver{}, + "test-context-path", + accessLoggingDep, + &externalConfigurationDep, + lifecycleDep, + loggingDep, + dc, + false, + ) Expect(entries).To(HaveLen(4)) Expect(entries[0].Name).To(Equal("tomcat-access-logging-support")) Expect(entries[0].Build).To(BeFalse()) @@ -309,6 +334,7 @@ func testBase(t *testing.T, context spec.G, it spec.S) { lifecycleDep, loggingDep, dc, + false, ) Expect(entries).To(HaveLen(3)) @@ -393,6 +419,7 @@ func testBase(t *testing.T, context spec.G, it spec.S) { lifecycleDep, loggingDep, dc, + false, ) Expect(entries).To(HaveLen(3)) @@ -418,4 +445,90 @@ func testBase(t *testing.T, context spec.G, it spec.S) { }) + context("Contribute multiple war files", func() { + files := []string{"api.war", "ui.war"} + it.Before(func() { + for _, file := range files { + in, err := os.Open(filepath.Join("testdata", "warfiles", file)) + Expect(err).NotTo(HaveOccurred()) + + out, err := os.OpenFile(filepath.Join(ctx.Application.Path, file), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + Expect(err).NotTo(HaveOccurred()) + + _, err = io.Copy(out, in) + Expect(err).NotTo(HaveOccurred()) + Expect(in.Close()).To(Succeed()) + Expect(out.Close()).To(Succeed()) + } + }) + + it.After(func() { + for _, file := range files { + os.Remove(filepath.Join(ctx.Application.Path, file)) + } + }) + + it("Multiple war files have been exploded in application path", func() { + accessLoggingDep := libpak.BuildpackDependency{ + ID: "tomcat-access-logging-support", + URI: "https://localhost/stub-tomcat-access-logging-support.jar", + SHA256: "d723bfe2ba67dfa92b24e3b6c7b2d0e6a963de7313350e306d470e44e330a5d2", + PURL: "pkg:generic/tomcat-access-logging-support@3.3.0", + CPEs: []string{"cpe:2.3:a:cloudfoundry:tomcat-access-logging-support:3.3.0:*:*:*:*:*:*:*"}, + } + lifecycleDep := libpak.BuildpackDependency{ + ID: "tomcat-lifecycle-support", + URI: "https://localhost/stub-tomcat-lifecycle-support.jar", + SHA256: "723126712c0b22a7fe409664adf1fbb78cf3040e313a82c06696f5058e190534", + PURL: "pkg:generic/tomcat-lifecycle-support@3.3.0", + CPEs: []string{"cpe:2.3:a:cloudfoundry:tomcat-lifecycle-support:3.3.0:*:*:*:*:*:*:*"}, + } + loggingDep := libpak.BuildpackDependency{ + ID: "tomcat-logging-support", + URI: "https://localhost/stub-tomcat-logging-support.jar", + SHA256: "e0a7e163cc9f1ffd41c8de3942c7c6b505090b7484c2ba9be846334e31c44a2c", + PURL: "pkg:generic/tomcat-logging-support@3.3.0", + CPEs: []string{"cpe:2.3:a:cloudfoundry:tomcat-logging-support:3.3.0:*:*:*:*:*:*:*"}, + } + + dc := libpak.DependencyCache{CachePath: "testdata"} + + contributor, entries := tomcat.NewBase( + ctx.Application.Path, + ctx.Buildpack.Path, + libpak.ConfigurationResolver{}, + "test-context-path", + accessLoggingDep, + nil, + lifecycleDep, + loggingDep, + dc, + true, + ) + + Expect(entries).To(HaveLen(3)) + Expect(entries[0].Name).To(Equal("tomcat-access-logging-support")) + Expect(entries[0].Build).To(BeFalse()) + Expect(entries[0].Launch).To(BeTrue()) + Expect(entries[1].Name).To(Equal("tomcat-lifecycle-support")) + Expect(entries[1].Build).To(BeFalse()) + Expect(entries[1].Launch).To(BeTrue()) + Expect(entries[2].Name).To(Equal("tomcat-logging-support")) + Expect(entries[2].Build).To(BeFalse()) + Expect(entries[2].Launch).To(BeTrue()) + + layer, err := ctx.Layers.Layer("test-layer") + Expect(err).NotTo(HaveOccurred()) + + layer, err = contributor.Contribute(layer) + Expect(err).NotTo(HaveOccurred()) + + Expect(os.Readlink(filepath.Join(layer.Path, "webapps"))).To(Equal(ctx.Application.Path)) + for _, file := range files { + targetDir := strings.TrimSuffix(file, filepath.Ext(file)) + Expect(filepath.Join(layer.Path, "webapps", targetDir, "META-INF", "MANIFEST.MF")).To(BeARegularFile()) + } + }) + }) + } diff --git a/tomcat/build.go b/tomcat/build.go index 380758d..b98df2e 100644 --- a/tomcat/build.go +++ b/tomcat/build.go @@ -18,6 +18,7 @@ package tomcat import ( "fmt" + "io/ioutil" "os" "path" "path/filepath" @@ -51,26 +52,31 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { return result, nil } - m, err := libjvm.NewManifest(context.Application.Path) - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to read manifest\n%w", err) - } + warFilesExist, _ := b.containsWarFiles(context.Application.Path) + if warFilesExist { + b.Logger.Infof("%s contains war files.", context.Application.Path) + } else { + m, err := libjvm.NewManifest(context.Application.Path) + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to read manifest\n%w", err) + } - if _, ok := m.Get("Main-Class"); ok { - for _, entry := range context.Plan.Entries { - result.Unmet = append(result.Unmet, libcnb.UnmetPlanEntry{Name: entry.Name}) + if _, ok := m.Get("Main-Class"); ok { + for _, entry := range context.Plan.Entries { + result.Unmet = append(result.Unmet, libcnb.UnmetPlanEntry{Name: entry.Name}) + } + return result, nil } - return result, nil - } - file := filepath.Join(context.Application.Path, "WEB-INF") - if _, err := os.Stat(file); err != nil && !os.IsNotExist(err) { - return libcnb.BuildResult{}, fmt.Errorf("unable to stat file %s\n%w", file, err) - } else if os.IsNotExist(err) { - for _, entry := range context.Plan.Entries { - result.Unmet = append(result.Unmet, libcnb.UnmetPlanEntry{Name: entry.Name}) + file := filepath.Join(context.Application.Path, "WEB-INF") + if _, err := os.Stat(file); err != nil && !os.IsNotExist(err) { + return libcnb.BuildResult{}, fmt.Errorf("unable to stat file %s\n%w", file, err) + } else if os.IsNotExist(err) { + for _, entry := range context.Plan.Entries { + result.Unmet = append(result.Unmet, libcnb.UnmetPlanEntry{Name: entry.Name}) + } + return result, nil } - return result, nil } b.Logger.Title(context.Buildpack) @@ -144,7 +150,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { } } - base, bomEntries := NewBase(context.Application.Path, context.Buildpack.Path, cr, b.ContextPath(cr), accessLoggingDependency, externalConfigurationDependency, lifecycleDependency, loggingDependency, dc) + base, bomEntries := NewBase(context.Application.Path, context.Buildpack.Path, cr, b.ContextPath(cr), accessLoggingDependency, externalConfigurationDependency, lifecycleDependency, loggingDependency, dc, warFilesExist) base.Logger = b.Logger result.Layers = append(result.Layers, base) @@ -217,3 +223,17 @@ func (b Build) tinyStartCommand(homePath, basePath string, loggingDep libpak.Bui return command, arguments } + +func (b Build) containsWarFiles(dir string) (bool, error) { + files, err := ioutil.ReadDir(dir) + if err != nil { + return false, err + } + + for _, file := range files { + if strings.HasSuffix(file.Name(), ".war") { + return true, nil + } + } + return false, nil +} diff --git a/tomcat/build_test.go b/tomcat/build_test.go index 5766a16..1acdfa3 100644 --- a/tomcat/build_test.go +++ b/tomcat/build_test.go @@ -459,4 +459,77 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { }) }) + it("contributes Tomcat with war files", func() { + Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "test.war"), []byte(`test`), 0644)).To(Succeed()) + + ctx.Buildpack.Metadata = map[string]interface{}{ + "dependencies": []map[string]interface{}{ + { + "id": "tomcat", + "version": "1.1.1", + "stacks": []interface{}{"test-stack-id"}, + "purl": "pkg:generic/tomcat@1.1.1", + "cpes": "cpe:2.3:a:apache:tomcat:1.1.1:*:*:*:*:*:*:*", + }, + { + "id": "tomcat-access-logging-support", + "version": "1.1.1", + "stacks": []interface{}{"test-stack-id"}, + "purl": "pkg:generic/tomcat-access-logging-support@1.1.1", + "cpes": "cpe:2.3:a:cloudfoundry:tomcat-access-logging-support:1.1.1:*:*:*:*:*:*:*", + }, + { + "id": "tomcat-lifecycle-support", + "version": "1.1.1", + "stacks": []interface{}{"test-stack-id"}, + "purl": "pkg:generic/tomcat-lifecycle-logging-support@1.1.1", + "cpes": "cpe:2.3:a:cloudfoundry:tomcat-lifecycle-logging-support:1.1.1:*:*:*:*:*:*:*", + }, + { + "id": "tomcat-logging-support", + "version": "1.1.1", + "uri": "https://example.com/releases/tomcat-logging-support-1.1.1.RELEASE.jar", + "stacks": []interface{}{"test-stack-id"}, + "purl": "pkg:generic/tomcat-logging-support@1.1.1", + "cpes": "cpe:2.3:a:cloudfoundry:tomcat-logging-support:1.1.1:*:*:*:*:*:*:*", + }, + }, + } + ctx.StackID = "test-stack-id" + + result, err := tomcat.Build{SBOMScanner: &sbomScanner}.Build(ctx) + Expect(err).NotTo(HaveOccurred()) + + Expect(result.Processes).To(ContainElements( + libcnb.Process{Type: "task", Command: "sh", Arguments: []string{"tomcat/bin/catalina.sh", "run"}, Direct: true}, + libcnb.Process{Type: "tomcat", Command: "sh", Arguments: []string{"tomcat/bin/catalina.sh", "run"}, Direct: true}, + libcnb.Process{Type: "web", Command: "sh", Arguments: []string{"tomcat/bin/catalina.sh", "run"}, Direct: true, Default: true}, + )) + + Expect(result.Layers).To(HaveLen(3)) + Expect(result.Layers[0].Name()).To(Equal("tomcat")) + Expect(result.Layers[1].Name()).To(Equal("helper")) + Expect(result.Layers[1].(libpak.HelperLayerContributor).Names).To(Equal([]string{"access-logging-support"})) + Expect(result.Layers[2].Name()).To(Equal("catalina-base")) + + Expect(result.BOM.Entries).To(HaveLen(5)) + Expect(result.BOM.Entries[0].Name).To(Equal("tomcat")) + Expect(result.BOM.Entries[0].Build).To(BeFalse()) + Expect(result.BOM.Entries[0].Launch).To(BeTrue()) + Expect(result.BOM.Entries[1].Name).To(Equal("helper")) + Expect(result.BOM.Entries[1].Build).To(BeFalse()) + Expect(result.BOM.Entries[1].Launch).To(BeTrue()) + Expect(result.BOM.Entries[2].Name).To(Equal("tomcat-access-logging-support")) + Expect(result.BOM.Entries[2].Build).To(BeFalse()) + Expect(result.BOM.Entries[2].Launch).To(BeTrue()) + Expect(result.BOM.Entries[3].Name).To(Equal("tomcat-lifecycle-support")) + Expect(result.BOM.Entries[3].Build).To(BeFalse()) + Expect(result.BOM.Entries[3].Launch).To(BeTrue()) + Expect(result.BOM.Entries[4].Name).To(Equal("tomcat-logging-support")) + Expect(result.BOM.Entries[4].Build).To(BeFalse()) + Expect(result.BOM.Entries[4].Launch).To(BeTrue()) + + sbomScanner.AssertCalled(t, "ScanLaunch", ctx.Application.Path, libcnb.SyftJSON, libcnb.CycloneDXJSON) + }) + } diff --git a/tomcat/detect.go b/tomcat/detect.go index 736655c..9d6bc1b 100644 --- a/tomcat/detect.go +++ b/tomcat/detect.go @@ -18,8 +18,10 @@ package tomcat import ( "fmt" + "io/ioutil" "os" "path/filepath" + "strings" "github.com/buildpacks/libcnb" "github.com/paketo-buildpacks/libjvm" @@ -52,14 +54,17 @@ func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error return libcnb.DetectResult{Pass: false}, nil } - m, err := libjvm.NewManifest(context.Application.Path) - if err != nil { - return libcnb.DetectResult{}, fmt.Errorf("unable to read manifest\n%w", err) - } + warFilesExist, _ := containsWarFiles(context.Application.Path) + if !warFilesExist { + m, err := libjvm.NewManifest(context.Application.Path) + if err != nil { + return libcnb.DetectResult{}, fmt.Errorf("unable to read manifest\n%w", err) + } - if _, ok := m.Get("Main-Class"); ok { - d.Logger.Info("SKIPPED: Manifest attribute 'Main-Class' was found") - return libcnb.DetectResult{Pass: false}, nil + if _, ok := m.Get("Main-Class"); ok { + d.Logger.Info("SKIPPED: Manifest attribute 'Main-Class' was found") + return libcnb.DetectResult{Pass: false}, nil + } } result := libcnb.DetectResult{ @@ -92,3 +97,17 @@ func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error result.Plans[0].Provides = append(result.Plans[0].Provides, libcnb.BuildPlanProvide{Name: PlanEntryJVMApplicationPackage}) return result, nil } + +func containsWarFiles(dir string) (bool, error) { + files, err := ioutil.ReadDir(dir) + if err != nil { + return false, err + } + + for _, file := range files { + if strings.HasSuffix(file.Name(), ".war") { + return true, nil + } + } + return false, nil +} diff --git a/tomcat/detect_test.go b/tomcat/detect_test.go index 284b983..c5ac84e 100644 --- a/tomcat/detect_test.go +++ b/tomcat/detect_test.go @@ -17,6 +17,7 @@ package tomcat_test import ( + "io" "os" "path/filepath" "testing" @@ -151,4 +152,52 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{Pass: false})) }) }) + + context("Multiple war files found", func() { + files := []string{"api.war", "ui.war"} + + it.Before(func() { + for _, file := range files { + in, err := os.Open(filepath.Join("testdata", "warfiles", file)) + Expect(err).NotTo(HaveOccurred()) + + out, err := os.OpenFile(filepath.Join(ctx.Application.Path, file), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + Expect(err).NotTo(HaveOccurred()) + + _, err = io.Copy(out, in) + Expect(err).NotTo(HaveOccurred()) + Expect(in.Close()).To(Succeed()) + Expect(out.Close()).To(Succeed()) + } + Expect(os.Setenv("BP_JAVA_APP_SERVER", "tomcat")).To(Succeed()) + }) + + it.After(func() { + Expect(os.Unsetenv("BP_JAVA_APP_SERVER")).To(Succeed()) + for _, file := range files { + os.Remove(filepath.Join(ctx.Application.Path, file)) + } + }) + + it("contributes Tomcat", func() { + Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ + Pass: true, + Plans: []libcnb.BuildPlan{ + { + Provides: []libcnb.BuildPlanProvide{ + {Name: "jvm-application"}, + {Name: "java-app-server"}, + }, + Requires: []libcnb.BuildPlanRequire{ + {Name: "syft"}, + {Name: "jre", Metadata: map[string]interface{}{"launch": true}}, + {Name: "jvm-application-package"}, + {Name: "jvm-application"}, + {Name: "java-app-server"}, + }, + }, + }, + })) + }) + }) } diff --git a/tomcat/testdata/warfiles/api.war b/tomcat/testdata/warfiles/api.war new file mode 100644 index 0000000..7535ce0 Binary files /dev/null and b/tomcat/testdata/warfiles/api.war differ diff --git a/tomcat/testdata/warfiles/ui.war b/tomcat/testdata/warfiles/ui.war new file mode 100644 index 0000000..4ed6f57 Binary files /dev/null and b/tomcat/testdata/warfiles/ui.war differ