@@ -80,6 +80,12 @@ import (
8080 v1appslister "k8s.io/client-go/listers/apps/v1"
8181 kube_record "k8s.io/client-go/tools/record"
8282 "k8s.io/klog/v2"
83+
84+ "k8s.io/apimachinery/pkg/util/version"
85+ utilfeature "k8s.io/apiserver/pkg/util/feature"
86+ ndf "k8s.io/component-helpers/nodedeclaredfeatures"
87+ ndffeatures "k8s.io/component-helpers/nodedeclaredfeatures/features"
88+ "k8s.io/kubernetes/pkg/features"
8389)
8490
8591type podListerMock struct {
@@ -343,7 +349,6 @@ func setupAutoscaler(config *autoscalerSetupConfig) (*StaticAutoscaler, error) {
343349}
344350
345351// TODO: Refactor tests to use setupAutoscaler
346-
347352func TestStaticAutoscalerRunOnce (t * testing.T ) {
348353 readyNodeLister := kubernetes .NewTestNodeLister (nil )
349354 allNodeLister := kubernetes .NewTestNodeLister (nil )
@@ -3343,3 +3348,267 @@ func assertNodesSoftTaintsStatus(t *testing.T, fakeClient *fake.Clientset, nodes
33433348 assert .Equal (t , tainted , taints .HasDeletionCandidateTaint (newNode ))
33443349 }
33453350}
3351+
3352+ // mockFeature is a mock implementation of the Feature interface for testing.
3353+ type mockFeature struct {
3354+ name string
3355+ maxVersion * version.Version
3356+ }
3357+
3358+ func (f * mockFeature ) Name () string {
3359+ return f .name
3360+ }
3361+
3362+ func (f * mockFeature ) Discover (cfg * ndf.NodeConfiguration ) bool {
3363+ return true
3364+ }
3365+
3366+ func (f * mockFeature ) InferForScheduling (podInfo * ndf.PodInfo ) bool {
3367+ // Check if any container has an env var matching the feature name.
3368+ if podInfo .Spec == nil {
3369+ return false
3370+ }
3371+ for _ , container := range podInfo .Spec .Containers {
3372+ for _ , envVar := range container .Env {
3373+ if envVar .Value == f .name {
3374+ return true
3375+ }
3376+ }
3377+ }
3378+ return false
3379+ }
3380+
3381+ func (f * mockFeature ) InferForUpdate (oldPodInfo , newPodInfo * ndf.PodInfo ) bool {
3382+ return false
3383+ }
3384+
3385+ func (f * mockFeature ) MaxVersion () * version.Version {
3386+ return f .maxVersion
3387+ }
3388+
3389+ func createMockFeature (name string , maxVersionStr string ) ndf.Feature {
3390+ var v * version.Version
3391+ if maxVersionStr != "" {
3392+ v = version .MustParseSemantic (maxVersionStr )
3393+ }
3394+ return & mockFeature {
3395+ name : name ,
3396+ maxVersion : v ,
3397+ }
3398+ }
3399+
3400+ func setupMockDeclaredFeatures (features ... string ) func () {
3401+ nodeFeatures := make ([]ndf.Feature , 0 , len (features ))
3402+ for _ , feature := range features {
3403+ nodeFeatures = append (nodeFeatures , createMockFeature (feature , "" ))
3404+ }
3405+ originalAllFeatures := ndffeatures .AllFeatures
3406+ ndffeatures .AllFeatures = nodeFeatures
3407+ return func () {
3408+ ndffeatures .AllFeatures = originalAllFeatures
3409+ }
3410+ }
3411+
3412+ func TestStaticAutoscalerWithNodeDeclaredFeatures (t * testing.T ) {
3413+ nodeInfoTmplA := framework .NewTestNodeInfo (BuildTestNode ("templateA" , 1000 , 1000 ))
3414+ nodeInfoTmplB := framework .NewTestNodeInfo (BuildTestNode ("templateB" , 1000 , 1000 ))
3415+
3416+ // stableTime is needed to make sure the existing nodes from a group are sampled to create template NodeInfo
3417+ stableTime := time .Now ().Add (- 2 * time .Minute )
3418+ nodeA := BuildTestNode ("initialNodeA" , 1000 , 1000 )
3419+ nodeA .Status .DeclaredFeatures = []string {"FeatureA" }
3420+ SetNodeReadyState (nodeA , true , stableTime )
3421+
3422+ nodeB := BuildTestNode ("initialNodeB" , 1000 , 1000 )
3423+ nodeB .Status .DeclaredFeatures = []string {"FeatureB" }
3424+ SetNodeReadyState (nodeB , true , stableTime )
3425+
3426+ fillerPodOnNodeA := BuildTestPod ("fillerPodA" , 900 , 900 )
3427+ fillerPodOnNodeA .Spec .NodeName = "initialNodeA"
3428+ fillerPodOnNodeB := BuildTestPod ("fillerPodB" , 900 , 900 )
3429+ fillerPodOnNodeB .Spec .NodeName = "initialNodeB"
3430+
3431+ podRequiresFeatureA := BuildTestPod ("podA" , 900 , 900 , MarkUnschedulable ())
3432+ podRequiresFeatureA .Spec .Containers [0 ].Env = []apiv1.EnvVar {{Name : "REQUIRE_FEATURE" , Value : "FeatureA" }}
3433+ podRequiresFeatureB := BuildTestPod ("podB" , 900 , 900 , MarkUnschedulable ())
3434+ podRequiresFeatureB .Spec .Containers [0 ].Env = []apiv1.EnvVar {{Name : "REQUIRE_FEATURE" , Value : "FeatureB" }}
3435+ podRequiresFeatureC := BuildTestPod ("podC" , 900 , 900 , MarkUnschedulable ())
3436+ podRequiresFeatureC .Spec .Containers [0 ].Env = []apiv1.EnvVar {{Name : "REQUIRE_FEATURE" , Value : "FeatureC" }}
3437+ anotherPodRequiresFeatureA := BuildTestPod ("podA2" , 900 , 900 , MarkUnschedulable ())
3438+ anotherPodRequiresFeatureA .Spec .Containers [0 ].Env = []apiv1.EnvVar {{Name : "REQUIRE_FEATURE" , Value : "FeatureA" }}
3439+
3440+ options := config.AutoscalingOptions {
3441+ NodeGroupDefaults : config.NodeGroupAutoscalingOptions {
3442+ ScaleDownUnneededTime : time .Minute ,
3443+ ScaleDownUnreadyTime : time .Minute ,
3444+ ScaleDownUtilizationThreshold : 0.5 ,
3445+ MaxNodeProvisionTime : 10 * time .Second ,
3446+ },
3447+ EstimatorName : estimator .BinpackingEstimatorName ,
3448+ EnforceNodeGroupMinSize : true ,
3449+ ScaleDownEnabled : false ,
3450+ MaxNodesTotal : 10 ,
3451+ MaxCoresTotal : 10 ,
3452+ MaxMemoryTotal : 1000 ,
3453+ MaxNodeGroupBinpackingDuration : 1 * time .Second ,
3454+ }
3455+
3456+ type testCase struct {
3457+ name string
3458+ nodeDeclaredFeaturesEnabled bool
3459+ declaredFeatures []string
3460+ initialNodes []* apiv1.Node
3461+ pods []* apiv1.Pod
3462+ expectedScaleUps []scaleCall
3463+ }
3464+ testCases := []testCase {
3465+ {
3466+ name : "Scale up node group A due to pod requiring FeatureA" ,
3467+ nodeDeclaredFeaturesEnabled : true ,
3468+ declaredFeatures : []string {"FeatureA" , "FeatureB" },
3469+ initialNodes : []* apiv1.Node {nodeA , nodeB },
3470+ pods : []* apiv1.Pod {fillerPodOnNodeA , podRequiresFeatureA },
3471+ expectedScaleUps : []scaleCall {{ng : "nodeGroupA" , delta : 1 }},
3472+ },
3473+ {
3474+ name : "Feature gate disabled - No scale up as pod requiring FeatureA can be scheduled on other nodes" ,
3475+ nodeDeclaredFeaturesEnabled : false ,
3476+ declaredFeatures : []string {"FeatureA" , "FeatureB" },
3477+ initialNodes : []* apiv1.Node {nodeA , nodeB },
3478+ pods : []* apiv1.Pod {fillerPodOnNodeA , podRequiresFeatureA },
3479+ expectedScaleUps : nil ,
3480+ },
3481+ {
3482+ name : "Two pods require FeatureA - Scale up by 2" ,
3483+ nodeDeclaredFeaturesEnabled : true ,
3484+ declaredFeatures : []string {"FeatureA" , "FeatureB" },
3485+ initialNodes : []* apiv1.Node {nodeA , nodeB },
3486+ pods : []* apiv1.Pod {fillerPodOnNodeA , podRequiresFeatureA , anotherPodRequiresFeatureA },
3487+ expectedScaleUps : []scaleCall {{ng : "nodeGroupA" , delta : 2 }},
3488+ },
3489+ {
3490+ name : "No scale up - sufficient capacity on existing node" ,
3491+ nodeDeclaredFeaturesEnabled : true ,
3492+ declaredFeatures : []string {"FeatureA" , "FeatureB" },
3493+ initialNodes : []* apiv1.Node {nodeA , nodeB },
3494+ pods : []* apiv1.Pod {podRequiresFeatureB },
3495+ expectedScaleUps : nil ,
3496+ },
3497+ {
3498+ name : "Scale up node group B due to pod requiring FeatureB" ,
3499+ nodeDeclaredFeaturesEnabled : true ,
3500+ declaredFeatures : []string {"FeatureA" , "FeatureB" },
3501+ initialNodes : []* apiv1.Node {nodeA , nodeB },
3502+ pods : []* apiv1.Pod {fillerPodOnNodeB , podRequiresFeatureB },
3503+ expectedScaleUps : []scaleCall {{ng : "nodeGroupB" , delta : 1 }},
3504+ },
3505+ {
3506+ name : "No scale up when pod requiring feature is not present on any node group" ,
3507+ nodeDeclaredFeaturesEnabled : false ,
3508+ declaredFeatures : []string {"FeatureA" , "FeatureB" },
3509+ initialNodes : []* apiv1.Node {nodeA , nodeB },
3510+ pods : []* apiv1.Pod {podRequiresFeatureC },
3511+ expectedScaleUps : nil ,
3512+ },
3513+ }
3514+
3515+ for _ , tc := range testCases {
3516+ t .Run (tc .name , func (t * testing.T ) {
3517+ allPodListerMock := & podListerMock {}
3518+ podDisruptionBudgetListerMock := & podDisruptionBudgetListerMock {}
3519+ daemonSetListerMock := & daemonSetListerMock {}
3520+ onScaleUpMock := & onScaleUpMock {}
3521+ onScaleDownMock := & onScaleDownMock {}
3522+
3523+ // Feature gate setup
3524+ utilfeature .DefaultMutableFeatureGate .Set (fmt .Sprintf ("%s=%v" , features .NodeDeclaredFeatures , tc .nodeDeclaredFeaturesEnabled ))
3525+ cleanup := setupMockDeclaredFeatures (tc .declaredFeatures ... )
3526+ defer cleanup ()
3527+
3528+ readyNodeLister := kubernetes .NewTestNodeLister (tc .initialNodes )
3529+ allNodeLister := kubernetes .NewTestNodeLister (tc .initialNodes )
3530+
3531+ provider := testprovider .NewTestCloudProviderBuilder ().WithOnScaleUp (func (id string , delta int ) error {
3532+ return onScaleUpMock .ScaleUp (id , delta )
3533+ }).WithMachineTemplates (map [string ]* framework.NodeInfo {
3534+ "nodeGroupA" : nodeInfoTmplA ,
3535+ "nodeGroupB" : nodeInfoTmplB ,
3536+ }).Build ()
3537+
3538+ // Group nodes by features to add to an appropriate node group
3539+ nodesByFeature := make (map [string ][]* apiv1.Node )
3540+ for _ , node := range tc .initialNodes {
3541+ if len (node .Status .DeclaredFeatures ) > 0 {
3542+ feature := node .Status .DeclaredFeatures [0 ]
3543+ nodesByFeature [feature ] = append (nodesByFeature [feature ], node )
3544+ }
3545+ }
3546+
3547+ if nodes , ok := nodesByFeature ["FeatureA" ]; ok {
3548+ provider .AddNodeGroup ("nodeGroupA" , 1 , 10 , len (nodes ))
3549+ for _ , node := range nodes {
3550+ provider .AddNode ("nodeGroupA" , node )
3551+ }
3552+ }
3553+ if nodes , ok := nodesByFeature ["FeatureB" ]; ok {
3554+ provider .AddNodeGroup ("nodeGroupB" , 1 , 10 , len (nodes ))
3555+ for _ , node := range nodes {
3556+ provider .AddNode ("nodeGroupB" , node )
3557+ }
3558+ }
3559+
3560+ processorCallbacks := newStaticAutoscalerProcessorCallbacks ()
3561+ listerRegistry := kube_util .NewListerRegistry (allNodeLister , readyNodeLister , allPodListerMock , podDisruptionBudgetListerMock , daemonSetListerMock , nil , nil , nil , nil )
3562+ autoscalingCtx , err := NewScaleTestAutoscalingContext (options , & fake.Clientset {}, listerRegistry , provider , processorCallbacks , nil )
3563+ assert .NoError (t , err )
3564+
3565+ for _ , node := range tc .initialNodes {
3566+ nodeInfo := framework .NewNodeInfo (node , nil )
3567+ err = autoscalingCtx .ClusterSnapshot .AddNodeInfo (nodeInfo )
3568+ assert .NoError (t , err )
3569+ }
3570+
3571+ processors := processorstest .NewTestProcessors (& autoscalingCtx )
3572+ clusterStateConfig := clusterstate.ClusterStateRegistryConfig {OkTotalUnreadyCount : 2 }
3573+ clusterState := clusterstate .NewClusterStateRegistry (provider , clusterStateConfig , autoscalingCtx .LogRecorder , NewBackoff (), processors .NodeGroupConfigProcessor , processors .AsyncNodeGroupStateChecker )
3574+
3575+ sdPlanner , sdActuator := newScaleDownPlannerAndActuator (& autoscalingCtx , processors , clusterState , nil )
3576+ autoscalingCtx .ScaleDownActuator = sdActuator
3577+ quotasTrackerFactory := newQuotasTrackerFactory (& autoscalingCtx , processors )
3578+ suOrchestrator := orchestrator .New ()
3579+ suOrchestrator .Initialize (& autoscalingCtx , processors , clusterState , newEstimatorBuilder (), taints.TaintConfig {}, quotasTrackerFactory )
3580+
3581+ autoscaler := & StaticAutoscaler {
3582+ AutoscalingContext : & autoscalingCtx ,
3583+ clusterStateRegistry : clusterState ,
3584+ lastScaleUpTime : time .Now ().Add (- time .Hour ),
3585+ lastScaleDownFailTime : time .Now ().Add (- time .Hour ),
3586+ scaleUpOrchestrator : suOrchestrator ,
3587+ scaleDownPlanner : sdPlanner ,
3588+ scaleDownActuator : sdActuator ,
3589+ processors : processors ,
3590+ loopStartNotifier : loopstart .NewObserversList (nil ),
3591+ processorCallbacks : processorCallbacks ,
3592+ initialized : true ,
3593+ }
3594+
3595+ // Update ClusterStateRegistry with initial nodes
3596+ err = clusterState .UpdateNodes (tc .initialNodes , make (map [string ]* framework.NodeInfo ), time .Now ())
3597+ assert .NoError (t , err )
3598+
3599+ allPodListerMock .On ("List" ).Return (tc .pods , nil ).Once ()
3600+ daemonSetListerMock .On ("List" , labels .Everything ()).Return ([]* appsv1.DaemonSet {}, nil ).Once ()
3601+ podDisruptionBudgetListerMock .On ("List" ).Return ([]* policyv1.PodDisruptionBudget {}, nil ).Once ()
3602+
3603+ // Expected scale up calls
3604+ for _ , scaleUp := range tc .expectedScaleUps {
3605+ onScaleUpMock .On ("ScaleUp" , scaleUp .ng , scaleUp .delta ).Return (nil ).Once ()
3606+ }
3607+
3608+ // Run the autoscaler
3609+ err = autoscaler .RunOnce (time .Now ())
3610+ assert .NoError (t , err )
3611+ mock .AssertExpectationsForObjects (t , allPodListerMock , podDisruptionBudgetListerMock , daemonSetListerMock , onScaleUpMock , onScaleDownMock )
3612+ })
3613+ }
3614+ }
0 commit comments