feat(usecase): Introduce IPPR with static CPU policy support to use node declared features framework.

This commit is contained in:
Praveen Krishna
2025-10-28 17:08:55 +00:00
parent e7a42e8e8e
commit 540ffbd723
4 changed files with 336 additions and 1 deletions

View File

@@ -0,0 +1,94 @@
/*
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 kubelet
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
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"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/kubelet/cm"
)
func TestGuaranteedPodExclusiveCPUsFeatureDiscovery(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeDeclaredFeatures, true)
testcases := []struct {
name string
cpuManagerPolicy string
inplacePodResizeExclusiveCPUsEnable bool
expectedFeature string
expectFeature bool
}{
{
name: "feature enabled with static cpu manager policy",
cpuManagerPolicy: "static",
inplacePodResizeExclusiveCPUsEnable: true,
expectedFeature: "GuaranteedQoSPodCPUResize",
expectFeature: true,
},
{
name: "feature enabled with none cpu manager policy",
cpuManagerPolicy: "none",
inplacePodResizeExclusiveCPUsEnable: true,
expectedFeature: "GuaranteedQoSPodCPUResize",
expectFeature: true,
},
{
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,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScalingExclusiveCPUs, tc.inplacePodResizeExclusiveCPUsEnable)
testKubelet := newTestKubelet(t, false /* controllerAttachDetachEnabled */)
defer testKubelet.Cleanup()
kubelet := testKubelet.kubelet
fakeCM := cm.NewFakeContainerManagerWithNodeConfig(cm.NodeConfig{
CPUManagerPolicy: tc.cpuManagerPolicy,
})
kubelet.containerManager = fakeCM
framework, err := ndflib.New(ndffeatures.AllFeatures)
require.NoError(t, err)
kubelet.nodeDeclaredFeaturesFramework = framework
features := kubelet.discoverNodeDeclaredFeatures()
if tc.expectFeature {
assert.Contains(t, features, tc.expectedFeature)
} else {
assert.NotContains(t, features, tc.expectedFeature)
}
})
}
}

View File

@@ -0,0 +1,85 @@
/*
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"
)
// Feature is the implementation of the `GuaranteedQoSPodCPUResize` feature.
var Feature = &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

@@ -0,0 +1,153 @@
/*
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"
"github.com/stretchr/testify/assert"
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.EXPECT().Enabled(IPPRExclusiveCPUsFeatureGate).Return(tc.gateEnabled)
config := &nodedeclaredfeatures.NodeConfiguration{
FeatureGates: mockFG,
StaticConfig: nodedeclaredfeatures.StaticConfiguration{CPUManagerPolicy: tc.cpuManagerPolicy},
}
enabled := feature.Discover(config)
assert.Equal(t, tc.expected, enabled)
})
}
}
func TestGuaranteedQoSPodCPUResizeFeature_InferForScheduling(t *testing.T) {
feature := &guaranteedQoSPodCPUResizeFeature{}
assert.False(t, feature.InferForScheduling(&nodedeclaredfeatures.PodInfo{Spec: &v1.PodSpec{}, Status: &v1.PodStatus{}}), "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}
assert.Equal(t, tc.expected, feature.InferForUpdate(oldPodInfo, newPodInfo))
})
}
}

View File

@@ -18,9 +18,12 @@ package features
import (
"k8s.io/component-helpers/nodedeclaredfeatures"
"k8s.io/component-helpers/nodedeclaredfeatures/features/inplacepodresize"
)
// AllFeatures is the central registry for all declared features.
// New features are added to this list to be automatically included in both
// discovery and inference logic.
var AllFeatures = []nodedeclaredfeatures.Feature{}
var AllFeatures = []nodedeclaredfeatures.Feature{
inplacepodresize.Feature,
}