diff --git a/pkg/apis/internalversion/exec_types.go b/pkg/apis/internalversion/exec_types.go index 8fd19f2155..f2c94d5d00 100644 --- a/pkg/apis/internalversion/exec_types.go +++ b/pkg/apis/internalversion/exec_types.go @@ -50,6 +50,8 @@ type ExecTargetLocal struct { WorkDir string // Envs is a list of environment variables to exec with. Envs []EnvVar + // User is the user to exec. + User *User } // EnvVar represents an environment variable present in a Container. @@ -59,3 +61,11 @@ type EnvVar struct { // Value of the environment variable. Value string } + +// User specifies the existing uid and gid to run exec command in container process. +type User struct { + // RunAsUser is the existing uid to run exec command in container process. + RunAsUser *int64 + // RunAsGroup is the existing gid to run exec command in container process. + RunAsGroup *int64 +} diff --git a/pkg/apis/internalversion/zz_generated.conversion.go b/pkg/apis/internalversion/zz_generated.conversion.go index b882eb33ee..420af11638 100644 --- a/pkg/apis/internalversion/zz_generated.conversion.go +++ b/pkg/apis/internalversion/zz_generated.conversion.go @@ -538,6 +538,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*User)(nil), (*v1alpha1.User)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_internalversion_User_To_v1alpha1_User(a.(*User), b.(*v1alpha1.User), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1alpha1.User)(nil), (*User)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_User_To_internalversion_User(a.(*v1alpha1.User), b.(*User), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*Volume)(nil), (*configv1alpha1.Volume)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_internalversion_Volume_To_v1alpha1_Volume(a.(*Volume), b.(*configv1alpha1.Volume), scope) }); err != nil { @@ -1036,6 +1046,7 @@ func Convert_v1alpha1_ExecTarget_To_internalversion_ExecTarget(in *v1alpha1.Exec func autoConvert_internalversion_ExecTargetLocal_To_v1alpha1_ExecTargetLocal(in *ExecTargetLocal, out *v1alpha1.ExecTargetLocal, s conversion.Scope) error { out.WorkDir = in.WorkDir out.Envs = *(*[]v1alpha1.EnvVar)(unsafe.Pointer(&in.Envs)) + out.User = (*v1alpha1.User)(unsafe.Pointer(in.User)) return nil } @@ -1047,6 +1058,7 @@ func Convert_internalversion_ExecTargetLocal_To_v1alpha1_ExecTargetLocal(in *Exe func autoConvert_v1alpha1_ExecTargetLocal_To_internalversion_ExecTargetLocal(in *v1alpha1.ExecTargetLocal, out *ExecTargetLocal, s conversion.Scope) error { out.WorkDir = in.WorkDir out.Envs = *(*[]EnvVar)(unsafe.Pointer(&in.Envs)) + out.User = (*User)(unsafe.Pointer(in.User)) return nil } @@ -2058,6 +2070,28 @@ func Convert_v1alpha1_StageSpec_To_internalversion_StageSpec(in *v1alpha1.StageS return autoConvert_v1alpha1_StageSpec_To_internalversion_StageSpec(in, out, s) } +func autoConvert_internalversion_User_To_v1alpha1_User(in *User, out *v1alpha1.User, s conversion.Scope) error { + out.RunAsUser = (*int64)(unsafe.Pointer(in.RunAsUser)) + out.RunAsGroup = (*int64)(unsafe.Pointer(in.RunAsGroup)) + return nil +} + +// Convert_internalversion_User_To_v1alpha1_User is an autogenerated conversion function. +func Convert_internalversion_User_To_v1alpha1_User(in *User, out *v1alpha1.User, s conversion.Scope) error { + return autoConvert_internalversion_User_To_v1alpha1_User(in, out, s) +} + +func autoConvert_v1alpha1_User_To_internalversion_User(in *v1alpha1.User, out *User, s conversion.Scope) error { + out.RunAsUser = (*int64)(unsafe.Pointer(in.RunAsUser)) + out.RunAsGroup = (*int64)(unsafe.Pointer(in.RunAsGroup)) + return nil +} + +// Convert_v1alpha1_User_To_internalversion_User is an autogenerated conversion function. +func Convert_v1alpha1_User_To_internalversion_User(in *v1alpha1.User, out *User, s conversion.Scope) error { + return autoConvert_v1alpha1_User_To_internalversion_User(in, out, s) +} + func autoConvert_internalversion_Volume_To_v1alpha1_Volume(in *Volume, out *configv1alpha1.Volume, s conversion.Scope) error { out.Name = in.Name if err := v1.Convert_bool_To_Pointer_bool(&in.ReadOnly, &out.ReadOnly, s); err != nil { diff --git a/pkg/apis/internalversion/zz_generated.deepcopy.go b/pkg/apis/internalversion/zz_generated.deepcopy.go index 2e1e9d8eb1..88210cfcf5 100644 --- a/pkg/apis/internalversion/zz_generated.deepcopy.go +++ b/pkg/apis/internalversion/zz_generated.deepcopy.go @@ -446,6 +446,11 @@ func (in *ExecTargetLocal) DeepCopyInto(out *ExecTargetLocal) { *out = make([]EnvVar, len(*in)) copy(*out, *in) } + if in.User != nil { + in, out := &in.User, &out.User + *out = new(User) + (*in).DeepCopyInto(*out) + } return } @@ -1131,6 +1136,32 @@ func (in *StageSpec) DeepCopy() *StageSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *User) DeepCopyInto(out *User) { + *out = *in + if in.RunAsUser != nil { + in, out := &in.RunAsUser, &out.RunAsUser + *out = new(int64) + **out = **in + } + if in.RunAsGroup != nil { + in, out := &in.RunAsGroup, &out.RunAsGroup + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User. +func (in *User) DeepCopy() *User { + if in == nil { + return nil + } + out := new(User) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Volume) DeepCopyInto(out *Volume) { *out = *in diff --git a/pkg/apis/v1alpha1/exec_types.go b/pkg/apis/v1alpha1/exec_types.go index ba0494e900..90d16567f7 100644 --- a/pkg/apis/v1alpha1/exec_types.go +++ b/pkg/apis/v1alpha1/exec_types.go @@ -59,6 +59,8 @@ type ExecTargetLocal struct { WorkDir string `json:"workDir,omitempty"` // Envs is a list of environment variables to exec with. Envs []EnvVar `json:"envs,omitempty"` + // User is the user to exec. + User *User `json:"user,omitempty"` } // EnvVar represents an environment variable present in a Container. @@ -68,3 +70,11 @@ type EnvVar struct { // Value of the environment variable. Value string `json:"value,omitempty"` } + +// User specifies the existing uid and gid to run exec command in container process. +type User struct { + // RunAsUser is the existing uid to run exec command in container process. + RunAsUser *int64 `json:"runAsUser,omitempty"` + // RunAsGroup is the existing gid to run exec command in container process. + RunAsGroup *int64 `json:"runAsGroup,omitempty"` +} diff --git a/pkg/apis/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/v1alpha1/zz_generated.deepcopy.go index cb50fb7cb1..86e720a444 100644 --- a/pkg/apis/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/v1alpha1/zz_generated.deepcopy.go @@ -416,6 +416,11 @@ func (in *ExecTargetLocal) DeepCopyInto(out *ExecTargetLocal) { *out = make([]EnvVar, len(*in)) copy(*out, *in) } + if in.User != nil { + in, out := &in.User, &out.User + *out = new(User) + (*in).DeepCopyInto(*out) + } return } @@ -1000,3 +1005,29 @@ func (in *StageSpec) DeepCopy() *StageSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *User) DeepCopyInto(out *User) { + *out = *in + if in.RunAsUser != nil { + in, out := &in.RunAsUser, &out.RunAsUser + *out = new(int64) + **out = **in + } + if in.RunAsGroup != nil { + in, out := &in.RunAsGroup, &out.RunAsGroup + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User. +func (in *User) DeepCopy() *User { + if in == nil { + return nil + } + out := new(User) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/kwok/server/debugging_exec.go b/pkg/kwok/server/debugging_exec.go index 3b7ec6c772..0a18aa50c9 100644 --- a/pkg/kwok/server/debugging_exec.go +++ b/pkg/kwok/server/debugging_exec.go @@ -53,6 +53,11 @@ func (s *Server) ExecInContainer(ctx context.Context, podName, podNamespace stri ctx = exec.WithEnv(ctx, envs) } + // Set the user. + if execTarget.Local.User != nil { + ctx = exec.WithUser(ctx, execTarget.Local.User.RunAsUser, execTarget.Local.User.RunAsGroup) + } + // Set the working directory. if execTarget.Local.WorkDir != "" { ctx = exec.WithDir(ctx, execTarget.Local.WorkDir) diff --git a/pkg/utils/exec/cmd_other.go b/pkg/utils/exec/cmd_other.go index 575c4dc1c7..b5f5f6f1bb 100644 --- a/pkg/utils/exec/cmd_other.go +++ b/pkg/utils/exec/cmd_other.go @@ -22,7 +22,11 @@ import ( "context" "os" "os/exec" + "os/user" + "strconv" "syscall" + + "sigs.k8s.io/kwok/pkg/log" ) func startProcess(ctx context.Context, name string, arg ...string) *exec.Cmd { @@ -47,3 +51,26 @@ func isRunning(pid int) bool { err = process.Signal(syscall.Signal(0)) return err == nil } + +func setUser(ctx context.Context, uid, gid *int64, cmd *exec.Cmd) { + logger := log.FromContext(ctx) + + u, err := user.LookupId(strconv.Itoa(int(*uid))) + if err != nil { + logger.Warn("failed to lookup user", "error", err.Error()) + return + } + g, err := user.LookupGroupId(strconv.Itoa(int(*gid))) + if err != nil { + logger.Warn("failed to lookup group", "error", err.Error()) + return + } + + logger.Info("uid", u.Uid, "username", u.Username, "gid", gid, "group name", g.Name) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(*uid), + Gid: uint32(*gid), + }, + } +} diff --git a/pkg/utils/exec/cmd_windows.go b/pkg/utils/exec/cmd_windows.go index 756d2efb1d..a1e39b7e33 100644 --- a/pkg/utils/exec/cmd_windows.go +++ b/pkg/utils/exec/cmd_windows.go @@ -25,6 +25,8 @@ import ( "syscall" "golang.org/x/sys/windows" + + "sigs.k8s.io/kwok/pkg/log" ) func startProcess(ctx context.Context, name string, arg ...string) *exec.Cmd { @@ -47,3 +49,9 @@ func isRunning(pid int) bool { _, err := os.FindProcess(pid) return err == nil } + +func setUser(ctx context.Context, username, groupName string, cmd *exec.Cmd) { + logger := log.FromContext(ctx) + err := "user and group are not supported in windows, ignoring the custom user and group" + logger.Warn(err) +} diff --git a/pkg/utils/exec/exec.go b/pkg/utils/exec/exec.go index b2816e710b..0c271e11ac 100644 --- a/pkg/utils/exec/exec.go +++ b/pkg/utils/exec/exec.go @@ -45,6 +45,10 @@ type execOptions struct { Dir string // Env is the environment variables of the command. Env []string + // UID is the user id of the command + UID *int64 + // GID is the group id of the command + GID *int64 // IOStreams contains the standard streams. IOStreams // PipeStdin is true if the command's stdin should be piped. @@ -65,6 +69,14 @@ func WithEnv(ctx context.Context, env []string) context.Context { return ctx } +// WithUser returns a context with the given username and group name. +func WithUser(ctx context.Context, uid, gid *int64) context.Context { + ctx, opt := withExecOptions(ctx) + opt.UID = uid + opt.GID = gid + return ctx +} + // WithDir returns a context with the given working directory. func WithDir(ctx context.Context, dir string) context.Context { ctx, opt := withExecOptions(ctx) @@ -141,6 +153,9 @@ func Exec(ctx context.Context, name string, arg ...string) error { cmd.Env = opt.Env cmd.Env = append(os.Environ(), cmd.Env...) } + if opt.UID != nil && opt.GID != nil { + setUser(ctx, opt.Username, opt.GroupName, cmd) + } cmd.Dir = opt.Dir if opt.In != nil { if opt.PipeStdin { diff --git a/site/content/en/docs/generated/apis.md b/site/content/en/docs/generated/apis.md index 6bde6982d7..fc44f392df 100644 --- a/site/content/en/docs/generated/apis.md +++ b/site/content/en/docs/generated/apis.md @@ -3219,6 +3219,19 @@ string

Envs is a list of environment variables to exec with.

+ + +user + + +User + + + + +

User is the user to exec.

+ +

@@ -4355,3 +4368,46 @@ bool +

+User + # +

+

+Appears on: +ExecTargetLocal +

+

+

User specifies the existing uid and gid to run exec command in container process.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+runAsUser + +int64 + + +

RunAsUser is the existing uid to run exec command in container process.

+
+runAsGroup + +int64 + + +

RunAsGroup is the existing gid to run exec command in container process.

+
diff --git a/test/kwokctl/exec.yaml b/test/kwokctl/exec.yaml index c0971adca3..7b05e07e64 100644 --- a/test/kwokctl/exec.yaml +++ b/test/kwokctl/exec.yaml @@ -24,3 +24,6 @@ spec: envs: - name: TEST_ENV value: test + user: + runAsUser: 1001 + runAsGroup: 1002 diff --git a/test/kwokctl/fake-deployment.yaml b/test/kwokctl/fake-deployment.yaml index 4c6a5ed587..03e9ed0f9b 100644 --- a/test/kwokctl/fake-deployment.yaml +++ b/test/kwokctl/fake-deployment.yaml @@ -13,6 +13,9 @@ spec: labels: app: fake-pod spec: + securityContext: + runAsUser: 1001 + runAsGroup: 1002 containers: - name: fake-pod image: fake diff --git a/test/kwokctl/kwokctl_exec_test.sh b/test/kwokctl/kwokctl_exec_test.sh index a2f432a4c4..612e544213 100755 --- a/test/kwokctl/kwokctl_exec_test.sh +++ b/test/kwokctl/kwokctl_exec_test.sh @@ -45,7 +45,7 @@ function test_exec() { local want="${5}" local result for ((i = 0; i < 10; i++)); do - result=$(kwokctl --name "${name}" kubectl -n "${namespace}" exec -i "${target}" -- "${cmd}" || :) + result=$(kwokctl --name "${name}" kubectl -n "${namespace}" exec -i "${target}" -- ${cmd} || :) if [[ "${result}" == *"${want}"* ]]; then break fi @@ -92,6 +92,8 @@ function main() { test_apply_node_and_pod "${name}" || failed+=("apply_node_and_pod") test_exec "${name}" other pod/fake-pod "pwd" "/tmp" || failed+=("${name}_target_exec") test_exec "${name}" default deploy/fake-pod "env" "TEST_ENV=test" || failed+=("${name}_cluster_default_exec") + test_exec "${name}" default deploy/fake-pod "id -u" "1001" || failed+=("${name}_cluster_default_exec") + test_exec "${name}" default deploy/fake-pod "id -g" "1002" || failed+=("${name}_cluster_default_exec") delete_cluster "${name}" done