Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support to handle multiple artifacts #93

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 93 additions & 21 deletions application.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,42 @@ func (a Application) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
return libcnb.Layer{}, fmt.Errorf("error running build\n%w", err)
}

artifact, err := a.ArtifactResolver.Resolve(a.ApplicationPath)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to resolve artifact\n%w", err)
}

in, err := os.Open(artifact)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", artifact, err)
}
defer in.Close()

file := filepath.Join(layer.Path, "application.zip")
if err := sherpa.CopyFile(in, file); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to copy %s to %s\n%w", artifact, file, err)
if ok, err := a.ArtifactResolver.singleArtifact(a.ApplicationPath); ok {
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to resolve artifact\n%w", err)
}
artifact, err := a.ArtifactResolver.Resolve(a.ApplicationPath)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to resolve artifact\n%w", err)
}
file := filepath.Join(layer.Path, "application.zip")
if err := copyFile(artifact, file); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to copy %s to %s\n%w", artifact, file, err)
}
} else {
a.Logger.Infof("After evaluating pattern %s, multiples artifacts were detected", a.ArtifactResolver.Pattern())
artifacts, err := a.ArtifactResolver.ResolveMultipleArtifacts(a.ApplicationPath)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to resolve multiple artifacts\n%w", err)
}
for _, artifact := range artifacts {
fileInfo, err := os.Stat(artifact)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to resolve artifact %s\n%w", artifact, err)
}
if fileInfo.IsDir() {
// It matches a folder or multiple folders (take everything in the folder, then multi-file behavior)
if err := copyDirectory(artifact, layer.Path); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to resolve multiple artifacts\n%w", err)
}
} else {
// It matches multiple files (multi-file behavior)
dest := filepath.Join(layer.Path, fileInfo.Name())
if err := copyFile(artifact, dest); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to resolve multiple artifacts\n%w", err)
}
}
}
}
return layer, nil
})
Expand Down Expand Up @@ -109,14 +131,26 @@ func (a Application) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
}

file := filepath.Join(layer.Path, "application.zip")
in, err := os.Open(file)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", file, err)
}
defer in.Close()
if _, err := os.Stat(file); err == nil {
in, err := os.Open(file)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", file, err)
}
defer in.Close()

if err := crush.ExtractZip(in, a.ApplicationPath, 0); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to extract %s\n%w", file, err)
if err := crush.ExtractZip(in, a.ApplicationPath, 0); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to extract %s\n%w", file, err)
}
} else {
if os.IsNotExist(err) {
a.Logger.Infof("Restoring multiple artifacts")
err := copyDirectory(layer.Path, a.ApplicationPath)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to restore multiple artifacts\n%w", err)
}
} else {
return libcnb.Layer{}, fmt.Errorf("unable to determine if multiple artifacts were created\n%w", err)
}
}

return layer, nil
Expand All @@ -125,3 +159,41 @@ func (a Application) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
func (Application) Name() string {
return "application"
}

func copyDirectory(from, to string) error {
files, err := ioutil.ReadDir(from)
if err != nil {
return err
}
for _, file := range files {
sourcePath := filepath.Join(from, file.Name())
destPath := filepath.Join(to, file.Name())
fileInfo, err := os.Stat(sourcePath)
if err != nil {
return err
}

if fileInfo.IsDir() {
if err := copyDirectory(sourcePath, destPath); err != nil {
return err
}
} else {
if err := copyFile(sourcePath, destPath); err != nil {
return err
}
}
}
return nil
}

func copyFile(from string, to string) error {
in, err := os.Open(from)
if err != nil {
return fmt.Errorf("unable to open file%s\n%w", from, err)
}
defer in.Close()
if err := sherpa.CopyFile(in, to); err != nil {
return fmt.Errorf("unable to copy %s to %s\n%w", from, to, err)
}
return nil
}
88 changes: 88 additions & 0 deletions application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,92 @@ func testApplication(t *testing.T, context spec.G, it spec.S) {
Expect(bom.Entries).To(HaveLen(0))
})

