Skip to content
This repository was archived by the owner on Feb 28, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/kubelet/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,7 @@ func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencie
} else {
startKubelet(k, podCfg, &kubeServer.KubeletConfiguration, kubeDeps, kubeServer.EnableCAdvisorJSONEndpoints, kubeServer.EnableServer)
klog.Info("Started kubelet")
klog.Info("Kubelet compiled with user namespaces support")
}
return nil
}
Expand Down
9 changes: 0 additions & 9 deletions pkg/features/kube_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,6 @@ const (
// beta: v1.11
DynamicKubeletConfig featuregate.Feature = "DynamicKubeletConfig"

// owner: @pweil-
// alpha: v1.5
//
// Default userns=host for containers that are using other host namespaces, host mounts, the pod
// contains a privileged container, or specific non-namespaced capabilities (MKNOD, SYS_MODULE,
// SYS_TIME). This should only be enabled if user namespace remapping is enabled in the docker daemon.
ExperimentalHostUserNamespaceDefaultingGate featuregate.Feature = "ExperimentalHostUserNamespaceDefaulting"

// owner: @jiayingz
// beta: v1.10
//
Expand Down Expand Up @@ -559,7 +551,6 @@ func init() {
var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
AppArmor: {Default: true, PreRelease: featuregate.Beta},
DynamicKubeletConfig: {Default: true, PreRelease: featuregate.Beta},
ExperimentalHostUserNamespaceDefaultingGate: {Default: false, PreRelease: featuregate.Beta},
DevicePlugins: {Default: true, PreRelease: featuregate.Beta},
TaintBasedEvictions: {Default: true, PreRelease: featuregate.Beta},
RotateKubeletServerCertificate: {Default: true, PreRelease: featuregate.Beta},
Expand Down
3 changes: 3 additions & 0 deletions pkg/kubelet/container/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,14 @@ type RuntimeHelper interface {
// of a pod.
GetPodCgroupParent(pod *v1.Pod) string
GetPodDir(podUID types.UID) string
GetPodVolumesDir(podUID types.UID) string
GeneratePodHostNameAndDomain(pod *v1.Pod) (hostname string, hostDomain string, err error)
// GetExtraSupplementalGroupsForPod returns a list of the extra
// supplemental groups for the Pod. These extra supplemental groups come
// from annotations on persistent volumes that the pod depends on.
GetExtraSupplementalGroupsForPod(pod *v1.Pod) []int64
// UserNamespaceForPod returns the mode for the user namespace of the pod passed as argument.
UserNamespaceForPod(pod *v1.Pod) (runtimeapi.NamespaceMode, error)
}

// ShouldContainerBeRestarted checks whether a container needs to be restarted.
Expand Down
92 changes: 92 additions & 0 deletions pkg/kubelet/container/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,3 +614,95 @@ func TestHashContainer(t *testing.T) {
assert.Equal(t, tc.expectedHash, hashVal, "the hash value here should not be changed.")
}
}

func TestRuntimeConfigInfo(t *testing.T) {
empty := &RuntimeConfigInfo{}
single := &RuntimeConfigInfo{
UserNamespaceConfig: UserNamespaceConfigInfo{
UidMappings: []*UserNSMapping{
&UserNSMapping{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a test with more than one mapping too, given that this is a slice?

ContainerID: 0,
HostID: 100000,
Size: 65536,
},
},
GidMappings: []*UserNSMapping{
&UserNSMapping{
ContainerID: 0,
HostID: 200000,
Size: 32768,
},
},
},
}

testCases := []struct {
config *RuntimeConfigInfo
uid uint32
gid uint32
expectedSupported bool
expectedEnabled bool
expectedUid uint32
expectedGid uint32
expectedError bool
}{
{
config: empty,
expectedSupported: false,
expectedEnabled: false,
expectedError: true,
},
{
config: single,
expectedSupported: true,
expectedEnabled: true,
expectedError: false,
uid: 0,
expectedUid: 100000,
gid: 0,
expectedGid: 200000,
},
{
config: single,
expectedSupported: true,
expectedEnabled: true,
expectedError: false,
uid: 65535,
expectedUid: 165535,
gid: 32767,
expectedGid: 232767,
},
{
config: single,
expectedSupported: true,
expectedEnabled: true,
expectedError: true,
uid: 65536,
gid: 32768,
},
}

for _, tc := range testCases {
actualSupported := tc.config.IsUserNamespaceSupported()
assert.Equal(t, tc.expectedSupported, actualSupported)

actualEnabled := tc.config.IsUserNamespaceEnabled()
assert.Equal(t, tc.expectedSupported, actualEnabled)

actualUid, err := tc.config.GetHostUIDFor(tc.uid)
if tc.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedUid, actualUid)
}

actualGid, err := tc.config.GetHostGIDFor(tc.gid)
if tc.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedGid, actualGid)
}
}
}
70 changes: 64 additions & 6 deletions pkg/kubelet/container/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ type Runtime interface {
// This method just proxies a new runtimeConfig with the updated
// CIDR value down to the runtime shim.
UpdatePodCIDR(podCIDR string) error
// GetRuntimeConfigInfo returns runtime's configuration details, eg: if user-namespaces are enabled or not
GetRuntimeConfigInfo() (*RuntimeConfigInfo, error)
}

