feat: Add common library for declared feature discovery, inference and matching.

This commit is contained in:
Praveen Krishna
2025-09-19 18:07:28 +00:00
parent c9327d00b1
commit e1f181e363
7 changed files with 1137 additions and 0 deletions

View File

@@ -33,6 +33,7 @@ require (
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect

View File

@@ -0,0 +1,13 @@
---
dir: testing
pkgname: testing
template: testify
template-data:
boilerplate-file: ../../../../../hack/boilerplate/boilerplate.generatego.txt
packages:
k8s.io/component-helpers/nodedeclaredfeatures:
config:
filename: mocks.go # New file name
interfaces:
Feature: {}
FeatureGate: {}

View File

@@ -0,0 +1,26 @@
/*
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 features
import (
"k8s.io/component-helpers/nodedeclaredfeatures"
)
// 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{}

View File

@@ -0,0 +1,158 @@
/*
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 nodedeclaredfeatures
import (
"fmt"
"slices"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version"
)
// Framework provides functions for discovering node features and inferring pod feature requirements.
// It is stateful and holds the feature registry.
type Framework struct {
registry []Feature
}
// FeatureSet is a set of node features.
type FeatureSet struct {
sets.Set[string]
}
// NewFeatureSet creates a FeatureSet from a list of feature names.
func NewFeatureSet(features ...string) FeatureSet {
return FeatureSet{Set: sets.New(features...)}
}
// Equal returns true if both the sets have the same features.
func (s *FeatureSet) Equal(other FeatureSet) bool {
return s.Set.Equal(other.Set)
}
// Clone returns a copy of the FeatureSet.
func (s *FeatureSet) Clone() FeatureSet {
if s.Set == nil {
return FeatureSet{Set: nil}
}
return FeatureSet{Set: s.Set.Clone()}
}
// New creates a new instance of the Framework.
func New(registry []Feature) (*Framework, error) {
if registry == nil {
return nil, fmt.Errorf("registry must not be nil")
}
return &Framework{
registry: registry,
}, nil
}
// DiscoverNodeFeatures determines which features from the registry are enabled
// for a specific node configuration. It returns a sorted, unique list of feature names.
func (f *Framework) DiscoverNodeFeatures(cfg *NodeConfiguration) []string {
var enabledFeatures []string
for _, f := range f.registry {
if f.Discover(cfg) {
if cfg.Version != nil && f.MaxVersion() != nil && cfg.Version.GreaterThan(f.MaxVersion()) {
continue
}
enabledFeatures = append(enabledFeatures, f.Name())
}
}
slices.Sort(enabledFeatures)
return enabledFeatures
}
// InferForPodScheduling determines which features from the registry are required by a pod scheduling for a given target version.
func (f *Framework) InferForPodScheduling(podInfo *PodInfo, targetVersion *version.Version) (FeatureSet, error) {
if targetVersion == nil {
return FeatureSet{}, fmt.Errorf("target version cannot be nil")
}
reqs := NewFeatureSet()
for _, f := range f.registry {
if f.MaxVersion() != nil && targetVersion.GreaterThan(f.MaxVersion()) {
// If target version is greater than the feature's max version, no need to require the feature
continue
}
if f.InferForScheduling(podInfo) {
reqs.Insert(f.Name())
}
}
return reqs, nil
}
// InferForPodUpdate determines which features are required by a pod update operation for a given target version.
func (f *Framework) InferForPodUpdate(oldPodInfo, newPodInfo *PodInfo, targetVersion *version.Version) (FeatureSet, error) {
if targetVersion == nil {
return FeatureSet{}, fmt.Errorf("target version cannot be nil")
}
reqs := NewFeatureSet()
for _, f := range f.registry {
if f.MaxVersion() != nil && targetVersion.GreaterThan(f.MaxVersion()) {
// If target version is greater than the feature's max version, no need to require the feature
continue
}
if f.InferForUpdate(oldPodInfo, newPodInfo) {
reqs.Insert(f.Name())
}
}
return reqs, nil
}
// MatchResult encapsulates the result of a feature match check.
type MatchResult struct {
// IsMatch is true if the node satisfies all feature requirements.
IsMatch bool
// UnsatisfiedRequirements lists the specific features that were not met.
// This field is only populated if IsMatch is false.
UnsatisfiedRequirements []string
}
// MatchNode checks if a node's declared features satisfy the pod's pre-computed requirements.
// It returns a MatchResult:
// - IsMatch is true if all requiredFeatures are present in node.status.declaredFeatures.
// - UnsatisfiedRequirements lists features in requiredFeatures but not in node.status.declaredFeatures.
func MatchNode(requiredFeatures FeatureSet, node *v1.Node) (*MatchResult, error) {
if node == nil {
return nil, fmt.Errorf("node cannot be nil")
}
return MatchNodeFeatureSet(requiredFeatures, NewFeatureSet(node.Status.DeclaredFeatures...))
}
// MatchNodeFeatureSet compares a set of required features against a set of features present on a node.
// It returns a MatchResult:
// - IsMatch is true if all requiredFeatures are present in nodeFeatures.
// - UnsatisfiedRequirements lists features in requiredFeatures but not in nodeFeatures.
func MatchNodeFeatureSet(requiredFeatures FeatureSet, nodeFeatures FeatureSet) (*MatchResult, error) {
if requiredFeatures.Len() == 0 {
return &MatchResult{IsMatch: true}, nil // No requirements to match.
}
var mismatched []string
for req := range requiredFeatures.Set {
if !nodeFeatures.Has(req) {
mismatched = append(mismatched, req)
continue
}
}
if len(mismatched) > 0 {
return &MatchResult{IsMatch: false, UnsatisfiedRequirements: mismatched}, nil
}
return &MatchResult{IsMatch: true}, nil
}

View File

@@ -0,0 +1,477 @@
/*
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 nodedeclaredfeatures
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/version"
)
// mockFeature is a mock implementation of the Feature interface for testing.
type mockFeature struct {
name string
discover func(cfg *NodeConfiguration) bool
inferForScheduling func(podInfo *PodInfo) bool
inferForUpdate func(oldPodInfo, newPodInfo *PodInfo) bool
maxVersion *version.Version
}
func (f *mockFeature) Name() string { return f.name }
func (f *mockFeature) Discover(cfg *NodeConfiguration) bool { return f.discover(cfg) }
func (f *mockFeature) InferForScheduling(podInfo *PodInfo) bool { return f.inferForScheduling(podInfo) }
func (f *mockFeature) InferForUpdate(oldPodInfo, newPodInfo *PodInfo) bool {
return f.inferForUpdate(oldPodInfo, newPodInfo)
}
func (f *mockFeature) MaxVersion() *version.Version { return f.maxVersion }
type mockFeatureGate struct {
features map[string]bool
}
func (m *mockFeatureGate) Enabled(key string) bool {
if m.features == nil {
return false
}
return m.features[key]
}
func newMockFeatureGate(features map[string]bool) *mockFeatureGate {
return &mockFeatureGate{features: features}
}
func TestNewFramework(t *testing.T) {
_, err := New(nil)
require.Error(t, err, "NewFramework should return an error with a nil registry")
_, err = New([]Feature{})
require.NoError(t, err, "NewFramework should not return an error with an empty registry")
}
func TestDiscoverNodeFeatures(t *testing.T) {
featureMaxVersion := version.MustParse("1.38.0")
registry := []Feature{
&mockFeature{
name: "FeatureA",
discover: func(cfg *NodeConfiguration) bool {
return cfg.FeatureGates.Enabled("feature-a")
},
maxVersion: featureMaxVersion,
},
&mockFeature{
name: "FeatureBWithStaticConfig",
discover: func(cfg *NodeConfiguration) bool {
return cfg.FeatureGates.Enabled("feature-b") && cfg.StaticConfig.CPUManagerPolicy == "static"
},
maxVersion: featureMaxVersion,
},
}
framework, _ := New(registry)
testCases := []struct {
name string
config *NodeConfiguration
expected []string
}{
{
name: "Feature Enabled",
config: &NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{string("feature-a"): true}),
StaticConfig: StaticConfiguration{},
},
expected: []string{"FeatureA"},
},
{
name: "multiple features enabled",
config: &NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{
string("feature-a"): true,
string("feature-b"): true,
}),
StaticConfig: StaticConfiguration{CPUManagerPolicy: "static"},
},
expected: []string{"FeatureA", "FeatureBWithStaticConfig"}, // Should be sorted
},
{
name: "no features enabled",
config: &NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{
string("feature-a"): false,
string("feature-b"): true,
}),
StaticConfig: StaticConfiguration{CPUManagerPolicy: "none"},
},
expected: []string{},
},
{
name: "feature past max version",
config: &NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{string("feature-a"): true}),
StaticConfig: StaticConfiguration{},
Version: featureMaxVersion.AddMinor(1),
},
expected: []string{}, // Not published
},
{
name: "feature past max version - pre-release version",
config: &NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{string("feature-a"): true}),
StaticConfig: StaticConfiguration{},
Version: version.MustParse("1.39.0-alpha.2.39+049eafd34dfbd2"),
},
expected: []string{}, // Not published
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
features := framework.DiscoverNodeFeatures(tc.config)
if len(tc.expected) == 0 {
assert.Empty(t, features)
} else {
assert.Equal(t, tc.expected, features)
}
})
}
}
func TestInferForPodScheduling(t *testing.T) {
inferPodlevelResources := func(p *PodInfo) bool {
return p.Spec.Resources != nil
}
podWithPodLevelResources := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-with-podlevelresources"},
Spec: v1.PodSpec{
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("500m"),
},
},
},
}
podWithoutPodLevelResources := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-without-podlevelresources"},
Spec: v1.PodSpec{},
}
testCases := []struct {
name string
registry []Feature
newPod *v1.Pod
targetVersion *version.Version
expectedReqs FeatureSet
expectErr bool
errContains string
}{
{
name: "pod with feature, inferred during scheduling",
registry: []Feature{
&mockFeature{
name: "PodLevelResources",
inferForScheduling: inferPodlevelResources,
},
},
newPod: podWithPodLevelResources,
targetVersion: version.MustParse("1.30.0"),
expectedReqs: NewFeatureSet("PodLevelResources"),
expectErr: false,
},
{
name: "pod without feature",
registry: []Feature{
&mockFeature{
name: "PodLevelResources",
inferForScheduling: inferPodlevelResources,
},
},
newPod: podWithoutPodLevelResources,
targetVersion: version.MustParse("1.30.0"),
expectedReqs: NewFeatureSet(),
expectErr: false,
},
{
name: "feature universally available, not inferred during create",
registry: []Feature{
&mockFeature{
name: "PodLevelResources",
inferForScheduling: inferPodlevelResources,
maxVersion: version.MustParse("1.30.0"),
},
},
newPod: podWithPodLevelResources,
targetVersion: version.MustParse("1.31.0"),
expectedReqs: NewFeatureSet(),
expectErr: false,
},
{
name: "pre-release target version",
registry: []Feature{
&mockFeature{
name: "PodLevelResources",
inferForScheduling: inferPodlevelResources,
maxVersion: version.MustParse("1.30.0"),
},
},
newPod: podWithPodLevelResources,
targetVersion: version.MustParse("0.0.0-alpha.2.39+049eafd34dfbd2"),
expectedReqs: NewFeatureSet("PodLevelResources"),
expectErr: false,
},
{
name: "target version nil",
registry: []Feature{
&mockFeature{
name: "PodLevelResources",
inferForScheduling: inferPodlevelResources,
},
},
newPod: podWithPodLevelResources,
targetVersion: nil,
expectedReqs: NewFeatureSet(),
expectErr: true,
errContains: "target version cannot be nil",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
framework, _ := New(tc.registry)
reqs, err := framework.InferForPodScheduling(&PodInfo{Spec: &tc.newPod.Spec, Status: &tc.newPod.Status}, tc.targetVersion)
if tc.expectErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errContains)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedReqs, reqs)
}
})
}
}
func TestInferForPodUpdate(t *testing.T) {
inferResize := func(oldPodInfo, newPodInfo *PodInfo) bool {
oldCPU := oldPodInfo.Spec.Containers[0].Resources.Requests.Cpu()
newCPU := newPodInfo.Spec.Containers[0].Resources.Requests.Cpu()
if oldCPU != nil && newCPU != nil && !oldCPU.Equal(*newCPU) {
return true
}
return false
}
basePod := func(cpuRequest string) *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(cpuRequest)},
},
},
},
},
}
}
podWith1CPU := basePod("1")
podWith2CPU := basePod("2")
testCases := []struct {
name string
registry []Feature
oldPod *v1.Pod
newPod *v1.Pod
targetVersion *version.Version
expectedReqs FeatureSet
expectErr bool
errContains string
}{
{
name: "pod update requires feature",
registry: []Feature{
&mockFeature{
name: "InPlacePodResize",
inferForUpdate: inferResize,
},
},
oldPod: podWith1CPU,
newPod: podWith2CPU,
targetVersion: version.MustParse("1.30.0"),
expectedReqs: NewFeatureSet("InPlacePodResize"),
expectErr: false,
},
{
name: "pod update requires feature with pre-release version",
registry: []Feature{
&mockFeature{
name: "InPlacePodResize",
inferForUpdate: inferResize,
},
},
oldPod: podWith1CPU,
newPod: podWith2CPU,
targetVersion: version.MustParse("1.35.0-alpha.2.39+049eafd34dfbd2"),
expectedReqs: NewFeatureSet("InPlacePodResize"),
expectErr: false,
},
{
name: "pod update does not require feature",
registry: []Feature{
&mockFeature{
name: "InPlacePodResize",
inferForUpdate: inferResize,
},
},
oldPod: podWith1CPU,
newPod: podWith1CPU,
targetVersion: version.MustParse("1.30.0"),
expectedReqs: NewFeatureSet(),
expectErr: false,
},
{
name: "feature universally available, not inferred during update",
registry: []Feature{
&mockFeature{
name: "InPlacePodResize",
inferForUpdate: inferResize,
maxVersion: version.MustParse("1.30.0"),
},
},
oldPod: podWith1CPU,
newPod: podWith2CPU,
targetVersion: version.MustParse("1.31.0"),
expectedReqs: NewFeatureSet(),
expectErr: false,
},
{
name: "target version nil",
registry: []Feature{
&mockFeature{
name: "InPlacePodResize",
inferForUpdate: inferResize,
},
},
oldPod: podWith1CPU,
newPod: podWith2CPU,
targetVersion: nil,
expectedReqs: NewFeatureSet(),
expectErr: true,
errContains: "target version cannot be nil",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
framework, _ := New(tc.registry)
reqs, err := framework.InferForPodUpdate(&PodInfo{Spec: &tc.oldPod.Spec, Status: &tc.oldPod.Status}, &PodInfo{Spec: &tc.newPod.Spec, Status: &tc.newPod.Status}, tc.targetVersion)
if tc.expectErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errContains)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedReqs, reqs)
}
})
}
}
func TestMatchNode(t *testing.T) {
testCases := []struct {
name string
podFeatureRequirements FeatureSet
nodeFeatures []string
expectedMatch bool
expectedUnsatisfied []string
}{
{
name: "all features match",
podFeatureRequirements: NewFeatureSet("feature-a", "feature-b"),
nodeFeatures: []string{"feature-a", "feature-b", "feature-c"},
expectedMatch: true,
expectedUnsatisfied: nil,
},
{
name: "some features missing",
podFeatureRequirements: NewFeatureSet("feature-a", "feature-b"),
nodeFeatures: []string{"feature-a", "feature-c"},
expectedMatch: false,
expectedUnsatisfied: []string{"feature-b"},
},
{
name: "all features missing",
podFeatureRequirements: NewFeatureSet("feature-a", "feature-b"),
nodeFeatures: []string{"feature-c"},
expectedMatch: false,
expectedUnsatisfied: []string{"feature-a", "feature-b"},
},
{
name: "no node features",
podFeatureRequirements: NewFeatureSet("feature-a", "feature-b"),
nodeFeatures: []string{},
expectedMatch: false,
expectedUnsatisfied: []string{"feature-a", "feature-b"},
},
{
name: "no requirements",
podFeatureRequirements: NewFeatureSet(),
nodeFeatures: []string{"feature-a", "feature-b", "feature-c"},
expectedMatch: true,
expectedUnsatisfied: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchVariations := []string{"MatchNode", "MatchNodeFeatureSet"}
for _, variationName := range matchVariations {
t.Run(variationName, func(t *testing.T) {
var result *MatchResult
var err error
switch variationName {
case "MatchNode":
node := &v1.Node{Status: v1.NodeStatus{DeclaredFeatures: tc.nodeFeatures}}
result, err = MatchNode(tc.podFeatureRequirements, node)
case "MatchNodeFeatureSet":
result, err = MatchNodeFeatureSet(tc.podFeatureRequirements, NewFeatureSet(tc.nodeFeatures...))
default:
t.Fatalf("unknown match variation: %s", variationName)
}
require.NoError(t, err)
assert.Equal(t, tc.expectedMatch, result.IsMatch)
if !tc.expectedMatch {
assert.ElementsMatch(t, tc.expectedUnsatisfied, result.UnsatisfiedRequirements)
}
})
}
})
}
// Test nil node
_, err := MatchNode(NewFeatureSet("feature-a"), nil)
require.Error(t, err, "MatchNode should return an error for a nil node")
}

View File

@@ -0,0 +1,381 @@
/*
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.
*/
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package testing
import (
mock "github.com/stretchr/testify/mock"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-helpers/nodedeclaredfeatures"
)
// NewMockFeature creates a new instance of MockFeature. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockFeature(t interface {
mock.TestingT
Cleanup(func())
}) *MockFeature {
mock := &MockFeature{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// MockFeature is an autogenerated mock type for the Feature type
type MockFeature struct {
mock.Mock
}
type MockFeature_Expecter struct {
mock *mock.Mock
}
func (_m *MockFeature) EXPECT() *MockFeature_Expecter {
return &MockFeature_Expecter{mock: &_m.Mock}
}
// Discover provides a mock function for the type MockFeature
func (_mock *MockFeature) Discover(cfg *nodedeclaredfeatures.NodeConfiguration) bool {
ret := _mock.Called(cfg)
if len(ret) == 0 {
panic("no return value specified for Discover")
}
var r0 bool
if returnFunc, ok := ret.Get(0).(func(*nodedeclaredfeatures.NodeConfiguration) bool); ok {
r0 = returnFunc(cfg)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockFeature_Discover_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Discover'
type MockFeature_Discover_Call struct {
*mock.Call
}
// Discover is a helper method to define mock.On call
// - cfg *nodedeclaredfeatures.NodeConfiguration
func (_e *MockFeature_Expecter) Discover(cfg interface{}) *MockFeature_Discover_Call {
return &MockFeature_Discover_Call{Call: _e.mock.On("Discover", cfg)}
}
func (_c *MockFeature_Discover_Call) Run(run func(cfg *nodedeclaredfeatures.NodeConfiguration)) *MockFeature_Discover_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *nodedeclaredfeatures.NodeConfiguration
if args[0] != nil {
arg0 = args[0].(*nodedeclaredfeatures.NodeConfiguration)
}
run(
arg0,
)
})
return _c
}
func (_c *MockFeature_Discover_Call) Return(b bool) *MockFeature_Discover_Call {
_c.Call.Return(b)
return _c
}
func (_c *MockFeature_Discover_Call) RunAndReturn(run func(cfg *nodedeclaredfeatures.NodeConfiguration) bool) *MockFeature_Discover_Call {
_c.Call.Return(run)
return _c
}
// InferForScheduling provides a mock function for the type MockFeature
func (_mock *MockFeature) InferForScheduling(podInfo *nodedeclaredfeatures.PodInfo) bool {
ret := _mock.Called(podInfo)
if len(ret) == 0 {
panic("no return value specified for InferForScheduling")
}
var r0 bool
if returnFunc, ok := ret.Get(0).(func(*nodedeclaredfeatures.PodInfo) bool); ok {
r0 = returnFunc(podInfo)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockFeature_InferForScheduling_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InferForScheduling'
type MockFeature_InferForScheduling_Call struct {
*mock.Call
}
// InferForScheduling is a helper method to define mock.On call
// - podInfo *nodedeclaredfeatures.PodInfo
func (_e *MockFeature_Expecter) InferForScheduling(podInfo interface{}) *MockFeature_InferForScheduling_Call {
return &MockFeature_InferForScheduling_Call{Call: _e.mock.On("InferForScheduling", podInfo)}
}
func (_c *MockFeature_InferForScheduling_Call) Run(run func(podInfo *nodedeclaredfeatures.PodInfo)) *MockFeature_InferForScheduling_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *nodedeclaredfeatures.PodInfo
if args[0] != nil {
arg0 = args[0].(*nodedeclaredfeatures.PodInfo)
}
run(
arg0,
)
})
return _c
}
func (_c *MockFeature_InferForScheduling_Call) Return(b bool) *MockFeature_InferForScheduling_Call {
_c.Call.Return(b)
return _c
}
func (_c *MockFeature_InferForScheduling_Call) RunAndReturn(run func(podInfo *nodedeclaredfeatures.PodInfo) bool) *MockFeature_InferForScheduling_Call {
_c.Call.Return(run)
return _c
}
// InferForUpdate provides a mock function for the type MockFeature
func (_mock *MockFeature) InferForUpdate(oldPodInfo *nodedeclaredfeatures.PodInfo, newPodInfo *nodedeclaredfeatures.PodInfo) bool {
ret := _mock.Called(oldPodInfo, newPodInfo)
if len(ret) == 0 {
panic("no return value specified for InferForUpdate")
}
var r0 bool
if returnFunc, ok := ret.Get(0).(func(*nodedeclaredfeatures.PodInfo, *nodedeclaredfeatures.PodInfo) bool); ok {
r0 = returnFunc(oldPodInfo, newPodInfo)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockFeature_InferForUpdate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InferForUpdate'
type MockFeature_InferForUpdate_Call struct {
*mock.Call
}
// InferForUpdate is a helper method to define mock.On call
// - oldPodInfo *nodedeclaredfeatures.PodInfo
// - newPodInfo *nodedeclaredfeatures.PodInfo
func (_e *MockFeature_Expecter) InferForUpdate(oldPodInfo interface{}, newPodInfo interface{}) *MockFeature_InferForUpdate_Call {
return &MockFeature_InferForUpdate_Call{Call: _e.mock.On("InferForUpdate", oldPodInfo, newPodInfo)}
}
func (_c *MockFeature_InferForUpdate_Call) Run(run func(oldPodInfo *nodedeclaredfeatures.PodInfo, newPodInfo *nodedeclaredfeatures.PodInfo)) *MockFeature_InferForUpdate_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *nodedeclaredfeatures.PodInfo
if args[0] != nil {
arg0 = args[0].(*nodedeclaredfeatures.PodInfo)
}
var arg1 *nodedeclaredfeatures.PodInfo
if args[1] != nil {
arg1 = args[1].(*nodedeclaredfeatures.PodInfo)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *MockFeature_InferForUpdate_Call) Return(b bool) *MockFeature_InferForUpdate_Call {
_c.Call.Return(b)
return _c
}
func (_c *MockFeature_InferForUpdate_Call) RunAndReturn(run func(oldPodInfo *nodedeclaredfeatures.PodInfo, newPodInfo *nodedeclaredfeatures.PodInfo) bool) *MockFeature_InferForUpdate_Call {
_c.Call.Return(run)
return _c
}
// MaxVersion provides a mock function for the type MockFeature
func (_mock *MockFeature) MaxVersion() *version.Version {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for MaxVersion")
}
var r0 *version.Version
if returnFunc, ok := ret.Get(0).(func() *version.Version); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*version.Version)
}
}
return r0
}
// MockFeature_MaxVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MaxVersion'
type MockFeature_MaxVersion_Call struct {
*mock.Call
}
// MaxVersion is a helper method to define mock.On call
func (_e *MockFeature_Expecter) MaxVersion() *MockFeature_MaxVersion_Call {
return &MockFeature_MaxVersion_Call{Call: _e.mock.On("MaxVersion")}
}
func (_c *MockFeature_MaxVersion_Call) Run(run func()) *MockFeature_MaxVersion_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockFeature_MaxVersion_Call) Return(version1 *version.Version) *MockFeature_MaxVersion_Call {
_c.Call.Return(version1)
return _c
}
func (_c *MockFeature_MaxVersion_Call) RunAndReturn(run func() *version.Version) *MockFeature_MaxVersion_Call {
_c.Call.Return(run)
return _c
}
// Name provides a mock function for the type MockFeature
func (_mock *MockFeature) Name() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Name")
}
var r0 string
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockFeature_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
type MockFeature_Name_Call struct {
*mock.Call
}
// Name is a helper method to define mock.On call
func (_e *MockFeature_Expecter) Name() *MockFeature_Name_Call {
return &MockFeature_Name_Call{Call: _e.mock.On("Name")}
}
func (_c *MockFeature_Name_Call) Run(run func()) *MockFeature_Name_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockFeature_Name_Call) Return(s string) *MockFeature_Name_Call {
_c.Call.Return(s)
return _c
}
func (_c *MockFeature_Name_Call) RunAndReturn(run func() string) *MockFeature_Name_Call {
_c.Call.Return(run)
return _c
}
// NewMockFeatureGate creates a new instance of MockFeatureGate. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockFeatureGate(t interface {
mock.TestingT
Cleanup(func())
}) *MockFeatureGate {
mock := &MockFeatureGate{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// MockFeatureGate is an autogenerated mock type for the FeatureGate type
type MockFeatureGate struct {
mock.Mock
}
type MockFeatureGate_Expecter struct {
mock *mock.Mock
}
func (_m *MockFeatureGate) EXPECT() *MockFeatureGate_Expecter {
return &MockFeatureGate_Expecter{mock: &_m.Mock}
}
// Enabled provides a mock function for the type MockFeatureGate
func (_mock *MockFeatureGate) Enabled(key string) bool {
ret := _mock.Called(key)
if len(ret) == 0 {
panic("no return value specified for Enabled")
}
var r0 bool
if returnFunc, ok := ret.Get(0).(func(string) bool); ok {
r0 = returnFunc(key)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockFeatureGate_Enabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Enabled'
type MockFeatureGate_Enabled_Call struct {
*mock.Call
}
// Enabled is a helper method to define mock.On call
// - key string
func (_e *MockFeatureGate_Expecter) Enabled(key interface{}) *MockFeatureGate_Enabled_Call {
return &MockFeatureGate_Enabled_Call{Call: _e.mock.On("Enabled", key)}
}
func (_c *MockFeatureGate_Enabled_Call) Run(run func(key string)) *MockFeatureGate_Enabled_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
})
return _c
}
func (_c *MockFeatureGate_Enabled_Call) Return(b bool) *MockFeatureGate_Enabled_Call {
_c.Call.Return(b)
return _c
}
func (_c *MockFeatureGate_Enabled_Call) RunAndReturn(run func(key string) bool) *MockFeatureGate_Enabled_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -0,0 +1,81 @@
/*
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.
*/
//go:generate mockery
package nodedeclaredfeatures
import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/version"
)
// PodInfo is an extensible data structure that wraps the pod object
// and can be expanded in the future to include ancillary resources
// like ResourceClaims or PVCs.
type PodInfo struct {
// Spec is the Pod's specification.
Spec *v1.PodSpec
// Status is the Pod's current status. This field can be nil, for example,
// when this struct represents a pod not yet created.
// (e.g., during scheduling).
Status *v1.PodStatus
// Add other ancillary resources here in the future as needed.
// Example: ResourceClaims []*v1.ResourceClaim
}
// Feature encapsulates all logic for a given declared feature.
type Feature interface {
// Name returns the feature's well-known name.
Name() string
// Discover checks if a node provides the feature based on its configuration.
Discover(cfg *NodeConfiguration) bool
// InferForScheduling checks if pod scheduling requires the feature.
InferForScheduling(podInfo *PodInfo) bool
// InferForUpdate checks if a pod update requires the feature.
InferForUpdate(oldPodInfo, newPodInfo *PodInfo) bool
// MaxVersion specifies the upper bound Kubernetes version (inclusive) for this feature's relevance
// as a scheduling factor. Should be set based on the feature's GA version
// and the cluster's version skew policy. Nil means no upper version bound.
// Comparisons use the full semantic versioning scheme.
MaxVersion() *version.Version
}
// FeatureGate is an interface that abstracts feature gate checking.
type FeatureGate interface {
// Enabled returns true if the named feature gate is enabled.
Enabled(key string) bool
}
// StaticConfiguration provides a view of a node's static configuration.
type StaticConfiguration struct {
// Kubelet's CPU Manager policy
CPUManagerPolicy string
}
// NodeConfiguration provides a generic view of a node's static configuration.
type NodeConfiguration struct {
// FeatureGates holds an implementation of the FeatureGate interface.
FeatureGates FeatureGate
// StaticConfig holds node static configuration.
StaticConfig StaticConfiguration
// 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
}