From 35954cd5b13ef6359cf6b304feb49d81771e06ee Mon Sep 17 00:00:00 2001 From: Imtiaz Uddin Date: Fri, 29 Sep 2023 15:08:11 +0600 Subject: [PATCH] Add quota decision in resource calculator create api Signed-off-by: Imtiaz Uddin --- .../meta/resourcecalculator/storage.go | 121 +++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/pkg/registry/meta/resourcecalculator/storage.go b/pkg/registry/meta/resourcecalculator/storage.go index ba58de8c2..0be5290f9 100644 --- a/pkg/registry/meta/resourcecalculator/storage.go +++ b/pkg/registry/meta/resourcecalculator/storage.go @@ -18,7 +18,10 @@ package resourceCalculator import ( "context" + "fmt" + "strings" + core "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -28,6 +31,8 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/rest" kmapi "kmodules.xyz/client-go/api/v1" + "kmodules.xyz/client-go/cluster" + "kmodules.xyz/resource-metadata/apis/management/v1alpha1" rsapi "kmodules.xyz/resource-metadata/apis/meta/v1alpha1" resourcemetrics "kmodules.xyz/resource-metrics" "kmodules.xyz/resource-metrics/api" @@ -96,8 +101,9 @@ func (r *Storage) Create(ctx context.Context, obj runtime.Object, createValidati return nil, apierrors.NewInternalError(err) } rid := kmapi.NewResourceID(mapping) + pq := getProjectQuota(r.kc, u) - resp, err := ToGenericResource(&u, rid) + resp, err := ToGenericResource(&u, rid, pq) if err != nil { return nil, apierrors.NewInternalError(err) } @@ -110,7 +116,7 @@ func (r *Storage) ConvertToTable(ctx context.Context, object runtime.Object, tab return r.convertor.ConvertToTable(ctx, object, tableOptions) } -func ToGenericResource(item *unstructured.Unstructured, apiType *kmapi.ResourceID) (*rsapi.ResourceCalculatorResponse, error) { +func ToGenericResource(item *unstructured.Unstructured, apiType *kmapi.ResourceID, pq *v1alpha1.ProjectQuota) (*rsapi.ResourceCalculatorResponse, error) { content := item.UnstructuredContent() var genres rsapi.ResourceCalculatorResponse @@ -184,6 +190,117 @@ func ToGenericResource(item *unstructured.Unstructured, apiType *kmapi.ResourceI } genres.RoleResourceLimits = rv } + { + rv, err := quota(content, pq, apiType) + if err != nil { + return nil, err + } + genres.Quota = *rv + } } return &genres, nil } + +func quota(obj map[string]interface{}, pq *v1alpha1.ProjectQuota, apiType *kmapi.ResourceID) (*rsapi.QuotaDecision, error) { + qd := &rsapi.QuotaDecision{ + Decision: rsapi.DecisionAllow, + Violations: make([]string, 0), + } + if pq == nil { + qd.Decision = rsapi.DecisionNoOpinion + return qd, nil + } + c, err := api.Load(obj) + if err != nil { + return nil, err + } + + requests, err := c.AppResourceRequests(obj) + if err != nil { + return nil, err + } + limits, err := c.AppResourceLimits(obj) + if err != nil { + return nil, err + } + + for _, quota := range pq.Status.Quotas { + if quota.Group == apiType.Group { + if quota.Kind != "" && quota.Kind != apiType.Kind { + continue + } + hardRequests, hardLimits := extractRequestsLimits(quota.Hard) + usedRequests, usedLimits := extractRequestsLimits(quota.Used) + + totRequestsUsage := api.AddResourceList(requests, usedRequests) + for rn, usageQuan := range totRequestsUsage { + hr, found := hardRequests[rn] + if !found { + continue + } + if usageQuan.Cmp(hr) > 0 { + r := requests[rn] + u := usedRequests[rn] + l := hardRequests[rn] + + qd.Decision = rsapi.DecisionDeny + qd.Violations = append(qd.Violations, + fmt.Sprintf("Project quota exceeded. Requested: requests.%s=%s, Used: requests.%s=%s, Limited: requests.%s=%s", rn, r.String(), rn, u.String(), rn, l.String())) + } + } + + totLimitsUsage := api.AddResourceList(limits, usedLimits) + for rn, usageQuan := range totLimitsUsage { + hl, found := hardLimits[rn] + if !found { + continue + } + if usageQuan.Cmp(hl) > 0 { + r := limits[rn] + u := usedLimits[rn] + l := hardLimits[rn] + + qd.Decision = rsapi.DecisionDeny + qd.Violations = append(qd.Violations, + fmt.Sprintf("Project quota exceeded. Requested: limits.%s=%s, Used: limits.%s=%s, Limited: limits.%s=%s", rn, r.String(), rn, u.String(), rn, l.String())) + } + } + } + } + + return qd, nil +} + +func extractRequestsLimits(res core.ResourceList) (core.ResourceList, core.ResourceList) { + requests := core.ResourceList{} + limits := core.ResourceList{} + + for fullName, quan := range res { + identifier, name, found := strings.Cut(fullName.String(), ".") + if !found { + continue + } + + if identifier == "requests" { + requests[core.ResourceName(name)] = quan + } else { + limits[core.ResourceName(name)] = quan + } + } + + return requests, limits +} + +func getProjectQuota(kc client.Client, u unstructured.Unstructured) *v1alpha1.ProjectQuota { + projectId, _, err := cluster.GetProjectId(kc, u.GetNamespace()) + if err != nil { + return nil + } + var pj v1alpha1.ProjectQuota + err = kc.Get(context.TODO(), client.ObjectKey{Name: projectId}, &pj) + if err != nil { + return nil + } + + return &pj +}