1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-18 05:18:24 +02:00

Feat(cnbBuild): collecting the telemetry data for cnbBuild (#3368)

Co-authored-by: Sumit Kulhadia <sumit.kulhadia@sap.com>
Co-authored-by: Ralf Pannemans <ralf.pannemans@sap.com>
Co-authored-by: Philipp Stehle <philipp.stehle@sap.com>
Co-authored-by: Pavel Busko <pavel.busko@sap.com>
This commit is contained in:
kulhadia 2022-01-31 12:27:29 +01:00 committed by GitHub
parent 880be73a4c
commit 6feb1217aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 408 additions and 5 deletions

View File

@ -2,6 +2,7 @@ package cmd
import (
"archive/zip"
"encoding/json"
"fmt"
"os"
"path"
@ -11,6 +12,7 @@ import (
"github.com/SAP/jenkins-library/pkg/certutils"
"github.com/SAP/jenkins-library/pkg/cnbutils"
"github.com/SAP/jenkins-library/pkg/cnbutils/bindings"
"github.com/SAP/jenkins-library/pkg/cnbutils/privacy"
"github.com/SAP/jenkins-library/pkg/cnbutils/project"
"github.com/SAP/jenkins-library/pkg/cnbutils/project/metadata"
"github.com/SAP/jenkins-library/pkg/command"
@ -34,6 +36,37 @@ type cnbBuildUtilsBundle struct {
*docker.Client
}
type cnbBuildTelemetryData struct {
Version int `json:"version"`
ImageTag string `json:"imageTag"`
AdditionalTags []string `json:"additionalTags"`
BindingKeys []string `json:"bindingKeys"`
Path string `json:"path"`
BuildEnv cnbBuildTelemetryDataBuildEnv `json:"buildEnv"`
Buildpacks cnbBuildTelemetryDataBuildpacks `json:"buildpacks"`
ProjectDescriptor cnbBuildTelemetryDataProjectDescriptor `json:"projectDescriptor"`
}
type cnbBuildTelemetryDataBuildEnv struct {
KeysFromConfig []string `json:"keysFromConfig"`
KeysFromProjectDescriptor []string `json:"keysFromProjectDescriptor"`
KeysOverall []string `json:"keysOverall"`
JVMVersion string `json:"jvmVersion"`
KeyValues map[string]interface{} `json:"keyValues"`
}
type cnbBuildTelemetryDataBuildpacks struct {
FromConfig []string `json:"FromConfig"`
FromProjectDescriptor []string `json:"FromProjectDescriptor"`
Overall []string `json:"overall"`
}
type cnbBuildTelemetryDataProjectDescriptor struct {
Used bool `json:"used"`
IncludeUsed bool `json:"includeUsed"`
ExcludeUsed bool `json:"excludeUsed"`
}
func setCustomBuildpacks(bpacks []string, dockerCreds string, utils cnbutils.BuildUtils) (string, string, error) {
buildpacksPath := "/tmp/buildpacks"
orderPath := "/tmp/buildpacks/order.toml"
@ -249,9 +282,51 @@ func (c *cnbBuildOptions) mergeEnvVars(vars map[string]interface{}) {
}
}
func addConfigTelemetryData(data *cnbBuildTelemetryData, config *cnbBuildOptions) {
bindingKeys := []string{}
for k := range config.Bindings {
bindingKeys = append(bindingKeys, k)
}
data.ImageTag = config.ContainerImageTag
data.AdditionalTags = config.AdditionalTags
data.BindingKeys = bindingKeys
data.Path = config.Path
configKeys := data.BuildEnv.KeysFromConfig
overallKeys := data.BuildEnv.KeysOverall
for key := range config.BuildEnvVars {
configKeys = append(configKeys, key)
overallKeys = append(overallKeys, key)
}
data.BuildEnv.KeysFromConfig = configKeys
data.BuildEnv.KeysOverall = overallKeys
data.Buildpacks.FromConfig = privacy.FilterBuildpacks(config.Buildpacks)
}
func addProjectDescriptorTelemetryData(data *cnbBuildTelemetryData, descriptor project.Descriptor) {
descriptorKeys := data.BuildEnv.KeysFromProjectDescriptor
overallKeys := data.BuildEnv.KeysOverall
for key := range descriptor.EnvVars {
descriptorKeys = append(descriptorKeys, key)
overallKeys = append(overallKeys, key)
}
data.BuildEnv.KeysFromProjectDescriptor = descriptorKeys
data.BuildEnv.KeysOverall = overallKeys
data.Buildpacks.FromProjectDescriptor = privacy.FilterBuildpacks(descriptor.Buildpacks)
data.ProjectDescriptor.Used = true
data.ProjectDescriptor.IncludeUsed = descriptor.Include != nil
data.ProjectDescriptor.ExcludeUsed = descriptor.Exclude != nil
}
func runCnbBuild(config *cnbBuildOptions, telemetryData *telemetry.CustomData, utils cnbutils.BuildUtils, commonPipelineEnvironment *cnbBuildCommonPipelineEnvironment, httpClient piperhttp.Sender) error {
var err error
customTelemetryData := cnbBuildTelemetryData{Version: 1}
addConfigTelemetryData(&customTelemetryData, config)
err = isBuilder(utils)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
@ -274,6 +349,7 @@ func runCnbBuild(config *cnbBuildOptions, telemetryData *telemetry.CustomData, u
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "failed to parse %s", config.ProjectDescriptor)
}
addProjectDescriptorTelemetryData(&customTelemetryData, *descriptor)
config.mergeEnvVars(descriptor.EnvVars)
@ -297,6 +373,16 @@ func runCnbBuild(config *cnbBuildOptions, telemetryData *telemetry.CustomData, u
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrap(err, "failed to retrieve target image configuration")
}
customTelemetryData.Buildpacks.Overall = config.Buildpacks
customTelemetryData.BuildEnv.KeyValues = privacy.FilterEnv(config.BuildEnvVars)
telemetryData.Custom1Label = "cnbBuildStepData"
customData, err := json.Marshal(customTelemetryData)
if err != nil {
log.SetErrorCategory(log.ErrorCustom)
return errors.Wrap(err, "failed to marshal custom telemetry data")
}
telemetryData.Custom1 = string(customData)
commonPipelineEnvironment.container.registryURL = fmt.Sprintf("%s://%s", targetImage.ContainerRegistry.Scheme, targetImage.ContainerRegistry.Host)
commonPipelineEnvironment.container.imageNameTag = fmt.Sprintf("%v:%v", targetImage.ContainerImageName, targetImage.ContainerImageTag)

View File

@ -1,6 +1,7 @@
package cmd
import (
"encoding/json"
"fmt"
"net/http"
"testing"
@ -313,7 +314,7 @@ func TestRunCnbBuild(t *testing.T) {
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":"dXNlcjpwYXNz"}}`))
addBuilderFiles(&utils)
err := runCnbBuild(&config, nil, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
err := runCnbBuild(&config, &telemetry.CustomData{}, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
assert.EqualError(t, err, "failed to generate CNB_REGISTRY_AUTH: json: cannot unmarshal string into Go struct field ConfigFile.auths of type types.AuthConfig")
})
@ -330,7 +331,7 @@ func TestRunCnbBuild(t *testing.T) {
utils := newCnbBuildTestsUtils()
addBuilderFiles(&utils)
err := runCnbBuild(&config, nil, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
err := runCnbBuild(&config, &telemetry.CustomData{}, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
assert.EqualError(t, err, "failed to generate CNB_REGISTRY_AUTH: could not read 'not-there/config.json'")
})
@ -347,7 +348,7 @@ func TestRunCnbBuild(t *testing.T) {
utils := newCnbBuildTestsUtils()
addBuilderFiles(&utils)
err := runCnbBuild(&config, nil, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
err := runCnbBuild(&config, &telemetry.CustomData{}, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
assert.EqualError(t, err, "failed to rename DockerConfigJSON file 'not-there': renaming file 'not-there' is not supported, since it does not exist, or is not a leaf-entry")
})
@ -358,7 +359,7 @@ func TestRunCnbBuild(t *testing.T) {
utils := newCnbBuildTestsUtils()
err := runCnbBuild(&config, nil, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
err := runCnbBuild(&config, &telemetry.CustomData{}, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
assert.EqualError(t, err, "the provided dockerImage is not a valid builder: binary '/cnb/lifecycle/creator' not found")
})
@ -378,7 +379,126 @@ func TestRunCnbBuild(t *testing.T) {
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
addBuilderFiles(&utils)
err := runCnbBuild(&config, nil, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
err := runCnbBuild(&config, &telemetry.CustomData{}, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
assert.EqualError(t, err, "failed to copy certificates: cannot copy '/etc/ssl/certs/ca-certificates.crt': file does not exist")
})
t.Run("success case (telemetry was added)", func(t *testing.T) {
t.Parallel()
commonPipelineEnvironment := cnbBuildCommonPipelineEnvironment{}
registry := "some-registry"
config := cnbBuildOptions{
ContainerImageName: "my-image",
ContainerImageTag: "3.1.5",
ContainerRegistryURL: registry,
DockerConfigJSON: "/path/to/config.json",
ProjectDescriptor: "project.toml",
AdditionalTags: []string{"latest"},
Buildpacks: []string{"paketobuildpacks/java", "gcr.io/paketo-buildpacks/node"},
Bindings: map[string]interface{}{"SECRET": map[string]string{"key": "KEY", "file": "a_file"}},
Path: "target",
}
utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`))
utils.FilesMock.AddFile("project.toml", []byte(`[project]
id = "test"
name = "test"
version = "1.0.0"
[build]
include = []
exclude = ["*.tar"]
[[build.buildpacks]]
uri = "some-buildpack"`))
utils.FilesMock.AddFile("a_file", []byte(`{}`))
utils.FilesMock.AddDir("target")
utils.FilesMock.AddFile("target/somelib.jar", []byte(`FFFFFF`))
addBuilderFiles(&utils)
telemetryData := telemetry.CustomData{}
err := runCnbBuild(&config, &telemetryData, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
assert.NoError(t, err)
customDataAsString := telemetryData.Custom1
customData := cnbBuildTelemetryData{}
err = json.Unmarshal([]byte(customDataAsString), &customData)
assert.NoError(t, err)
assert.Equal(t, 1, customData.Version)
assert.Equal(t, "3.1.5", customData.ImageTag)
assert.Equal(t, "target", customData.Path)
assert.Contains(t, customData.AdditionalTags, "latest")
assert.Contains(t, customData.BindingKeys, "SECRET")
assert.Contains(t, customData.Buildpacks.FromConfig, "paketobuildpacks/java")
assert.NotContains(t, customData.Buildpacks.FromProjectDescriptor, "paketobuildpacks/java")
assert.Contains(t, customData.Buildpacks.FromProjectDescriptor, "<redacted>")
assert.NotContains(t, customData.Buildpacks.Overall, "<redacted>")
assert.Contains(t, customData.Buildpacks.Overall, "paketobuildpacks/java")
assert.True(t, customData.ProjectDescriptor.Used)
assert.False(t, customData.ProjectDescriptor.IncludeUsed)
assert.True(t, customData.ProjectDescriptor.ExcludeUsed)
})
t.Run("success case (build env telemetry was added)", func(t *testing.T) {
t.Parallel()
commonPipelineEnvironment := cnbBuildCommonPipelineEnvironment{}
registry := "some-registry"
config := cnbBuildOptions{
ContainerImageName: "my-image",
ContainerImageTag: "3.1.5",
ContainerRegistryURL: registry,
ProjectDescriptor: "project.toml",
BuildEnvVars: map[string]interface{}{"CONFIG_KEY": "var", "BP_JVM_VERSION": "8"},
}
utils := newCnbBuildTestsUtils()
utils.FilesMock.AddFile("project.toml", []byte(`[project]
id = "test"
[build]
include = []
[[build.env]]
name='PROJECT_KEY'
value='var'
[[build.env]]
name='BP_NODE_VERSION'
value='11'
[[build.buildpacks]]
uri = "some-buildpack"
`))
addBuilderFiles(&utils)
telemetryData := telemetry.CustomData{}
err := runCnbBuild(&config, &telemetryData, &utils, &commonPipelineEnvironment, &piperhttp.Client{})
assert.NoError(t, err)
customDataAsString := telemetryData.Custom1
customData := cnbBuildTelemetryData{}
err = json.Unmarshal([]byte(customDataAsString), &customData)
assert.NoError(t, err)
assert.Contains(t, customData.BuildEnv.KeysFromConfig, "CONFIG_KEY")
assert.NotContains(t, customData.BuildEnv.KeysFromProjectDescriptor, "CONFIG_KEY")
assert.Contains(t, customData.BuildEnv.KeysOverall, "CONFIG_KEY")
assert.NotContains(t, customData.BuildEnv.KeysFromConfig, "PROJECT_KEY")
assert.Contains(t, customData.BuildEnv.KeysFromProjectDescriptor, "PROJECT_KEY")
assert.Contains(t, customData.BuildEnv.KeysOverall, "PROJECT_KEY")
assert.Equal(t, "8", customData.BuildEnv.KeyValues["BP_JVM_VERSION"])
assert.Equal(t, "11", customData.BuildEnv.KeyValues["BP_NODE_VERSION"])
assert.NotContains(t, customData.BuildEnv.KeyValues, "PROJECT_KEY")
assert.Contains(t, customData.Buildpacks.Overall, "some-buildpack")
})
}

View File

@ -0,0 +1,85 @@
package privacy
import (
"strings"
containerName "github.com/google/go-containerregistry/pkg/name"
)
var allowedBuildpackSources = []struct {
registry, repositoryPrefix string
}{
// Paketo
{
registry: "gcr.io",
repositoryPrefix: "paketo-buildpacks/",
}, {
registry: "index.docker.io",
repositoryPrefix: "paketobuildpacks/",
},
// Google Buildpacks
{
registry: "gcr.io",
repositoryPrefix: "buildpacks/",
},
// Heroku
{
registry: "public.ecr.aws",
repositoryPrefix: "heroku-buildpacks/",
},
}
// FilterBuildpacks filters a list of buildpacks to redact Personally Identifiable Information (PII) like the hostname of a personal registry
func FilterBuildpacks(buildpacks []string) []string {
result := make([]string, 0, len(buildpacks))
for _, buildpack := range buildpacks {
ref, err := containerName.ParseReference(strings.ToLower(buildpack))
if err != nil {
result = append(result, "<error>")
continue
}
registry := ref.Context().Registry.Name()
repository := ref.Context().RepositoryStr()
allowed := false
for _, allowedBuildpackSource := range allowedBuildpackSources {
if registry == allowedBuildpackSource.registry && strings.HasPrefix(repository, allowedBuildpackSource.repositoryPrefix) {
allowed = true
break
}
}
if allowed {
result = append(result, buildpack)
} else {
result = append(result, "<redacted>")
}
}
return result
}
var allowedEnvKeys = map[string]interface{}{
// Java
// https://github.com/paketo-buildpacks/sap-machine and https://github.com/paketo-buildpacks/bellsoft-liberica
"BP_JVM_VERSION": nil,
"BP_JVM_TYPE": nil,
// https://github.com/paketo-buildpacks/apache-tomcat
"BP_TOMCAT_VERSION": nil,
// Node
// https://github.com/paketo-buildpacks/node-engine
"BP_NODE_VERSION": nil,
}
// FilterEnv filters a map of environment variables to redact Personally Identifiable Information (PII)
func FilterEnv(in map[string]interface{}) map[string]interface{} {
out := map[string]interface{}{}
for key, value := range in {
_, allowed := allowedEnvKeys[key]
if allowed {
out[key] = value
}
}
return out
}

View File

@ -0,0 +1,112 @@
package privacy_test
import (
"testing"
"github.com/SAP/jenkins-library/pkg/cnbutils/privacy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCnbPrivacy_FilterBuildpacks(t *testing.T) {
t.Parallel()
t.Run("allows paketo", func(t *testing.T) {
aliases := []string{
"paketobuildpacks/nodejs:v1",
"docker.io/paketobuildpacks/nodejs:v1",
"index.docker.io/paketobuildpacks/nodejs:v1",
"gcr.io/paketo-buildpacks/nodejs:v1",
}
filtered := privacy.FilterBuildpacks(aliases)
require.Len(t, filtered, len(aliases))
for i := range filtered {
assert.Equal(t, aliases[i], filtered[i])
}
})
t.Run("allows heroku", func(t *testing.T) {
aliases := []string{
"public.ecr.aws/heroku-buildpacks/heroku-jvm-buildpack@sha256:3a8ee9ebf88e47c5e30bc5712fb2794380aed75552499f92bd6773ec446421ef",
}
filtered := privacy.FilterBuildpacks(aliases)
require.Len(t, filtered, len(aliases))
for i := range filtered {
assert.Equal(t, aliases[i], filtered[i])
}
})
t.Run("allows google buildpacks", func(t *testing.T) {
aliases := []string{
"gcr.io/buildpacks/java:latest",
"gcr.io/buildpacks/java",
}
filtered := privacy.FilterBuildpacks(aliases)
require.Len(t, filtered, len(aliases))
for i := range filtered {
assert.Equal(t, aliases[i], filtered[i])
}
})
t.Run("filters others", func(t *testing.T) {
images := []string{
"test/nodejs:v1",
"my-mirror.de/paketobuildpacks/nodejs:v1",
"gcr.io/my-project/paketo-buildpacks/nodejs:v1",
}
filtered := privacy.FilterBuildpacks(images)
require.Len(t, filtered, len(images))
for _, image := range filtered {
assert.Equal(t, "<redacted>", image)
}
})
t.Run("fails gracefully on parse error", func(t *testing.T) {
images := []string{
"test/nodejs v1 spaces are not allowed",
}
filtered := privacy.FilterBuildpacks(images)
require.Len(t, filtered, len(images))
for _, image := range filtered {
assert.Equal(t, "<error>", image)
}
})
}
func TestCnbPrivacy_FilterEnv(t *testing.T) {
t.Parallel()
t.Run("copies only allow listed keys", func(t *testing.T) {
env := map[string]interface{}{
"PRIVATE": "paketobuildpacks/nodejs:v1",
"BP_NODE_VERSION": "8",
"BP_JVM_VERSION": "11",
}
filteredEnv := privacy.FilterEnv(env)
assert.Equal(t, map[string]interface{}{
"BP_NODE_VERSION": "8",
"BP_JVM_VERSION": "11",
}, filteredEnv)
})
t.Run("works on nil map", func(t *testing.T) {
var env map[string]interface{} = nil
filteredEnv := privacy.FilterEnv(env)
assert.Empty(t, filteredEnv)
})
}