// StreamingRuntime is the interface implemented by runtimes that handle the serving of the
Expand Down Expand Up @@ -422,12 +424,6 @@ type RunContainerOptions struct {
ReadOnly bool
// hostname for pod containers
Hostname string
// EnableHostUserNamespace sets userns=host when users request host namespaces (pid, ipc, net),
// are using non-namespaced capabilities (mknod, sys_time, sys_module), the pod contains a privileged container,
// or using host path volumes.
// This should only be enabled when the container runtime is performing user remapping AND if the
// experimental behavior is desired.
EnableHostUserNamespace bool
Comment thread
rata marked this conversation as resolved.
}

// VolumeInfo contains information about the volume.
Expand Down Expand Up @@ -465,6 +461,68 @@ type RuntimeStatus struct {
Conditions []RuntimeCondition
}

// RuntimeConfigInfo contains runtime's configuration details, eg: user-namespaces mapping between host and container
type RuntimeConfigInfo struct {
UserNamespaceConfig UserNamespaceConfigInfo
}

// UserNamespaceConfigInfo contains runtime's user-namespace configuration
type UserNamespaceConfigInfo struct {
UidMappings []*UserNSMapping
GidMappings []*UserNSMapping
}

// UserNSMaping represents mapping of user-namespaces between host and container
type UserNSMapping struct {
ContainerID uint32
HostID uint32
Size uint32
}

// IsUserNamespaceEnabled returns true if user-namespace feature is enabled at runtime
func (c *RuntimeConfigInfo) IsUserNamespaceEnabled() bool {
if len(c.UserNamespaceConfig.UidMappings) == 0 {
return false
}
if len(c.UserNamespaceConfig.UidMappings) == 1 &&
c.UserNamespaceConfig.UidMappings[0].HostID == uint32(0) && c.UserNamespaceConfig.UidMappings[0].Size == uint32(4294967295) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit picking: is the uint32() needed? I think we shouldn't need it. Or am I missing something?

I mean, golang does force to use some explicit conversions, but as explained here I don't think it should be needed to compare a var with a constant.

Am I missing something?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I'll remove them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why c.UserNamespaceConfig.UidMappings[0].Size == uint32(4294967295)? I mean, why equal to that constant value? It is set to that when the runtime doesn't support user ns?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4294967295 encapsulates the entire uid/gid range I believe. The initial root namespace has a uid_map of 0 0 4294967295. But I agree that the use of it here is unclear

Copy link
Copy Markdown
Member Author

@mauriciovasquezbernal mauriciovasquezbernal Jul 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the idea is that if the configured mapping is 0 0 4294967295 it means that the support is not enabled (all the ids in the container are mapped to the same ids in the host).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, I see. Thanks! Maybe a comment can make it very clear to everyone :)

