Skip to content

Commit

Permalink
Merge pull request #154 from hibell/build-server-config
Browse files Browse the repository at this point in the history
Provide server config during build
  • Loading branch information
Daniel Mikusa authored May 11, 2022
2 parents dffa87e + c21eaeb commit cc8063e
Show file tree
Hide file tree
Showing 14 changed files with 336 additions and 88 deletions.
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ When building a web application, this buildpack will participate if all the foll
When building from a packaged Liberty server or from a Liberty root directory, the buildpack will participate if all the
following conditions are met:

* `<APPLICATION_ROOT>/wlp/usr/servers/defaultServer/server.xml` exists
* At least one application is installed at either `<APPLICATION_ROOT>/wlp/usr/servers/defaultServer/apps` or
`<APPLICATION_ROOT>/wlp/usr/servers/defaultServer/dropins`

* `<APPLICATION_ROOT>/wlp/usr/servers/$BP_LIBERTY_SERVER_NAME/server.xml` exists
* At least one application is installed at either `<APPLICATION_ROOT>/wlp/usr/servers/$BP_LIBERTY_SERVER_NAME/apps` or
`<APPLICATION_ROOT>/wlp/usr/servers/$BP_LIBERTY_SERVER_NAME/dropins`

The buildpack will do the following:

Expand Down Expand Up @@ -58,6 +57,34 @@ By default, the Liberty buildpack will log in `json` format. This will aid in lo

All of these defaults can be overridden by setting the appropriate properties found in Open Liberty's [documentation](https://openliberty.io/docs/21.0.0.11/log-trace-configuration.html). They can be set as environment variables, or in [`bootstrap.properties`](#bindings).

## Including Server Configuration in the Application Image

The following server configuration files can be included in the application image:

* server.xml
* server.env
* bootstrap.properties

**IMPORTANT NOTE:** Do not put secrets in any of these configuration files! The files will be included in the resulting
image and can leak your secrets.

At the moment, these files can only be included in the build by telling the Maven or Gradle buildpacks to provide them. Thus this method of including server configuration can only be performed when building from source code, it will not work when building with a pre-compiled WAR file.

For example, to provide server configuration in the `src/main/liberty/config`, set one of the following environment
variables in your `pack build` command.

### Including Server Configuration with Maven Applications

```
--env BP_MAVEN_BUILT_ARTIFACT="target/*.[ejw]ar src/main/liberty/config/*"
```

### Including Server Configuration with Gradle Applications

```
--env BP_GRADLE_BUILT_ARTIFACT="build/libs/*.[ejw]ar src/main/liberty/config/*"
```

## Install Types

The different installation types that can be configured are:
Expand Down
117 changes: 90 additions & 27 deletions helper/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ package helper

import (
"encoding/xml"
"errors"
"fmt"
"github.com/heroku/color"
"github.com/paketo-buildpacks/liberty/internal/core"
"github.com/paketo-buildpacks/liberty/internal/server"
"github.com/paketo-buildpacks/libpak/crush"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"

"github.com/paketo-buildpacks/liberty/internal/util"
Expand Down Expand Up @@ -55,6 +60,10 @@ type ServerConfig struct {
XMLName xml.Name `xml:"application"`
Name string `xml:"name,attr"`
} `xml:"application"`
HTTPEndpoint struct {
XMLName xml.Name `xml:"httpEndpoint"`
Host string `xml:"host,attr"`
} `xml:"httpEndpoint"`
}

