Add alpha 2 phase implementation for UserNamespacesHostNetworkSupport

This commit is contained in:
HirazawaUi 2026-02-13 14:16:20 +08:00
parent 1132a4e4ef
commit 0ffc845789
16 changed files with 969 additions and 817 deletions

View file

@ -2667,7 +2667,7 @@ var defaultKubernetesFeatureGateDependencies = map[featuregate.Feature][]feature
TranslateStreamCloseWebsocketRequests: {},
UserNamespacesHostNetworkSupport: {UserNamespacesSupport},
UserNamespacesHostNetworkSupport: {UserNamespacesSupport, NodeDeclaredFeatures},
UserNamespacesSupport: {},

View file

@ -666,7 +666,8 @@ func (c *RuntimeCondition) String() string {
// RuntimeFeatures contains the set of features implemented by the runtime
type RuntimeFeatures struct {
SupplementalGroupsPolicy bool
SupplementalGroupsPolicy bool
UserNamespacesHostNetwork bool
}
// String formats the runtime condition into a human readable string.
@ -674,7 +675,7 @@ func (f *RuntimeFeatures) String() string {
if f == nil {
return "nil"
}
return fmt.Sprintf("SupplementalGroupsPolicy: %v", f.SupplementalGroupsPolicy)
return fmt.Sprintf("SupplementalGroupsPolicy: %v UserNamespacesHostNetwork: %v", f.SupplementalGroupsPolicy, f.UserNamespacesHostNetwork)
}
// Pods represents the list of pods

View file

@ -603,11 +603,11 @@ func TestRuntimeStatusString(t *testing.T) {
{Name: "handler1", SupportsRecursiveReadOnlyMounts: true, SupportsUserNamespaces: false},
{Name: "handler2", SupportsRecursiveReadOnlyMounts: false, SupportsUserNamespaces: true},
},
Features: &RuntimeFeatures{SupplementalGroupsPolicy: true},
Features: &RuntimeFeatures{SupplementalGroupsPolicy: true, UserNamespacesHostNetwork: true},
}
result := status.String()
expected := "Runtime Conditions: RuntimeReady=true reason:ready message:runtime is ready, NetworkReady=false reason:not ready message:network is not ready; Handlers: Name=handler1 SupportsRecursiveReadOnlyMounts: true SupportsUserNamespaces: false, Name=handler2 SupportsRecursiveReadOnlyMounts: false SupportsUserNamespaces: true, Features: SupplementalGroupsPolicy: true"
expected := "Runtime Conditions: RuntimeReady=true reason:ready message:runtime is ready, NetworkReady=false reason:not ready message:network is not ready; Handlers: Name=handler1 SupportsRecursiveReadOnlyMounts: true SupportsUserNamespaces: false, Name=handler2 SupportsRecursiveReadOnlyMounts: false SupportsUserNamespaces: true, Features: SupplementalGroupsPolicy: true UserNamespacesHostNetwork: true"
assert.Equal(t, expected, result, "String()")
}
@ -688,18 +688,20 @@ func TestRuntimeFeaturesString(t *testing.T) {
expected string
}{
{
name: "features with SupplementalGroupsPolicy true",
name: "features with both flags true",
features: &RuntimeFeatures{
SupplementalGroupsPolicy: true,
SupplementalGroupsPolicy: true,
UserNamespacesHostNetwork: true,
},
expected: "SupplementalGroupsPolicy: true",
expected: "SupplementalGroupsPolicy: true UserNamespacesHostNetwork: true",
},
{
name: "features with SupplementalGroupsPolicy false",
name: "features with both flags false",
features: &RuntimeFeatures{
SupplementalGroupsPolicy: false,
SupplementalGroupsPolicy: false,
UserNamespacesHostNetwork: false,
},
expected: "SupplementalGroupsPolicy: false",
expected: "SupplementalGroupsPolicy: false UserNamespacesHostNetwork: false",
},
{
name: "nil features",

View file

@ -1079,6 +1079,11 @@ func NewMainKubelet(ctx context.Context,
handlers = append(handlers, evictionAdmitHandler)
if utilfeature.DefaultFeatureGate.Enabled(features.NodeDeclaredFeatures) {
if status, err := klet.containerRuntime.Status(ctx); err == nil && status != nil {
klet.runtimeState.setRuntimeFeatures(status.Features)
} else if err != nil {
logger.V(4).Info("Unable to prefetch container runtime features for node declared features", "err", err)
}
v, err := versionutil.Parse(version.Get().String())
if err != nil {
return nil, fmt.Errorf("failed to parse version: %w", err)

View file

@ -37,9 +37,16 @@ func (a FeatureGateAdapter) Enabled(key string) bool {
// discoverNodeDeclaredFeatures determines the final set of node features to be declared by using the discovery library.
func (kl *Kubelet) discoverNodeDeclaredFeatures() []string {
adaptedFG := FeatureGateAdapter{FeatureGate: utilfeature.DefaultFeatureGate}
runtimeFeatures := nodedeclaredfeatures.RuntimeFeatures{}
if features := kl.runtimeState.runtimeFeatures(); features != nil {
runtimeFeatures.UserNamespacesHostNetwork = features.UserNamespacesHostNetwork
}
cfg := &nodedeclaredfeatures.NodeConfiguration{
FeatureGates: adaptedFG,
Version: kl.version,
FeatureGates: adaptedFG,
Version: kl.version,
RuntimeFeatures: runtimeFeatures,
}
return kl.nodeDeclaredFeaturesFramework.DiscoverNodeFeatures(cfg)
}

View file

@ -267,7 +267,8 @@ func toKubeRuntimeStatus(status *runtimeapi.RuntimeStatus, handlers []*runtimeap
var retFeatures *kubecontainer.RuntimeFeatures
if features != nil {
retFeatures = &kubecontainer.RuntimeFeatures{
SupplementalGroupsPolicy: features.SupplementalGroupsPolicy,
SupplementalGroupsPolicy: features.SupplementalGroupsPolicy,
UserNamespacesHostNetwork: features.UserNamespacesHostNetwork,
}
}
return &kubecontainer.RuntimeStatus{Conditions: conditions, Handlers: retHandlers, Features: retFeatures}

View file

@ -20,6 +20,7 @@ import (
"k8s.io/component-helpers/nodedeclaredfeatures/features/extendwebsocketstokubelet"
"k8s.io/component-helpers/nodedeclaredfeatures/features/inplacepodresize"
"k8s.io/component-helpers/nodedeclaredfeatures/features/restartallcontainers"
"k8s.io/component-helpers/nodedeclaredfeatures/features/usernamespaceshostnetwork"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
@ -31,4 +32,5 @@ var AllFeatures = []types.Feature{
inplacepodresize.PodLevelResourcesResizeFeature,
extendwebsocketstokubelet.Feature,
inplacepodresize.NonSidecarInitContainerResizeFeature,
usernamespaceshostnetwork.Feature,
}

View file

@ -50,6 +50,9 @@ func TestFeatureRequirementsConsistency(t *testing.T) {
FeatureGates: mockFG,
Version: version.MustParse("1.36.0"),
}
if reqs.RequiredRuntimeFeatures != nil {
discoverCfg.RuntimeFeatures = *reqs.RequiredRuntimeFeatures
}
featureEnabled := registeredFeature.Discover(discoverCfg)
if !featureEnabled {

View file

@ -0,0 +1,77 @@
/*
Copyright 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.
*/
package usernamespaceshostnetwork
import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
// Ensure the feature struct implements the unified Feature interface.
var _ types.Feature = &userNamespacesHostNetworkFeature{}
const (
// UserNamespacesHostNetworkSupportFeatureGate is the feature gate name.
UserNamespacesHostNetworkSupportFeatureGate = "UserNamespacesHostNetworkSupport"
// UserNamespacesHostNetworkSupport is the declared feature name.
UserNamespacesHostNetworkSupport = "UserNamespacesHostNetworkSupport"
)
// Feature is the implementation of the `UserNamespacesHostNetworkSupport` feature.
var Feature = &userNamespacesHostNetworkFeature{}
type userNamespacesHostNetworkFeature struct{}
func (f *userNamespacesHostNetworkFeature) Requirements() *types.FeatureRequirements {
return &types.FeatureRequirements{
EnabledFeatureGates: []string{UserNamespacesHostNetworkSupportFeatureGate},
RequiredRuntimeFeatures: &types.RuntimeFeatures{
UserNamespacesHostNetwork: true,
},
}
}
func (f *userNamespacesHostNetworkFeature) Name() string {
return UserNamespacesHostNetworkSupport
}
func (f *userNamespacesHostNetworkFeature) Discover(cfg *types.NodeConfiguration) bool {
// This feature requires both the feature gate to be enabled AND
// runtime-level support for user namespaces with host network.
if !cfg.FeatureGates.Enabled(UserNamespacesHostNetworkSupportFeatureGate) {
return false
}
return cfg.RuntimeFeatures.UserNamespacesHostNetwork
}
func (f *userNamespacesHostNetworkFeature) InferForScheduling(podInfo *types.PodInfo) bool {
// A pod needs this feature if it uses both host network AND user namespaces.
if podInfo.Spec.HostNetwork && podInfo.Spec.HostUsers != nil && !*podInfo.Spec.HostUsers {
return true
}
return false
}
func (f *userNamespacesHostNetworkFeature) InferForUpdate(oldPodInfo, newPodInfo *types.PodInfo) bool {
// HostNetwork and HostUsers fields are immutable, so no update inference is needed.
return false
}
func (f *userNamespacesHostNetworkFeature) MaxVersion() *version.Version {
return nil
}

View file

@ -0,0 +1,101 @@
/*
Copyright 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.
*/
package usernamespaceshostnetwork
import (
"testing"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
type fakeFeatureGate struct {
features map[string]bool
}
func (m *fakeFeatureGate) Enabled(key string) bool {
return m.features[key]
}
func TestDiscoverFeature(t *testing.T) {
tests := []struct {
name string
featureGate bool
runtimeFeatures types.RuntimeFeatures
expected bool
}{
{
name: "feature gate disabled",
featureGate: false,
runtimeFeatures: types.RuntimeFeatures{
UserNamespacesHostNetwork: true,
},
expected: false,
},
{
name: "feature gate enabled but no runtime support",
featureGate: true,
runtimeFeatures: types.RuntimeFeatures{
UserNamespacesHostNetwork: false,
},
expected: false,
},
{
name: "feature gate enabled and runtime supports it",
featureGate: true,
runtimeFeatures: types.RuntimeFeatures{
UserNamespacesHostNetwork: true,
},
expected: true,
},
{
name: "runtime support is on",
featureGate: true,
runtimeFeatures: types.RuntimeFeatures{
UserNamespacesHostNetwork: true,
},
expected: true,
},
{
name: "runtime support is off",
featureGate: true,
runtimeFeatures: types.RuntimeFeatures{
UserNamespacesHostNetwork: false,
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockFG := &fakeFeatureGate{
features: map[string]bool{
UserNamespacesHostNetworkSupportFeatureGate: tt.featureGate,
},
}
cfg := &types.NodeConfiguration{
FeatureGates: mockFG,
RuntimeFeatures: tt.runtimeFeatures,
}
result := Feature.Discover(cfg)
if result != tt.expected {
t.Fatalf("Feature.Discover() = %v, want %v", result, tt.expected)
}
})
}
}

View file

@ -25,6 +25,7 @@ import (
type PodInfo = types.PodInfo
type Feature = types.Feature
type FeatureRequirements = types.FeatureRequirements
type RuntimeFeatures = types.RuntimeFeatures
type FeatureGate = types.FeatureGate
type StaticConfiguration = types.StaticConfiguration
type NodeConfiguration = types.NodeConfiguration

View file

@ -63,6 +63,9 @@ type Feature interface {
type FeatureRequirements struct {
// EnabledFeatureGates lists feature gate strings that the feature depends on.
EnabledFeatureGates []string
// RequiredRuntimeFeatures lists runtime capabilities that must be true for the
// feature to be declared. Nil means the feature has no runtime requirements.
RequiredRuntimeFeatures *RuntimeFeatures
}
// FeatureGate is an interface that abstracts feature gate checking.
@ -71,6 +74,12 @@ type FeatureGate interface {
Enabled(key string) bool
}
// RuntimeFeatures provides information about CRI runtime-level capabilities.
type RuntimeFeatures struct {
// UserNamespacesHostNetwork indicates if the runtime supports user namespaces with host network.
UserNamespacesHostNetwork bool
}
// StaticConfiguration provides a view of a node's static configuration required for feature discovery.
type StaticConfiguration struct {
// Add configuration fields here as required by registered features.
@ -85,6 +94,8 @@ type NodeConfiguration struct {
// Version holds the current node version. This is used for full semantic version comparisons
// with Feature.MaxVersion() to determine if a feature needs to be reported.
Version *version.Version
// RuntimeFeatures holds runtime-level capabilities discovered from CRI.
RuntimeFeatures RuntimeFeatures
}
// FeatureGateMap is provided as a convenience implementation of FeatureGate

File diff suppressed because it is too large Load diff

View file

@ -1872,6 +1872,9 @@ message RuntimeHandler {
message RuntimeFeatures {
// supplemental_groups_policy is set to true if the runtime supports SupplementalGroupsPolicy and ContainerUser.
bool supplemental_groups_policy = 1;
// user_namespaces_host_network is set to true if the runtime supports containers using both
// host network and user namespace simultaneously.
bool user_namespaces_host_network = 2;
}
message StatusResponse {

View file

@ -223,7 +223,7 @@
| UnauthenticatedHTTP2DOSMitigation | :ballot_box_with_check: 1.29+ | | | 1.25 | | | | [code](https://cs.k8s.io/?q=%5CbUnauthenticatedHTTP2DOSMitigation%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbUnauthenticatedHTTP2DOSMitigation%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| UnknownVersionInteroperabilityProxy | :ballot_box_with_check: 1.36+ | | 1.281.35 | 1.36 | | | APIServerIdentity | [code](https://cs.k8s.io/?q=%5CbUnknownVersionInteroperabilityProxy%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbUnknownVersionInteroperabilityProxy%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| UnlockWhileProcessingFIFO | :ballot_box_with_check: 1.36+ | | | 1.36 | | | | [code](https://cs.k8s.io/?q=%5CbUnlockWhileProcessingFIFO%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbUnlockWhileProcessingFIFO%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| UserNamespacesHostNetworkSupport | | | 1.35 | | | | UserNamespacesSupport | [code](https://cs.k8s.io/?q=%5CbUserNamespacesHostNetworkSupport%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbUserNamespacesHostNetworkSupport%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| UserNamespacesHostNetworkSupport | | | 1.35 | | | | NodeDeclaredFeatures<br>UserNamespacesSupport | [code](https://cs.k8s.io/?q=%5CbUserNamespacesHostNetworkSupport%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbUserNamespacesHostNetworkSupport%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| UserNamespacesSupport | :ballot_box_with_check:&nbsp;1.33+ | :closed_lock_with_key:&nbsp;1.36+ | 1.251.29 | 1.301.35 | 1.36 | | | [code](https://cs.k8s.io/?q=%5CbUserNamespacesSupport%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbUserNamespacesSupport%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| VolumeAttributesClass | :ballot_box_with_check:&nbsp;1.34+ | :closed_lock_with_key:&nbsp;1.36+ | 1.291.30 | 1.311.33 | 1.34 | | | [code](https://cs.k8s.io/?q=%5CbVolumeAttributesClass%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbVolumeAttributesClass%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| VolumeLimitScaling | | | 1.35 | | | | | [code](https://cs.k8s.io/?q=%5CbVolumeLimitScaling%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbVolumeLimitScaling%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |

View file

@ -28,7 +28,6 @@ import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/kubelet/events"
"k8s.io/kubernetes/pkg/kubelet/metrics"
"k8s.io/kubernetes/test/e2e/feature"
@ -126,81 +125,6 @@ var _ = SIGDescribe("Security Context", func() {
}
})
f.It("must create a user namespace and use host network when hostUsers is false and hostNetwork is true [LinuxOnly]", feature.UserNamespacesHostNetworkSupport, framework.WithFeatureGate(features.UserNamespacesHostNetworkSupport),
feature.UserNamespacesSupport, func(ctx context.Context) {
// with hostUsers=false the pod must use a new user namespace.
// with hostNetwork=true the pod must use the host network namespace.
podClient := e2epod.PodClientNS(f, f.Namespace.Name)
// Schedule pods on the same node to ensure they share the same host network namespace.
targetNode, err := findLinuxNode(ctx, f)
framework.ExpectNoError(err, "Error finding Linux node")
makePodForHostNetTest := func(nodeName string) *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "userns-hostnet-" + string(uuid.NewUUID()),
},
Spec: v1.PodSpec{
NodeName: nodeName,
Containers: []v1.Container{
{
Name: containerName,
Image: imageutils.GetE2EImage(imageutils.BusyBox),
Command: []string{"sh", "-c", "cat /proc/self/uid_map && readlink /proc/self/ns/net"},
},
},
RestartPolicy: v1.RestartPolicyNever,
HostUsers: ptr.To(false),
HostNetwork: true,
},
}
}
createdPod1 := podClient.Create(ctx, makePodForHostNetTest(targetNode.Name))
createdPod2 := podClient.Create(ctx, makePodForHostNetTest(targetNode.Name))
ginkgo.DeferCleanup(func(ctx context.Context) {
ginkgo.By("delete the pods")
podClient.DeleteSync(ctx, createdPod1.Name, metav1.DeleteOptions{}, f.Timeouts.PodDelete)
podClient.DeleteSync(ctx, createdPod2.Name, metav1.DeleteOptions{}, f.Timeouts.PodDelete)
})
getLogs := func(pod *v1.Pod) (string, string, error) {
err := e2epod.WaitForPodSuccessInNamespaceTimeout(ctx, f.ClientSet, pod.Name, f.Namespace.Name, f.Timeouts.PodStart)
if err != nil {
return "", "", err
}
podStatus, err := podClient.Get(ctx, pod.Name, metav1.GetOptions{})
if err != nil {
return "", "", err
}
logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, podStatus.Name, containerName)
if err != nil {
return "", "", err
}
parts := strings.Split(strings.TrimSpace(logs), "\n")
if len(parts) != 2 {
return "", "", fmt.Errorf("expected 2 lines of logs, got %d: %q", len(parts), logs)
}
return parts[0], parts[1], nil
}
uidMap1, netNs1, err := getLogs(createdPod1)
framework.ExpectNoError(err)
uidMap2, netNs2, err := getLogs(createdPod2)
framework.ExpectNoError(err)
// 65536 is the size used for a user namespace. Verify that the value is present
// in the /proc/self/uid_map file.
gomega.Expect(uidMap1).To(gomega.ContainSubstring("65536"), "user namespace not created for pod1")
gomega.Expect(uidMap2).To(gomega.ContainSubstring("65536"), "user namespace not created for pod2")
// Check they are in different user namespaces.
gomega.Expect(uidMap1).NotTo(gomega.Equal(uidMap2), "two different pods are running with the same user namespace configuration")
// Check they are in the same network namespace (the host's one).
gomega.Expect(netNs1).To(gomega.Equal(netNs2), "two different pods with hostNetwork=true should be in the same network namespace, but they are not. NetNS1: %s, NetNS2: %s", netNs1, netNs2)
})
f.It("must create the user namespace in the configured hostUID/hostGID range [LinuxOnly]", feature.UserNamespacesSupport, func(ctx context.Context) {
// We need to check with the binary "getsubuids" the mappings for the kubelet.
// If something is not present, we skip the test as the node wasn't configured to run this test.