Comment on lines +487 to +488
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be more clear.

Suggested change
if len(c.UserNamespaceConfig.UidMappings) == 1 &&
c.UserNamespaceConfig.UidMappings[0].HostID == uint32(0) && c.UserNamespaceConfig.UidMappings[0].Size == uint32(4294967295) {
if len(c.UserNamespaceConfig.UidMappings) == 1 &&
c.UserNamespaceConfig.UidMappings[0].HostID == c.UserNamespaceConfig.UidMappings[0].ContainerID {

return false
}
return true
}

// IsUserNamespaceSupported returns true if user-namespace feature is supported at runtime
func (c *RuntimeConfigInfo) IsUserNamespaceSupported() bool {
if len(c.UserNamespaceConfig.UidMappings) == 0 {
return false
}
if len(c.UserNamespaceConfig.UidMappings) == 1 &&
c.UserNamespaceConfig.UidMappings[0].HostID == uint32(0) && c.UserNamespaceConfig.UidMappings[0].Size == uint32(0) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem regarding uint32(). Is it needed?

Comment on lines +499 to +500
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why if len(...) == 1 and hostID == 0 and size == 0? Not sure I follow what you are trying to detect.

Is it just when the size is 0? Why does it matter, in that case, that the hostID is 0 too? Or why wouldn't it be a problem if the specified mappings are more than one?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's trying to detect the case where a mapping like 0 1000 0 is configured. You're totally right, the whole logic to check if the usernamespace is supported or enabled has to be checked again. I think it could be simplified a lot but I avoided touching that at this time.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

return false
}
return true
}

// GetHostUIDFor returns uid on host usernamespace that is mapped to the given uid in container usernamespace
func (c *RuntimeConfigInfo) GetHostUIDFor(containerUID uint32) (uint32, error) {
for _, mapping := range c.UserNamespaceConfig.UidMappings {
if containerUID >= mapping.ContainerID && containerUID < mapping.ContainerID+mapping.Size {
return mapping.HostID + (containerUID - mapping.ContainerID), nil
}
}
return 0, fmt.Errorf("IdMapping not found for container usernamespace UID %v", containerUID)
}

// GetHostGIDFor returns gid on host usernamespace that is mapped to the given gid in container usernamespace
func (c *RuntimeConfigInfo) GetHostGIDFor(containerGID uint32) (uint32, error) {
for _, mapping := range c.UserNamespaceConfig.GidMappings {
if containerGID >= mapping.ContainerID && containerGID < mapping.ContainerID+mapping.Size {
return mapping.HostID + (containerGID - mapping.ContainerID), nil
}
}
return 0, fmt.Errorf("IdMapping not found for container usernamespace GID %v", containerGID)
}

