Skip to content

Commit

Permalink
Support multiple war files
Browse files Browse the repository at this point in the history
Weirong-Zhu committed Nov 2, 2023
1 parent 77971b8 commit 3799ad2
Showing 8 changed files with 354 additions and 33 deletions.
61 changes: 54 additions & 7 deletions tomcat/base.go
Original file line number Diff line number Diff line change
@@ -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"
}
117 changes: 115 additions & 2 deletions tomcat/base_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
})
})

}
54 changes: 37 additions & 17 deletions tomcat/build.go
Original file line number Diff line number Diff line change
@@ -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
}
73 changes: 73 additions & 0 deletions tomcat/build_test.go
Original file line number Diff line number Diff line change
@@ -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)
})

}
33 changes: 26 additions & 7 deletions tomcat/detect.go
Original file line number Diff line number Diff line change
@@ -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
}
49 changes: 49 additions & 0 deletions tomcat/detect_test.go
Original file line number Diff line number Diff line change
@@ -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"},
},
},
},
}))
})
})
}
Binary file added tomcat/testdata/warfiles/api.war
Binary file not shown.
Binary file added tomcat/testdata/warfiles/ui.war
Binary file not shown.

0 comments on commit 3799ad2

Please sign in to comment.