Skip to content

Commit 50f2483

Browse files
authored
Merge pull request kubernetes#8952 from pravk03/ndf
feat: Add tests for NodeDeclaredFeatures
2 parents efde1e2 + c38bf04 commit 50f2483

File tree

2 files changed

+311
-12
lines changed

2 files changed

+311
-12
lines changed

cluster-autoscaler/core/static_autoscaler_test.go

Lines changed: 270 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

8591
type podListerMock struct {
@@ -343,7 +349,6 @@ func setupAutoscaler(config *autoscalerSetupConfig) (*StaticAutoscaler, error) {
343349
}
344350

345351
// TODO: Refactor tests to use setupAutoscaler
346-
347352
func 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

Comments
 (0)