Skip to content

Commit 94a8a8e

Browse files
authored
Merge pull request #6481 from mszacillo/quota-exceeded-condition
Add quota exceeded conditions in case scheduler fails to update resourcebinding
2 parents 8bcd2c3 + ee05454 commit 94a8a8e

File tree

5 files changed

+48
-18
lines changed

5 files changed

+48
-18
lines changed

pkg/apis/work/v1alpha2/binding_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,11 @@ const (
432432
// BindingReasonUnschedulable reason in Scheduled condition means that the scheduler can't schedule
433433
// the binding right now, for example due to insufficient resources in the clusters.
434434
BindingReasonUnschedulable = "Unschedulable"
435+
436+
// BindingReasonQuotaExceeded reason in Scheduled condition means that the scheduler can't schedule
437+
// the binding because the resource requirement exceeds one or more of the FederatedResourceQuotas
438+
// defined in the namespace.
439+
BindingReasonQuotaExceeded = "QuotaExceeded"
435440
)
436441

437442
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

pkg/scheduler/helper.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ package scheduler
1919
import (
2020
"encoding/json"
2121
"errors"
22+
"net/http"
2223
"reflect"
2324

25+
apierrors "k8s.io/apimachinery/pkg/api/errors"
2426
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2527
utilerrors "k8s.io/apimachinery/pkg/util/errors"
2628
"k8s.io/klog/v2"
@@ -125,13 +127,19 @@ func getConditionByError(err error) (metav1.Condition, bool) {
125127
if fitErrMatcher(err) {
126128
return util.NewCondition(workv1alpha2.Scheduled, workv1alpha2.BindingReasonNoClusterFit, err.Error(), metav1.ConditionFalse), true
127129
}
130+
128131
var aggregatedErr utilerrors.Aggregate
129132
if errors.As(err, &aggregatedErr) {
130133
for _, ae := range aggregatedErr.Errors() {
131134
if fitErrMatcher(ae) {
132135
// if aggregated NoClusterFit error got, we do not ignore error but retry scheduling.
133136
return util.NewCondition(workv1alpha2.Scheduled, workv1alpha2.BindingReasonNoClusterFit, err.Error(), metav1.ConditionFalse), false
134137
}
138+
// ResourceBinding validation webhook will return error with "FederatedResourceQuota" if quota exceeded
139+
var statusErr *apierrors.StatusError
140+
if errors.As(ae, &statusErr) && statusErr.Status().Code == http.StatusForbidden && statusErr.Status().Reason == util.QuotaExceededReason {
141+
return util.NewCondition(workv1alpha2.Scheduled, workv1alpha2.BindingReasonQuotaExceeded, ae.Error(), metav1.ConditionFalse), false
142+
}
135143
}
136144
}
137145
return util.NewCondition(workv1alpha2.Scheduled, workv1alpha2.BindingReasonSchedulerError, err.Error(), metav1.ConditionFalse), false

pkg/util/constants.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"time"
2121

2222
discoveryv1 "k8s.io/api/discovery/v1"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2324

2425
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
2526
)
@@ -216,6 +217,11 @@ const (
216217
CompletionsField = "completions"
217218
)
218219

220+
const (
221+
// QuotaExceededReason is a unique reason to describe QuotaExceeded events
222+
QuotaExceededReason metav1.StatusReason = "QuotaExceeded"
223+
)
224+
219225
// ContextKey is the key of context.
220226
type ContextKey string
221227

pkg/webhook/resourcebinding/validating.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
admissionv1 "k8s.io/api/admission/v1"
2727
corev1 "k8s.io/api/core/v1"
2828
apierrors "k8s.io/apimachinery/pkg/api/errors"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2930
"k8s.io/client-go/util/retry"
3031
"k8s.io/klog/v2"
3132
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -309,19 +310,33 @@ func (v *ValidatingAdmission) processSingleFRQ(frqItem policyv1alpha1.FederatedR
309310

310311
potentialNewOverallUsedForThisFRQ := addResourceLists(frqItem.Status.OverallUsed, deltaForThisFRQ)
311312

312-
if !isAllowed(potentialNewOverallUsedForThisFRQ, frqItem.Spec.Overall) {
313-
errMsg := fmt.Sprintf("Quota exceeded for FederatedResourceQuota %s/%s. ResourceBinding %s/%s will be denied.",
313+
isAllowed, errMsg := isAllowed(potentialNewOverallUsedForThisFRQ, frqItem)
314+
if !isAllowed {
315+
klog.Warningf("Quota exceeded for FederatedResourceQuota %s/%s. ResourceBinding %s/%s will be denied.",
314316
frqItem.Namespace, frqItem.Name, rbNamespace, rbName)
315-
klog.Warning(errMsg)
316-
resp := admission.Denied(errMsg)
317-
return nil, "", &resp
317+
resp := buildDenyResponse(errMsg)
318+
return nil, "", resp
318319
}
319320

320321
msg := fmt.Sprintf("Quota check passed for FRQ %s/%s.", frqItem.Namespace, frqItem.Name)
321322
klog.V(3).Infof("FRQ %s/%s will be updated. New OverallUsed: %v", frqItem.Namespace, frqItem.Name, potentialNewOverallUsedForThisFRQ)
322323
return potentialNewOverallUsedForThisFRQ, msg, nil
323324
}
324325

326+
func buildDenyResponse(errMsg string) *admission.Response {
327+
resp := admission.Response{
328+
AdmissionResponse: admissionv1.AdmissionResponse{
329+
Allowed: false,
330+
Result: &metav1.Status{
331+
Message: errMsg,
332+
Reason: util.QuotaExceededReason,
333+
Code: int32(http.StatusForbidden),
334+
},
335+
},
336+
}
337+
return &resp
338+
}
339+
325340
func calculateResourceUsage(rb *workv1alpha2.ResourceBinding) (corev1.ResourceList, error) {
326341
if rb == nil || rb.Spec.ReplicaRequirements == nil || len(rb.Spec.ReplicaRequirements.ResourceRequest) == 0 || len(rb.Spec.Clusters) == 0 {
327342
return corev1.ResourceList{}, nil
@@ -437,9 +452,10 @@ func addResourceLists(list1, list2 corev1.ResourceList) corev1.ResourceList {
437452
return result
438453
}
439454

440-
func isAllowed(requested, allowedLimits corev1.ResourceList) bool {
455+
func isAllowed(requested corev1.ResourceList, frqItem policyv1alpha1.FederatedResourceQuota) (bool, string) {
456+
allowedLimits := frqItem.Spec.Overall
441457
if allowedLimits == nil {
442-
return true
458+
return true, ""
443459
}
444460
for name, reqQty := range requested {
445461
if reqQty.IsZero() {
@@ -451,11 +467,12 @@ func isAllowed(requested, allowedLimits corev1.ResourceList) bool {
451467
continue
452468
}
453469
if reqQty.Cmp(limitQty) > 0 {
454-
klog.Warningf("Quota exceeded for resource %s: requested sum %s, limit %s", name, reqQty.String(), limitQty.String())
455-
return false
470+
msg := fmt.Sprintf("FederatedResourceQuota(%s/%s) exceeded for resource %s: requested sum %s, limit %s.", frqItem.Namespace, frqItem.Name, name, reqQty.String(), limitQty.String())
471+
klog.Warning(msg)
472+
return false, msg
456473
}
457474
}
458-
return true
475+
return true, ""
459476
}
460477

461478
func filterResourceListByKeys(original corev1.ResourceList, filterKeySource corev1.ResourceList) corev1.ResourceList {

pkg/webhook/resourcebinding/validating_test.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,7 @@ func TestValidatingAdmission_Handle(t *testing.T) {
405405
decoder: &fakeDecoder{decodeObj: rbCreateExceeds},
406406
clientObjects: []client.Object{frqForCreateExceeds},
407407
featureGateEnabled: true,
408-
wantResponse: admission.Denied(
409-
fmt.Sprintf("Quota exceeded for FederatedResourceQuota %s/%s. ResourceBinding %s/%s will be denied.",
410-
frqForCreateExceeds.Namespace, frqForCreateExceeds.Name, rbCreateExceeds.Namespace, rbCreateExceeds.Name),
411-
),
408+
wantResponse: admission.Denied("FederatedResourceQuota(quota-ns/frq-create-exceeds) exceeded for resource cpu: requested sum 200m, limit 150m."),
412409
},
413410
{
414411
name: "update passes quota (allowed response, non-dryrun)",
@@ -449,10 +446,7 @@ func TestValidatingAdmission_Handle(t *testing.T) {
449446
decoder: &fakeDecoder{decodeObj: rbUpdateFailNew, rawDecodedObj: rbUpdateFailOld},
450447
clientObjects: []client.Object{frqForUpdateFail},
451448
featureGateEnabled: true,
452-
wantResponse: admission.Denied(
453-
fmt.Sprintf("Quota exceeded for FederatedResourceQuota %s/%s. ResourceBinding %s/%s will be denied.",
454-
frqForUpdateFail.Namespace, frqForUpdateFail.Name, rbUpdateFailNew.Namespace, rbUpdateFailNew.Name),
455-
),
449+
wantResponse: admission.Denied("FederatedResourceQuota(quota-ns/frq-update-fail) exceeded for resource cpu: requested sum 110m, limit 100m."),
456450
},
457451
}
458452

0 commit comments

Comments
 (0)