diff --git a/apis/extension/elastic_quota.go b/apis/extension/elastic_quota.go index ac7e06596..18fc9afe0 100644 --- a/apis/extension/elastic_quota.go +++ b/apis/extension/elastic_quota.go @@ -55,6 +55,7 @@ const ( AnnotationNonPreemptibleRequest = QuotaKoordinatorPrefix + "/non-preemptible-request" AnnotationNonPreemptibleUsed = QuotaKoordinatorPrefix + "/non-preemptible-used" AnnotationAdmission = QuotaKoordinatorPrefix + "/admission" + AnnotationMinExcess = QuotaKoordinatorPrefix + "/min-excess" ) func GetParentQuotaName(quota *v1alpha1.ElasticQuota) string { @@ -218,3 +219,13 @@ func GetAdmission(quota *v1alpha1.ElasticQuota) (corev1.ResourceList, error) { } return admission, nil } + +func GetMinExcess(quota *v1alpha1.ElasticQuota) (corev1.ResourceList, error) { + minExcess := corev1.ResourceList{} + if quota.Annotations[AnnotationMinExcess] != "" { + if err := json.Unmarshal([]byte(quota.Annotations[AnnotationMinExcess]), &minExcess); err != nil { + return minExcess, fmt.Errorf("failed to unmarshal min-excess, err=%v", err) + } + } + return minExcess, nil +} diff --git a/pkg/features/features.go b/pkg/features/features.go index 763e8ea50..a89ea23ca 100644 --- a/pkg/features/features.go +++ b/pkg/features/features.go @@ -68,6 +68,9 @@ const ( // to belong to the users and will not be preempted back. ElasticQuotaGuaranteeUsage featuregate.Feature = "ElasticQuotaGuaranteeUsage" + // ElasticQuotaMinExcess enable control the upper limit of the min-excess resources + ElasticQuotaMinExcess featuregate.Feature = "ElasticQuotaMinExcess" + // DisableDefaultQuota disable default quota. DisableDefaultQuota featuregate.Feature = "DisableDefaultQuota" diff --git a/pkg/features/scheduler_features.go b/pkg/features/scheduler_features.go index bc05e943e..3431e16d6 100644 --- a/pkg/features/scheduler_features.go +++ b/pkg/features/scheduler_features.go @@ -82,6 +82,7 @@ var defaultSchedulerFeatureGates = map[featuregate.Feature]featuregate.FeatureSp ElasticQuotaIgnoreTerminatingPod: {Default: false, PreRelease: featuregate.Alpha}, ElasticQuotaImmediateIgnoreTerminatingPod: {Default: false, PreRelease: featuregate.Alpha}, ElasticQuotaGuaranteeUsage: {Default: false, PreRelease: featuregate.Alpha}, + ElasticQuotaMinExcess: {Default: false, PreRelease: featuregate.Alpha}, DisableDefaultQuota: {Default: false, PreRelease: featuregate.Alpha}, SupportParentQuotaSubmitPod: {Default: false, PreRelease: featuregate.Alpha}, LazyReservationRestore: {Default: false, PreRelease: featuregate.Alpha}, diff --git a/pkg/scheduler/plugins/elasticquota/core/group_quota_manager.go b/pkg/scheduler/plugins/elasticquota/core/group_quota_manager.go index c2fcf052f..18ae06497 100644 --- a/pkg/scheduler/plugins/elasticquota/core/group_quota_manager.go +++ b/pkg/scheduler/plugins/elasticquota/core/group_quota_manager.go @@ -224,6 +224,12 @@ func (gqm *GroupQuotaManager) recursiveUpdateGroupTreeWithDeltaRequest(deltaReq, } } +type addUsedRecursiveState struct { + isSelfUsed bool + minExcessEnabled bool + minExcessUsedDelta v1.ResourceList +} + // updateGroupDeltaUsedNoLock updates the usedQuota of a node, it also updates all parent nodes // no need to lock gqm.hierarchyUpdateLock func (gqm *GroupQuotaManager) updateGroupDeltaUsedNoLock(quotaName string, delta, deltaNonPreemptibleUsed v1.ResourceList) { @@ -234,10 +240,13 @@ func (gqm *GroupQuotaManager) updateGroupDeltaUsedNoLock(quotaName string, delta } defer gqm.scopedLockForQuotaInfo(curToAllParInfos)() + recursiveState := &addUsedRecursiveState{ + minExcessEnabled: utilfeature.DefaultFeatureGate.Enabled(features.ElasticQuotaMinExcess), + } for i := 0; i < allQuotaInfoLen; i++ { quotaInfo := curToAllParInfos[i] - isSelfUsed := i == 0 - quotaInfo.addUsedNonNegativeNoLock(delta, deltaNonPreemptibleUsed, isSelfUsed) + recursiveState.isSelfUsed = i == 0 + quotaInfo.addUsedNonNegativeNoLock(delta, deltaNonPreemptibleUsed, recursiveState) } if utilfeature.DefaultFeatureGate.Enabled(features.ElasticQuotaGuaranteeUsage) { @@ -1009,6 +1018,10 @@ func (gqm *GroupQuotaManager) updateQuotaInternalNoLock(newQuotaInfo, oldQuotaIn gqm.doUpdateOneGroupSharedWeightNoLock(newQuotaInfo.Name, newQuotaInfo.CalculateInfo.SharedWeight) } + // min-excess changed + if !quotav1.Equals(newQuotaInfo.CalculateInfo.MinExcess, oldQuotaInfo.CalculateInfo.MinExcess) { + oldQuotaInfo.setMinExcessNoLock(newQuotaInfo.CalculateInfo.MinExcess) + } } func (gqm *GroupQuotaManager) doUpdateOneGroupMaxQuotaNoLock(quotaName string, newMax v1.ResourceList) { diff --git a/pkg/scheduler/plugins/elasticquota/core/group_quota_manager_test.go b/pkg/scheduler/plugins/elasticquota/core/group_quota_manager_test.go index a0a6e488b..35ad8bb37 100644 --- a/pkg/scheduler/plugins/elasticquota/core/group_quota_manager_test.go +++ b/pkg/scheduler/plugins/elasticquota/core/group_quota_manager_test.go @@ -119,6 +119,7 @@ func TestGroupQuotaManager_UpdateQuotaInternal(t *testing.T) { AddQuotaToManager(t, gqm, "test1", extension.RootQuotaName, 96, 160*GigaByte, 50, 80*GigaByte, true, false) quota := CreateQuota("test1", extension.RootQuotaName, 64, 100*GigaByte, 50, 80*GigaByte, true, false) + quota.Annotations[extension.AnnotationMinExcess] = `{"cpu":10, "memory": "40Gi"}` gqm.UpdateQuota(quota, false) quotaInfo := gqm.quotaInfoMap["test1"] assert.True(t, quotaInfo != nil) @@ -127,11 +128,13 @@ func TestGroupQuotaManager_UpdateQuotaInternal(t *testing.T) { assert.Equal(t, createResourceList(50, 80*GigaByte), quotaInfo.CalculateInfo.AutoScaleMin) assert.Equal(t, int64(64), quotaInfo.CalculateInfo.SharedWeight.Cpu().Value()) assert.Equal(t, int64(100*GigaByte), quotaInfo.CalculateInfo.SharedWeight.Memory().Value()) + assert.True(t, quotav1.Equals(createResourceList(10, 40*GigaByte), quotaInfo.CalculateInfo.MinExcess)) AddQuotaToManager(t, gqm, "test2", extension.RootQuotaName, 96, 160*GigaByte, 80, 80*GigaByte, true, false) quota = CreateQuota("test1", extension.RootQuotaName, 84, 120*GigaByte, 60, 100*GigaByte, true, false) quota.Labels[extension.LabelQuotaIsParent] = "true" + quota.Annotations[extension.AnnotationMinExcess] = `{"cpu":30, "memory": "120Gi"}` err := gqm.UpdateQuota(quota, false) assert.Nil(t, err) quotaInfo = gqm.quotaInfoMap["test1"] @@ -142,6 +145,7 @@ func TestGroupQuotaManager_UpdateQuotaInternal(t *testing.T) { assert.Equal(t, createResourceList(60, 100*GigaByte), quotaInfo.CalculateInfo.AutoScaleMin) assert.Equal(t, int64(84), quotaInfo.CalculateInfo.SharedWeight.Cpu().Value()) assert.Equal(t, int64(120*GigaByte), quotaInfo.CalculateInfo.SharedWeight.Memory().Value()) + assert.True(t, quotav1.Equals(createResourceList(30, 120*GigaByte), quotaInfo.CalculateInfo.MinExcess)) } func TestGroupQuotaManager_UpdateQuota(t *testing.T) { @@ -2108,3 +2112,84 @@ func TestGroupQuotaManager_ImmediateIgnoreTerminatingPod(t *testing.T) { assert.Equal(t, createResourceList(0, 0), gqm.GetQuotaInfoByName("1").GetRequest()) assert.Equal(t, createResourceList(0, 0), gqm.GetQuotaInfoByName("1").GetUsed()) } + +func TestGroupQuotaManager_UpdateGroupDeltaUsedAndMinExcessUsed(t *testing.T) { + defer utilfeature.SetFeatureGateDuringTest(t, k8sfeature.DefaultFeatureGate, + features.ElasticQuotaMinExcess, true)() + gqm := NewGroupQuotaManagerForTest() + + // quota1 Max[40, 40] Min[20,20] Used[0,0] MinExcess[20,20] + // |-- quota2 Max[30, 30] Min[10,10] Used[0,0] MinExcess[10,10] + // |-- quota3 Max[20, 20] Min[5,5] Used[0,0] MinExcess[15,15] + q1 := createQuota("1", extension.RootQuotaName, 40, 40, 20, 20) + q1.Annotations[extension.AnnotationMinExcess] = `{"cpu":20,"memory":20}` + q2 := createQuota("2", "1", 30, 30, 10, 10) + q2.Annotations[extension.AnnotationMinExcess] = `{"cpu":10,"memory":10}` + q3 := createQuota("3", "1", 20, 20, 5, 5) + q3.Annotations[extension.AnnotationMinExcess] = `{"cpu":15,"memory":15}` + gqm.UpdateQuota(q1, false) + gqm.UpdateQuota(q2, false) + gqm.UpdateQuota(q3, false) + + qi1 := gqm.GetQuotaInfoByName("1") + qi2 := gqm.GetQuotaInfoByName("2") + qi3 := gqm.GetQuotaInfoByName("3") + assert.NotNil(t, qi1) + assert.NotNil(t, qi2) + assert.NotNil(t, qi3) + + // 1. quota2 used [15,20] + // expected: quota2 minExcessUsed [5, 10], quota1 minExcessUsed [5,10] + delta := createResourceList(15, 20) + nonPreemptibleUsed := createResourceList(0, 0) + gqm.updateGroupDeltaUsedNoLock("2", delta, nonPreemptibleUsed) + assert.Equal(t, createResourceList(15, 20), qi1.CalculateInfo.Used) + assert.Equal(t, createResourceList(5, 10), qi1.CalculateInfo.MinExcessUsed) + assert.Equal(t, createResourceList(15, 20), qi2.CalculateInfo.Used) + assert.Equal(t, createResourceList(5, 10), qi2.CalculateInfo.MinExcessUsed) + + // 2. quota3 used [10,10] + // expected: quota3 minExcessUsed [5,5], quota1 minExcessUsed [10,15] + delta = createResourceList(10, 10) + gqm.updateGroupDeltaUsedNoLock("3", delta, nonPreemptibleUsed) + assert.Equal(t, createResourceList(10, 10), qi3.CalculateInfo.Used) + assert.Equal(t, createResourceList(5, 5), qi3.CalculateInfo.MinExcessUsed) + assert.Equal(t, createResourceList(10, 15), qi1.CalculateInfo.MinExcessUsed) + + // 3. quota2 used decreases to [5,15] + // expected: quota2 minExcessUsed [0,5], quota1 minExcessUsed [5,10] + delta = createResourceList(-10, -5) + gqm.updateGroupDeltaUsedNoLock("2", delta, nonPreemptibleUsed) + assert.Equal(t, createResourceList(5, 15), qi2.CalculateInfo.Used) + assert.Equal(t, createResourceList(0, 5), qi2.CalculateInfo.MinExcessUsed) + assert.Equal(t, createResourceList(5, 10), qi1.CalculateInfo.MinExcessUsed) + + // 4. quota3 used decreases to [2,2] + // expected: quota3 minExcessUsed [0,0], quota1 minExcessUsed [0,5] + delta = createResourceList(-8, -8) + gqm.updateGroupDeltaUsedNoLock("3", delta, nonPreemptibleUsed) + assert.Equal(t, createResourceList(2, 2), qi3.CalculateInfo.Used) + assert.Equal(t, createResourceList(0, 0), qi3.CalculateInfo.MinExcessUsed) + assert.Equal(t, createResourceList(0, 5), qi1.CalculateInfo.MinExcessUsed) + + // 5. quota2 used increases to [20,20] + // expected: quota2 minExcessUsed [10,10], quota1 minExcessUsed [10,10] + delta = createResourceList(15, 5) + gqm.updateGroupDeltaUsedNoLock("2", delta, nonPreemptibleUsed) + assert.Equal(t, createResourceList(20, 20), qi2.CalculateInfo.Used) + assert.Equal(t, createResourceList(10, 10), qi2.CalculateInfo.MinExcessUsed) + assert.Equal(t, createResourceList(10, 10), qi1.CalculateInfo.MinExcessUsed) + + // 6. quota2 and quota3 decrease to [0,0] + // expected: minExcessUsed [0,0] for all quotas + delta = createResourceList(-20, -20) + gqm.updateGroupDeltaUsedNoLock("2", delta, nonPreemptibleUsed) + delta = createResourceList(-2, -2) + gqm.updateGroupDeltaUsedNoLock("3", delta, nonPreemptibleUsed) + assert.Equal(t, createResourceList(0, 0), qi1.CalculateInfo.Used) + assert.Equal(t, createResourceList(0, 0), qi2.CalculateInfo.Used) + assert.Equal(t, createResourceList(0, 0), qi3.CalculateInfo.Used) + assert.Equal(t, createResourceList(0, 0), qi1.CalculateInfo.MinExcessUsed) + assert.Equal(t, createResourceList(0, 0), qi2.CalculateInfo.MinExcessUsed) + assert.Equal(t, createResourceList(0, 0), qi3.CalculateInfo.MinExcessUsed) +} diff --git a/pkg/scheduler/plugins/elasticquota/core/helper.go b/pkg/scheduler/plugins/elasticquota/core/helper.go index 61d3a2c81..fc2eb2099 100644 --- a/pkg/scheduler/plugins/elasticquota/core/helper.go +++ b/pkg/scheduler/plugins/elasticquota/core/helper.go @@ -18,6 +18,7 @@ package core import ( corev1 "k8s.io/api/core/v1" + quotav1 "k8s.io/apiserver/pkg/quota/v1" k8sfeature "k8s.io/apiserver/pkg/util/feature" apiresource "k8s.io/kubernetes/pkg/api/v1/resource" @@ -32,3 +33,11 @@ func PodRequests(pod *corev1.Pod) (reqs corev1.ResourceList) { } return apiresource.PodRequests(pod, apiresource.PodResourcesOptions{}) } + +func CalculateMinExcessUsedDelta(min, used, delta corev1.ResourceList) (minExcessUsedDelta corev1.ResourceList) { + minResourceNames := quotav1.ResourceNames(min) + minExcessUsed := quotav1.SubtractWithNonNegativeResult(quotav1.Mask(used, minResourceNames), min) + newUsed := quotav1.Add(quotav1.Mask(used, minResourceNames), quotav1.Mask(delta, minResourceNames)) + newMinExcessUsed := quotav1.SubtractWithNonNegativeResult(quotav1.Mask(newUsed, minResourceNames), min) + return quotav1.Subtract(newMinExcessUsed, minExcessUsed) +} diff --git a/pkg/scheduler/plugins/elasticquota/core/helper_test.go b/pkg/scheduler/plugins/elasticquota/core/helper_test.go index f6e6a88bc..b0e6f4919 100644 --- a/pkg/scheduler/plugins/elasticquota/core/helper_test.go +++ b/pkg/scheduler/plugins/elasticquota/core/helper_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + quotav1 "k8s.io/apiserver/pkg/quota/v1" k8sfeature "k8s.io/apiserver/pkg/util/feature" koordfeatures "github.com/koordinator-sh/koordinator/pkg/features" @@ -88,3 +89,193 @@ func TestPodRequestsAndLimits(t *testing.T) { } } + +func TestCalculateMinExcessUsedDelta(t *testing.T) { + tests := []struct { + name string + min corev1.ResourceList + used corev1.ResourceList + usedDelta corev1.ResourceList + expected corev1.ResourceList + }{ + { + name: "min > used + usedDelta, used = 0, usedDelta > 0 ==> min-excess-used-delta = 0", + min: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024*1024, resource.BinarySI), + }, + used: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI), + }, + usedDelta: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(512*1024*1024, resource.BinarySI), + }, + expected: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI), + }, + }, + { + name: "min > used + usedDelta, used > 0, usedDelta > 0 ==> min-excess-used-delta = 0", + min: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024*1024, resource.BinarySI), + }, + used: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(512*1024*1024, resource.BinarySI), + }, + usedDelta: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(512*1024*1024, resource.BinarySI), + }, + expected: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI), + }, + }, + { + name: "min < used + usedDelta, used > 0, usedDelta > 0 ==> min-excess-used-delta > 0", + min: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024*1024, resource.BinarySI), + }, + used: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(512*1024*1024, resource.BinarySI), + }, + usedDelta: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(501, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(513*1024*1024, resource.BinarySI), + }, + expected: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024, resource.BinarySI), + }, + }, + { + name: "min < used, usedDelta > 0 ==> min-excess-used-delta = usedDelta", + min: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024*1024, resource.BinarySI), + }, + used: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1124*1024*1024, resource.BinarySI), + }, + usedDelta: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100*1024*1024, resource.BinarySI), + }, + expected: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100*1024*1024, resource.BinarySI), + }, + }, + { + name: "min(cpu) < used(cpu), usedDelta > 0 ==> min-excess-used-delta(cpu) = usedDelta(cpu)", + min: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024*1024, resource.BinarySI), + }, + used: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(512*1024*1024, resource.BinarySI), + }, + usedDelta: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100*1024*1024, resource.BinarySI), + }, + expected: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI), + }, + }, + { + name: "min < used, usedDelta > 0 ==> min-excess-used-delta = usedDelta (exclude other resources: pods)", + min: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024*1024, resource.BinarySI), + }, + used: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024*1024, resource.BinarySI), + corev1.ResourcePods: *resource.NewQuantity(10, resource.BinarySI), + }, + usedDelta: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100*1024*1024, resource.BinarySI), + corev1.ResourcePods: *resource.NewQuantity(1, resource.BinarySI), + }, + expected: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100*1024*1024, resource.BinarySI), + }, + }, + { + name: "min > used, usedDelta < 0 ==> min-excess-used-delta = 0", + min: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024*1024, resource.BinarySI), + }, + used: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100*1024*1024, resource.BinarySI), + }, + usedDelta: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(-100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(-100*1024*1024, resource.BinarySI), + }, + expected: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI), + }, + }, + { + name: "min < used, usedDelta < 0 ==> min-excess-used-delta = usedDelta", + min: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024*1024, resource.BinarySI), + }, + used: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1124*1024*1024, resource.BinarySI), + }, + usedDelta: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(-100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(-100*1024*1024, resource.BinarySI), + }, + expected: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(-100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(-100*1024*1024, resource.BinarySI), + }, + }, + { + name: "min < used, usedDelta < 0 ==> min-excess-used-delta = * usedDelta", + min: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024*1024*1024, resource.BinarySI), + }, + used: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(1050, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1074*1024*1024, resource.BinarySI), + }, + usedDelta: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(-100, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(-100*1024*1024, resource.BinarySI), + }, + expected: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewMilliQuantity(-50, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(-50*1024*1024, resource.BinarySI), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + delta := CalculateMinExcessUsedDelta(tt.min, tt.used, tt.usedDelta) + assert.True(t, quotav1.Equals(tt.expected, delta), "expected=%+v\n actual=%+v", tt.expected, delta) + }) + } +} diff --git a/pkg/scheduler/plugins/elasticquota/core/quota_info.go b/pkg/scheduler/plugins/elasticquota/core/quota_info.go index 51e64630d..1a8d4f619 100644 --- a/pkg/scheduler/plugins/elasticquota/core/quota_info.go +++ b/pkg/scheduler/plugins/elasticquota/core/quota_info.go @@ -65,6 +65,12 @@ type QuotaCalculateInfo struct { // Allocated is the allocated resource. It's the sum of children quota guarantee. If the quota is leaf, it's // the sum of scheduled pods Allocated v1.ResourceList + + // The semantics of "min-excess" is the quota group's upper limit of min-excess resources. + MinExcess v1.ResourceList + // MinExcessUsed is the sum of MinExcessUsed of all children if the quota is parent. + // If the quota is leaf, it's the sum of used - min + MinExcessUsed v1.ResourceList } type QuotaInfo struct { @@ -108,6 +114,8 @@ func NewQuotaInfo(isParent, allowLentResource bool, name, parentName string) *Qu SelfUsed: v1.ResourceList{}, SelfNonPreemptibleRequest: v1.ResourceList{}, SelfNonPreemptibleUsed: v1.ResourceList{}, + MinExcess: v1.ResourceList{}, + MinExcessUsed: v1.ResourceList{}, }, } } @@ -143,6 +151,8 @@ func (qi *QuotaInfo) DeepCopy() *QuotaInfo { SelfUsed: qi.CalculateInfo.SelfUsed.DeepCopy(), SelfNonPreemptibleRequest: qi.CalculateInfo.SelfNonPreemptibleRequest.DeepCopy(), SelfNonPreemptibleUsed: qi.CalculateInfo.SelfNonPreemptibleUsed.DeepCopy(), + MinExcess: qi.CalculateInfo.MinExcess.DeepCopy(), + MinExcessUsed: qi.CalculateInfo.MinExcessUsed.DeepCopy(), }, } for name, pod := range qi.PodCache { @@ -178,6 +188,8 @@ func (qi *QuotaInfo) GetQuotaSummary(treeID string, includePods bool) *QuotaInfo quotaInfoSummary.SelfRequest = qi.CalculateInfo.SelfRequest.DeepCopy() quotaInfoSummary.SelfNonPreemptibleUsed = qi.CalculateInfo.SelfNonPreemptibleUsed.DeepCopy() quotaInfoSummary.SelfNonPreemptibleRequest = qi.CalculateInfo.SelfNonPreemptibleRequest.DeepCopy() + quotaInfoSummary.MinExcess = qi.CalculateInfo.MinExcess.DeepCopy() + quotaInfoSummary.MinExcessUsed = qi.CalculateInfo.MinExcessUsed.DeepCopy() if includePods { for podName, podInfo := range qi.PodCache { @@ -207,6 +219,7 @@ func (qi *QuotaInfo) updateQuotaInfoFromRemote(quotaInfo *QuotaInfo) { qi.AllowLentResource = quotaInfo.AllowLentResource qi.IsParent = quotaInfo.IsParent qi.ParentName = quotaInfo.ParentName + qi.setMinExcessNoLock(quotaInfo.CalculateInfo.MinExcess) } // getLimitRequestNoLock returns the min value of request and max, as max is the quotaGroup's upper limit of resources. @@ -276,8 +289,10 @@ func (qi *QuotaInfo) GetAllocated() v1.ResourceList { return qi.CalculateInfo.Allocated.DeepCopy() } -func (qi *QuotaInfo) addUsedNonNegativeNoLock(delta, deltaNonPreemptibleUsed v1.ResourceList, isSelfUsed bool) { - qi.CalculateInfo.Used = quotav1.Add(qi.CalculateInfo.Used, delta) +func (qi *QuotaInfo) addUsedNonNegativeNoLock(delta, deltaNonPreemptibleUsed v1.ResourceList, + recursiveState *addUsedRecursiveState) { + usedBefore := qi.CalculateInfo.Used + qi.CalculateInfo.Used = quotav1.Add(usedBefore, delta) qi.CalculateInfo.NonPreemptibleUsed = quotav1.Add(qi.CalculateInfo.NonPreemptibleUsed, deltaNonPreemptibleUsed) for _, resName := range quotav1.IsNegative(qi.CalculateInfo.Used) { qi.CalculateInfo.Used[resName] = createQuantity(0, resName) @@ -286,7 +301,7 @@ func (qi *QuotaInfo) addUsedNonNegativeNoLock(delta, deltaNonPreemptibleUsed v1. qi.CalculateInfo.NonPreemptibleUsed[resName] = createQuantity(0, resName) } - if isSelfUsed { + if recursiveState.isSelfUsed { qi.CalculateInfo.SelfUsed = quotav1.Add(qi.CalculateInfo.SelfUsed, delta) for _, resName := range quotav1.IsNegative(qi.CalculateInfo.SelfUsed) { qi.CalculateInfo.SelfUsed[resName] = createQuantity(0, resName) @@ -295,6 +310,15 @@ func (qi *QuotaInfo) addUsedNonNegativeNoLock(delta, deltaNonPreemptibleUsed v1. for _, resName := range quotav1.IsNegative(qi.CalculateInfo.SelfNonPreemptibleUsed) { qi.CalculateInfo.SelfNonPreemptibleUsed[resName] = createQuantity(0, resName) } + if recursiveState.minExcessEnabled && !quotav1.IsZero(delta) { + // calculate only for leaf quota + recursiveState.minExcessUsedDelta = CalculateMinExcessUsedDelta( + qi.CalculateInfo.Min, usedBefore, delta) + } + } + // update min-excess-used + if recursiveState.minExcessEnabled && !quotav1.IsZero(recursiveState.minExcessUsedDelta) { + qi.CalculateInfo.MinExcessUsed = quotav1.Add(qi.CalculateInfo.MinExcessUsed, recursiveState.minExcessUsedDelta) } } @@ -313,6 +337,10 @@ func (qi *QuotaInfo) setMinQuotaNoLock(res v1.ResourceList) { qi.CalculateInfo.Min = res.DeepCopy() } +func (qi *QuotaInfo) setMinExcessNoLock(res v1.ResourceList) { + qi.CalculateInfo.MinExcess = res.DeepCopy() +} + func (qi *QuotaInfo) setAutoScaleMinQuotaNoLock(res v1.ResourceList) { qi.CalculateInfo.AutoScaleMin = res.DeepCopy() } @@ -375,6 +403,18 @@ func (qi *QuotaInfo) GetSelfNonPreemptibleRequest() v1.ResourceList { return qi.CalculateInfo.SelfNonPreemptibleRequest.DeepCopy() } +func (qi *QuotaInfo) GetMinExcess() v1.ResourceList { + qi.lock.Lock() + defer qi.lock.Unlock() + return qi.CalculateInfo.MinExcess.DeepCopy() +} + +func (qi *QuotaInfo) GetMinExcessUsed() v1.ResourceList { + qi.lock.Lock() + defer qi.lock.Unlock() + return qi.CalculateInfo.MinExcessUsed.DeepCopy() +} + func (qi *QuotaInfo) GetRuntime() v1.ResourceList { qi.lock.Lock() defer qi.lock.Unlock() @@ -407,6 +447,8 @@ func NewQuotaInfoFromQuota(quota *v1alpha1.ElasticQuota) *QuotaInfo { quotaInfo.setMaxQuotaNoLock(quota.Spec.Max) newSharedWeight := extension.GetSharedWeight(quota) quotaInfo.setSharedWeightNoLock(newSharedWeight) + minExcess, _ := extension.GetMinExcess(quota) + quotaInfo.setMinExcessNoLock(minExcess) return quotaInfo } @@ -428,6 +470,7 @@ func (qi *QuotaInfo) clearForResetNoLock() { qi.CalculateInfo.SelfRequest = v1.ResourceList{} qi.CalculateInfo.SelfNonPreemptibleUsed = v1.ResourceList{} qi.CalculateInfo.SelfNonPreemptibleRequest = v1.ResourceList{} + qi.CalculateInfo.MinExcessUsed = v1.ResourceList{} qi.RuntimeVersion = 0 } diff --git a/pkg/scheduler/plugins/elasticquota/core/quota_summary.go b/pkg/scheduler/plugins/elasticquota/core/quota_summary.go index 1c38e9958..5b21ac9ec 100644 --- a/pkg/scheduler/plugins/elasticquota/core/quota_summary.go +++ b/pkg/scheduler/plugins/elasticquota/core/quota_summary.go @@ -49,6 +49,8 @@ type QuotaInfoSummary struct { SelfNonPreemptibleUsed v1.ResourceList `json:"selfNonPreemptibleUsed"` SelfRequest v1.ResourceList `json:"selfRequest"` SelfNonPreemptibleRequest v1.ResourceList `json:"selfNonPreemptibleRequest"` + MinExcess v1.ResourceList `json:"minExcess"` + MinExcessUsed v1.ResourceList `json:"minExcessUsed"` PodCache map[string]*SimplePodInfo `json:"podCache,omitempty"` } diff --git a/pkg/scheduler/plugins/elasticquota/core/runtime_quota_calculator_test.go b/pkg/scheduler/plugins/elasticquota/core/runtime_quota_calculator_test.go index 356c4ff8d..0c457b463 100644 --- a/pkg/scheduler/plugins/elasticquota/core/runtime_quota_calculator_test.go +++ b/pkg/scheduler/plugins/elasticquota/core/runtime_quota_calculator_test.go @@ -58,7 +58,8 @@ func TestQuotaInfo_AddRequestNonNegativeNoLock(t *testing.T) { }, } quotaInfo.addRequestNonNegativeNoLock(req1, req1, false) - quotaInfo.addUsedNonNegativeNoLock(req1, createResourceList(0, 0), false) + quotaInfo.addUsedNonNegativeNoLock(req1, createResourceList(0, 0), + &addUsedRecursiveState{isSelfUsed: false}) assert.Equal(t, quotaInfo.CalculateInfo.Request, createResourceList(0, 0)) assert.Equal(t, quotaInfo.CalculateInfo.Used, createResourceList(0, 0)) } diff --git a/pkg/scheduler/plugins/elasticquota/plugin.go b/pkg/scheduler/plugins/elasticquota/plugin.go index b48fe8e0c..a770a0ff6 100644 --- a/pkg/scheduler/plugins/elasticquota/plugin.go +++ b/pkg/scheduler/plugins/elasticquota/plugin.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" quotav1 "k8s.io/apiserver/pkg/quota/v1" + k8sfeature "k8s.io/apiserver/pkg/util/feature" v1 "k8s.io/client-go/listers/core/v1" policylisters "k8s.io/client-go/listers/policy/v1" "k8s.io/client-go/tools/cache" @@ -40,6 +41,7 @@ import ( "github.com/koordinator-sh/koordinator/apis/thirdparty/scheduler-plugins/pkg/generated/clientset/versioned" "github.com/koordinator-sh/koordinator/apis/thirdparty/scheduler-plugins/pkg/generated/informers/externalversions" "github.com/koordinator-sh/koordinator/apis/thirdparty/scheduler-plugins/pkg/generated/listers/scheduling/v1alpha1" + "github.com/koordinator-sh/koordinator/pkg/features" "github.com/koordinator-sh/koordinator/pkg/scheduler/apis/config" "github.com/koordinator-sh/koordinator/pkg/scheduler/apis/config/validation" "github.com/koordinator-sh/koordinator/pkg/scheduler/frameworkext" @@ -249,7 +251,10 @@ func (g *Plugin) PreFilter(ctx context.Context, cycleState *framework.CycleState } if g.pluginArgs.EnableCheckParentQuota { - return nil, g.checkQuotaRecursive(quotaName, []string{quotaName}, podRequest) + recursiveState := &checkQuotaRecursiveState{ + minExcessEnabled: k8sfeature.DefaultFeatureGate.Enabled(features.ElasticQuotaMinExcess), + } + return nil, g.checkQuotaRecursive(quotaName, []string{quotaName}, podRequest, recursiveState) } return nil, framework.NewStatus(framework.Success, "") diff --git a/pkg/scheduler/plugins/elasticquota/plugin_helper.go b/pkg/scheduler/plugins/elasticquota/plugin_helper.go index aa8a83bca..6dfe42de4 100644 --- a/pkg/scheduler/plugins/elasticquota/plugin_helper.go +++ b/pkg/scheduler/plugins/elasticquota/plugin_helper.go @@ -278,7 +278,13 @@ func getPostFilterState(cycleState *framework.CycleState) (*PostFilterState, err return s, nil } -func (g *Plugin) checkQuotaRecursive(curQuotaName string, quotaNameTopo []string, podRequest v1.ResourceList) *framework.Status { +type checkQuotaRecursiveState struct { + minExcessEnabled bool + minExcessUsedDelta v1.ResourceList +} + +func (g *Plugin) checkQuotaRecursive(curQuotaName string, quotaNameTopo []string, podRequest v1.ResourceList, + recursiveState *checkQuotaRecursiveState) *framework.Status { quotaInfo := g.groupQuotaManager.GetQuotaInfoByName(curQuotaName) quotaUsed := quotaInfo.GetUsed() quotaUsedLimit := g.getQuotaInfoUsedLimit(quotaInfo) @@ -289,9 +295,31 @@ func (g *Plugin) checkQuotaRecursive(curQuotaName string, quotaNameTopo []string "quotaNameTopo: %v, runtime: %v, used: %v, pod's request: %v, exceedDimensions: %v", quotaNameTopo, printResourceList(quotaUsedLimit), printResourceList(quotaUsed), printResourceList(podRequest), exceedDimensions)) } + if recursiveState.minExcessEnabled { + if len(quotaNameTopo) == 1 { + // calculate only for leaf quota + recursiveState.minExcessUsedDelta = core.CalculateMinExcessUsedDelta( + quotaInfo.GetMin(), quotaUsed, podRequest) + } + // check min-excess limit if configured min-excess and min-excess-used-delta is not zero + if len(quotaInfo.GetMinExcess()) > 0 && !quotav1.IsZero(recursiveState.minExcessUsedDelta) { + minExcess := quotaInfo.GetMinExcess() + // calculate new min-excess-used then check for current quota + minExcessUsed := quotaInfo.GetMinExcessUsed() + newMinExcessUsed := quotav1.Add(minExcessUsed, recursiveState.minExcessUsedDelta) + if isLessEqual, exceedDimensions := quotav1.LessThanOrEqual(newMinExcessUsed, minExcess); !isLessEqual { + return framework.NewStatus(framework.Unschedulable, fmt.Sprintf("Insufficient min-excess quotas, "+ + "quotaNameTopo: %v, min-excess: %v, min-excess-used: %v, min-excess-used-delta: %v,"+ + " pod's request: %v, exceedDimensions: %v", + quotaNameTopo, printResourceList(minExcess), printResourceList(minExcessUsed), + printResourceList(recursiveState.minExcessUsedDelta), + printResourceList(podRequest), exceedDimensions)) + } + } + } if quotaInfo.ParentName != extension.RootQuotaName { quotaNameTopo = append([]string{quotaInfo.ParentName}, quotaNameTopo...) - return g.checkQuotaRecursive(quotaInfo.ParentName, quotaNameTopo, podRequest) + return g.checkQuotaRecursive(quotaInfo.ParentName, quotaNameTopo, podRequest, recursiveState) } return framework.NewStatus(framework.Success, "") } diff --git a/pkg/scheduler/plugins/elasticquota/plugin_test.go b/pkg/scheduler/plugins/elasticquota/plugin_test.go index 0855cabb4..f9ab6fbbd 100644 --- a/pkg/scheduler/plugins/elasticquota/plugin_test.go +++ b/pkg/scheduler/plugins/elasticquota/plugin_test.go @@ -770,7 +770,8 @@ func TestPlugin_PreFilter_CheckParent(t *testing.T) { qi1.CalculateInfo.Runtime = tt.parentRuntime.DeepCopy() qi1.UnLock() podRequests := core.PodRequests(tt.pod) - status := *gp.checkQuotaRecursive(tt.quotaInfo.Name, []string{tt.quotaInfo.Name}, podRequests) + status := *gp.checkQuotaRecursive(tt.quotaInfo.Name, []string{tt.quotaInfo.Name}, podRequests, + &checkQuotaRecursiveState{}) assert.Equal(t, tt.expectedStatus, status) }) } @@ -1363,3 +1364,131 @@ func TestPostFilterState(t *testing.T) { assert.Equal(t, got, got1) }) } + +func TestPlugin_PreFilter_CheckParentMinExcess(t *testing.T) { + test := []struct { + name string + pod *corev1.Pod + quotaInfo *v1alpha1.ElasticQuota + parentQuotaInfo *v1alpha1.ElasticQuota + expectedStatus framework.Status + }{ + { + name: "accept", + pod: MakePod("t1-ns1", "pod1").Label(extension.LabelQuotaName, "test-child").Container( + MakeResourceList().CPU(2).Mem(2).Obj()).Obj(), + quotaInfo: &v1alpha1.ElasticQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-child", + Labels: map[string]string{ + extension.LabelQuotaParent: "test", + }, + }, + Spec: v1alpha1.ElasticQuotaSpec{ + Max: MakeResourceList().CPU(10).Mem(10).Obj(), + Min: MakeResourceList().CPU(1).Mem(1).Obj(), + }, + }, + parentQuotaInfo: &v1alpha1.ElasticQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + extension.AnnotationMinExcess: `{"cpu":1,"memory":1}`, + }, + }, + Spec: v1alpha1.ElasticQuotaSpec{ + Max: MakeResourceList().CPU(10).Mem(10).Obj(), + Min: MakeResourceList().CPU(5).Mem(5).Obj(), + }, + }, + expectedStatus: *framework.NewStatus(framework.Success, ""), + }, { + name: "reject when cpu reach the min-excess limit", + pod: MakePod("t1-ns1", "pod1").Label(extension.LabelQuotaName, "test-child").Container( + MakeResourceList().CPU(3).Mem(3).Obj()).Obj(), + quotaInfo: &v1alpha1.ElasticQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-child", + Labels: map[string]string{ + extension.LabelQuotaParent: "test", + }, + }, + Spec: v1alpha1.ElasticQuotaSpec{ + Max: MakeResourceList().CPU(10).Mem(10).Obj(), + Min: MakeResourceList().CPU(1).Mem(1).Obj(), + }, + }, + parentQuotaInfo: &v1alpha1.ElasticQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + extension.AnnotationMinExcess: `{"cpu":1,"memory":10}`, + }, + }, + Spec: v1alpha1.ElasticQuotaSpec{ + Max: MakeResourceList().CPU(30).Mem(30).Obj(), + Min: MakeResourceList().CPU(5).Mem(5).Obj(), + }, + }, + expectedStatus: *framework.NewStatus(framework.Unschedulable, + fmt.Sprintf("Insufficient min-excess quotas, "+ + "quotaNameTopo: %v, min-excess: %v, min-excess-used: %v, min-excess-used-delta: %v, "+ + "pod's request: %v, exceedDimensions: [cpu]", + []string{"test", "test-child"}, printResourceList(MakeResourceList().CPU(1).Mem(10).Obj()), + printResourceList(corev1.ResourceList{}), + printResourceList(MakeResourceList().CPU(2).Mem(2).Obj()), + printResourceList(MakeResourceList().CPU(3).Mem(3).Obj()))), + }, { + name: "reject when memory reach the min-excess limit", + pod: MakePod("t1-ns1", "pod1").Label(extension.LabelQuotaName, "test-child").Container( + MakeResourceList().CPU(3).Mem(3).Obj()).Obj(), + quotaInfo: &v1alpha1.ElasticQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-child", + Labels: map[string]string{ + extension.LabelQuotaParent: "test", + }, + }, + Spec: v1alpha1.ElasticQuotaSpec{ + Max: MakeResourceList().CPU(10).Mem(10).Obj(), + Min: MakeResourceList().CPU(1).Mem(1).Obj(), + }, + }, + parentQuotaInfo: &v1alpha1.ElasticQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + extension.AnnotationMinExcess: `{"cpu":10,"memory":1}`, + }, + }, + Spec: v1alpha1.ElasticQuotaSpec{ + Max: MakeResourceList().CPU(30).Mem(30).Obj(), + Min: MakeResourceList().CPU(5).Mem(5).Obj(), + }, + }, + expectedStatus: *framework.NewStatus(framework.Unschedulable, + fmt.Sprintf("Insufficient min-excess quotas, "+ + "quotaNameTopo: %v, min-excess: %v, min-excess-used: %v, min-excess-used-delta: %v, "+ + "pod's request: %v, exceedDimensions: [memory]", + []string{"test", "test-child"}, printResourceList(MakeResourceList().CPU(10).Mem(1).Obj()), + printResourceList(corev1.ResourceList{}), + printResourceList(MakeResourceList().CPU(2).Mem(2).Obj()), + printResourceList(MakeResourceList().CPU(3).Mem(3).Obj()))), + }, + } + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + suit := newPluginTestSuit(t, nil) + p, err := suit.proxyNew(suit.elasticQuotaArgs, suit.Handle) + assert.Nil(t, err) + gp := p.(*Plugin) + gp.pluginArgs.EnableCheckParentQuota = true + gp.OnQuotaAdd(tt.parentQuotaInfo) + gp.OnQuotaAdd(tt.quotaInfo) + podRequests := core.PodRequests(tt.pod) + status := *gp.checkQuotaRecursive(tt.quotaInfo.Name, []string{tt.quotaInfo.Name}, podRequests, + &checkQuotaRecursiveState{minExcessEnabled: true}) + assert.Equal(t, tt.expectedStatus, status) + }) + } +} diff --git a/pkg/webhook/elasticquota/quota_topology_check.go b/pkg/webhook/elasticquota/quota_topology_check.go index ec3bbaba9..f949b9f1c 100644 --- a/pkg/webhook/elasticquota/quota_topology_check.go +++ b/pkg/webhook/elasticquota/quota_topology_check.go @@ -64,6 +64,11 @@ func (qt *quotaTopology) validateQuotaSelfItem(quota *v1alpha1.ElasticQuota) err } } + // sanity check for min-excess + if _, err := extension.GetMinExcess(quota); err != nil { + return err + } + return nil } diff --git a/pkg/webhook/elasticquota/quota_topology_test.go b/pkg/webhook/elasticquota/quota_topology_test.go index a5f9baccc..00137ab18 100644 --- a/pkg/webhook/elasticquota/quota_topology_test.go +++ b/pkg/webhook/elasticquota/quota_topology_test.go @@ -94,6 +94,13 @@ func TestQuotaTopology_basicItemCheck(t *testing.T) { quota: MakeQuota("temp").sharedWeight(MakeResourceList().CPU(-1).Mem(1048576).Obj()).Obj(), err: fmt.Errorf("%v quota.Annotation[%v]'s value < 0, in dimension :%v", "temp", extension.AnnotationSharedWeight, "[cpu]"), }, + { + name: "invalid min-excess", + quota: MakeQuota("temp").Annotations(map[string]string{ + extension.AnnotationMinExcess: "invalid-min-excess", + }).Obj(), + err: fmt.Errorf("failed to unmarshal min-excess, err=invalid character 'i' looking for beginning of value"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {