1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-09-16 09:26:22 +02:00

feat(cpe): provide go templating functions (#3872)

* feat(cpe): provide go templating functions

* change type

* fix: type in test

* chore: add comment for exported function

* fix: ensure that custom returns string properly

* fix types and add tests

Co-authored-by: Anil Keshav <anil.keshav@sap.com>
This commit is contained in:
Oliver Nocon
2022-07-14 16:20:11 +02:00
committed by GitHub
parent 5cc1b8f418
commit 53f4ce96ae
6 changed files with 319 additions and 27 deletions

3
.gitignore vendored
View File

@@ -48,5 +48,4 @@ AUnitResults.html
cmd/checkmarx/piper_checkmarx_report.json
cmd/fortify/piper_fortify_report.html
cmd/fortify/piper_fortify_report.json
cmd/toolruns/toolrun_malwarescan_20220519143229.json
cmd/toolruns/toolrun_protecode_20220519143230.json
cmd/toolruns

View File

@@ -9,7 +9,6 @@ import (
"path/filepath"
"regexp"
"strings"
"text/template"
"github.com/SAP/jenkins-library/pkg/buildsettings"
"github.com/SAP/jenkins-library/pkg/certutils"
@@ -174,11 +173,11 @@ func runGolangBuild(config *golangBuildOptions, telemetryData *telemetry.CustomD
ldflags := ""
if len(config.LdflagsTemplate) > 0 {
var err error
ldflags, err = prepareLdflags(config, utils, GeneralConfig.EnvRootPath)
ldf, err := prepareLdflags(config, utils, GeneralConfig.EnvRootPath)
if err != nil {
return err
}
ldflags = (*ldf).String()
log.Entry().Infof("ldflags from template: '%v'", ldflags)
}
@@ -405,7 +404,7 @@ func reportGolangTestCoverage(config *golangBuildOptions, utils golangBuildUtils
return nil
}
func prepareLdflags(config *golangBuildOptions, utils golangBuildUtils, envRootPath string) (string, error) {
func prepareLdflags(config *golangBuildOptions, utils golangBuildUtils, envRootPath string) (*bytes.Buffer, error) {
cpe := piperenv.CPEMap{}
err := cpe.LoadFromDisk(path.Join(envRootPath, "commonPipelineEnvironment"))
if err != nil {
@@ -413,23 +412,7 @@ func prepareLdflags(config *golangBuildOptions, utils golangBuildUtils, envRootP
}
log.Entry().Debugf("ldflagsTemplate in use: %v", config.LdflagsTemplate)
tmpl, err := template.New("ldflags").Parse(config.LdflagsTemplate)
if err != nil {
return "", fmt.Errorf("failed to parse ldflagsTemplate '%v': %w", config.LdflagsTemplate, err)
}
ldflagsParams := struct {
CPE map[string]interface{}
}{
CPE: map[string]interface{}(cpe),
}
var generatedLdflags bytes.Buffer
err = tmpl.Execute(&generatedLdflags, ldflagsParams)
if err != nil {
return "", fmt.Errorf("failed to execute ldflagsTemplate '%v': %w", config.LdflagsTemplate, err)
}
return generatedLdflags.String(), nil
return cpe.ParseTemplate(config.LdflagsTemplate)
}
func runGolangBuildPerArchitecture(config *golangBuildOptions, goModFile *modfile.File, utils golangBuildUtils, ldflags string, architecture multiarch.Platform) ([]string, error) {

View File

@@ -310,7 +310,7 @@ go 1.17`
telemetryData := telemetry.CustomData{}
err := runGolangBuild(&config, &telemetryData, utils, &cpe)
assert.Contains(t, fmt.Sprint(err), "failed to parse ldflagsTemplate")
assert.Contains(t, fmt.Sprint(err), "failed to parse cpe template")
})
t.Run("failure - build failure", func(t *testing.T) {
@@ -626,14 +626,14 @@ func TestPrepareLdflags(t *testing.T) {
utils := newGolangBuildTestsUtils()
result, err := prepareLdflags(&config, utils, dir)
assert.NoError(t, err)
assert.Equal(t, "-X version=1.2.3", result)
assert.Equal(t, "-X version=1.2.3", (*result).String())
})
t.Run("error - template parsing", func(t *testing.T) {
config := golangBuildOptions{LdflagsTemplate: "-X version={{ .CPE.artifactVersion "}
utils := newGolangBuildTestsUtils()
_, err := prepareLdflags(&config, utils, dir)
assert.Contains(t, fmt.Sprint(err), "failed to parse ldflagsTemplate")
assert.Contains(t, fmt.Sprint(err), "failed to parse cpe template")
})
}

View File

@@ -2,6 +2,7 @@ package piperenv
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -56,7 +57,10 @@ func writeToDisk(filename string, data []byte) error {
if _, err := os.Stat(filepath.Dir(filename)); os.IsNotExist(err) {
log.Entry().Debugf("Creating directory: %v", filepath.Dir(filename))
os.MkdirAll(filepath.Dir(filename), 0777)
cErr := os.MkdirAll(filepath.Dir(filename), 0777)
if cErr != nil {
return fmt.Errorf("failed to create directory %v, %w", filepath.Dir(filename), cErr)
}
}
//ToDo: make sure to not overwrite file but rather add another file? Create error if already existing?

View File

@@ -0,0 +1,87 @@
package piperenv
import (
"bytes"
"fmt"
"strings"
"text/template"
)
// ParseTemplate allows to parse a template which contains references to the CPE
// Utility functions make it simple to access specific parts of the CPE
func (c *CPEMap) ParseTemplate(cpeTemplate string) (*bytes.Buffer, error) {
funcMap := template.FuncMap{
"cpe": c.cpe,
"cpecustom": c.custom,
"git": c.git,
"imageDigest": c.imageDigest,
"imageTag": c.imageTag,
// ToDo: add template function for artifacts
// This requires alignment on artifact handling before, though
}
tmpl, err := template.New("cpetemplate").Funcs(funcMap).Parse(cpeTemplate)
if err != nil {
return nil, fmt.Errorf("failed to parse cpe template '%v': %w", cpeTemplate, err)
}
tmplParams := struct {
CPE map[string]interface{}
}{
CPE: map[string]interface{}(*c),
}
var generated bytes.Buffer
err = tmpl.Execute(&generated, tmplParams)
if err != nil {
return nil, fmt.Errorf("failed to execute cpe template '%v': %w", cpeTemplate, err)
}
return &generated, nil
}
func (c *CPEMap) cpe(element string) string {
// ToDo: perform validity checks to allow only selected fields for now?
// This would allow a stable contract and could perform conversions in case a contract changes.
return fmt.Sprint(map[string]interface{}(*c)[element])
}
func (c *CPEMap) custom(element string) string {
return fmt.Sprint(map[string]interface{}(*c)[fmt.Sprintf("custom/%v", element)])
}
func (c *CPEMap) git(element string) string {
var el string
if element == "organization" || element == "repository" {
el = fmt.Sprint(map[string]interface{}(*c)[fmt.Sprintf("github/%v", element)])
} else {
el = fmt.Sprint(map[string]interface{}(*c)[fmt.Sprintf("git/%v", element)])
}
return el
}
func (c *CPEMap) imageDigest(imageName string) string {
digests, _ := map[string]interface{}(*c)["container/imageDigests"].([]interface{})
imageNames, _ := map[string]interface{}(*c)["container/imageNames"].([]interface{})
if len(digests) > 0 && len(digests) == len(imageNames) {
for i, image := range imageNames {
if fmt.Sprint(image) == imageName {
return fmt.Sprint(digests[i])
}
}
}
return ""
}
func (c *CPEMap) imageTag(imageName string) string {
nameTags, _ := map[string]interface{}(*c)["container/imageNameTags"].([]interface{})
for _, nameTag := range nameTags {
nt := strings.Split(fmt.Sprint(nameTag), ":")
if nt[0] == imageName {
return nt[1]
}
}
return ""
}

View File

@@ -0,0 +1,219 @@
package piperenv
import (
"fmt"
"io/ioutil"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseTemplate(t *testing.T) {
tt := []struct {
template string
cpe CPEMap
expected string
expectedError error
}{
{template: `version: {{index .CPE "artifactVersion"}}, sha: {{git "commitId"}}`, expected: "version: 1.2.3, sha: thisIsMyTestSha"},
{template: "version: {{", expectedError: fmt.Errorf("failed to parse cpe template 'version: {{'")},
}
cpe := CPEMap{
"artifactVersion": "1.2.3",
"git/commitId": "thisIsMyTestSha",
}
for _, test := range tt {
res, err := cpe.ParseTemplate(test.template)
if test.expectedError != nil {
assert.Contains(t, fmt.Sprint(err), fmt.Sprint(test.expectedError))
} else {
assert.NoError(t, err)
assert.Equal(t, test.expected, (*res).String())
}
}
}
func TestTemplateFunctionCpe(t *testing.T) {
t.Run("CPE from object", func(t *testing.T) {
tt := []struct {
element string
expected string
}{
{element: "artifactVersion", expected: "1.2.3"},
{element: "git/commitId", expected: "thisIsMyTestSha"},
}
cpe := CPEMap{
"artifactVersion": "1.2.3",
"git/commitId": "thisIsMyTestSha",
}
for _, test := range tt {
assert.Equal(t, test.expected, cpe.cpe(test.element))
}
})
t.Run("CPE from files", func(t *testing.T) {
theVersion := "1.2.3"
dir := t.TempDir()
assert.NoError(t, ioutil.WriteFile(filepath.Join(dir, "artifactVersion"), []byte(theVersion), 0o666))
cpe := CPEMap{}
assert.NoError(t, cpe.LoadFromDisk(dir))
res, err := cpe.ParseTemplate(`{{cpe "artifactVersion"}}`)
assert.NoError(t, err)
assert.Equal(t, theVersion, (*res).String())
})
}
func TestTemplateFunctionCustom(t *testing.T) {
tt := []struct {
element string
expected string
}{
{element: "repositoryUrl", expected: "https://this.is.the.repo.url"},
{element: "repositoryId", expected: "repoTestId"},
}
cpe := CPEMap{
"custom/repositoryUrl": "https://this.is.the.repo.url",
"custom/repositoryId": "repoTestId",
}
for _, test := range tt {
assert.Equal(t, test.expected, cpe.custom(test.element))
}
}
func TestTemplateFunctionGit(t *testing.T) {
tt := []struct {
element string
expected string
}{
{element: "commitId", expected: "thisIsMyTestSha"},
{element: "repository", expected: "testRepo"},
}
cpe := CPEMap{
"git/commitId": "thisIsMyTestSha",
"github/repository": "testRepo",
}
for _, test := range tt {
assert.Equal(t, test.expected, cpe.git(test.element))
}
}
func TestTemplateFunctionImageDigest(t *testing.T) {
t.Run("CPE from object", func(t *testing.T) {
tt := []struct {
imageName string
cpe CPEMap
expected string
}{
{
imageName: "image1",
cpe: CPEMap{},
expected: "",
},
{
imageName: "image2",
cpe: CPEMap{
"container/imageDigests": []interface{}{"digest1", "digest2", "digest3"},
"container/imageNames": []interface{}{"image1", "image2", "image3"},
},
expected: "digest2",
},
{
imageName: "image4",
cpe: CPEMap{
"container/imageDigests": []interface{}{"digest1", "digest2", "digest3"},
"container/imageNames": []interface{}{"image1", "image2", "image3"},
},
expected: "",
},
{
imageName: "image1",
cpe: CPEMap{
"container/imageDigests": []interface{}{"digest1", "digest3"},
"container/imageNames": []interface{}{"image1", "image2", "image3"},
},
expected: "",
},
}
for _, test := range tt {
assert.Equal(t, test.expected, test.cpe.imageDigest(test.imageName))
}
})
t.Run("CPE from files", func(t *testing.T) {
dir := t.TempDir()
imageDigests := []string{"digest1", "digest2", "digest3"}
imageNames := []string{"image1", "image2", "image3"}
cpeOut := CPEMap{"container/imageDigests": imageDigests, "container/imageNames": imageNames}
assert.NoError(t, cpeOut.WriteToDisk(dir))
cpe := CPEMap{}
assert.NoError(t, cpe.LoadFromDisk(dir))
res, err := cpe.ParseTemplate(`{{imageDigest "image2"}}`)
assert.NoError(t, err)
assert.Equal(t, "digest2", (*res).String())
})
}
func TestTemplateFunctionImageTag(t *testing.T) {
t.Run("CPE from object", func(t *testing.T) {
tt := []struct {
imageName string
cpe CPEMap
expected string
}{
{
imageName: "image1",
cpe: CPEMap{},
expected: "",
},
{
imageName: "image2",
cpe: CPEMap{
"container/imageNameTags": []interface{}{"image1:tag1", "image2:tag2", "image3:tag3"},
},
expected: "tag2",
},
{
imageName: "image4",
cpe: CPEMap{
"container/imageNameTags": []interface{}{"image1:tag1", "image2:tag2", "image3:tag3"},
},
expected: "",
},
}
for _, test := range tt {
assert.Equal(t, test.expected, test.cpe.imageTag(test.imageName))
}
})
t.Run("CPE from files", func(t *testing.T) {
dir := t.TempDir()
imageNameTags := []string{"image1:tag1", "image2:tag2", "image3:tag3"}
imageNames := []string{"image1", "image2", "image3"}
cpeOut := CPEMap{"container/imageNameTags": imageNameTags, "container/imageNames": imageNames}
assert.NoError(t, cpeOut.WriteToDisk(dir))
cpe := CPEMap{}
assert.NoError(t, cpe.LoadFromDisk(dir))
res, err := cpe.ParseTemplate(`{{imageTag "image2"}}`)
assert.NoError(t, err)
assert.Equal(t, "tag2", (*res).String())
})
}