context("contributes layer with ", func() {
context("folder with ", func() {
it.Before(func() {
artifactResolver := libbs.ArtifactResolver{
ConfigurationResolver: libpak.ConfigurationResolver{
Configurations: []libpak.BuildpackConfiguration{{Default: "target/native-sources"}},
},
}
application.ArtifactResolver = artifactResolver
})

it("multiple files", func() {

folder := filepath.Join(ctx.Application.Path, "target", "native-sources")
os.MkdirAll(folder, os.ModePerm)

files := []string{"stub-application.jar", "stub-executable.jar"}
for _, file := range files {
in, err := os.Open(filepath.Join("testdata", file))
Expect(err).NotTo(HaveOccurred())
out, err := os.OpenFile(filepath.Join(folder, 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())
}
application.Logger = bard.NewLogger(ioutil.Discard)
executor.On("Execute", mock.Anything).Return(nil)

layer, err := ctx.Layers.Layer("test-layer")
Expect(err).NotTo(HaveOccurred())

layer, err = application.Contribute(layer)

Expect(err).NotTo(HaveOccurred())

e := executor.Calls[0].Arguments[0].(effect.Execution)
Expect(e.Command).To(Equal("test-command"))
Expect(e.Args).To(Equal([]string{"test-argument"}))
Expect(e.Dir).To(Equal(ctx.Application.Path))
Expect(e.Stdout).NotTo(BeNil())
Expect(e.Stderr).NotTo(BeNil())

Expect(filepath.Join(layer.Path, "application.zip")).NotTo(BeAnExistingFile())
Expect(filepath.Join(ctx.Application.Path, "stub-application.jar")).To(BeAnExistingFile())
Expect(filepath.Join(ctx.Application.Path, "stub-executable.jar")).To(BeAnExistingFile())

})
})

context("multiple files", func() {
it("", func() {
files := []string{"stub-application.jar", "stub-executable.jar"}
for _, file := range files {
in, err := os.Open(filepath.Join("testdata", 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())
}
application.Logger = bard.NewLogger(ioutil.Discard)
executor.On("Execute", mock.Anything).Return(nil)

layer, err := ctx.Layers.Layer("test-layer")
Expect(err).NotTo(HaveOccurred())

layer, err = application.Contribute(layer)

Expect(err).NotTo(HaveOccurred())

e := executor.Calls[0].Arguments[0].(effect.Execution)
Expect(e.Command).To(Equal("test-command"))
Expect(e.Args).To(Equal([]string{"test-argument"}))
Expect(e.Dir).To(Equal(ctx.Application.Path))
Expect(e.Stdout).NotTo(BeNil())
Expect(e.Stderr).NotTo(BeNil())

Expect(filepath.Join(layer.Path, "application.zip")).NotTo(BeAnExistingFile())
Expect(filepath.Join(ctx.Application.Path, "stub-application.jar")).To(BeAnExistingFile())
Expect(filepath.Join(ctx.Application.Path, "stub-executable.jar")).To(BeAnExistingFile())

})
})
})
}
43 changes: 42 additions & 1 deletion resolvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"archive/zip"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"

Expand Down Expand Up @@ -131,7 +132,6 @@ func (a *ArtifactResolver) Pattern() string {
// Resolve resolves the artifact that was created by the build system.
func (a *ArtifactResolver) Resolve(applicationPath string) (string, error) {
pattern := a.Pattern()

file := filepath.Join(applicationPath, pattern)
candidates, err := filepath.Glob(file)
if err != nil {
Expand Down Expand Up @@ -163,6 +163,30 @@ func (a *ArtifactResolver) Resolve(applicationPath string) (string, error) {
return "", fmt.Errorf(helpMsg)
}

func (a *ArtifactResolver) ResolveMultipleArtifacts(applicationPath string) ([]string, error) {
pattern := a.Pattern()
file := filepath.Join(applicationPath, pattern)
candidates, err := filepath.Glob(file)
if err != nil {
return nil, fmt.Errorf("unable to find files with %s\n%w", pattern, err)
}
if len(candidates) == 0 {
return nil, fmt.Errorf("unable to find any artifact to resolve\n")
}
if len(candidates) == 1 {
// Double check it is a directory
fileInfo, err := os.Stat(candidates[0])
if err != nil {
return nil, err
}
if !fileInfo.IsDir() {
return nil, fmt.Errorf("multiple artifacts resolver expecting a directory but received a file %s\n",
fileInfo.Name())
}
}
return candidates, nil
}

// ResolveArguments resolves the arguments that should be passed to a build system.
func ResolveArguments(configurationKey string, configurationResolver libpak.ConfigurationResolver) ([]string, error) {
s, _ := configurationResolver.Resolve(configurationKey)
Expand All @@ -173,3 +197,20 @@ func ResolveArguments(configurationKey string, configurationResolver libpak.Conf

return w, nil
}

// singleArtifact evaluates if the Pattern() expression points to a single file artifact
func (a *ArtifactResolver) singleArtifact(applicationPath string) (bool, error) {
file := filepath.Join(applicationPath, a.Pattern())
candidates, err := filepath.Glob(file)
if err != nil {
return false, err
}
if len(candidates) == 1 {
f, err := os.Stat(candidates[0])
if err != nil {
return false, err
}
return !f.IsDir(), nil
}
return false, nil
}
61 changes: 60 additions & 1 deletion resolvers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func testResolvers(t *testing.T, context spec.G, it spec.S) {
})
})

context("ArtifactResolver", func() {
context("Resolve", func() {
var (
detector *mocks.InterestingFileDetector
path string
Expand Down Expand Up @@ -204,4 +204,63 @@ func testResolvers(t *testing.T, context spec.G, it spec.S) {
})
})
})

context("ResolveMultipleArtifacts", func() {
var (
detector *mocks.InterestingFileDetector
path string
resolver libbs.ArtifactResolver
)

it.Before(func() {
var err error

detector = &mocks.InterestingFileDetector{}

path, err = ioutil.TempDir("", "multiple-artifact-resolver")
Expect(err).NotTo(HaveOccurred())

resolver = libbs.ArtifactResolver{
ArtifactConfigurationKey: "TEST_ARTIFACT_CONFIGURATION_KEY",
ConfigurationResolver: libpak.ConfigurationResolver{
Configurations: []libpak.BuildpackConfiguration{
{Name: "TEST_ARTIFACT_CONFIGURATION_KEY", Default: "test-*"},
},
},
ModuleConfigurationKey: "TEST_MODULE_CONFIGURATION_KEY",
InterestingFileDetector: detector,
}
})

it.After(func() {
Expect(os.RemoveAll(path)).To(Succeed())
})

it("fails with a single candidate", func() {
Expect(ioutil.WriteFile(filepath.Join(path, "test-file"), []byte{}, 0644)).To(Succeed())

_, err := resolver.ResolveMultipleArtifacts(path)

Expect(err).To(MatchError(fmt.Sprintf("multiple artifacts resolver expecting a directory but received a file %s\n",
"test-file")))

})
it("passes with multiple candidates", func() {
Expect(ioutil.WriteFile(filepath.Join(path, "test-file"), []byte{}, 0644)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(path, "test-file-1"), []byte{}, 0644)).To(Succeed())

Expect(resolver.ResolveMultipleArtifacts(path)).To(ContainElements(filepath.Join(path, "test-file"), filepath.Join(path, "test-file-1")))
})

it("passes with a single folder candidate", func() {
Expect(os.Mkdir(filepath.Join(path, "test-folder"), os.ModePerm)).To(Succeed())
Expect(resolver.ResolveMultipleArtifacts(path)).To(ContainElement(filepath.Join(path, "test-folder")))
})

it("fails with zero candidates", func() {
_, err := resolver.ResolveMultipleArtifacts(path)

Expect(err).To(MatchError("unable to find any artifact to resolve\n"))
})
})
}