diff --git a/hack/boilerplate/boilerplate.go.txt b/hack/boilerplate/boilerplate.go.txt
old mode 100644
new mode 100755
diff --git a/images/kwok/Dockerfile b/images/kwok/Dockerfile
index 20bcdab321..a1ed6066f6 100644
--- a/images/kwok/Dockerfile
+++ b/images/kwok/Dockerfile
@@ -16,5 +16,7 @@ ARG BASE_IMAGE=docker.io/library/alpine:3.18
FROM --platform=$TARGETPLATFORM $BASE_IMAGE AS cache
ARG TARGETPLATFORM
COPY --chmod=0755 bin/$TARGETPLATFORM/kwok /usr/local/bin/
+COPY --chmod=0755 images/kwok/entrypoint.sh /entrypoint.sh
+COPY --chmod=0755 images/kwok/setuser.sh /setuser.sh
-ENTRYPOINT ["/usr/local/bin/kwok"]
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/images/kwok/entrypoint.sh b/images/kwok/entrypoint.sh
new file mode 100644
index 0000000000..6ba40c8da6
--- /dev/null
+++ b/images/kwok/entrypoint.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+# Copyright 2023 The Kubernetes Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+if [ -n "${USER_NAME}" ] && [ -n "${USER_UID}" ] && [ -n "${USER_GROUP_NAME}" ] && [ -n "${USER_GID}" ] && [ -n "${USER_HOME}" ] && [ -n "${USER_SHELL}" ]; then
+ /setuser.sh --username="${USER_NAME}" --uid="${USER_UID}" --groupname="${USER_GROUP_NAME}" --gid="${USER_GID}" --home="${USER_HOME}" --shell="${USER_SHELL}"
+fi
+
+/usr/local/bin/kwok "$@"
diff --git a/images/kwok/setuser.sh b/images/kwok/setuser.sh
new file mode 100644
index 0000000000..13d8fac664
--- /dev/null
+++ b/images/kwok/setuser.sh
@@ -0,0 +1,174 @@
+#!/bin/sh
+# Copyright 2023 The Kubernetes Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# command_exist checks if a command exists.
+command_exist() {
+ type "${1}" >/dev/null 2>&1
+}
+
+# add_group adds a group to the system. It tries to use addgroup or groupadd if they exist,
+# otherwise it writes directly to /etc/group.
+add_group() {
+ gid="$1"
+ groupname="$2"
+ if command_exist addgroup; then # use addgroup if it exists
+ addgroup --gid "${gid}" "${groupname}"
+ elif command_exist groupadd; then # use groupadd if it exists
+ groupadd --gid "${gid}" "${groupname}"
+ else # write /etc/group directly
+ echo "${groupname}:x:${gid}:" >>/etc/group
+ fi
+}
+
+# add_user adds a user to the system. It tries to use adduser or useradd if they exist,
+# otherwise it writes directly to /etc/passwd.
+add_user() {
+ uid="$1"
+ username="$2"
+ gid="$3"
+ home="$4"
+ shell="$5"
+ if command_exist adduser; then # use adduser if it exists
+ adduser -u "${uid}" -G "${groupname}" -h "${home}" -s "${shell}" "${username}" -D
+ elif command_exist useradd; then # use useradd if it exists
+ useradd -u "${uid}" -G "${groupname}" -h "${home}" -s "${shell}" "${username}" -D
+ else # write /etc/passwd directly
+ echo "${username}:x:${uid}:${gid}::${home}:${shell}" >>/etc/passwd
+ fi
+}
+
+# get_ent gets an entry from a file. It tries to use getent if it exists,
+get_ent() {
+ name="$1"
+ id="$2"
+ if command_exist getent; then
+ getent "${name}" "${id}"
+ else
+ case "${name}" in
+ group)
+ grep ":${id}:" /etc/group
+ ;;
+ passwd)
+ grep ":${id}:${id}:" /etc/passwd
+ ;;
+ *)
+ log "get_ent: unknown name ${name}"
+ exit 1
+ ;;
+ esac
+ fi
+}
+
+# log prints a log message.
+log() {
+ echo "$*" >&2
+}
+
+# usage prints the usage of this script.
+usage() {
+ cat <&2
+Usage: $(basename "$0") [options]
+ --username: the name of the user to create
+ --uid: the uid of the user to create
+ --groupname: the name of the group to create (defaults to username)
+ --gid: the gid of the user to create (defaults to uid)
+ --home: the home directory of the user to create (defaults to /home/)
+ --shell: the shell of the user to create (defaults to /bin/sh)
+ --help: print this help message
+EOF
+}
+
+USER_NAME=""
+USER_UID=""
+USER_GROUP_NAME=""
+USER_GID=""
+USER_HOME=""
+USER_SHELL=""
+args() {
+ while [ $# -gt 0 ]; do
+ arg="$1"
+ case "${arg}" in
+ --username | --username=*)
+ [ "${arg#*=}" != "${arg}" ] && USER_NAME="${arg#*=}" || { USER_NAME="${2}" && shift; } || : shift
+ ;;
+ --uid | --uid=*)
+ [ "${arg#*=}" != "${arg}" ] && USER_UID="${arg#*=}" || { USER_UID="${2}" && shift; } || : shift
+ ;;
+ --groupname | --groupname=*)
+ [ "${arg#*=}" != "${arg}" ] && USER_GROUP_NAME="${arg#*=}" || { USER_GROUP_NAME="${2}" && shift; } || : shift
+ ;;
+ --gid | --gid=*)
+ [ "${arg#*=}" != "${arg}" ] && USER_GID="${arg#*=}" || { USER_GID="${2}" && shift; } || : shift
+ ;;
+ --home | --home=*)
+ [ "${arg#*=}" != "${arg}" ] && USER_HOME="${arg#*=}" || { USER_HOME="${2}" && shift; } || : shift
+ ;;
+ --shell | --shell=*)
+ [ "${arg#*=}" != "${arg}" ] && USER_SHELL="${arg#*=}" || { USER_SHELL="${2}" && shift; } || : shift
+ ;;
+ --help | -h)
+ usage
+ exit 0
+ ;;
+ *)
+ log "Unknown option: $1"
+ usage
+ exit 1
+ ;;
+ esac
+ shift
+ done
+}
+
+main() {
+ args "$@"
+ USER_GROUP_NAME="${USER_GROUP_NAME:-${USER_NAME}}"
+ USER_GID="${USER_GID:-${USER_UID}}"
+ USER_HOME="${USER_HOME:-/home/${USER_NAME}}"
+ USER_SHELL="${USER_SHELL:-/bin/sh}"
+ if [ -z "${USER_NAME}" ] && [ -z "${USER_GROUP_NAME}" ]; then
+ log "username or groupname is required"
+ usage
+ return 1
+ fi
+ if [ -n "${USER_GROUP_NAME}" ] && [ -z "${USER_GID}" ]; then
+ log "groupname requires gid"
+ usage
+ return 1
+ fi
+ if [ -n "${USER_NAME}" ] && [ -z "${USER_UID}" ]; then
+ log "username requires uid"
+ usage
+ return 1
+ fi
+ if [ -n "${USER_GROUP_NAME}" ] && [ -n "${USER_GID}" ]; then
+ if ! get_ent group "${USER_GID}"; then
+ log "Group ${USER_GROUP_NAME} (${USER_GID}) does not exist, creating."
+ add_group "${USER_GID}" "${USER_GROUP_NAME}"
+ else
+ log "Group ${USER_GROUP_NAME} (${USER_GID}) already exists, not creating."
+ fi
+ fi
+ if [ -n "${USER_NAME}" ] && [ -n "${USER_UID}" ]; then
+ if ! get_ent passwd "${USER_UID}"; then
+ log "User ${USER_NAME} (${USER_UID}) does not exist, creating."
+ add_user "${USER_UID}" "${USER_NAME}" "${USER_GID}" "${USER_HOME}" "${USER_SHELL}"
+ else
+ log "User ${USER_NAME} (${USER_UID}) already exists, not creating."
+ fi
+ fi
+}
+
+main "$@"
diff --git a/kustomize/crd/bases/kwok.x-k8s.io_clusterexecs.yaml b/kustomize/crd/bases/kwok.x-k8s.io_clusterexecs.yaml
index c5cdadd997..d942d4bda5 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 90d8d854e3..c1d6b0a153 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 8fd19f2155..f8230abc91 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 b9db9c6084..d304644aff 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 a1deb3b2f8..780cd6ded8 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 ba467d14a6..344c259aaf 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 63fc322de1..9fd80dc906 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 a6a0d7b019..16688d4d55 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/kwokctl/components/kwok_controller.go b/pkg/kwokctl/components/kwok_controller.go
index 9f75b8192c..1bfa223acf 100644
--- a/pkg/kwokctl/components/kwok_controller.go
+++ b/pkg/kwokctl/components/kwok_controller.go
@@ -130,7 +130,6 @@ func BuildKwokControllerComponent(conf BuildKwokControllerComponentConfig) (comp
"kube-apiserver",
},
Ports: ports,
- Command: []string{"kwok"},
Volumes: volumes,
Args: kwokControllerArgs,
Binary: conf.Binary,
diff --git a/pkg/kwokctl/runtime/compose/self_compose.go b/pkg/kwokctl/runtime/compose/self_compose.go
index 4d89b17c44..33e838c565 100644
--- a/pkg/kwokctl/runtime/compose/self_compose.go
+++ b/pkg/kwokctl/runtime/compose/self_compose.go
@@ -201,10 +201,13 @@ func (c *Cluster) createComponent(ctx context.Context, componentName string) err
args := []string{"create",
"--name=" + c.Name() + "-" + componentName,
"--pull=never",
- "--entrypoint=" + strings.Join(component.Command, " "),
"--network=" + c.networkName(),
}
+ if len(component.Command) > 0 {
+ args = append(args, "--entrypoint="+strings.Join(component.Command, " "))
+ }
+
switch c.runtime {
case consts.RuntimeTypeDocker:
for _, link := range component.Links {
diff --git a/pkg/utils/exec/cmd_other.go b/pkg/utils/exec/cmd_other.go
index 575c4dc1c7..b7c76bbf40 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,52 @@ func isRunning(pid int) bool {
err = process.Signal(syscall.Signal(0))
return err == nil
}
+
+func setUser(uid, gid *int64, cmd *exec.Cmd) 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 {
+ 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 756d2efb1d..0b984ce7b5 100644
--- a/pkg/utils/exec/cmd_windows.go
+++ b/pkg/utils/exec/cmd_windows.go
@@ -47,3 +47,7 @@ func isRunning(pid int) bool {
_, err := os.FindProcess(pid)
return err == nil
}
+
+func setUser(uid, gid *int64, cmd *exec.Cmd) error {
+ 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 355d0420f5..f239318bfb 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)
@@ -168,9 +182,14 @@ func Command(ctx context.Context, name string, args ...string) (cmd *exec.Cmd, e
} else {
cmd = command(ctx, name, args...)
}
- if opt.Env != nil {
+ if len(opt.Env) > 0 {
+ cmd.Env = opt.Env
+ } else {
cmd.Env = append(os.Environ(), opt.Env...)
}
+ if err = setUser(opt.UID, opt.GID, cmd); 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 c4ebd3f9db..9efceea7db 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.
+ |
+
@@ -4404,6 +4417,49 @@ PortForwardStatus
+
+SecurityContext
+ #
+
+
+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.
+ |
+
+
+
SelectorOperator
(string
alias)
diff --git a/test/kwokctl/exec-security-context.yaml b/test/kwokctl/exec-security-context.yaml
new file mode 100644
index 0000000000..f9c36c2c50
--- /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 20baf5f719..1ef1a5a848 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,45 @@ 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" ]]; then
+ yaml="${DIR}/exec-security-context.yaml"
+ else
+ yaml="${DIR}/exec.yaml"
+ fi
+ create_cluster "${name}" "${release}" --config - </kwok.yaml
docker network create kwok- --label=com.docker.compose.project=kwok-
-docker create --name=kwok--etcd --pull=never --entrypoint=etcd --network=kwok- --restart=unless-stopped --label=com.docker.compose.project=kwok- registry.k8s.io/etcd:3.5.9-0 --name=node0 --auto-compaction-retention=1 --quota-backend-bytes=8589934592 --data-dir=/etcd-data --initial-advertise-peer-urls=http://0.0.0.0:2380 --listen-peer-urls=http://0.0.0.0:2380 --advertise-client-urls=http://0.0.0.0:2379 --listen-client-urls=http://0.0.0.0:2379 --initial-cluster=node0=http://0.0.0.0:2380
-docker create --name=kwok--kube-apiserver --pull=never --entrypoint=kube-apiserver --network=kwok- --link=kwok--etcd --restart=unless-stopped --label=com.docker.compose.project=kwok- --publish=32766:6443/tcp --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro registry.k8s.io/kube-apiserver:v1.27.3 --etcd-prefix=/registry --allow-privileged=true --max-requests-inflight=0 --max-mutating-requests-inflight=0 --enable-priority-and-fairness=false --etcd-servers=http://kwok--etcd:2379 --authorization-mode=Node,RBAC --bind-address=0.0.0.0 --secure-port=6443 --tls-cert-file=/etc/kubernetes/pki/admin.crt --tls-private-key-file=/etc/kubernetes/pki/admin.key --client-ca-file=/etc/kubernetes/pki/ca.crt --service-account-key-file=/etc/kubernetes/pki/admin.key --service-account-signing-key-file=/etc/kubernetes/pki/admin.key --service-account-issuer=https://kubernetes.default.svc.cluster.local
-docker create --name=kwok--kube-controller-manager --pull=never --entrypoint=kube-controller-manager --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro registry.k8s.io/kube-controller-manager:v1.27.3 --node-monitor-period=10m0s --node-monitor-grace-period=1h0m0s --kubeconfig=~/.kube/config --authorization-always-allow-paths=/healthz,/readyz,/livez,/metrics --bind-address=0.0.0.0 --secure-port=10257 --root-ca-file=/etc/kubernetes/pki/ca.crt --service-account-private-key-file=/etc/kubernetes/pki/admin.key --kube-api-qps=5000 --kube-api-burst=10000
-docker create --name=kwok--kube-scheduler --pull=never --entrypoint=kube-scheduler --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro registry.k8s.io/kube-scheduler:v1.27.3 --kubeconfig=~/.kube/config --authorization-always-allow-paths=/healthz,/readyz,/livez,/metrics --bind-address=0.0.0.0 --secure-port=10259 --kube-api-qps=5000 --kube-api-burst=10000
-docker create --name=kwok--kwok-controller --pull=never --entrypoint=kwok --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --volume=~/.kwok/clusters//kwok.yaml:~/.kwok/kwok.yaml:ro localhost/kwok:test --manage-all-nodes=true --kubeconfig=~/.kube/config --config=~/.kwok/kwok.yaml --tls-cert-file=/etc/kubernetes/pki/admin.crt --tls-private-key-file=/etc/kubernetes/pki/admin.key --node-name=kwok--kwok-controller --node-port=10247 --server-address=0.0.0.0:10247 --node-lease-duration-seconds=1200
+docker create --name=kwok--etcd --pull=never --network=kwok- --entrypoint=etcd --restart=unless-stopped --label=com.docker.compose.project=kwok- registry.k8s.io/etcd:3.5.9-0 --name=node0 --auto-compaction-retention=1 --quota-backend-bytes=8589934592 --data-dir=/etcd-data --initial-advertise-peer-urls=http://0.0.0.0:2380 --listen-peer-urls=http://0.0.0.0:2380 --advertise-client-urls=http://0.0.0.0:2379 --listen-client-urls=http://0.0.0.0:2379 --initial-cluster=node0=http://0.0.0.0:2380
+docker create --name=kwok--kube-apiserver --pull=never --network=kwok- --entrypoint=kube-apiserver --link=kwok--etcd --restart=unless-stopped --label=com.docker.compose.project=kwok- --publish=32766:6443/tcp --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro registry.k8s.io/kube-apiserver:v1.27.3 --etcd-prefix=/registry --allow-privileged=true --max-requests-inflight=0 --max-mutating-requests-inflight=0 --enable-priority-and-fairness=false --etcd-servers=http://kwok--etcd:2379 --authorization-mode=Node,RBAC --bind-address=0.0.0.0 --secure-port=6443 --tls-cert-file=/etc/kubernetes/pki/admin.crt --tls-private-key-file=/etc/kubernetes/pki/admin.key --client-ca-file=/etc/kubernetes/pki/ca.crt --service-account-key-file=/etc/kubernetes/pki/admin.key --service-account-signing-key-file=/etc/kubernetes/pki/admin.key --service-account-issuer=https://kubernetes.default.svc.cluster.local
+docker create --name=kwok--kube-controller-manager --pull=never --network=kwok- --entrypoint=kube-controller-manager --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro registry.k8s.io/kube-controller-manager:v1.27.3 --node-monitor-period=10m0s --node-monitor-grace-period=1h0m0s --kubeconfig=~/.kube/config --authorization-always-allow-paths=/healthz,/readyz,/livez,/metrics --bind-address=0.0.0.0 --secure-port=10257 --root-ca-file=/etc/kubernetes/pki/ca.crt --service-account-private-key-file=/etc/kubernetes/pki/admin.key --kube-api-qps=5000 --kube-api-burst=10000
+docker create --name=kwok--kube-scheduler --pull=never --network=kwok- --entrypoint=kube-scheduler --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro registry.k8s.io/kube-scheduler:v1.27.3 --kubeconfig=~/.kube/config --authorization-always-allow-paths=/healthz,/readyz,/livez,/metrics --bind-address=0.0.0.0 --secure-port=10259 --kube-api-qps=5000 --kube-api-burst=10000
+docker create --name=kwok--kwok-controller --pull=never --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --volume=~/.kwok/clusters//kwok.yaml:~/.kwok/kwok.yaml:ro localhost/kwok:test --manage-all-nodes=true --kubeconfig=~/.kube/config --config=~/.kwok/kwok.yaml --tls-cert-file=/etc/kubernetes/pki/admin.crt --tls-private-key-file=/etc/kubernetes/pki/admin.key --node-name=kwok--kwok-controller --node-port=10247 --server-address=0.0.0.0:10247 --node-lease-duration-seconds=1200
# Add context kwok- to ~/.kube/config
docker start kwok--etcd
docker start kwok--kube-apiserver
diff --git a/test/kwokctl/testdata/docker/create_cluster_with_extra.txt b/test/kwokctl/testdata/docker/create_cluster_with_extra.txt
index c7b2785949..409c3b8eae 100644
--- a/test/kwokctl/testdata/docker/create_cluster_with_extra.txt
+++ b/test/kwokctl/testdata/docker/create_cluster_with_extra.txt
@@ -136,12 +136,12 @@ users:
EOF
# Save cluster config to ~/.kwok/clusters//kwok.yaml
docker network create kwok- --label=com.docker.compose.project=kwok-
-docker create --name=kwok--etcd --pull=never --entrypoint=etcd --network=kwok- --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=/extras/etcd:/extras/tmp --env=TEST_KEY=TEST_VALUE registry.k8s.io/etcd:3.5.9-0 --name=node0 --auto-compaction-retention=1 --quota-backend-bytes=8589934592 --log-level=debug --data-dir=/etcd-data --initial-advertise-peer-urls=http://0.0.0.0:2380 --listen-peer-urls=http://0.0.0.0:2380 --advertise-client-urls=http://0.0.0.0:2379 --listen-client-urls=http://0.0.0.0:2379 --initial-cluster=node0=http://0.0.0.0:2380
-docker create --name=kwok--kube-apiserver --pull=never --entrypoint=kube-apiserver --network=kwok- --link=kwok--etcd --restart=unless-stopped --label=com.docker.compose.project=kwok- --publish=32766:6443/tcp --volume=/extras/apiserver:/extras/tmp --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --env=TEST_KEY=TEST_VALUE registry.k8s.io/kube-apiserver:v1.27.3 --etcd-prefix=/registry --allow-privileged=true --v=5 --max-requests-inflight=0 --max-mutating-requests-inflight=0 --enable-priority-and-fairness=false --etcd-servers=http://kwok--etcd:2379 --authorization-mode=Node,RBAC --bind-address=0.0.0.0 --secure-port=6443 --tls-cert-file=/etc/kubernetes/pki/admin.crt --tls-private-key-file=/etc/kubernetes/pki/admin.key --client-ca-file=/etc/kubernetes/pki/ca.crt --service-account-key-file=/etc/kubernetes/pki/admin.key --service-account-signing-key-file=/etc/kubernetes/pki/admin.key --service-account-issuer=https://kubernetes.default.svc.cluster.local
-docker create --name=kwok--kube-controller-manager --pull=never --entrypoint=kube-controller-manager --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=/extras/controller-manager:/extras/tmp --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --env=TEST_KEY=TEST_VALUE registry.k8s.io/kube-controller-manager:v1.27.3 --v=5 --node-monitor-period=10m0s --node-monitor-grace-period=1h0m0s --kubeconfig=~/.kube/config --authorization-always-allow-paths=/healthz,/readyz,/livez,/metrics --bind-address=0.0.0.0 --secure-port=10257 --root-ca-file=/etc/kubernetes/pki/ca.crt --service-account-private-key-file=/etc/kubernetes/pki/admin.key --kube-api-qps=5000 --kube-api-burst=10000
-docker create --name=kwok--kube-scheduler --pull=never --entrypoint=kube-scheduler --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=/extras/scheduler:/extras/tmp --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --env=TEST_KEY=TEST_VALUE registry.k8s.io/kube-scheduler:v1.27.3 --v=5 --kubeconfig=~/.kube/config --authorization-always-allow-paths=/healthz,/readyz,/livez,/metrics --bind-address=0.0.0.0 --secure-port=10259 --kube-api-qps=5000 --kube-api-burst=10000
-docker create --name=kwok--kwok-controller --pull=never --entrypoint=kwok --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=/extras/controller:/extras/tmp --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --volume=~/.kwok/clusters//kwok.yaml:~/.kwok/kwok.yaml:ro --env=TEST_KEY=TEST_VALUE localhost/kwok:test --manage-all-nodes=true --v=-4 --kubeconfig=~/.kube/config --config=~/.kwok/kwok.yaml --tls-cert-file=/etc/kubernetes/pki/admin.crt --tls-private-key-file=/etc/kubernetes/pki/admin.key --node-name=kwok--kwok-controller --node-port=10247 --server-address=0.0.0.0:10247 --node-lease-duration-seconds=1200
-docker create --name=kwok--prometheus --pull=never --entrypoint=prometheus --network=kwok- --link=kwok--etcd --link=kwok--kube-apiserver --link=kwok--kube-controller-manager --link=kwok--kube-scheduler --link=kwok--kwok-controller --restart=unless-stopped --label=com.docker.compose.project=kwok- --publish=9090:9090/tcp --volume=/extras/prometheus:/extras/tmp --volume=~/.kwok/clusters//prometheus.yaml:/etc/prometheus/prometheus.yaml:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --env=TEST_KEY=TEST_VALUE docker.io/prom/prometheus:v2.44.0 --log.level=debug --config.file=/etc/prometheus/prometheus.yaml --web.listen-address=0.0.0.0:9090
+docker create --name=kwok--etcd --pull=never --network=kwok- --entrypoint=etcd --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=/extras/etcd:/extras/tmp --env=TEST_KEY=TEST_VALUE registry.k8s.io/etcd:3.5.9-0 --name=node0 --auto-compaction-retention=1 --quota-backend-bytes=8589934592 --log-level=debug --data-dir=/etcd-data --initial-advertise-peer-urls=http://0.0.0.0:2380 --listen-peer-urls=http://0.0.0.0:2380 --advertise-client-urls=http://0.0.0.0:2379 --listen-client-urls=http://0.0.0.0:2379 --initial-cluster=node0=http://0.0.0.0:2380
+docker create --name=kwok--kube-apiserver --pull=never --network=kwok- --entrypoint=kube-apiserver --link=kwok--etcd --restart=unless-stopped --label=com.docker.compose.project=kwok- --publish=32766:6443/tcp --volume=/extras/apiserver:/extras/tmp --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --env=TEST_KEY=TEST_VALUE registry.k8s.io/kube-apiserver:v1.27.3 --etcd-prefix=/registry --allow-privileged=true --v=5 --max-requests-inflight=0 --max-mutating-requests-inflight=0 --enable-priority-and-fairness=false --etcd-servers=http://kwok--etcd:2379 --authorization-mode=Node,RBAC --bind-address=0.0.0.0 --secure-port=6443 --tls-cert-file=/etc/kubernetes/pki/admin.crt --tls-private-key-file=/etc/kubernetes/pki/admin.key --client-ca-file=/etc/kubernetes/pki/ca.crt --service-account-key-file=/etc/kubernetes/pki/admin.key --service-account-signing-key-file=/etc/kubernetes/pki/admin.key --service-account-issuer=https://kubernetes.default.svc.cluster.local
+docker create --name=kwok--kube-controller-manager --pull=never --network=kwok- --entrypoint=kube-controller-manager --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=/extras/controller-manager:/extras/tmp --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --env=TEST_KEY=TEST_VALUE registry.k8s.io/kube-controller-manager:v1.27.3 --v=5 --node-monitor-period=10m0s --node-monitor-grace-period=1h0m0s --kubeconfig=~/.kube/config --authorization-always-allow-paths=/healthz,/readyz,/livez,/metrics --bind-address=0.0.0.0 --secure-port=10257 --root-ca-file=/etc/kubernetes/pki/ca.crt --service-account-private-key-file=/etc/kubernetes/pki/admin.key --kube-api-qps=5000 --kube-api-burst=10000
+docker create --name=kwok--kube-scheduler --pull=never --network=kwok- --entrypoint=kube-scheduler --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=/extras/scheduler:/extras/tmp --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --env=TEST_KEY=TEST_VALUE registry.k8s.io/kube-scheduler:v1.27.3 --v=5 --kubeconfig=~/.kube/config --authorization-always-allow-paths=/healthz,/readyz,/livez,/metrics --bind-address=0.0.0.0 --secure-port=10259 --kube-api-qps=5000 --kube-api-burst=10000
+docker create --name=kwok--kwok-controller --pull=never --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=/extras/controller:/extras/tmp --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --volume=~/.kwok/clusters//kwok.yaml:~/.kwok/kwok.yaml:ro --env=TEST_KEY=TEST_VALUE localhost/kwok:test --manage-all-nodes=true --v=-4 --kubeconfig=~/.kube/config --config=~/.kwok/kwok.yaml --tls-cert-file=/etc/kubernetes/pki/admin.crt --tls-private-key-file=/etc/kubernetes/pki/admin.key --node-name=kwok--kwok-controller --node-port=10247 --server-address=0.0.0.0:10247 --node-lease-duration-seconds=1200
+docker create --name=kwok--prometheus --pull=never --network=kwok- --entrypoint=prometheus --link=kwok--etcd --link=kwok--kube-apiserver --link=kwok--kube-controller-manager --link=kwok--kube-scheduler --link=kwok--kwok-controller --restart=unless-stopped --label=com.docker.compose.project=kwok- --publish=9090:9090/tcp --volume=/extras/prometheus:/extras/tmp --volume=~/.kwok/clusters//prometheus.yaml:/etc/prometheus/prometheus.yaml:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --env=TEST_KEY=TEST_VALUE docker.io/prom/prometheus:v2.44.0 --log.level=debug --config.file=/etc/prometheus/prometheus.yaml --web.listen-address=0.0.0.0:9090
# Add context kwok- to ~/.kube/config
docker start kwok--etcd
docker start kwok--kube-apiserver
diff --git a/test/kwokctl/testdata/docker/create_cluster_with_verbosity.txt b/test/kwokctl/testdata/docker/create_cluster_with_verbosity.txt
index 2e5207241d..17b2160ecd 100644
--- a/test/kwokctl/testdata/docker/create_cluster_with_verbosity.txt
+++ b/test/kwokctl/testdata/docker/create_cluster_with_verbosity.txt
@@ -136,12 +136,12 @@ users:
EOF
# Save cluster config to ~/.kwok/clusters//kwok.yaml
docker network create kwok- --label=com.docker.compose.project=kwok-
-docker create --name=kwok--etcd --pull=never --entrypoint=etcd --network=kwok- --restart=unless-stopped --label=com.docker.compose.project=kwok- registry.k8s.io/etcd:3.5.9-0 --name=node0 --auto-compaction-retention=1 --quota-backend-bytes=8589934592 --data-dir=/etcd-data --initial-advertise-peer-urls=http://0.0.0.0:2380 --listen-peer-urls=http://0.0.0.0:2380 --advertise-client-urls=http://0.0.0.0:2379 --listen-client-urls=http://0.0.0.0:2379 --initial-cluster=node0=http://0.0.0.0:2380 --log-level=debug
-docker create --name=kwok--kube-apiserver --pull=never --entrypoint=kube-apiserver --network=kwok- --link=kwok--etcd --restart=unless-stopped --label=com.docker.compose.project=kwok- --publish=32766:6443/tcp --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro registry.k8s.io/kube-apiserver:v1.27.3 --etcd-prefix=/registry --allow-privileged=true --max-requests-inflight=0 --max-mutating-requests-inflight=0 --enable-priority-and-fairness=false --etcd-servers=http://kwok--etcd:2379 --authorization-mode=Node,RBAC --bind-address=0.0.0.0 --secure-port=6443 --tls-cert-file=/etc/kubernetes/pki/admin.crt --tls-private-key-file=/etc/kubernetes/pki/admin.key --client-ca-file=/etc/kubernetes/pki/ca.crt --service-account-key-file=/etc/kubernetes/pki/admin.key --service-account-signing-key-file=/etc/kubernetes/pki/admin.key --service-account-issuer=https://kubernetes.default.svc.cluster.local --v=4
-docker create --name=kwok--kube-controller-manager --pull=never --entrypoint=kube-controller-manager --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro registry.k8s.io/kube-controller-manager:v1.27.3 --node-monitor-period=10m0s --node-monitor-grace-period=1h0m0s --kubeconfig=~/.kube/config --authorization-always-allow-paths=/healthz,/readyz,/livez,/metrics --bind-address=0.0.0.0 --secure-port=10257 --root-ca-file=/etc/kubernetes/pki/ca.crt --service-account-private-key-file=/etc/kubernetes/pki/admin.key --kube-api-qps=5000 --kube-api-burst=10000 --v=4
-docker create --name=kwok--kube-scheduler --pull=never --entrypoint=kube-scheduler --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro registry.k8s.io/kube-scheduler:v1.27.3 --kubeconfig=~/.kube/config --authorization-always-allow-paths=/healthz,/readyz,/livez,/metrics --bind-address=0.0.0.0 --secure-port=10259 --kube-api-qps=5000 --kube-api-burst=10000 --v=4
-docker create --name=kwok--kwok-controller --pull=never --entrypoint=kwok --network=kwok- --link=kwok--kube-apiserver --restart=unless-stopped --label=com.docker.compose.project=kwok- --volume=~/.kwok/clusters//kubeconfig:~/.kube/config:ro --volume=~/.kwok/clusters//pki/ca.crt:/etc/kubernetes/pki/ca.crt:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro --volume=~/.kwok/clusters//kwok.yaml:~/.kwok/kwok.yaml:ro localhost/kwok:test --manage-all-nodes=true --kubeconfig=~/.kube/config --config=~/.kwok/kwok.yaml --tls-cert-file=/etc/kubernetes/pki/admin.crt --tls-private-key-file=/etc/kubernetes/pki/admin.key --node-name=kwok--kwok-controller --node-port=10247 --server-address=0.0.0.0:10247 --node-lease-duration-seconds=1200 --v=DEBUG
-docker create --name=kwok--prometheus --pull=never --entrypoint=prometheus --network=kwok- --link=kwok--etcd --link=kwok--kube-apiserver --link=kwok--kube-controller-manager --link=kwok--kube-scheduler --link=kwok--kwok-controller --restart=unless-stopped --label=com.docker.compose.project=kwok- --publish=9090:9090/tcp --volume=~/.kwok/clusters//prometheus.yaml:/etc/prometheus/prometheus.yaml:ro --volume=~/.kwok/clusters//pki/admin.crt:/etc/kubernetes/pki/admin.crt:ro --volume=~/.kwok/clusters//pki/admin.key:/etc/kubernetes/pki/admin.key:ro docker.io/prom/prometheus:v2.44.0 --config.file=/etc/prometheus/prometheus.yaml --web.listen-address=0.0.0.0:9090 --log.level=debug
+docker create --name=kwok--etcd --pull=never --network=kwok- --entrypoint=etcd --restart=unless-stopped --label=com.docker.compose.project=kwok- registry.k8s.io/etcd:3.5.9-0 --name=node0 --auto-compaction-retention=1 --quota-backend-bytes=8589934592 --data-dir=/etcd-data --initial-advertise-peer-urls=http://0.0.0.0:2380 --listen-peer-urls=http://0.0.0.0:2380 --advertise-client-urls=http://0.0.0.0:2379 --listen-client-urls=http://0.0.0.0:2379 --initial-cluster=node0=http://0.0.0.0:2380 --log-level=debug
+docker create --name=kwok--kube-apiserver --pull=never --network=kwok- --entrypoint=kube-apiserver --link=kwok--etcd --restart=unless-stopped --label=com.docker.compose.project=kwok- --publish=32766:6443/tcp --volume=~/.kwok/clusters/