Merge pull request #136759 from pravk03/ndf-guaranteedQos-fix

Remove `GuaranteedQoSPodCPUResize` declared feature
This commit is contained in:
Kubernetes Prow Robot
2026-03-06 02:00:18 +05:30
committed by GitHub
8 changed files with 68 additions and 317 deletions

View File

@@ -36,11 +36,9 @@ 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 {
staticConfig := nodedeclaredfeatures.StaticConfiguration{
CPUManagerPolicy: kl.containerManager.GetNodeConfig().CPUManagerPolicy,
}
adaptedFG := FeatureGateAdapter{FeatureGate: utilfeature.DefaultFeatureGate}
// Add fields from node configuration fields as needed by registered features.
staticConfig := nodedeclaredfeatures.StaticConfiguration{}
cfg := &nodedeclaredfeatures.NodeConfiguration{
FeatureGates: adaptedFG,
StaticConfig: staticConfig,

View File

@@ -21,73 +21,82 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/version"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
ndflib "k8s.io/component-helpers/nodedeclaredfeatures"
ndffeatures "k8s.io/component-helpers/nodedeclaredfeatures/features"
ndf "k8s.io/component-helpers/nodedeclaredfeatures"
ndftesting "k8s.io/component-helpers/nodedeclaredfeatures/testing"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/kubelet/cm"
)
func TestGuaranteedPodExclusiveCPUsFeatureDiscovery(t *testing.T) {
func TestDeclaredFeatureDiscovery(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeDeclaredFeatures, true)
podLevelResourcesIPPRFeatureGate := features.InPlacePodLevelResourcesVerticalScaling
featureMaxVersion := version.MustParseSemantic("v1.36.0")
createMockFeature := func(t *testing.T, name string, cfg *ndf.NodeConfiguration) *ndftesting.MockFeature {
mockFeature := ndftesting.NewMockFeature(t)
mockFeature.SetName(name)
mockFeature.SetMaxVersion(featureMaxVersion)
mockFeature.SetDiscover(func(*ndf.NodeConfiguration) bool { return cfg.FeatureGates.Enabled(name) })
return mockFeature
}
testcases := []struct {
name string
cpuManagerPolicy string
inplacePodResizeExclusiveCPUsEnable bool
expectedFeature string
expectFeature bool
name string
podLevelResourcesIPPREnabled bool
featureDiscovered bool
kubeletVersion *version.Version
}{
{
name: "feature enabled with static cpu manager policy",
cpuManagerPolicy: "static",
inplacePodResizeExclusiveCPUsEnable: true,
expectedFeature: "GuaranteedQoSPodCPUResize",
expectFeature: true,
name: "feature gate enabled, feature discovered",
podLevelResourcesIPPREnabled: true,
featureDiscovered: true,
kubeletVersion: featureMaxVersion.SubtractMinor(1),
},
{
name: "feature enabled with none cpu manager policy",
cpuManagerPolicy: "none",
inplacePodResizeExclusiveCPUsEnable: true,
expectedFeature: "GuaranteedQoSPodCPUResize",
expectFeature: true,
name: "feature gate enabled, feature not discovered as kubelet version higher than max version",
podLevelResourcesIPPREnabled: true,
featureDiscovered: false,
kubeletVersion: featureMaxVersion.AddMinor(1),
},
{
name: "feature disabled with static cpu manager policy",
cpuManagerPolicy: "static",
inplacePodResizeExclusiveCPUsEnable: false,
expectedFeature: "GuaranteedQoSPodCPUResize",
expectFeature: false,
},
{
name: "feature disabled with none cpu manager policy",
cpuManagerPolicy: "none",
inplacePodResizeExclusiveCPUsEnable: false,
expectedFeature: "GuaranteedQoSPodCPUResize",
expectFeature: true,
name: "feature disabled and not discovered",
podLevelResourcesIPPREnabled: false,
featureDiscovered: false,
kubeletVersion: featureMaxVersion.SubtractMinor(1),
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScalingExclusiveCPUs, tc.inplacePodResizeExclusiveCPUsEnable)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, podLevelResourcesIPPRFeatureGate, tc.podLevelResourcesIPPREnabled)
cfg := &ndf.NodeConfiguration{
FeatureGates: FeatureGateAdapter{
FeatureGate: utilfeature.DefaultFeatureGate,
},
Version: tc.kubeletVersion,
}
registeredFeatures := []ndf.Feature{
createMockFeature(t, string(podLevelResourcesIPPRFeatureGate), cfg),
}
testKubelet := newTestKubelet(t, false /* controllerAttachDetachEnabled */)
defer testKubelet.Cleanup()
kubelet := testKubelet.kubelet
fakeCM := cm.NewFakeContainerManagerWithNodeConfig(cm.NodeConfig{
CPUManagerPolicy: tc.cpuManagerPolicy,
})
fakeCM := cm.NewFakeContainerManagerWithNodeConfig(cm.NodeConfig{})
kubelet.containerManager = fakeCM
framework, err := ndflib.New(ndffeatures.AllFeatures)
framework, err := ndf.New(registeredFeatures)
require.NoError(t, err)
kubelet.nodeDeclaredFeaturesFramework = framework
kubelet.version = tc.kubeletVersion
features := kubelet.discoverNodeDeclaredFeatures()
if tc.expectFeature {
assert.Contains(t, features, tc.expectedFeature)
if tc.featureDiscovered {
assert.Contains(t, features, string(podLevelResourcesIPPRFeatureGate))
} else {
assert.NotContains(t, features, tc.expectedFeature)
assert.NotContains(t, features, string(podLevelResourcesIPPRFeatureGate))
}
})
}

