From e1f181e36320575665ec439b8ffa3b3faf7d2ba5 Mon Sep 17 00:00:00 2001 From: Praveen Krishna Date: Fri, 19 Sep 2025 18:07:28 +0000 Subject: [PATCH] feat: Add common library for declared feature discovery, inference and matching. --- staging/src/k8s.io/component-helpers/go.mod | 1 + .../nodedeclaredfeatures/.mockery.yaml | 13 + .../nodedeclaredfeatures/features/registry.go | 26 + .../nodedeclaredfeatures/framework.go | 158 ++++++ .../nodedeclaredfeatures/framework_test.go | 477 ++++++++++++++++++ .../nodedeclaredfeatures/testing/mocks.go | 381 ++++++++++++++ .../nodedeclaredfeatures/types.go | 81 +++ 7 files changed, 1137 insertions(+) create mode 100644 staging/src/k8s.io/component-helpers/nodedeclaredfeatures/.mockery.yaml create mode 100644 staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/registry.go create mode 100644 staging/src/k8s.io/component-helpers/nodedeclaredfeatures/framework.go create mode 100644 staging/src/k8s.io/component-helpers/nodedeclaredfeatures/framework_test.go create mode 100644 staging/src/k8s.io/component-helpers/nodedeclaredfeatures/testing/mocks.go create mode 100644 staging/src/k8s.io/component-helpers/nodedeclaredfeatures/types.go diff --git a/staging/src/k8s.io/component-helpers/go.mod b/staging/src/k8s.io/component-helpers/go.mod index e8b196f7b2a..dd0032e67e0 100644 --- a/staging/src/k8s.io/component-helpers/go.mod +++ b/staging/src/k8s.io/component-helpers/go.mod @@ -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 diff --git a/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/.mockery.yaml b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/.mockery.yaml new file mode 100644 index 00000000000..bb07cc3ad56 --- /dev/null +++ b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/.mockery.yaml @@ -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: {} \ No newline at end of file diff --git a/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/registry.go b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/registry.go new file mode 100644 index 00000000000..ccdd95f8b4d --- /dev/null +++ b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/registry.go @@ -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{} diff --git a/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/framework.go b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/framework.go new file mode 100644 index 00000000000..fa4986e0883 --- /dev/null +++ b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/framework.go @@ -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 +} diff --git a/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/framework_test.go b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/framework_test.go new file mode 100644 index 00000000000..81e13b15a1b --- /dev/null +++ b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/framework_test.go @@ -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") +} diff --git a/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/testing/mocks.go b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/testing/mocks.go new file mode 100644 index 00000000000..e40a048e8e4 --- /dev/null +++ b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/testing/mocks.go @@ -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 +} diff --git a/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/types.go b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/types.go new file mode 100644 index 00000000000..c56fa0c1c5f --- /dev/null +++ b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/types.go @@ -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 +}