NodeDeclaredFeatures: Extract ndf types to subpackage to avoid circular dependencies

This commit is contained in:
Tim Allclair
2026-01-09 12:52:21 -08:00
parent 4221620945
commit 52b8fa1d05
10 changed files with 198 additions and 152 deletions

View File

@@ -19,11 +19,11 @@ package inplacepodresize
import (
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-helpers/nodedeclaredfeatures"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
// Ensure the feature struct implements the unified Feature interface.
var _ nodedeclaredfeatures.Feature = &podLevelResourcesResizeFeature{}
var _ types.Feature = &podLevelResourcesResizeFeature{}
// IPPRPodLevelResourcesFeatureGate is the feature gate for IPPRPodLevelResourcesVerticalScaling.
const IPPRPodLevelResourcesFeatureGate = "InPlacePodLevelResourcesVerticalScaling"
@@ -37,21 +37,22 @@ func (f *podLevelResourcesResizeFeature) Name() string {
return IPPRPodLevelResourcesFeatureGate
}
func (f *podLevelResourcesResizeFeature) Discover(cfg *nodedeclaredfeatures.NodeConfiguration) bool {
func (f *podLevelResourcesResizeFeature) Discover(cfg *types.NodeConfiguration) bool {
return cfg.FeatureGates.Enabled(IPPRPodLevelResourcesFeatureGate)
}
func (f *podLevelResourcesResizeFeature) Requirements() *nodedeclaredfeatures.FeatureRequirements {
return &nodedeclaredfeatures.FeatureRequirements{
func (f *podLevelResourcesResizeFeature) Requirements() *types.FeatureRequirements {
return &types.FeatureRequirements{
EnabledFeatureGates: []string{IPPRPodLevelResourcesFeatureGate},
}
}
func (f *podLevelResourcesResizeFeature) InferForScheduling(podInfo *nodedeclaredfeatures.PodInfo) bool {
func (f *podLevelResourcesResizeFeature) InferForScheduling(podInfo *types.PodInfo) bool {
// This feature is only relevant for pod updates.
return false
}
func (f *podLevelResourcesResizeFeature) InferForUpdate(oldPodInfo, newPodInfo *nodedeclaredfeatures.PodInfo) bool {
func (f *podLevelResourcesResizeFeature) InferForUpdate(oldPodInfo, newPodInfo *types.PodInfo) bool {
if oldPodInfo.Spec.Resources == nil && newPodInfo.Spec.Resources == nil {
return false
}

View File

@@ -21,8 +21,7 @@ import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/component-helpers/nodedeclaredfeatures"
test "k8s.io/component-helpers/nodedeclaredfeatures/testing"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
func TestPodLevelResourcesResizeFeature_Requirements(t *testing.T) {
@@ -56,10 +55,7 @@ func TestPodLevelResourcesResizeFeatureDiscover(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockFG := test.NewMockFeatureGate(t)
mockFG.SetEnabled(IPPRPodLevelResourcesFeatureGate, tt.featureGate)
cfg := &nodedeclaredfeatures.NodeConfiguration{FeatureGates: mockFG}
cfg := &types.NodeConfiguration{FeatureGates: types.FeatureGateMap{IPPRPodLevelResourcesFeatureGate: tt.featureGate}}
enabled := PodLevelResourcesResizeFeature.Discover(cfg)
if want, got := tt.expected, enabled; want != got {
t.Fatalf("want=%v,got=%v", want, got)
@@ -69,7 +65,7 @@ func TestPodLevelResourcesResizeFeatureDiscover(t *testing.T) {
}
func TestPodLevelResourcesResizeFeatureInferForScheduling(t *testing.T) {
podInfo := &nodedeclaredfeatures.PodInfo{Spec: &v1.PodSpec{}, Status: &v1.PodStatus{}}
podInfo := &types.PodInfo{Spec: &v1.PodSpec{}, Status: &v1.PodStatus{}}
if PodLevelResourcesResizeFeature.InferForScheduling(podInfo) {
t.Fatalf("InferForScheduling should always be false")
}
@@ -78,13 +74,13 @@ func TestPodLevelResourcesResizeFeatureInferForScheduling(t *testing.T) {
func TestPodLevelResourcesResizeFeatureInferForUpdate(t *testing.T) {
tests := []struct {
name string
oldPodInfo *nodedeclaredfeatures.PodInfo
newPodInfo *nodedeclaredfeatures.PodInfo
oldPodInfo *types.PodInfo
newPodInfo *types.PodInfo
expected bool
}{
{
name: "NoResourcesChanged",
oldPodInfo: &nodedeclaredfeatures.PodInfo{
oldPodInfo: &types.PodInfo{
Spec: &v1.PodSpec{
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1")},
@@ -93,7 +89,7 @@ func TestPodLevelResourcesResizeFeatureInferForUpdate(t *testing.T) {
},
Status: &v1.PodStatus{},
},
newPodInfo: &nodedeclaredfeatures.PodInfo{
newPodInfo: &types.PodInfo{
Spec: &v1.PodSpec{
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1")},
@@ -106,11 +102,11 @@ func TestPodLevelResourcesResizeFeatureInferForUpdate(t *testing.T) {
},
{
name: "ResourcesAdded",
oldPodInfo: &nodedeclaredfeatures.PodInfo{
oldPodInfo: &types.PodInfo{
Spec: &v1.PodSpec{},
Status: &v1.PodStatus{},
},
newPodInfo: &nodedeclaredfeatures.PodInfo{
newPodInfo: &types.PodInfo{
Spec: &v1.PodSpec{
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1")},
@@ -123,7 +119,7 @@ func TestPodLevelResourcesResizeFeatureInferForUpdate(t *testing.T) {
},
{
name: "ResourcesRemoved",
oldPodInfo: &nodedeclaredfeatures.PodInfo{
oldPodInfo: &types.PodInfo{
Spec: &v1.PodSpec{
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1")},
@@ -132,7 +128,7 @@ func TestPodLevelResourcesResizeFeatureInferForUpdate(t *testing.T) {
},
Status: &v1.PodStatus{},
},
newPodInfo: &nodedeclaredfeatures.PodInfo{
newPodInfo: &types.PodInfo{
Spec: &v1.PodSpec{},
Status: &v1.PodStatus{},
},
@@ -140,7 +136,7 @@ func TestPodLevelResourcesResizeFeatureInferForUpdate(t *testing.T) {
},
{
name: "ResourcesChanged",
oldPodInfo: &nodedeclaredfeatures.PodInfo{
oldPodInfo: &types.PodInfo{
Spec: &v1.PodSpec{
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1")},
@@ -149,7 +145,7 @@ func TestPodLevelResourcesResizeFeatureInferForUpdate(t *testing.T) {
},
Status: &v1.PodStatus{},
},
newPodInfo: &nodedeclaredfeatures.PodInfo{
newPodInfo: &types.PodInfo{
Spec: &v1.PodSpec{
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("2")},
@@ -162,11 +158,11 @@ func TestPodLevelResourcesResizeFeatureInferForUpdate(t *testing.T) {
},
{
name: "NilResources",
oldPodInfo: &nodedeclaredfeatures.PodInfo{
oldPodInfo: &types.PodInfo{
Spec: &v1.PodSpec{},
Status: &v1.PodStatus{},
},
newPodInfo: &nodedeclaredfeatures.PodInfo{
newPodInfo: &types.PodInfo{
Spec: &v1.PodSpec{},
Status: &v1.PodStatus{},
},

View File

@@ -17,16 +17,16 @@ limitations under the License.
package features
import (
"k8s.io/component-helpers/nodedeclaredfeatures"
"k8s.io/component-helpers/nodedeclaredfeatures/features/extendwebsocketstokubelet"
"k8s.io/component-helpers/nodedeclaredfeatures/features/inplacepodresize"
"k8s.io/component-helpers/nodedeclaredfeatures/features/restartallcontainers"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
// 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 = []types.Feature{
restartallcontainers.Feature,
inplacepodresize.PodLevelResourcesResizeFeature,
extendwebsocketstokubelet.Feature,

View File

@@ -19,11 +19,11 @@ package restartallcontainers
import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-helpers/nodedeclaredfeatures"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
// Ensure the feature struct implements the unified Feature interface.
var _ nodedeclaredfeatures.Feature = &restartAllContainersFeature{}
var _ types.Feature = &restartAllContainersFeature{}
const (
RestartRulesFeatureGate = "ContainerRestartRules"
@@ -39,16 +39,17 @@ func (f *restartAllContainersFeature) Name() string {
return RestartAllContainersOnContainerExits
}
func (f *restartAllContainersFeature) Discover(cfg *nodedeclaredfeatures.NodeConfiguration) bool {
func (f *restartAllContainersFeature) Discover(cfg *types.NodeConfiguration) bool {
return cfg.FeatureGates.Enabled(RestartAllContainersOnContainerExits)
}
func (f *restartAllContainersFeature) Requirements() *nodedeclaredfeatures.FeatureRequirements {
return &nodedeclaredfeatures.FeatureRequirements{
func (f *restartAllContainersFeature) Requirements() *types.FeatureRequirements {
return &types.FeatureRequirements{
EnabledFeatureGates: []string{RestartAllContainersOnContainerExits},
}
}
func (f *restartAllContainersFeature) InferForScheduling(podInfo *nodedeclaredfeatures.PodInfo) bool {
func (f *restartAllContainersFeature) InferForScheduling(podInfo *types.PodInfo) bool {
for _, c := range podInfo.Spec.Containers {
for _, rule := range c.RestartPolicyRules {
if rule.Action == v1.ContainerRestartRuleActionRestartAllContainers {
@@ -66,7 +67,7 @@ func (f *restartAllContainersFeature) InferForScheduling(podInfo *nodedeclaredfe
return false
}
func (f *restartAllContainersFeature) InferForUpdate(oldPodInfo, newPodInfo *nodedeclaredfeatures.PodInfo) bool {
func (f *restartAllContainersFeature) InferForUpdate(oldPodInfo, newPodInfo *types.PodInfo) bool {
// container.restartPolicy and container.restartPolicyRules are not mutable.
return false
}

View File

@@ -20,8 +20,7 @@ import (
"testing"
v1 "k8s.io/api/core/v1"
"k8s.io/component-helpers/nodedeclaredfeatures"
test "k8s.io/component-helpers/nodedeclaredfeatures/testing"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
func TestRequirements(t *testing.T) {
@@ -56,10 +55,8 @@ func TestDiscover(t *testing.T) {
feature := &restartAllContainersFeature{}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mockFG := test.NewMockFeatureGate(t)
mockFG.SetEnabled(RestartAllContainersOnContainerExits, tc.featureGateEnabled)
config := &nodedeclaredfeatures.NodeConfiguration{
FeatureGates: mockFG,
config := &types.NodeConfiguration{
FeatureGates: types.FeatureGateMap{RestartAllContainersOnContainerExits: tc.featureGateEnabled},
}
enabled := feature.Discover(config)
if want, got := tc.expected, enabled; want != got {
@@ -118,7 +115,7 @@ func TestInferForScheduling(t *testing.T) {
feature := &restartAllContainersFeature{}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
podInfo := &nodedeclaredfeatures.PodInfo{Spec: tc.pod}
podInfo := &types.PodInfo{Spec: tc.pod}
if want, got := tc.expected, feature.InferForScheduling(podInfo); want != got {
t.Fatalf("want=%v,got=%v", want, got)
}
@@ -128,7 +125,7 @@ func TestInferForScheduling(t *testing.T) {
func TestInferForUpdate(t *testing.T) {
feature := &restartAllContainersFeature{}
podInfo := &nodedeclaredfeatures.PodInfo{Spec: &v1.PodSpec{}}
podInfo := &types.PodInfo{Spec: &v1.PodSpec{}}
if feature.InferForUpdate(nil, podInfo) {
t.Fatalf("expect InferForUpdate to be false")
}

View File

@@ -23,12 +23,13 @@ import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
// 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
registry []types.Feature
}
// FeatureSet is a set of node features.
@@ -55,7 +56,7 @@ func (s *FeatureSet) Clone() FeatureSet {
}
// New creates a new instance of the Framework.
func New(registry []Feature) (*Framework, error) {
func New(registry []types.Feature) (*Framework, error) {
if registry == nil {
return nil, fmt.Errorf("registry must not be nil")
}
@@ -66,7 +67,7 @@ func New(registry []Feature) (*Framework, error) {
// 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 {
func (f *Framework) DiscoverNodeFeatures(cfg *types.NodeConfiguration) []string {
var enabledFeatures []string
for _, f := range f.registry {
if f.Discover(cfg) {
@@ -81,7 +82,7 @@ func (f *Framework) DiscoverNodeFeatures(cfg *NodeConfiguration) []string {
}
// 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) {
func (f *Framework) InferForPodScheduling(podInfo *types.PodInfo, targetVersion *version.Version) (FeatureSet, error) {
if targetVersion == nil {
return FeatureSet{}, fmt.Errorf("target version cannot be nil")
}
@@ -99,7 +100,7 @@ func (f *Framework) InferForPodScheduling(podInfo *PodInfo, targetVersion *versi
}
// 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) {
func (f *Framework) InferForPodUpdate(oldPodInfo, newPodInfo *types.PodInfo, targetVersion *version.Version) (FeatureSet, error) {
if targetVersion == nil {
return FeatureSet{}, fmt.Errorf("target version cannot be nil")
}

View File

@@ -27,22 +27,25 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
// 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
discover func(cfg *types.NodeConfiguration) bool
inferForScheduling func(podInfo *types.PodInfo) bool
inferForUpdate func(oldPodInfo, newPodInfo *types.PodInfo) bool
maxVersion *version.Version
requirements func() *FeatureRequirements
}
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 {
func (f *mockFeature) Name() string { return f.name }
func (f *mockFeature) Discover(cfg *types.NodeConfiguration) bool { return f.discover(cfg) }
func (f *mockFeature) InferForScheduling(podInfo *types.PodInfo) bool {
return f.inferForScheduling(podInfo)
}
func (f *mockFeature) InferForUpdate(oldPodInfo, newPodInfo *types.PodInfo) bool {
return f.inferForUpdate(oldPodInfo, newPodInfo)
}
func (f *mockFeature) MaxVersion() *version.Version { return f.maxVersion }
@@ -69,7 +72,7 @@ func TestNewFramework(t *testing.T) {
t.Fatalf("NewFramework should return an error with a nil registry")
}
_, err = New([]Feature{})
_, err = New([]types.Feature{})
if err != nil {
t.Fatalf("NewFramework should not return an error with an empty registry")
}
@@ -77,10 +80,10 @@ func TestNewFramework(t *testing.T) {
func TestDiscoverNodeFeatures(t *testing.T) {
featureMaxVersion := version.MustParse("1.38.0")
registry := []Feature{
registry := []types.Feature{
&mockFeature{
name: "FeatureA",
discover: func(cfg *NodeConfiguration) bool {
discover: func(cfg *types.NodeConfiguration) bool {
return cfg.FeatureGates.Enabled("feature-a")
},
requirements: func() *FeatureRequirements {
@@ -92,7 +95,7 @@ func TestDiscoverNodeFeatures(t *testing.T) {
},
&mockFeature{
name: "FeatureB",
discover: func(cfg *NodeConfiguration) bool {
discover: func(cfg *types.NodeConfiguration) bool {
return cfg.FeatureGates.Enabled("feature-b")
},
requirements: func() *FeatureRequirements {
@@ -108,19 +111,19 @@ func TestDiscoverNodeFeatures(t *testing.T) {
testCases := []struct {
name string
config *NodeConfiguration
config *types.NodeConfiguration
expected []string
}{
{
name: "Feature Enabled",
config: &NodeConfiguration{
config: &types.NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{string("feature-a"): true}),
},
expected: []string{"FeatureA"},
},
{
name: "multiple features enabled",
config: &NodeConfiguration{
config: &types.NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{
string("feature-a"): true,
string("feature-b"): true,
@@ -130,7 +133,7 @@ func TestDiscoverNodeFeatures(t *testing.T) {
},
{
name: "no features enabled",
config: &NodeConfiguration{
config: &types.NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{
string("feature-a"): false,
string("feature-b"): false,
@@ -140,7 +143,7 @@ func TestDiscoverNodeFeatures(t *testing.T) {
},
{
name: "feature past max version",
config: &NodeConfiguration{
config: &types.NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{string("feature-a"): true}),
Version: featureMaxVersion.AddMinor(1),
},
@@ -148,7 +151,7 @@ func TestDiscoverNodeFeatures(t *testing.T) {
},
{
name: "feature past max version - pre-release version",
config: &NodeConfiguration{
config: &types.NodeConfiguration{
FeatureGates: newMockFeatureGate(map[string]bool{string("feature-a"): true}),
Version: version.MustParse("1.39.0-alpha.2.39+049eafd34dfbd2"),
},
@@ -167,7 +170,7 @@ func TestDiscoverNodeFeatures(t *testing.T) {
}
func TestInferForPodScheduling(t *testing.T) {
inferPodlevelResources := func(p *PodInfo) bool {
inferPodlevelResources := func(p *types.PodInfo) bool {
return p.Spec.Resources != nil
}
podWithPodLevelResources := &v1.Pod{
@@ -194,7 +197,7 @@ func TestInferForPodScheduling(t *testing.T) {
testCases := []struct {
name string
registry []Feature
registry []types.Feature
newPod *v1.Pod
targetVersion *version.Version
expectedReqs FeatureSet
@@ -203,7 +206,7 @@ func TestInferForPodScheduling(t *testing.T) {
}{
{
name: "pod with feature, inferred during scheduling",
registry: []Feature{
registry: []types.Feature{
&mockFeature{
name: "PodLevelResources",
inferForScheduling: inferPodlevelResources,
@@ -217,7 +220,7 @@ func TestInferForPodScheduling(t *testing.T) {
},
{
name: "pod without feature",
registry: []Feature{
registry: []types.Feature{
&mockFeature{
name: "PodLevelResources",
inferForScheduling: inferPodlevelResources,
@@ -231,7 +234,7 @@ func TestInferForPodScheduling(t *testing.T) {
},
{
name: "feature universally available, not inferred during create",
registry: []Feature{
registry: []types.Feature{
&mockFeature{
name: "PodLevelResources",
inferForScheduling: inferPodlevelResources,
@@ -246,7 +249,7 @@ func TestInferForPodScheduling(t *testing.T) {
},
{
name: "pre-release target version",
registry: []Feature{
registry: []types.Feature{
&mockFeature{
name: "PodLevelResources",
inferForScheduling: inferPodlevelResources,
@@ -261,7 +264,7 @@ func TestInferForPodScheduling(t *testing.T) {
},
{
name: "target version nil",
registry: []Feature{
registry: []types.Feature{
&mockFeature{
name: "PodLevelResources",
inferForScheduling: inferPodlevelResources,
@@ -279,7 +282,7 @@ func TestInferForPodScheduling(t *testing.T) {
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)
reqs, err := framework.InferForPodScheduling(&types.PodInfo{Spec: &tc.newPod.Spec, Status: &tc.newPod.Status}, tc.targetVersion)
if tc.expectErr {
if err == nil {
@@ -299,7 +302,7 @@ func TestInferForPodScheduling(t *testing.T) {
}
func TestInferForPodUpdate(t *testing.T) {
inferResize := func(oldPodInfo, newPodInfo *PodInfo) bool {
inferResize := func(oldPodInfo, newPodInfo *types.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) {
@@ -336,7 +339,7 @@ func TestInferForPodUpdate(t *testing.T) {
testCases := []struct {
name string
registry []Feature
registry []types.Feature
oldPod *v1.Pod
newPod *v1.Pod
targetVersion *version.Version
@@ -346,7 +349,7 @@ func TestInferForPodUpdate(t *testing.T) {
}{
{
name: "pod update requires feature",
registry: []Feature{
registry: []types.Feature{
&mockFeature{
name: "InPlacePodResize",
inferForUpdate: inferResize,
@@ -361,7 +364,7 @@ func TestInferForPodUpdate(t *testing.T) {
},
{
name: "pod update requires feature with pre-release version",
registry: []Feature{
registry: []types.Feature{
&mockFeature{
name: "InPlacePodResize",
inferForUpdate: inferResize,
@@ -376,7 +379,7 @@ func TestInferForPodUpdate(t *testing.T) {
},
{
name: "pod update does not require feature",
registry: []Feature{
registry: []types.Feature{
&mockFeature{
name: "InPlacePodResize",
inferForUpdate: inferResize,
@@ -391,7 +394,7 @@ func TestInferForPodUpdate(t *testing.T) {
},
{
name: "feature universally available, not inferred during update",
registry: []Feature{
registry: []types.Feature{
&mockFeature{
name: "InPlacePodResize",
inferForUpdate: inferResize,
@@ -407,7 +410,7 @@ func TestInferForPodUpdate(t *testing.T) {
},
{
name: "target version nil",
registry: []Feature{
registry: []types.Feature{
&mockFeature{
name: "InPlacePodResize",
inferForUpdate: inferResize,
@@ -426,7 +429,7 @@ func TestInferForPodUpdate(t *testing.T) {
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)
reqs, err := framework.InferForPodUpdate(&types.PodInfo{Spec: &tc.oldPod.Spec, Status: &tc.oldPod.Status}, &types.PodInfo{Spec: &tc.newPod.Spec, Status: &tc.newPod.Status}, tc.targetVersion)
if tc.expectErr {
if err == nil {

View File

@@ -21,9 +21,10 @@ import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-helpers/nodedeclaredfeatures"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
var _ = nodedeclaredfeatures.FeatureGate((*MockFeatureGate)(nil))
var _ = types.FeatureGate((*MockFeatureGate)(nil))
type MockFeatureGate struct {
t *testing.T
@@ -48,16 +49,16 @@ func NewMockFeatureGate(t *testing.T) *MockFeatureGate {
}
}
var _ = nodedeclaredfeatures.Feature((*MockFeature)(nil))
var _ = types.Feature((*MockFeature)(nil))
type MockFeature struct {
t *testing.T
name *string
discover func(cfg *nodedeclaredfeatures.NodeConfiguration) bool
inferForScheduling func(podInfo *nodedeclaredfeatures.PodInfo) bool
inferForUpdate func(oldPodInfo, newPodInfo *nodedeclaredfeatures.PodInfo) bool
discover func(cfg *types.NodeConfiguration) bool
inferForScheduling func(podInfo *types.PodInfo) bool
inferForUpdate func(oldPodInfo, newPodInfo *types.PodInfo) bool
maxVersion **version.Version
requirements **nodedeclaredfeatures.FeatureRequirements
requirements **types.FeatureRequirements
}
func (m *MockFeature) Name() string {
@@ -70,34 +71,34 @@ func (m *MockFeature) Name() string {
func (m *MockFeature) SetName(name string) {
m.name = &name
}
func (m *MockFeature) Discover(cfg *nodedeclaredfeatures.NodeConfiguration) bool {
func (m *MockFeature) Discover(cfg *types.NodeConfiguration) bool {
if m.discover == nil {
m.t.Errorf("unexpected call to Discover")
return false
}
return m.discover(cfg)
}
func (m *MockFeature) SetDiscover(discover func(cfg *nodedeclaredfeatures.NodeConfiguration) bool) {
func (m *MockFeature) SetDiscover(discover func(cfg *types.NodeConfiguration) bool) {
m.discover = discover
}
func (m *MockFeature) InferForScheduling(podInfo *nodedeclaredfeatures.PodInfo) bool {
func (m *MockFeature) InferForScheduling(podInfo *types.PodInfo) bool {
if m.inferForScheduling == nil {
m.t.Errorf("unexpected call to InferForScheduling")
return false
}
return m.inferForScheduling(podInfo)
}
func (m *MockFeature) SetInferForScheduling(inferForScheduling func(podInfo *nodedeclaredfeatures.PodInfo) bool) {
func (m *MockFeature) SetInferForScheduling(inferForScheduling func(podInfo *types.PodInfo) bool) {
m.inferForScheduling = inferForScheduling
}
func (m *MockFeature) InferForUpdate(oldPodInfo, newPodInfo *nodedeclaredfeatures.PodInfo) bool {
func (m *MockFeature) InferForUpdate(oldPodInfo, newPodInfo *types.PodInfo) bool {
if m.inferForUpdate == nil {
m.t.Errorf("unexpected call to InferForUpdate")
return false
}
return m.inferForUpdate(oldPodInfo, newPodInfo)
}
func (m *MockFeature) SetInferForUpdate(inferForUpdate func(oldPodInfo, newPodInfo *nodedeclaredfeatures.PodInfo) bool) {
func (m *MockFeature) SetInferForUpdate(inferForUpdate func(oldPodInfo, newPodInfo *types.PodInfo) bool) {
m.inferForUpdate = inferForUpdate
}
func (m *MockFeature) MaxVersion() *version.Version {

View File

@@ -17,65 +17,14 @@ limitations under the License.
package nodedeclaredfeatures
import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-helpers/nodedeclaredfeatures/types"
)
// 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
}
// The following type aliases are provided as convenience to framework users.
// 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
// Requirements returns the feature's feature gate and static config dependencies.
Requirements() *FeatureRequirements
// 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
}
// FeatureRequirements lists the potential dependencies of a feature.
type FeatureRequirements struct {
// EnabledFeatureGates lists feature gate strings that the feature depends on.
EnabledFeatureGates []string
}
// 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
}
// NodeConfiguration provides a generic view of a node's static configuration.
type NodeConfiguration struct {
// FeatureGates holds an implementation of the FeatureGate interface.
FeatureGates FeatureGate
// 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
}
type PodInfo = types.PodInfo
type Feature = types.Feature
type FeatureRequirements = types.FeatureRequirements
type FeatureGate = types.FeatureGate
type StaticConfiguration = types.StaticConfiguration
type NodeConfiguration = types.NodeConfiguration

View File

@@ -0,0 +1,97 @@
/*
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 types
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
// Requirements returns the feature's feature gate and static config dependencies.
Requirements() *FeatureRequirements
// 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
}
// FeatureRequirements lists the potential dependencies of a feature.
type FeatureRequirements struct {
// EnabledFeatureGates lists feature gate strings that the feature depends on.
EnabledFeatureGates []string
}
// 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 required for feature discovery.
type StaticConfiguration struct {
// Add configuration fields here as required by registered features.
}
// 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
}
// FeatureGateMap is provided as a convenience implementation of FeatureGate
type FeatureGateMap map[string]bool
func (m FeatureGateMap) Enabled(key string) bool {
return m[key]
}
var _ FeatureGate = FeatureGateMap{}