Skip to content

Commit cd8d6ac

Browse files
authored
Merge pull request karmada-io#1429 from XiShanYongYe-Chang/federated-resource-qupta-validation
add validation for federatedResourceQuota create/update
2 parents a0325a1 + e7b4436 commit cd8d6ac

File tree

5 files changed

+404
-0
lines changed

5 files changed

+404
-0
lines changed

artifacts/deploy/webhook-configuration.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,17 @@ webhooks:
139139
sideEffects: None
140140
admissionReviewVersions: ["v1"]
141141
timeoutSeconds: 3
142+
- name: federatedresourcequota.karmada.io
143+
rules:
144+
- operations: ["CREATE", "UPDATE"]
145+
apiGroups: ["policy.karmada.io"]
146+
apiVersions: ["*"]
147+
resources: ["federatedresourcequotas"]
148+
scope: "Namespaced"
149+
clientConfig:
150+
url: https://karmada-webhook.karmada-system.svc:443/validate-federatedresourcequota
151+
caBundle: {{caBundle}}
152+
failurePolicy: Fail
153+
sideEffects: None
154+
admissionReviewVersions: [ "v1" ]
155+
timeoutSeconds: 3

cmd/webhook/app/webhook.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/karmada-io/karmada/pkg/webhook/clusteroverridepolicy"
2222
"github.com/karmada-io/karmada/pkg/webhook/clusterpropagationpolicy"
2323
"github.com/karmada-io/karmada/pkg/webhook/configuration"
24+
"github.com/karmada-io/karmada/pkg/webhook/federatedresourcequota"
2425
"github.com/karmada-io/karmada/pkg/webhook/overridepolicy"
2526
"github.com/karmada-io/karmada/pkg/webhook/propagationpolicy"
2627
"github.com/karmada-io/karmada/pkg/webhook/work"
@@ -102,6 +103,7 @@ func Run(ctx context.Context, opts *options.Options) error {
102103
hookServer.Register("/mutate-work", &webhook.Admission{Handler: &work.MutatingAdmission{}})
103104
hookServer.Register("/convert", &conversion.Webhook{})
104105
hookServer.Register("/validate-resourceinterpreterwebhookconfiguration", &webhook.Admission{Handler: &configuration.ValidatingAdmission{}})
106+
hookServer.Register("/validate-federatedresourcequota", &webhook.Admission{Handler: &federatedresourcequota.ValidatingAdmission{}})
105107
hookServer.WebhookMux.Handle("/readyz/", http.StripPrefix("/readyz/", &healthz.Handler{}))
106108

107109
// blocks until the context is done.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
Copyright 2014 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// This code is directly lifted from the Kubernetes codebase in order to avoid relying on the k8s.io/kubernetes package.
18+
// For reference:
19+
// https://github.com/kubernetes/kubernetes/blob/release-1.22/pkg/apis/core/helper/helpers.go#L57-L61
20+
// https://github.com/kubernetes/kubernetes/blob/release-1.22/pkg/apis/core/helper/helpers.go#L169-L192
21+
// https://github.com/kubernetes/kubernetes/blob/release-1.22/pkg/apis/core/helper/helpers.go#L212-L283
22+
23+
package helper
24+
25+
import (
26+
"fmt"
27+
"strings"
28+
29+
corev1 "k8s.io/api/core/v1"
30+
"k8s.io/apimachinery/pkg/util/sets"
31+
"k8s.io/apimachinery/pkg/util/validation"
32+
)
33+
34+
// IsQuotaHugePageResourceName returns true if the resource name has the quota
35+
// related huge page resource prefix.
36+
func IsQuotaHugePageResourceName(name corev1.ResourceName) bool {
37+
return strings.HasPrefix(string(name), corev1.ResourceHugePagesPrefix) || strings.HasPrefix(string(name), corev1.ResourceRequestsHugePagesPrefix)
38+
}
39+
40+
// IsExtendedResourceName returns true if:
41+
// 1. the resource name is not in the default namespace;
42+
// 2. resource name does not have "requests." prefix,
43+
// to avoid confusion with the convention in quota
44+
// 3. it satisfies the rules in IsQualifiedName() after converted into quota resource name
45+
func IsExtendedResourceName(name corev1.ResourceName) bool {
46+
if IsNativeResource(name) || strings.HasPrefix(string(name), corev1.DefaultResourceRequestsPrefix) {
47+
return false
48+
}
49+
// Ensure it satisfies the rules in IsQualifiedName() after converted into quota resource name
50+
nameForQuota := fmt.Sprintf("%s%s", corev1.DefaultResourceRequestsPrefix, string(name))
51+
if errs := validation.IsQualifiedName(string(nameForQuota)); len(errs) != 0 {
52+
return false
53+
}
54+
return true
55+
}
56+
57+
// IsNativeResource returns true if the resource name is in the
58+
// *kubernetes.io/ namespace. Partially-qualified (unprefixed) names are
59+
// implicitly in the kubernetes.io/ namespace.
60+
func IsNativeResource(name corev1.ResourceName) bool {
61+
return !strings.Contains(string(name), "/") ||
62+
strings.Contains(string(name), corev1.ResourceDefaultNamespacePrefix)
63+
}
64+
65+
var standardQuotaResources = sets.NewString(
66+
string(corev1.ResourceCPU),
67+
string(corev1.ResourceMemory),
68+
string(corev1.ResourceEphemeralStorage),
69+
string(corev1.ResourceRequestsCPU),
70+
string(corev1.ResourceRequestsMemory),
71+
string(corev1.ResourceRequestsStorage),
72+
string(corev1.ResourceRequestsEphemeralStorage),
73+
string(corev1.ResourceLimitsCPU),
74+
string(corev1.ResourceLimitsMemory),
75+
string(corev1.ResourceLimitsEphemeralStorage),
76+
string(corev1.ResourcePods),
77+
string(corev1.ResourceQuotas),
78+
string(corev1.ResourceServices),
79+
string(corev1.ResourceReplicationControllers),
80+
string(corev1.ResourceSecrets),
81+
string(corev1.ResourcePersistentVolumeClaims),
82+
string(corev1.ResourceConfigMaps),
83+
string(corev1.ResourceServicesNodePorts),
84+
string(corev1.ResourceServicesLoadBalancers),
85+
)
86+
87+
// IsStandardQuotaResourceName returns true if the resource is known to
88+
// the quota tracking system
89+
func IsStandardQuotaResourceName(str string) bool {
90+
return standardQuotaResources.Has(str) || IsQuotaHugePageResourceName(corev1.ResourceName(str))
91+
}
92+
93+
var standardResources = sets.NewString(
94+
string(corev1.ResourceCPU),
95+
string(corev1.ResourceMemory),
96+
string(corev1.ResourceEphemeralStorage),
97+
string(corev1.ResourceRequestsCPU),
98+
string(corev1.ResourceRequestsMemory),
99+
string(corev1.ResourceRequestsEphemeralStorage),
100+
string(corev1.ResourceLimitsCPU),
101+
string(corev1.ResourceLimitsMemory),
102+
string(corev1.ResourceLimitsEphemeralStorage),
103+
string(corev1.ResourcePods),
104+
string(corev1.ResourceQuotas),
105+
string(corev1.ResourceServices),
106+
string(corev1.ResourceReplicationControllers),
107+
string(corev1.ResourceSecrets),
108+
string(corev1.ResourceConfigMaps),
109+
string(corev1.ResourcePersistentVolumeClaims),
110+
string(corev1.ResourceStorage),
111+
string(corev1.ResourceRequestsStorage),
112+
string(corev1.ResourceServicesNodePorts),
113+
string(corev1.ResourceServicesLoadBalancers),
114+
)
115+
116+
// IsStandardResourceName returns true if the resource is known to the system
117+
func IsStandardResourceName(str string) bool {
118+
return standardResources.Has(str) || IsQuotaHugePageResourceName(corev1.ResourceName(str))
119+
}
120+
121+
var integerResources = sets.NewString(
122+
string(corev1.ResourcePods),
123+
string(corev1.ResourceQuotas),
124+
string(corev1.ResourceServices),
125+
string(corev1.ResourceReplicationControllers),
126+
string(corev1.ResourceSecrets),
127+
string(corev1.ResourceConfigMaps),
128+
string(corev1.ResourcePersistentVolumeClaims),
129+
string(corev1.ResourceServicesNodePorts),
130+
string(corev1.ResourceServicesLoadBalancers),
131+
)
132+
133+
// IsIntegerResourceName returns true if the resource is measured in integer values
134+
func IsIntegerResourceName(str string) bool {
135+
return integerResources.Has(str) || IsExtendedResourceName(corev1.ResourceName(str))
136+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package federatedresourcequota
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"strings"
7+
8+
corev1 "k8s.io/api/core/v1"
9+
"k8s.io/apimachinery/pkg/api/resource"
10+
"k8s.io/apimachinery/pkg/util/validation/field"
11+
"k8s.io/klog/v2"
12+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
13+
14+
clustervalidation "github.com/karmada-io/karmada/pkg/apis/cluster/validation"
15+
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
16+
)
17+
18+
// ValidatingAdmission validates FederatedResourceQuota object when creating/updating.
19+
type ValidatingAdmission struct {
20+
decoder *admission.Decoder
21+
}
22+
23+
// Check if our ValidatingAdmission implements necessary interface
24+
var _ admission.Handler = &ValidatingAdmission{}
25+
var _ admission.DecoderInjector = &ValidatingAdmission{}
26+
27+
// Handle implements admission.Handler interface.
28+
// It yields a response to an AdmissionRequest.
29+
func (v *ValidatingAdmission) Handle(ctx context.Context, req admission.Request) admission.Response {
30+
quota := &policyv1alpha1.FederatedResourceQuota{}
31+
32+
err := v.decoder.Decode(req, quota)
33+
if err != nil {
34+
return admission.Errored(http.StatusBadRequest, err)
35+
}
36+
klog.V(2).Infof("Validating FederatedResourceQuote(%s) for request: %s", klog.KObj(quota).String(), req.Operation)
37+
38+
if errs := validateFederatedResourceQuota(quota); len(errs) != 0 {
39+
klog.Errorf("%v", errs)
40+
return admission.Denied(errs.ToAggregate().Error())
41+
}
42+
43+
return admission.Allowed("")
44+
}
45+
46+
// InjectDecoder implements admission.DecoderInjector interface.
47+
// A decoder will be automatically injected.
48+
func (v *ValidatingAdmission) InjectDecoder(d *admission.Decoder) error {
49+
v.decoder = d
50+
return nil
51+
}
52+
53+
func validateFederatedResourceQuota(quota *policyv1alpha1.FederatedResourceQuota) field.ErrorList {
54+
errs := field.ErrorList{}
55+
errs = append(errs, validateFederatedResourceQuotaSpec(&quota.Spec, field.NewPath("spec"))...)
56+
errs = append(errs, validateFederatedResourceQuotaStatus(&quota.Status, field.NewPath("status"))...)
57+
return errs
58+
}
59+
60+
func validateFederatedResourceQuotaSpec(quotaSpec *policyv1alpha1.FederatedResourceQuotaSpec, fld *field.Path) field.ErrorList {
61+
errs := field.ErrorList{}
62+
63+
errs = append(errs, validateResourceList(quotaSpec.Overall, fld.Child("overall"))...)
64+
65+
fldPath := fld.Child("staticAssignments")
66+
for index := range quotaSpec.StaticAssignments {
67+
errs = append(errs, validateStaticAssignment(&quotaSpec.StaticAssignments[index], fldPath.Index(index))...)
68+
}
69+
70+
errs = append(errs, validateOverallAndAssignments(quotaSpec, fld)...)
71+
72+
return errs
73+
}
74+
75+
func validateStaticAssignment(staticAssignment *policyv1alpha1.StaticClusterAssignment, fld *field.Path) field.ErrorList {
76+
errs := field.ErrorList{}
77+
78+
if errMegs := clustervalidation.ValidateClusterName(staticAssignment.ClusterName); len(errMegs) > 0 {
79+
errs = append(errs, field.Invalid(fld.Child("clusterName"), staticAssignment.ClusterName, strings.Join(errMegs, ",")))
80+
}
81+
errs = append(errs, validateResourceList(staticAssignment.Hard, fld.Child("hard"))...)
82+
83+
return errs
84+
}
85+
86+
func validateOverallAndAssignments(quotaSpec *policyv1alpha1.FederatedResourceQuotaSpec, fld *field.Path) field.ErrorList {
87+
errs := field.ErrorList{}
88+
89+
overallPath := fld.Child("overall")
90+
for k, v := range quotaSpec.Overall {
91+
assignment := calculateAssignmentForResourceKey(k, quotaSpec.StaticAssignments)
92+
if v.Cmp(assignment) < 0 {
93+
errs = append(errs, field.Invalid(overallPath.Key(string(k)), v, "overall is less than assignments"))
94+
}
95+
}
96+
97+
staticAssignmentsPath := fld.Child("staticAssignments")
98+
for index, assignment := range quotaSpec.StaticAssignments {
99+
restPath := staticAssignmentsPath.Index(index).Child("hard")
100+
for k := range assignment.Hard {
101+
if _, exist := quotaSpec.Overall[k]; !exist {
102+
errs = append(errs, field.Invalid(restPath.Key(string(k)), k, "assignment resourceName is not exist in overall"))
103+
}
104+
}
105+
}
106+
107+
return errs
108+
}
109+
110+
func calculateAssignmentForResourceKey(resourceKey corev1.ResourceName, staticAssignments []policyv1alpha1.StaticClusterAssignment) resource.Quantity {
111+
quantity := resource.Quantity{}
112+
for index := range staticAssignments {
113+
q, exist := staticAssignments[index].Hard[resourceKey]
114+
if exist {
115+
quantity.Add(q)
116+
}
117+
}
118+
return quantity
119+
}
120+
121+
func validateFederatedResourceQuotaStatus(quotaStatus *policyv1alpha1.FederatedResourceQuotaStatus, fld *field.Path) field.ErrorList {
122+
errs := field.ErrorList{}
123+
124+
errs = append(errs, validateResourceList(quotaStatus.Overall, fld.Child("overall"))...)
125+
errs = append(errs, validateResourceList(quotaStatus.OverallUsed, fld.Child("overallUsed"))...)
126+
127+
fldPath := fld.Child("aggregatedStatus")
128+
for index := range quotaStatus.AggregatedStatus {
129+
errs = append(errs, validateClusterQuotaStatus(&quotaStatus.AggregatedStatus[index], fldPath.Index(index))...)
130+
}
131+
132+
return errs
133+
}
134+
135+
func validateClusterQuotaStatus(aggregatedStatus *policyv1alpha1.ClusterQuotaStatus, fld *field.Path) field.ErrorList {
136+
errs := field.ErrorList{}
137+
138+
if errMegs := clustervalidation.ValidateClusterName(aggregatedStatus.ClusterName); len(errMegs) > 0 {
139+
errs = append(errs, field.Invalid(fld.Child("clusterName"), aggregatedStatus.ClusterName, strings.Join(errMegs, ",")))
140+
}
141+
errs = append(errs, validateResourceList(aggregatedStatus.Hard, fld.Child("hard"))...)
142+
errs = append(errs, validateResourceList(aggregatedStatus.Used, fld.Child("used"))...)
143+
144+
return errs
145+
}
146+
147+
func validateResourceList(resourceList corev1.ResourceList, fld *field.Path) field.ErrorList {
148+
errs := field.ErrorList{}
149+
150+
for k, v := range resourceList {
151+
resPath := fld.Key(string(k))
152+
errs = append(errs, ValidateResourceQuotaResourceName(string(k), resPath)...)
153+
errs = append(errs, ValidateResourceQuantityValue(string(k), v, resPath)...)
154+
}
155+
156+
return errs
157+
}

0 commit comments

Comments
 (0)