diff --git a/kustomize/crd/bases/kwok.x-k8s.io_clusterexecs.yaml b/kustomize/crd/bases/kwok.x-k8s.io_clusterexecs.yaml index c5cdadd99..d942d4bda 100644 --- a/kustomize/crd/bases/kwok.x-k8s.io_clusterexecs.yaml +++ b/kustomize/crd/bases/kwok.x-k8s.io_clusterexecs.yaml @@ -67,6 +67,20 @@ spec: - name type: object type: array + securityContext: + description: SecurityContext is the user context to exec. + properties: + runAsGroup: + description: RunAsGroup is the existing gid to run exec + command in container process. + format: int64 + type: integer + runAsUser: + description: RunAsUser is the existing uid to run exec + command in container process. + format: int64 + type: integer + type: object workDir: description: WorkDir is the working directory to exec with. type: string diff --git a/kustomize/crd/bases/kwok.x-k8s.io_execs.yaml b/kustomize/crd/bases/kwok.x-k8s.io_execs.yaml index 90d8d854e..c1d6b0a15 100644 --- a/kustomize/crd/bases/kwok.x-k8s.io_execs.yaml +++ b/kustomize/crd/bases/kwok.x-k8s.io_execs.yaml @@ -67,6 +67,20 @@ spec: - name type: object type: array + securityContext: + description: SecurityContext is the user context to exec. + properties: + runAsGroup: + description: RunAsGroup is the existing gid to run exec + command in container process. + format: int64 + type: integer + runAsUser: + description: RunAsUser is the existing uid to run exec + command in container process. + format: int64 + type: integer + type: object workDir: description: WorkDir is the working directory to exec with. type: string diff --git a/pkg/apis/internalversion/exec_types.go b/pkg/apis/internalversion/exec_types.go index 8fd19f215..f8230abc9 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 + // SecurityContext is the user context to exec. + SecurityContext *SecurityContext } // EnvVar represents an environment variable present in a Container. @@ -59,3 +61,11 @@ type EnvVar struct { // Value of the environment variable. Value string } + +// SecurityContext specifies the existing uid and gid to run exec command in container process. +type SecurityContext 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 b9db9c608..d304644af 100644 --- a/pkg/apis/internalversion/zz_generated.conversion.go +++ b/pkg/apis/internalversion/zz_generated.conversion.go @@ -448,6 +448,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*SecurityContext)(nil), (*v1alpha1.SecurityContext)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_internalversion_SecurityContext_To_v1alpha1_SecurityContext(a.(*SecurityContext), b.(*v1alpha1.SecurityContext), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1alpha1.SecurityContext)(nil), (*SecurityContext)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_SecurityContext_To_internalversion_SecurityContext(a.(*v1alpha1.SecurityContext), b.(*SecurityContext), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*SelectorRequirement)(nil), (*v1alpha1.SelectorRequirement)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_internalversion_SelectorRequirement_To_v1alpha1_SelectorRequirement(a.(*SelectorRequirement), b.(*v1alpha1.SelectorRequirement), scope) }); err != nil { @@ -1044,6 +1054,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.SecurityContext = (*v1alpha1.SecurityContext)(unsafe.Pointer(in.SecurityContext)) return nil } @@ -1055,6 +1066,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.SecurityContext = (*SecurityContext)(unsafe.Pointer(in.SecurityContext)) return nil } @@ -1832,6 +1844,28 @@ func Convert_v1alpha1_PortForwardSpec_To_internalversion_PortForwardSpec(in *v1a return autoConvert_v1alpha1_PortForwardSpec_To_internalversion_PortForwardSpec(in, out, s) } +func autoConvert_internalversion_SecurityContext_To_v1alpha1_SecurityContext(in *SecurityContext, out *v1alpha1.SecurityContext, s conversion.Scope) error { + out.RunAsUser = (*int64)(unsafe.Pointer(in.RunAsUser)) + out.RunAsGroup = (*int64)(unsafe.Pointer(in.RunAsGroup)) + return nil +} + +// Convert_internalversion_SecurityContext_To_v1alpha1_SecurityContext is an autogenerated conversion function. +func Convert_internalversion_SecurityContext_To_v1alpha1_SecurityContext(in *SecurityContext, out *v1alpha1.SecurityContext, s conversion.Scope) error { + return autoConvert_internalversion_SecurityContext_To_v1alpha1_SecurityContext(in, out, s) +} + +func autoConvert_v1alpha1_SecurityContext_To_internalversion_SecurityContext(in *v1alpha1.SecurityContext, out *SecurityContext, s conversion.Scope) error { + out.RunAsUser = (*int64)(unsafe.Pointer(in.RunAsUser)) + out.RunAsGroup = (*int64)(unsafe.Pointer(in.RunAsGroup)) + return nil +} + +// Convert_v1alpha1_SecurityContext_To_internalversion_SecurityContext is an autogenerated conversion function. +func Convert_v1alpha1_SecurityContext_To_internalversion_SecurityContext(in *v1alpha1.SecurityContext, out *SecurityContext, s conversion.Scope) error { + return autoConvert_v1alpha1_SecurityContext_To_internalversion_SecurityContext(in, out, s) +} + func autoConvert_internalversion_SelectorRequirement_To_v1alpha1_SelectorRequirement(in *SelectorRequirement, out *v1alpha1.SelectorRequirement, s conversion.Scope) error { out.Key = in.Key out.Operator = v1alpha1.SelectorOperator(in.Operator) diff --git a/pkg/apis/internalversion/zz_generated.deepcopy.go b/pkg/apis/internalversion/zz_generated.deepcopy.go index a1deb3b2f..780cd6ded 100644 --- a/pkg/apis/internalversion/zz_generated.deepcopy.go +++ b/pkg/apis/internalversion/zz_generated.deepcopy.go @@ -451,6 +451,11 @@ func (in *ExecTargetLocal) DeepCopyInto(out *ExecTargetLocal) { *out = make([]EnvVar, len(*in)) copy(*out, *in) } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(SecurityContext) + (*in).DeepCopyInto(*out) + } return } @@ -917,6 +922,32 @@ func (in *PortForwardSpec) DeepCopy() *PortForwardSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityContext) DeepCopyInto(out *SecurityContext) { + *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 SecurityContext. +func (in *SecurityContext) DeepCopy() *SecurityContext { + if in == nil { + return nil + } + out := new(SecurityContext) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SelectorRequirement) DeepCopyInto(out *SelectorRequirement) { *out = *in diff --git a/pkg/apis/v1alpha1/exec_types.go b/pkg/apis/v1alpha1/exec_types.go index ba467d14a..344c259aa 100644 --- a/pkg/apis/v1alpha1/exec_types.go +++ b/pkg/apis/v1alpha1/exec_types.go @@ -75,6 +75,8 @@ type ExecTargetLocal struct { WorkDir string `json:"workDir,omitempty"` // Envs is a list of environment variables to exec with. Envs []EnvVar `json:"envs,omitempty"` + // SecurityContext is the user context to exec. + SecurityContext *SecurityContext `json:"securityContext,omitempty"` } // EnvVar represents an environment variable present in a Container. @@ -87,6 +89,14 @@ type EnvVar struct { Value string `json:"value,omitempty"` } +// SecurityContext specifies the existing uid and gid to run exec command in container process. +type SecurityContext 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"` +} + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:object:root=true diff --git a/pkg/apis/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/v1alpha1/zz_generated.deepcopy.go index 63fc322de..9fd80dc90 100644 --- a/pkg/apis/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/v1alpha1/zz_generated.deepcopy.go @@ -775,6 +775,11 @@ func (in *ExecTargetLocal) DeepCopyInto(out *ExecTargetLocal) { *out = make([]EnvVar, len(*in)) copy(*out, *in) } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(SecurityContext) + (*in).DeepCopyInto(*out) + } return } @@ -1293,6 +1298,32 @@ func (in *PortForwardStatus) DeepCopy() *PortForwardStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityContext) DeepCopyInto(out *SecurityContext) { + *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 SecurityContext. +func (in *SecurityContext) DeepCopy() *SecurityContext { + if in == nil { + return nil + } + out := new(SecurityContext) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SelectorRequirement) DeepCopyInto(out *SelectorRequirement) { *out = *in diff --git a/pkg/kwok/server/debugging_exec.go b/pkg/kwok/server/debugging_exec.go index a6a0d7b01..16688d4d5 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.SecurityContext != nil { + ctx = exec.WithUser(ctx, execTarget.Local.SecurityContext.RunAsUser, execTarget.Local.SecurityContext.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 575c4dc1c..3f8b976fe 100644 --- a/pkg/utils/exec/cmd_other.go +++ b/pkg/utils/exec/cmd_other.go @@ -22,6 +22,8 @@ import ( "context" "os" "os/exec" + "os/user" + "strconv" "syscall" ) @@ -47,3 +49,60 @@ func isRunning(pid int) bool { err = process.Signal(syscall.Signal(0)) return err == nil } + +func setUser(cmd *exec.Cmd, uid, gid *int64) error { + if uid == nil && gid == nil { + return nil + } + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + if cmd.SysProcAttr.Credential == nil { + cmd.SysProcAttr.Credential = &syscall.Credential{} + } + // If both uid and gid are both set, use them directly + if uid != nil && gid != nil { + _, err := user.LookupId(strconv.Itoa(int(*uid))) + if err != nil { + return err + } + _, err = user.LookupGroupId(strconv.Itoa(int(*gid))) + if err != nil { + return err + } + cmd.SysProcAttr.Credential.Uid = uint32(*uid) + cmd.SysProcAttr.Credential.Gid = uint32(*gid) + return nil + } + // If only uid is set, use that user's gid + if uid != nil { + userInfo, err := user.LookupId(strconv.Itoa(int(*uid))) + if err != nil { + return err + } + u, err := strconv.Atoi(userInfo.Uid) + if err != nil { + return err + } + g, err := strconv.Atoi(userInfo.Gid) + if err != nil { + return err + } + cmd.SysProcAttr.Credential.Uid = uint32(u) + cmd.SysProcAttr.Credential.Gid = uint32(g) + } + // If only gid is set, use the current user's uid + if gid != nil { + userInfo, err := user.Current() + if err != nil { + return err + } + u, err := strconv.Atoi(userInfo.Uid) + if err != nil { + return err + } + cmd.SysProcAttr.Credential.Uid = uint32(u) + cmd.SysProcAttr.Credential.Gid = uint32(*gid) + } + return nil +} diff --git a/pkg/utils/exec/cmd_windows.go b/pkg/utils/exec/cmd_windows.go index 756d2efb1..635307fd5 100644 --- a/pkg/utils/exec/cmd_windows.go +++ b/pkg/utils/exec/cmd_windows.go @@ -20,6 +20,7 @@ package exec import ( "context" + "fmt" "os" "os/exec" "syscall" @@ -47,3 +48,10 @@ func isRunning(pid int) bool { _, err := os.FindProcess(pid) return err == nil } + +func setUser(cmd *exec.Cmd, uid, gid *int64) error { + if uid == nil && gid == nil { + return nil + } + return fmt.Errorf("user and group are not supported in windows") +} diff --git a/pkg/utils/exec/exec.go b/pkg/utils/exec/exec.go index 355d0420f..e6cbf33b3 100644 --- a/pkg/utils/exec/exec.go +++ b/pkg/utils/exec/exec.go @@ -44,6 +44,10 @@ type Options 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. @@ -56,6 +60,8 @@ func (e *Options) deepCopy() *Options { return &Options{ Dir: e.Dir, Env: append([]string(nil), e.Env...), + GID: e.GID, + UID: e.UID, IOStreams: e.IOStreams, PipeStdin: e.PipeStdin, Fork: e.Fork, @@ -76,6 +82,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) @@ -171,6 +185,9 @@ func Command(ctx context.Context, name string, args ...string) (cmd *exec.Cmd, e if opt.Env != nil { cmd.Env = append(os.Environ(), opt.Env...) } + if err = setUser(cmd, opt.UID, opt.GID); err != nil { + return nil, fmt.Errorf("cmd set user: %s %s: %w", name, strings.Join(args, " "), err) + } cmd.Dir = opt.Dir diff --git a/site/content/en/docs/generated/apis.md b/site/content/en/docs/generated/apis.md index c4ebd3f9d..9efceea7d 100644 --- a/site/content/en/docs/generated/apis.md +++ b/site/content/en/docs/generated/apis.md @@ -3724,6 +3724,19 @@ string
Envs is a list of environment variables to exec with.
+securityContext
+
+
+SecurityContext
+
+
+SecurityContext is the user context to exec.
++Appears on: +ExecTargetLocal +
++
SecurityContext specifies the existing uid and gid to run exec command in container process.
+ +Field | +Description | +
---|---|
+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. + |
+
string
alias)
diff --git a/test/kwokctl/exec-security-context.yaml b/test/kwokctl/exec-security-context.yaml
new file mode 100644
index 000000000..f9c36c2c5
--- /dev/null
+++ b/test/kwokctl/exec-security-context.yaml
@@ -0,0 +1,29 @@
+kind: Exec
+apiVersion: kwok.x-k8s.io/v1alpha1
+metadata:
+ name: fake-pod
+ namespace: other
+spec:
+ execs:
+ - containers:
+ - fake-pod
+ local:
+ workDir: /tmp
+
+---
+kind: ClusterExec
+apiVersion: kwok.x-k8s.io/v1alpha1
+metadata:
+ name: cluster-exec-rules
+spec:
+ selector:
+ matchNamespaces:
+ - default
+ execs:
+ - local:
+ envs:
+ - name: TEST_ENV
+ value: test
+ securityContext:
+ runAsUser: 1001
+ runAsGroup: 1002
diff --git a/test/kwokctl/kwokctl_exec_test.sh b/test/kwokctl/kwokctl_exec_test.sh
index 20baf5f71..9775375df 100755
--- a/test/kwokctl/kwokctl_exec_test.sh
+++ b/test/kwokctl/kwokctl_exec_test.sh
@@ -38,14 +38,16 @@ function args() {
}
function test_exec() {
+ local cmds=()
local name="${1}"
local namespace="${2}"
local target="${3}"
local cmd="${4}"
local want="${5}"
local result
+ mapfile -t cmds < <(echo "${cmd}" | tr " " "\n")
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}" -- "${cmds[@]}" || :)
if [[ "${result}" == *"${want}"* ]]; then
break
fi
@@ -88,10 +90,30 @@ function main() {
echo "------------------------------"
echo "Testing exec on ${KWOK_RUNTIME} for ${release}"
name="exec-cluster-${KWOK_RUNTIME}-${release//./-}"
- create_cluster "${name}" "${release}" --config "${DIR}/exec.yaml"
+ if [[ "${KWOK_RUNTIME}" != "binary" && "${KWOK_RUNTIME}" != "kind" && "${KWOK_RUNTIME}" != "kind-podman" ]]; then
+ yaml="${DIR}/exec-security-context.yaml"
+ else
+ yaml="${DIR}/exec.yaml"
+ fi
+ create_cluster "${name}" "${release}" --config - <