func (f FileLinker) Execute() (map[string]string, error) {
Expand All @@ -77,7 +86,7 @@ func (f FileLinker) Execute() (map[string]string, error) {
return map[string]string{}, nil
}

func (f FileLinker) Configure(appDir string) error {
func (f FileLinker) Configure(workspacePath string) error {
b, hasBindings, err := bindings.ResolveOne(f.Bindings, bindings.OfType("liberty"))
if err != nil {
return fmt.Errorf("unable to resolve bindings\n%w", err)
Expand All @@ -92,7 +101,7 @@ func (f FileLinker) Configure(appDir string) error {
f.BaseLayerPath = sherpa.GetEnvWithDefault("BPI_LIBERTY_BASE_ROOT", "/layers/paketo-buildpacks_liberty/base")

// Check if we are contributing a packaged server
serverBuildSource := core.NewServerBuildSource(appDir, serverName, f.Logger)
serverBuildSource := core.NewServerBuildSource(workspacePath, serverName, f.Logger)
isPackagedServer, err := serverBuildSource.Detect()
if err != nil {
return fmt.Errorf("unable to check package server directory\n%w", err)
Expand All @@ -106,52 +115,102 @@ func (f FileLinker) Configure(appDir string) error {
if err := server.SetUserDirectory(usrPath, destUserPath, serverName); err != nil {
return fmt.Errorf("unable to contribute packaged server\n%w", err)
}
} else {
if err = f.ContributeApp(appDir, f.RuntimeRootPath, serverName, b); err != nil {
return fmt.Errorf("unable to contribute app and config to runtime root\n%w", err)
}
return nil
}

if err = f.ContributeDefaultHttpEndpoint(f.RuntimeRootPath, serverName, b); err != nil {
return fmt.Errorf("unable to contribute default http endpoint\n%w", err)
}
// Contribute server config files if found in workspace
configs := []string{
"server.xml",
"server.env",
"bootstrap.properties",
}

if err = f.ContributeUserFeatures(serverName, f.getConfigTemplate(b, "features.tmpl")); err != nil {
return fmt.Errorf("unable to contribute user features\n%w", err)
for _, config := range configs {
configPath := filepath.Join(workspacePath, config)
toPath := filepath.Join(f.ServerRootPath, config)
err = util.DeleteAndLinkPath(configPath, toPath)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("unable to copy config from workspace\n%w", err)
}
continue
}
f.Logger.Info(color.YellowString("Reminder: Do not include secrets in %s; this file has been included in the image and that can leak your secrets", config))
}

configPath := filepath.Join(f.ServerRootPath, "server.xml")
if hasBindings {
if bindingXML, ok := b.SecretFilePath("server.xml"); ok {
if err = util.DeleteAndLinkPath(bindingXML, configPath); err != nil {
if err = util.DeleteAndLinkPath(bindingXML, filepath.Join(f.ServerRootPath, "server.xml")); err != nil {
return fmt.Errorf("unable to replace server.xml\n%w", err)
}
}

if bootstrapProperties, ok := b.SecretFilePath("bootstrap.properties"); ok {
existingBSP := filepath.Join(f.RuntimeRootPath, "usr", "servers", serverName, "bootstrap.properties")
existingBSP := filepath.Join(f.ServerRootPath, "bootstrap.properties")
if err = util.DeleteAndLinkPath(bootstrapProperties, existingBSP); err != nil {
return fmt.Errorf("unable to replace bootstrap.properties\n%w", err)
}
}
}

// Skip contributing app config if already defined in the server.xml
configPath := filepath.Join(f.ServerRootPath, "server.xml")
config, err := readServerConfig(configPath)
if err != nil {
return fmt.Errorf("unable to read server config\n%w", err)
}

if err = f.ContributeApp(workspacePath, config, b); err != nil {
return fmt.Errorf("unable to contribute app and config to runtime root\n%w", err)
}

if err = f.ContributeDefaultHttpEndpoint(config, b); err != nil {
return fmt.Errorf("unable to contribute default http endpoint\n%w", err)
}

if err = f.ContributeUserFeatures(serverName, f.getConfigTemplate(b, "features.tmpl")); err != nil {
return fmt.Errorf("unable to contribute user features\n%w", err)
}

return nil
}

func (f FileLinker) ContributeApp(appPath, runtimeRoot, serverName string, binding libcnb.Binding) error {
linkPath := filepath.Join(runtimeRoot, "usr", "servers", serverName, "apps", "app")
_ = os.Remove(linkPath) // we don't care if this succeeds or fails necessarily, we just want to try to remove anything in the way of the relinking
func (f FileLinker) ContributeApp(workspacePath string, config ServerConfig, binding libcnb.Binding) error {
// Determine app path
var appPath string
if appPaths, err := util.GetApps(workspacePath); err != nil {
return fmt.Errorf("unable to determine apps to contribute\n%w", err)
} else if len(appPaths) == 0 {
appPath = workspacePath
} else if len(appPaths) == 1 {
appPath = appPaths[0]
} else {
return fmt.Errorf("expected one app but found several: %s", strings.Join(appPaths, ","))
}

if err := os.Symlink(appPath, linkPath); err != nil {
return fmt.Errorf("unable to symlink application to '%s'\n%w", linkPath, err)
linkPath := filepath.Join(f.ServerRootPath, "apps", "app")
if err := os.Remove(linkPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("unable to remove app\n%w", err)
}

// Skip contributing app config if already defined in the server.xml
configPath := filepath.Join(f.ServerRootPath, "server.xml")
config, err := readServerConfig(configPath)
// Expand app if needed
isDir, err := util.DirExists(appPath)
if err != nil {
return fmt.Errorf("unable to read server config\n%w", err)
return fmt.Errorf("unable to check if app path is a directory\n%w", err)
}
if isDir {
if err := os.Symlink(appPath, linkPath); err != nil {
return fmt.Errorf("unable to symlink application to '%s'\n%w", linkPath, err)
}
} else {
compiledArtifact, err := os.Open(appPath)
if err != nil {
return fmt.Errorf("unable to open compiled artifact\n%w", err)
}
err = crush.Extract(compiledArtifact, linkPath, 0)
if err != nil {
return fmt.Errorf("unable to extract compiled artifact\n%w", err)
}
}

if config.Application.Name == "app" {
Expand All @@ -177,7 +236,7 @@ func (f FileLinker) ContributeApp(appPath, runtimeRoot, serverName string, bindi
return fmt.Errorf("unable to create app template\n%w", err)
}

configOverridesPath := filepath.Join(runtimeRoot, "usr", "servers", serverName, "configDropins", "overrides")
configOverridesPath := filepath.Join(f.ServerRootPath, "configDropins", "overrides")
if err := os.MkdirAll(configOverridesPath, 0755); err != nil {
return fmt.Errorf("unable to make config overrides directory\n%w", err)
}
Expand Down Expand Up @@ -232,8 +291,12 @@ func (f FileLinker) ContributeUserFeatures(serverName, configTemplatePath string
return nil
}

func (f FileLinker) ContributeDefaultHttpEndpoint(runtimeRoot, serverName string, binding libcnb.Binding) error {
configDefaultsPath := filepath.Join(runtimeRoot, "usr", "servers", serverName, "configDropins", "defaults")
func (f FileLinker) ContributeDefaultHttpEndpoint(config ServerConfig, binding libcnb.Binding) error {
if config.HTTPEndpoint.Host != "" {
f.Logger.Debugf("server.xml already has an httpEndpoint defined; skipping contribution of default HTTP endpoint config snippet...")
return nil
}
configDefaultsPath := filepath.Join(f.ServerRootPath, "configDropins", "defaults")
if err := os.MkdirAll(configDefaultsPath, 0755); err != nil {
return fmt.Errorf("unable to make config defaults directory\n%w", err)
}
Expand All @@ -255,7 +318,7 @@ func (f FileLinker) getConfigTemplate(binding libcnb.Binding, template string) s
func readServerConfig(configPath string) (ServerConfig, error) {
xmlFile, err := os.Open(configPath)
if err != nil {
return ServerConfig{}, fmt.Errorf("unable to open server.xml '%s'\n%w", configPath, err)
return ServerConfig{}, fmt.Errorf("unable to open server.xml\n%w", err)
}
defer xmlFile.Close()

Expand Down
58 changes: 54 additions & 4 deletions helper/linker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package helper_test

import (
"github.com/paketo-buildpacks/liberty/internal/util"
"io/ioutil"
"os"
"path/filepath"
Expand Down Expand Up @@ -44,15 +45,26 @@ func testLink(t *testing.T, context spec.G, it spec.S) {

appDir, err = ioutil.TempDir("", "execd-helper-apps")
Expect(err).NotTo(HaveOccurred())
appDir, err = filepath.EvalSymlinks(appDir)
Expect(err).ToNot(HaveOccurred())

layerDir, err = ioutil.TempDir("", "execd-helper-layers")
Expect(err).NotTo(HaveOccurred())
layerDir, err = filepath.EvalSymlinks(layerDir)
Expect(err).ToNot(HaveOccurred())

baseLayerDir, err = ioutil.TempDir("", "base-layer")
Expect(err).NotTo(HaveOccurred())
baseLayerDir, err = filepath.EvalSymlinks(baseLayerDir)
Expect(err).ToNot(HaveOccurred())

configDir = filepath.Join(baseLayerDir, "conf")
Expect(os.MkdirAll(configDir, 0755)).To(Succeed())

templatesDir := filepath.Join(baseLayerDir, "templates")
Expect(os.MkdirAll(templatesDir, 0755)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(templatesDir, "app.tmpl"), []byte{}, 0644)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(templatesDir, "default-http-endpoint.tmpl"), []byte{}, 0644)).To(Succeed())
})

it.After(func() {
Expand Down Expand Up @@ -99,10 +111,6 @@ func testLink(t *testing.T, context spec.G, it spec.S) {

Expect(os.WriteFile(filepath.Join(layerDir, "usr", "servers", "defaultServer", "server.xml"), []byte("<server/>"), 0644)).To(Succeed())

templatesDir := filepath.Join(baseLayerDir, "templates")
Expect(os.MkdirAll(templatesDir, 0755)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(templatesDir, "app.tmpl"), []byte{}, 0644)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(templatesDir, "default-http-endpoint.tmpl"), []byte{}, 0644)).To(Succeed())
})

it.After(func() {
Expand Down Expand Up @@ -288,4 +296,46 @@ func testLink(t *testing.T, context spec.G, it spec.S) {
Expect(filepath.Join(layerDir, "usr", "servers", "defaultServer", "configDropins", "defaults", "features.xml")).To(BeARegularFile())
})
})

context("when building a compiled artifact with server config", func() {
it.Before(func() {
Expect(os.Setenv("BPI_LIBERTY_DROPIN_DIR", appDir)).To(Succeed())
Expect(os.Setenv("BPI_LIBERTY_RUNTIME_ROOT", layerDir)).To(Succeed())
Expect(os.Setenv("BPI_LIBERTY_BASE_ROOT", baseLayerDir)).To(Succeed())
Expect(os.Setenv("BPI_LIBERTY_SERVER_NAME", "defaultServer")).To(Succeed())

Expect(os.MkdirAll(filepath.Join(layerDir, "usr", "servers", "defaultServer"), 0755)).To(Succeed())
Expect(util.CopyFile(filepath.Join("testdata", "test.war"), filepath.Join(appDir, "test.war"))).To(Succeed())
Expect(os.WriteFile(filepath.Join(appDir, "server.xml"), []byte("<server/>"), 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(appDir, "server.env"), []byte("TEST_ENV=foo"), 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(appDir, "bootstrap.properties"), []byte("test.property=foo"), 0644)).To(Succeed())
})

it.After(func() {
Expect(os.Unsetenv("BPI_LIBERTY_DROPIN_DIR")).To(Succeed())
Expect(os.Unsetenv("BPI_LIBERTY_RUNTIME_ROOT")).To(Succeed())
Expect(os.Unsetenv("BPI_LIBERTY_BASE_ROOT")).To(Succeed())
Expect(os.Unsetenv("BPI_LIBERTY_SERVER_NAME")).To(Succeed())
Expect(os.RemoveAll(filepath.Join(layerDir, "usr"))).To(Succeed())
})

it("works", func() {
_, err := linker.Execute()
Expect(err).NotTo(HaveOccurred())

Expect(filepath.Join(layerDir, "usr", "servers", "defaultServer", "apps", "app")).To(BeADirectory())
Expect(filepath.Join(layerDir, "usr", "servers", "defaultServer", "apps", "app", "index.html")).To(BeAnExistingFile())

serverDir := filepath.Join(layerDir, "usr", "servers", "defaultServer")
for _, file := range []string{
"server.xml",
"server.env",
"bootstrap.properties",
} {
resolved, err := filepath.EvalSymlinks(filepath.Join(serverDir, file))
Expect(err).ToNot(HaveOccurred())
Expect(resolved).To(Equal(filepath.Join(appDir, file)))
}
})
})
}
Binary file added helper/testdata/test.war
Binary file not shown.
Loading

0 comments on commit cc8063e

Please sign in to comment.