diff --git a/cmd/cnbBuild.go b/cmd/cnbBuild.go index e676dd602..4e0a943ba 100644 --- a/cmd/cnbBuild.go +++ b/cmd/cnbBuild.go @@ -6,6 +6,7 @@ import ( "os" "path" "path/filepath" + "syscall" "github.com/SAP/jenkins-library/pkg/buildpacks" "github.com/SAP/jenkins-library/pkg/buildsettings" @@ -285,8 +286,14 @@ func (config *cnbBuildOptions) resolvePath(utils cnbutils.BuildUtils) (buildpack func callCnbBuild(config *cnbBuildOptions, telemetryData *telemetry.CustomData, utils cnbutils.BuildUtils, commonPipelineEnvironment *cnbBuildCommonPipelineEnvironment, httpClient piperhttp.Sender) error { stepName := "cnbBuild" - telemetry := buildpacks.NewTelemetry(telemetryData) + err := isBuilder(utils) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return errors.Wrap(err, "the provided dockerImage is not a valid builder") + } + + telemetry := buildpacks.NewTelemetry(telemetryData) dockerImage, err := GetDockerImageValue(stepName) if err != nil { log.Entry().Warnf("failed to retrieve dockerImage configuration: '%v'", err) @@ -362,11 +369,22 @@ func runCnbBuild(config *cnbBuildOptions, telemetry *buildpacks.Telemetry, image return errors.Wrap(err, fmt.Sprintf("failed to clean up platform folder %s", platformPath)) } - tempdir, err := os.MkdirTemp("", "cnbBuild-") + tempdir, err := utils.TempDir("", "cnbBuild-") if err != nil { return errors.Wrap(err, "failed to create tempdir") } - defer os.RemoveAll(tempdir) + defer utils.RemoveAll(tempdir) + + uid, gid, err := cnbutils.CnbUserInfo() + if err != nil { + return errors.Wrap(err, "failed to get user information") + } + + err = utils.Chown(tempdir, uid, gid) + if err != nil { + return errors.Wrap(err, "failed to change tempdir ownership") + } + if config.BuildEnvVars == nil { config.BuildEnvVars = map[string]interface{}{} } @@ -374,12 +392,6 @@ func runCnbBuild(config *cnbBuildOptions, telemetry *buildpacks.Telemetry, image telemetrySegment := createInitialTelemetrySegment(config, utils) - err = isBuilder(utils) - if err != nil { - log.SetErrorCategory(log.ErrorConfiguration) - return errors.Wrap(err, "the provided dockerImage is not a valid builder") - } - include := ignore.CompileIgnoreLines("**/*") exclude := ignore.CompileIgnoreLines("piper", ".pipeline", ".git") @@ -486,6 +498,10 @@ func runCnbBuild(config *cnbBuildOptions, telemetry *buildpacks.Telemetry, image } } + if err := utils.Chown(target, uid, gid); err != nil { + return err + } + if ok, _ := utils.FileExists(filepath.Join(target, "pom.xml")); ok { err = linkTargetFolder(utils, source, target) if err != nil { @@ -565,7 +581,14 @@ func runCnbBuild(config *cnbBuildOptions, telemetry *buildpacks.Telemetry, image } creatorArgs = append(creatorArgs, fmt.Sprintf("%s:%s", containerImage, targetImage.ContainerImageTag)) - err = utils.RunExecutable(creatorPath, creatorArgs...) + attr := &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + }, + } + + err = utils.RunExecutableWithAttrs(creatorPath, attr, creatorArgs...) if err != nil { log.SetErrorCategory(log.ErrorBuild) return errors.Wrapf(err, "execution of '%s' failed", creatorArgs) diff --git a/cmd/cnbBuild_generated.go b/cmd/cnbBuild_generated.go index d2d22c488..88181e68b 100644 --- a/cmd/cnbBuild_generated.go +++ b/cmd/cnbBuild_generated.go @@ -519,7 +519,7 @@ func cnbBuildMetadata() config.StepData { }, }, Containers: []config.Container{ - {Image: "paketobuildpacks/builder:base"}, + {Image: "paketobuildpacks/builder:base", Options: []config.Option{{Name: "-u", Value: "0"}}}, }, Outputs: config.StepOutputs{ Resources: []config.StepResources{ diff --git a/cmd/cnbBuild_test.go b/cmd/cnbBuild_test.go index 24dd2331d..cbfbae266 100644 --- a/cmd/cnbBuild_test.go +++ b/cmd/cnbBuild_test.go @@ -107,6 +107,9 @@ func assetBuildEnv(t *testing.T, utils cnbutils.MockUtils, key, value string) bo func TestRunCnbBuild(t *testing.T) { configOptions.OpenFile = piperconf.OpenPiperFile + t.Setenv("CNB_USER_ID", "1000") + t.Setenv("CNB_GROUP_ID", "1000") + t.Run("prefers direct configuration", func(t *testing.T) { t.Parallel() commonPipelineEnvironment := cnbBuildCommonPipelineEnvironment{} diff --git a/integration/integration_cnb_test.go b/integration/integration_cnb_test.go index 6c5d2a42e..67cffbe5d 100644 --- a/integration/integration_cnb_test.go +++ b/integration/integration_cnb_test.go @@ -43,7 +43,7 @@ func TestCNBIntegrationNPMProject(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestCnbIntegration"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), Environment: map[string]string{ @@ -53,7 +53,7 @@ func TestCNBIntegrationNPMProject(t *testing.T) { container2 := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestCnbIntegration"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), Environment: map[string]string{ @@ -93,7 +93,7 @@ func TestCNBIntegrationProjectDescriptor(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestCnbIntegration", "project"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), }) @@ -123,7 +123,7 @@ func TestCNBIntegrationBuildSummary(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestCnbIntegration", "project"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), }) @@ -148,7 +148,7 @@ func TestCNBIntegrationZipPath(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestCnbIntegration", "zip"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), }) @@ -177,7 +177,7 @@ func TestCNBIntegrationNonZipPath(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestMtaIntegration", "npm"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), }) @@ -197,7 +197,7 @@ func TestCNBIntegrationNPMCustomBuildpacksFullProject(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestMtaIntegration", "npm"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), }) @@ -225,7 +225,7 @@ func TestCNBIntegrationNPMCustomBuildpacksBuildpacklessProject(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: "paketobuildpacks/builder:buildpackless-full", - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestMtaIntegration", "npm"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), }) @@ -266,7 +266,7 @@ func TestCNBIntegrationBindings(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestCnbIntegration"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), Environment: map[string]string{ @@ -294,7 +294,7 @@ func TestCNBIntegrationMultiImage(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestCnbIntegration"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), }) @@ -328,7 +328,7 @@ func TestCNBIntegrationPreserveFiles(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestCnbIntegration"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), }) @@ -348,7 +348,7 @@ func TestCNBIntegrationPreserveFilesIgnored(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestCnbIntegration"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), }) @@ -367,7 +367,7 @@ func TestCNBIntegrationPrePostBuildpacks(t *testing.T) { container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ Image: baseBuilder, - User: "cnb", + User: "0", TestDir: []string{"testdata", "TestCnbIntegration"}, Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), Environment: map[string]string{ diff --git a/pkg/cnbutils/user.go b/pkg/cnbutils/user.go new file mode 100644 index 000000000..189f8a15f --- /dev/null +++ b/pkg/cnbutils/user.go @@ -0,0 +1,32 @@ +package cnbutils + +import ( + "os" + "strconv" + + "github.com/pkg/errors" +) + +func CnbUserInfo() (int, int, error) { + uidStr, ok := os.LookupEnv("CNB_USER_ID") + if !ok { + return 0, 0, errors.New("environment variable CNB_USER_ID not found") + } + + gidStr, ok := os.LookupEnv("CNB_GROUP_ID") + if !ok { + return 0, 0, errors.New("environment variable CNB_GROUP_ID not found") + } + + uid, err := strconv.Atoi(uidStr) + if err != nil { + return 0, 0, err + } + + gid, err := strconv.Atoi(gidStr) + if err != nil { + return 0, 0, err + } + + return uid, gid, nil +} diff --git a/pkg/command/command.go b/pkg/command/command.go index d745a1a68..521d47ac2 100644 --- a/pkg/command/command.go +++ b/pkg/command/command.go @@ -42,6 +42,7 @@ type runner interface { type ExecRunner interface { runner RunExecutable(executable string, params ...string) error + RunExecutableWithAttrs(executable string, sysProcAttr *syscall.SysProcAttr, params ...string) error RunExecutableInBackground(executable string, params ...string) (Execution, error) } @@ -127,9 +128,18 @@ func (c *Command) RunShell(shell, script string) error { // // Thus the executable needs to be on the PATH of the current process and it is not sufficient to alter the PATH on cmd.Env. func (c *Command) RunExecutable(executable string, params ...string) error { + return c.RunExecutableWithAttrs(executable, nil, params...) +} + +// RunExecutableWithAttrs runs the specified executable with parameters and as a specified UID and GID +// !! While the cmd.Env is applied during command execution, it is NOT involved when the actual executable is resolved. +// +// Thus the executable needs to be on the PATH of the current process and it is not sufficient to alter the PATH on cmd.Env. +func (c *Command) RunExecutableWithAttrs(executable string, sysProcAttr *syscall.SysProcAttr, params ...string) error { c.prepareOut() cmd := ExecCommand(executable, params...) + cmd.SysProcAttr = sysProcAttr if len(c.dir) > 0 { cmd.Dir = c.dir diff --git a/pkg/mock/fileUtils.go b/pkg/mock/fileUtils.go index 883ef46e7..e3d3079c6 100644 --- a/pkg/mock/fileUtils.go +++ b/pkg/mock/fileUtils.go @@ -511,6 +511,10 @@ func (f *FilesMock) Chmod(path string, mode os.FileMode) error { return nil } +func (f *FilesMock) Chown(path string, uid, gid int) error { + return nil +} + func (f *FilesMock) Abs(path string) (string, error) { f.init() return f.toAbsPath(path), nil diff --git a/pkg/mock/runner.go b/pkg/mock/runner.go index 4ad53fba0..5fa6e552a 100644 --- a/pkg/mock/runner.go +++ b/pkg/mock/runner.go @@ -7,6 +7,7 @@ import ( "io" "regexp" "strings" + "syscall" "github.com/SAP/jenkins-library/pkg/command" ) @@ -25,10 +26,11 @@ type ExecMockRunner struct { } type ExecCall struct { - Execution *Execution - Async bool - Exec string - Params []string + Execution *Execution + SysProcAttrs *syscall.SysProcAttr + Async bool + Exec string + Params []string } type Execution struct { @@ -61,8 +63,11 @@ func (m *ExecMockRunner) AppendEnv(e []string) { } func (m *ExecMockRunner) RunExecutable(e string, p ...string) error { + return m.RunExecutableWithAttrs(e, nil, p...) +} - exec := ExecCall{Exec: e, Params: p} +func (m *ExecMockRunner) RunExecutableWithAttrs(e string, attrs *syscall.SysProcAttr, p ...string) error { + exec := ExecCall{Exec: e, SysProcAttrs: attrs, Params: p} m.Calls = append(m.Calls, exec) c := strings.Join(append([]string{e}, p...), " ") diff --git a/pkg/piperutils/fileUtils.go b/pkg/piperutils/fileUtils.go index be6c1889e..4cc5091ba 100644 --- a/pkg/piperutils/fileUtils.go +++ b/pkg/piperutils/fileUtils.go @@ -31,6 +31,7 @@ type FileUtils interface { FileRemove(path string) error MkdirAll(path string, perm os.FileMode) error Chmod(path string, mode os.FileMode) error + Chown(path string, uid, gid int) error Glob(pattern string) (matches []string, err error) Chdir(path string) error TempDir(string, string) (string, error) @@ -144,6 +145,17 @@ func (f Files) Chmod(path string, mode os.FileMode) error { return os.Chmod(path, mode) } +// Chown is a recursive wrapper for os.Chown(). +func (f Files) Chown(path string, uid, gid int) error { + return filepath.WalkDir(path, func(name string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + return os.Chown(name, uid, gid) + }) +} + // Unzip will decompress a zip archive, moving all files and folders // within the zip file (parameter 1) to an output directory (parameter 2). // from https://golangcode.com/unzip-files-in-go/ with the following license: diff --git a/resources/metadata/cnbBuild.yaml b/resources/metadata/cnbBuild.yaml index ffd5c44fb..b28b7e705 100644 --- a/resources/metadata/cnbBuild.yaml +++ b/resources/metadata/cnbBuild.yaml @@ -363,3 +363,6 @@ spec: type: sbom containers: - image: "paketobuildpacks/builder:base" + options: + - name: -u + value: "0"