View File

@@ -1,85 +0,0 @@
/*
Copyright 2025 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 inplacepodresize
import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-helpers/nodedeclaredfeatures"
)
// Ensure the feature struct implements the unified Feature interface.
var _ nodedeclaredfeatures.Feature = &guaranteedQoSPodCPUResizeFeature{}
const (
// IPPRExclusiveCPUsFeatureGate is the feature gate for IPPRExclusiveCPUsFeatureGate.
IPPRExclusiveCPUsFeatureGate = "InPlacePodVerticalScalingExclusiveCPUs"
// CPUManagerPolicyStatic is the value for the static CPUManagerPolicy.
CPUManagerPolicyStatic = "static"
// CPUManagerPolicyNone is the value for the none CPUManagerPolicy.
CPUManagerPolicyNone = "none"
// GuaranteedQoSPodCPUResize is a declared feature that indicates a node supports in-place pod resize for guaranteed QoS pods with exclusive CPUs.
GuaranteedQoSPodCPUResize = "GuaranteedQoSPodCPUResize"
)
// GuaranteedQoSPodCPUResizeFeature is the implementation of the `GuaranteedQoSPodCPUResize` feature.
var GuaranteedQoSPodCPUResizeFeature = &guaranteedQoSPodCPUResizeFeature{}
type guaranteedQoSPodCPUResizeFeature struct{}
func (f *guaranteedQoSPodCPUResizeFeature) Name() string {
return GuaranteedQoSPodCPUResize
}
func (f *guaranteedQoSPodCPUResizeFeature) Discover(cfg *nodedeclaredfeatures.NodeConfiguration) bool {
featureGateEnabled := cfg.FeatureGates.Enabled(IPPRExclusiveCPUsFeatureGate)
cpuManagerPolicy := cfg.StaticConfig.CPUManagerPolicy
if (featureGateEnabled && cpuManagerPolicy == CPUManagerPolicyStatic) || (cpuManagerPolicy == CPUManagerPolicyNone) {
return true
}
return false
}
func (f *guaranteedQoSPodCPUResizeFeature) InferForScheduling(podInfo *nodedeclaredfeatures.PodInfo) bool {
// This feature is only relevant for pod updates (resizes).
return false
}
func (f *guaranteedQoSPodCPUResizeFeature) InferForUpdate(oldPodInfo, newPodInfo *nodedeclaredfeatures.PodInfo) bool {
// Since this is an update, the QOS class must already be se ans must be the same for old and new pod spec.
if oldPodInfo.Status != nil && oldPodInfo.Status.QOSClass != v1.PodQOSGuaranteed {
return false
}
oldPodSpec := oldPodInfo.Spec
newPodSpec := newPodInfo.Spec
// Check if CPU request is changing for any container.
for i := range oldPodSpec.Containers {
oldCPU := oldPodSpec.Containers[i].Resources.Requests.Cpu()
newCPU := newPodSpec.Containers[i].Resources.Requests.Cpu()
if oldCPU != nil && newCPU != nil && !oldCPU.Equal(*newCPU) {
return true
}
}
return false
}
func (f *guaranteedQoSPodCPUResizeFeature) MaxVersion() *version.Version {
return nil
}

View File

@@ -1,158 +0,0 @@
/*
Copyright 2025 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 inplacepodresize
import (
"testing"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/component-helpers/nodedeclaredfeatures"
test "k8s.io/component-helpers/nodedeclaredfeatures/testing"
)
func TestGuaranteedQoSPodCPUResizeFeature_Discover(t *testing.T) {
testCases := []struct {
name string
cpuManagerPolicy string
gateEnabled bool
expected bool
}{
{
name: "feature gate enabled, static policy",
cpuManagerPolicy: CPUManagerPolicyStatic,
gateEnabled: true,
expected: true,
},
{
name: "feature gate disabled, static policy",
cpuManagerPolicy: CPUManagerPolicyStatic,
gateEnabled: false,
expected: false,
},
{
name: "none policy",
cpuManagerPolicy: CPUManagerPolicyNone,
gateEnabled: true,
expected: true,
},
{
name: "feature gate missing",
cpuManagerPolicy: CPUManagerPolicyStatic,
gateEnabled: false, // Effectively disabled if not found
expected: false,
},
}
feature := &guaranteedQoSPodCPUResizeFeature{}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockFG := test.NewMockFeatureGate(t)
mockFG.SetEnabled(IPPRExclusiveCPUsFeatureGate, tc.gateEnabled)
config := &nodedeclaredfeatures.NodeConfiguration{
FeatureGates: mockFG,
StaticConfig: nodedeclaredfeatures.StaticConfiguration{CPUManagerPolicy: tc.cpuManagerPolicy},
}
enabled := feature.Discover(config)
if tc.expected != enabled {
t.Fatalf("expected %v, got %v", tc.expected, enabled)
}
})
}
}
func TestGuaranteedQoSPodCPUResizeFeature_InferForScheduling(t *testing.T) {
feature := &guaranteedQoSPodCPUResizeFeature{}
if feature.InferForScheduling(&nodedeclaredfeatures.PodInfo{Spec: &v1.PodSpec{}, Status: &v1.PodStatus{}}) {
t.Fatalf("InferForScheduling should always be false")
}
}
func TestGuaranteedQoSPodCPUResizeFeature_InferFromUpdate(t *testing.T) {
basePod := func() *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "test-pod"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "c1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1")},
Limits: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1")},
},
},
},
},
Status: v1.PodStatus{
QOSClass: v1.PodQOSGuaranteed,
},
}
}
testCases := []struct {
name string
oldPod *v1.Pod
newPod *v1.Pod
expected bool
}{
{
name: "not guaranteed QOS",
oldPod: func() *v1.Pod { p := basePod(); p.Status.QOSClass = v1.PodQOSBurstable; return p }(),
newPod: func() *v1.Pod { p := basePod(); p.Status.QOSClass = v1.PodQOSBurstable; return p }(),
expected: false,
},
{
name: "no CPU request change",
oldPod: basePod(),
newPod: basePod(),
expected: false,
},
{
name: "CPU request changed",
oldPod: basePod(),
newPod: func() *v1.Pod {
p := basePod()
p.Spec.Containers[0].Resources.Requests[v1.ResourceCPU] = resource.MustParse("2")
return p
}(),
expected: true,
},
{
name: "CPU limit changed, but not request",
oldPod: basePod(),
newPod: func() *v1.Pod {
p := basePod()
p.Spec.Containers[0].Resources.Limits[v1.ResourceCPU] = resource.MustParse("2")
return p
}(),
expected: false, // Only request changes matter for this feature
},
}
feature := &guaranteedQoSPodCPUResizeFeature{}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
oldPodInfo := &nodedeclaredfeatures.PodInfo{Spec: &tc.oldPod.Spec, Status: &tc.oldPod.Status}
newPodInfo := &nodedeclaredfeatures.PodInfo{Spec: &tc.newPod.Spec, Status: &tc.newPod.Status}
if want, got := tc.expected, feature.InferForUpdate(oldPodInfo, newPodInfo); want != got {
t.Fatalf("want=%v,got=%v", want, got)
}
})
}
}

View File

@@ -27,6 +27,5 @@ import (
// discovery and inference logic.
var AllFeatures = []nodedeclaredfeatures.Feature{
restartallcontainers.Feature,
inplacepodresize.GuaranteedQoSPodCPUResizeFeature,
inplacepodresize.PodLevelResourcesResizeFeature,
}

View File

@@ -84,9 +84,9 @@ func TestDiscoverNodeFeatures(t *testing.T) {
maxVersion: featureMaxVersion,
},
&mockFeature{
name: "FeatureBWithStaticConfig",
name: "FeatureB",
discover: func(cfg *NodeConfiguration) bool {
return cfg.FeatureGates.Enabled("feature-b") && cfg.StaticConfig.CPUManagerPolicy == "static"
return cfg.FeatureGates.Enabled("feature-b")
},
maxVersion: featureMaxVersion,
},
@@ -114,18 +114,16 @@ func TestDiscoverNodeFeatures(t *testing.T) {
string("feature-a"): true,
string("feature-b"): true,
}),
StaticConfig: StaticConfiguration{CPUManagerPolicy: "static"},
},
expected: []string{"FeatureA", "FeatureBWithStaticConfig"}, // Should be sorted
expected: []string{"FeatureA", "FeatureB"}, // Should be sorted
},
{
name: "no features enabled",
config: &NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{
string("feature-a"): false,
string("feature-b"): true,
string("feature-b"): false,
}),
StaticConfig: StaticConfiguration{CPUManagerPolicy: "none"},
},
expected: []string{},
},

View File

@@ -62,10 +62,9 @@ type FeatureGate interface {
Enabled(key string) bool
}
// StaticConfiguration provides a view of a node's static configuration.
// StaticConfiguration provides a view of a node's static configuration required for feature discovery.
type StaticConfiguration struct {
// Kubelet's CPU Manager policy
CPUManagerPolicy string
// Add configuration fields here as required by registered features.
}
// NodeConfiguration provides a generic view of a node's static configuration.

View File

@@ -1503,8 +1503,9 @@ func TestRelaxedDNSSearchValidation(t *testing.T) {
func TestNodeDeclaredFeatureAdmission(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.NodeDeclaredFeatures: true,
features.InPlacePodVerticalScaling: true,
features.NodeDeclaredFeatures: true,
features.PodLevelResources: true,
features.InPlacePodLevelResourcesVerticalScaling: true,
})
server := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
defer server.TearDownFn()
@@ -1519,30 +1520,20 @@ func TestNodeDeclaredFeatureAdmission(t *testing.T) {
},
Spec: v1.PodSpec{
NodeName: nodeName,
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1"), v1.ResourceMemory: resource.MustParse("1Gi")},
Limits: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1"), v1.ResourceMemory: resource.MustParse("1Gi")},
},
Containers: []v1.Container{
{
Name: "test-container",
Image: "fakeimage",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1"), v1.ResourceMemory: resource.MustParse("1Gi")},
Limits: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1"), v1.ResourceMemory: resource.MustParse("1Gi")},
},
},
},
RestartPolicy: v1.RestartPolicyAlways,
},
Status: v1.PodStatus{
Phase: v1.PodRunning,
ContainerStatuses: []v1.ContainerStatus{
{
Name: "test-container",
Ready: true,
AllocatedResources: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("1"),
v1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
}
@@ -1558,19 +1549,19 @@ func TestNodeDeclaredFeatureAdmission(t *testing.T) {
nodeDeclaredFeatures: []string{"SomeOtherFeature"},
nodeVersion: "1.35.0",
podUpdateFn: func(pod *v1.Pod) {
pod.Spec.Containers[0].Resources.Requests[v1.ResourceCPU] = resource.MustParse("2")
pod.Spec.Containers[0].Resources.Limits[v1.ResourceCPU] = resource.MustParse("2")
pod.Spec.Resources.Requests[v1.ResourceCPU] = resource.MustParse("2")
pod.Spec.Resources.Limits[v1.ResourceCPU] = resource.MustParse("2")
},
expectError: "pod update requires features GuaranteedQoSPodCPUResize which are not available on node",
expectError: "pod update requires features InPlacePodLevelResourcesVerticalScaling which are not available on node",
},
{
name: "admission succeeds when required feature is declared on node",
nodeDeclaredFeatures: []string{ipprfeature.GuaranteedQoSPodCPUResize},
nodeDeclaredFeatures: []string{ipprfeature.PodLevelResourcesResizeFeature.Name()},
nodeVersion: "1.35.0",
podUpdateFn: func(pod *v1.Pod) {
pod.Spec.Containers[0].Resources.Requests[v1.ResourceCPU] = resource.MustParse("2")
pod.Spec.Containers[0].Resources.Limits[v1.ResourceCPU] = resource.MustParse("2")
pod.Spec.Resources.Requests[v1.ResourceCPU] = resource.MustParse("2")
pod.Spec.Resources.Limits[v1.ResourceCPU] = resource.MustParse("2")
},
expectError: "",
},