// GetRuntimeCondition gets a specified runtime condition from the runtime status.
func (r *RuntimeStatus) GetRuntimeCondition(t RuntimeConditionType) *RuntimeCondition {
for i := range r.Conditions {
Expand Down
46 changes: 29 additions & 17 deletions pkg/kubelet/container/testing/fake_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,25 @@ type FakePod struct {
// FakeRuntime is a fake container runtime for testing.
type FakeRuntime struct {
sync.Mutex
CalledFunctions []string
PodList []*FakePod
AllPodList []*FakePod
ImageList []kubecontainer.Image
APIPodStatus v1.PodStatus
PodStatus kubecontainer.PodStatus
StartedPods []string
KilledPods []string
StartedContainers []string
KilledContainers []string
RuntimeStatus *kubecontainer.RuntimeStatus
VersionInfo string
APIVersionInfo string
RuntimeType string
Err error
InspectErr error
StatusErr error
CalledFunctions []string
PodList []*FakePod
AllPodList []*FakePod
ImageList []kubecontainer.Image
APIPodStatus v1.PodStatus
PodStatus kubecontainer.PodStatus
StartedPods []string
KilledPods []string
StartedContainers []string
KilledContainers []string
RuntimeStatus *kubecontainer.RuntimeStatus
RuntimeConfigInfo *kubecontainer.RuntimeConfigInfo
RuntimeConfigInfoErr error
VersionInfo string
APIVersionInfo string
RuntimeType string
Err error
InspectErr error
StatusErr error
}

const FakeHost = "localhost:12345"
Expand Down Expand Up @@ -123,6 +125,8 @@ func (f *FakeRuntime) ClearCalls() {
f.StartedContainers = []string{}
f.KilledContainers = []string{}
f.RuntimeStatus = nil
f.RuntimeConfigInfo = nil
f.RuntimeConfigInfoErr = nil
f.VersionInfo = ""
f.RuntimeType = ""
f.Err = nil
Expand Down Expand Up @@ -205,6 +209,14 @@ func (f *FakeRuntime) Status() (*kubecontainer.RuntimeStatus, error) {
return f.RuntimeStatus, f.StatusErr
}

func (f *FakeRuntime) GetRuntimeConfigInfo() (*kubecontainer.RuntimeConfigInfo, error) {
f.Lock()
defer f.Unlock()

f.CalledFunctions = append(f.CalledFunctions, "GetRuntimeConfigInfo")
return f.RuntimeConfigInfo, f.RuntimeConfigInfoErr
}

func (f *FakeRuntime) GetPods(all bool) ([]*kubecontainer.Pod, error) {
f.Lock()
defer f.Unlock()
Expand Down
8 changes: 8 additions & 0 deletions pkg/kubelet/container/testing/fake_runtime_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ func (f *FakeRuntimeHelper) GetPodDir(podUID kubetypes.UID) string {
return "/poddir/" + string(podUID)
}

func (f *FakeRuntimeHelper) GetPodVolumesDir(podUID kubetypes.UID) string {
return f.GetPodDir(podUID) + "/volumes/"
}

func (f *FakeRuntimeHelper) UserNamespaceForPod(pod *v1.Pod) (runtimeapi.NamespaceMode, error) {
return runtimeapi.NamespaceMode_NODE, nil
}

func (f *FakeRuntimeHelper) GetExtraSupplementalGroupsForPod(pod *v1.Pod) []int64 {
return nil
}
5 changes: 5 additions & 0 deletions pkg/kubelet/container/testing/runtime_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ func (r *Mock) Status() (*kubecontainer.RuntimeStatus, error) {
return args.Get(0).(*kubecontainer.RuntimeStatus), args.Error(0)
}

func (r *Mock) GetRuntimeConfigInfo() (*kubecontainer.RuntimeConfigInfo, error) {
args := r.Called()
return args.Get(0).(*kubecontainer.RuntimeConfigInfo), args.Error(1)
}

func (r *Mock) GetPods(all bool) ([]*kubecontainer.Pod, error) {
args := r.Called(all)
return args.Get(0).([]*kubecontainer.Pod), args.Error(1)
Expand Down
12 changes: 12 additions & 0 deletions pkg/kubelet/dockershim/docker_sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ func (ds *dockerService) PodSandboxStatus(ctx context.Context, req *runtimeapi.P
Network: networkNamespaceMode(r),
Pid: pidNamespaceMode(r),
Ipc: ipcNamespaceMode(r),
User: userNamespaceMode(r),
},
},
},
Expand Down Expand Up @@ -690,6 +691,17 @@ func ipcNamespaceMode(container *dockertypes.ContainerJSON) runtimeapi.Namespace
return runtimeapi.NamespaceMode_POD
}

// userNamespaceMode returns the user runtimeapi.NamespaceMode for this container.
// Supports: POD, NODE
func userNamespaceMode(container *dockertypes.ContainerJSON) runtimeapi.NamespaceMode {
if container != nil && container.HostConfig != nil {
if string(container.HostConfig.UsernsMode) == namespaceModeHost {
return runtimeapi.NamespaceMode_NODE
}
}
return runtimeapi.NamespaceMode_POD
}

func constructPodSandboxCheckpoint(config *runtimeapi.PodSandboxConfig) checkpointmanager.Checkpoint {
data := CheckpointData{}
for _, pm := range config.GetPortMappings() {
Expand Down
Loading