mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 22:40:35 +00:00
feat(usecase): Introduce IPPR with static CPU policy support to use node declared features framework.
This commit is contained in:
94
pkg/kubelet/kubelet_node_declared_features_test.go
Normal file
94
pkg/kubelet/kubelet_node_declared_features_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user