From 1750b75cb88eb01a857adf3dff46b7d9a8803cd4 Mon Sep 17 00:00:00 2001 From: Pavel Busko Date: Fri, 14 Jan 2022 11:05:11 +0100 Subject: [PATCH] feat(cnbBuild): preserve maven test results in the workspace (#3429) Co-authored-by: Pavel Busko Co-authored-by: Ralf Pannemans --- cmd/cnbBuild.go | 48 +++++++++++++++------ cmd/cnbBuild_test.go | 49 ++++++++++++++++++++++ pkg/mock/fileUtils.go | 42 +++++++++++++++++++ pkg/mock/fileUtils_test.go | 31 ++++++++++++++ pkg/piperutils/FileUtils.go | 6 +++ resources/default_pipeline_environment.yml | 4 +- 6 files changed, 166 insertions(+), 14 deletions(-) diff --git a/cmd/cnbBuild.go b/cmd/cnbBuild.go index c3addd04d..84966b093 100644 --- a/cmd/cnbBuild.go +++ b/cmd/cnbBuild.go @@ -98,14 +98,6 @@ func isIgnored(find string, include, exclude *ignore.GitIgnore) bool { return false } -func isDir(path string) (bool, error) { - info, err := os.Stat(path) - if err != nil { - return false, err - } - return info.IsDir(), nil -} - func isBuilder(utils cnbutils.BuildUtils) error { exists, err := utils.FileExists(creatorPath) if err != nil { @@ -163,7 +155,7 @@ func copyProject(source, target string, include, exclude *ignore.GitIgnore, util } if !isIgnored(relPath, include, exclude) { target := path.Join(target, strings.ReplaceAll(sourceFile, source, "")) - dir, err := isDir(sourceFile) + dir, err := utils.DirExists(sourceFile) if err != nil { log.SetErrorCategory(log.ErrorBuild) return errors.Wrapf(err, "Checking file info '%s' failed", target) @@ -221,6 +213,27 @@ func prepareDockerConfig(source string, utils cnbutils.BuildUtils) (string, erro return source, nil } +func linkTargetFolder(utils cnbutils.BuildUtils, source, target string) error { + var err error + linkPath := filepath.Join(target, "target") + targetPath := filepath.Join(source, "target") + if ok, _ := utils.DirExists(targetPath); !ok { + err = utils.MkdirAll(targetPath, os.ModePerm) + if err != nil { + return err + } + } + + if ok, _ := utils.DirExists(linkPath); ok { + err = utils.RemoveAll(linkPath) + if err != nil { + return err + } + } + + return utils.Symlink(targetPath, linkPath) +} + func (c *cnbBuildOptions) mergeEnvVars(vars map[string]interface{}) { if c.BuildEnvVars == nil { c.BuildEnvVars = vars @@ -302,20 +315,23 @@ func runCnbBuild(config *cnbBuildOptions, telemetryData *telemetry.CustomData, u } target := "/workspace" - source, err := utils.Getwd() + pwd, err := utils.Getwd() if err != nil { log.SetErrorCategory(log.ErrorBuild) return errors.Wrap(err, "failed to get current working directory") } + var source string if len(config.Path) > 0 { source = config.Path + } else { + source = pwd } - dir, err := isDir(source) + dir, err := utils.DirExists(source) if err != nil { log.SetErrorCategory(log.ErrorBuild) - return errors.Wrapf(err, "Checking file info '%s' failed", target) + return errors.Wrapf(err, "Checking file info '%s' failed", source) } if dir { @@ -332,6 +348,14 @@ func runCnbBuild(config *cnbBuildOptions, telemetryData *telemetry.CustomData, u } } + if ok, _ := utils.FileExists(filepath.Join(target, "pom.xml")); ok { + err = linkTargetFolder(utils, pwd, target) + if err != nil { + log.SetErrorCategory(log.ErrorBuild) + return err + } + } + metadata.WriteProjectMetadata(GeneralConfig.EnvRootPath, utils) var buildpacksPath = "/cnb/buildpacks" diff --git a/cmd/cnbBuild_test.go b/cmd/cnbBuild_test.go index f3ac9803e..55aa13863 100644 --- a/cmd/cnbBuild_test.go +++ b/cmd/cnbBuild_test.go @@ -192,6 +192,55 @@ func TestRunCnbBuild(t *testing.T) { assert.Contains(t, runner.Calls[0].Params, fmt.Sprintf("%s/%s:3.1.5", registry, config.ContainerImageName)) }) + t.Run("pom.xml exists (symlink for the target folder)", func(t *testing.T) { + t.Parallel() + config := cnbBuildOptions{ + ContainerImageName: "my-image", + ContainerImageTag: "3.1.5", + ContainerRegistryURL: "some-registry", + DockerConfigJSON: "/path/to/config.json", + } + + utils := newCnbBuildTestsUtils() + utils.FilesMock.CurrentDir = "/jenkins" + utils.FilesMock.AddDir("/jenkins") + utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`)) + utils.FilesMock.AddFile("/workspace/pom.xml", []byte("test")) + addBuilderFiles(&utils) + + err := runCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{}) + assert.NoError(t, err) + + runner := utils.ExecMockRunner + assertLifecycleCalls(t, runner) + + assert.True(t, utils.FilesMock.HasCreatedSymlink("/jenkins/target", "/workspace/target")) + }) + + t.Run("no pom.xml exists (no symlink for the target folder)", func(t *testing.T) { + t.Parallel() + config := cnbBuildOptions{ + ContainerImageName: "my-image", + ContainerImageTag: "3.1.5", + ContainerRegistryURL: "some-registry", + DockerConfigJSON: "/path/to/config.json", + } + + utils := newCnbBuildTestsUtils() + utils.FilesMock.CurrentDir = "/jenkins" + utils.FilesMock.AddDir("/jenkins") + utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`)) + addBuilderFiles(&utils) + + err := runCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{}) + assert.NoError(t, err) + + runner := utils.ExecMockRunner + assertLifecycleCalls(t, runner) + + assert.False(t, utils.FilesMock.HasCreatedSymlink("/jenkins/target", "/workspace/target")) + }) + t.Run("error case: Invalid DockerConfigJSON file", func(t *testing.T) { t.Parallel() commonPipelineEnvironment := cnbBuildCommonPipelineEnvironment{} diff --git a/pkg/mock/fileUtils.go b/pkg/mock/fileUtils.go index cba5e87d8..f29bb8bea 100644 --- a/pkg/mock/fileUtils.go +++ b/pkg/mock/fileUtils.go @@ -1,3 +1,4 @@ +//go:build !release // +build !release package mock @@ -38,6 +39,8 @@ func (fInfo fileInfoMock) Sys() interface{} { return nil } type fileProperties struct { content *[]byte mode os.FileMode + isLink bool + target string } // isDir returns true when the properties describe a directory entry. @@ -153,6 +156,18 @@ func (f *FilesMock) HasCopiedFile(src string, dest string) bool { return f.copiedFiles[f.toAbsPath(src)] == f.toAbsPath(dest) } +// HasCreatedSymlink returns true if the virtual file system has a symlink with a specific target. +func (f *FilesMock) HasCreatedSymlink(oldname, newname string) bool { + if f.files == nil { + return false + } + props, exists := f.files[f.toAbsPath(newname)] + if !exists { + return false + } + return props.isLink && props.target == oldname +} + // FileExists returns true if file content has been associated with the given path, false otherwise. // Only relative paths are supported. func (f *FilesMock) FileExists(path string) (bool, error) { @@ -455,6 +470,33 @@ func (f *FilesMock) Abs(path string) (string, error) { return f.toAbsPath(path), nil } +func (f *FilesMock) Symlink(oldname, newname string) error { + if f.FileWriteError != nil { + return f.FileWriteError + } + + if f.FileWriteErrors[newname] != nil { + return f.FileWriteErrors[newname] + } + + parentExists, err := f.DirExists(filepath.Dir(newname)) + if err != nil { + return err + } + if !parentExists { + return fmt.Errorf("failed to create symlink: parent directory %s doesn't exist", filepath.Dir(newname)) + } + + f.init() + + f.files[newname] = &fileProperties{ + isLink: true, + target: oldname, + } + + return nil +} + // FileMock can be used in places where a io.Closer, io.StringWriter or io.Writer is expected. // It is the concrete type returned from FilesMock.Open() type FileMock struct { diff --git a/pkg/mock/fileUtils_test.go b/pkg/mock/fileUtils_test.go index 57d25dbfa..7f50a46d8 100644 --- a/pkg/mock/fileUtils_test.go +++ b/pkg/mock/fileUtils_test.go @@ -1,6 +1,7 @@ package mock import ( + "errors" "os" "path/filepath" "testing" @@ -642,3 +643,33 @@ func TestFilesMockTempDir(t *testing.T) { assert.True(t, ok) }) } + +func TestFilesMockSymlink(t *testing.T) { + t.Parallel() + t.Run("creates a symlink", func(t *testing.T) { + files := FilesMock{} + files.AddDir("/backup") + assert.NoError(t, files.Symlink("/folder", "/backup/folder")) + + assert.True(t, files.HasCreatedSymlink("/folder", "/backup/folder")) + }) + + t.Run("fails if parent directory doesn't exist", func(t *testing.T) { + files := FilesMock{} + err := files.Symlink("/non/existent/folder", "/symbolic/link") + assert.Error(t, err) + assert.Equal(t, "failed to create symlink: parent directory /symbolic doesn't exist", err.Error()) + }) + + t.Run("fails if FileWriteError is specified", func(t *testing.T) { + expectedErr := errors.New("test") + files := FilesMock{ + FileWriteErrors: map[string]error{ + "/symbolic/link": expectedErr, + }, + } + err := files.Symlink("/non/existent/folder", "/symbolic/link") + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + }) +} diff --git a/pkg/piperutils/FileUtils.go b/pkg/piperutils/FileUtils.go index c186b8e19..7bff3e59c 100644 --- a/pkg/piperutils/FileUtils.go +++ b/pkg/piperutils/FileUtils.go @@ -32,6 +32,7 @@ type FileUtils interface { RemoveAll(string) error FileRename(string, string) error Getwd() (string, error) + Symlink(oldname string, newname string) error } // Files ... @@ -384,3 +385,8 @@ func (f Files) Stat(path string) (os.FileInfo, error) { func (f Files) Abs(path string) (string, error) { return filepath.Abs(path) } + +// Symlink is a wrapper for os.Symlink +func (f Files) Symlink(oldname, newname string) error { + return os.Symlink(oldname, newname) +} diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index 7e4041f73..b6e62b612 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -548,5 +548,5 @@ steps: whitesource: 'whitesourceExecuteScan' labelPrefix: pr_ cnbBuild: - stashExcludes: - stashBack: '**/*' + stashIncludes: + stashBack: '**/target/*.exec, **/*.jtl, **/target/**/*.xml'