sigs.k8s.io/kueue@v0.6.2/pkg/webhooks/clusterqueue_webhook.go (about) 1 /* 2 Copyright 2021 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 package webhooks 18 19 import ( 20 "context" 21 22 corev1 "k8s.io/api/core/v1" 23 "k8s.io/apimachinery/pkg/api/resource" 24 apivalidation "k8s.io/apimachinery/pkg/api/validation" 25 "k8s.io/apimachinery/pkg/apis/meta/v1/validation" 26 "k8s.io/apimachinery/pkg/runtime" 27 "k8s.io/apimachinery/pkg/util/sets" 28 "k8s.io/apimachinery/pkg/util/validation/field" 29 "k8s.io/klog/v2" 30 ctrl "sigs.k8s.io/controller-runtime" 31 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 32 "sigs.k8s.io/controller-runtime/pkg/webhook" 33 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 34 35 kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1" 36 "sigs.k8s.io/kueue/pkg/constants" 37 "sigs.k8s.io/kueue/pkg/features" 38 ) 39 40 const ( 41 limitIsEmptyErrorMsg string = `must be nil when cohort is empty` 42 lendingLimitErrorMsg string = `must be less than or equal to the nominalQuota` 43 ) 44 45 type ClusterQueueWebhook struct{} 46 47 func setupWebhookForClusterQueue(mgr ctrl.Manager) error { 48 return ctrl.NewWebhookManagedBy(mgr). 49 For(&kueue.ClusterQueue{}). 50 WithDefaulter(&ClusterQueueWebhook{}). 51 WithValidator(&ClusterQueueWebhook{}). 52 Complete() 53 } 54 55 // +kubebuilder:webhook:path=/mutate-kueue-x-k8s-io-v1beta1-clusterqueue,mutating=true,failurePolicy=fail,sideEffects=None,groups=kueue.x-k8s.io,resources=clusterqueues,verbs=create,versions=v1beta1,name=mclusterqueue.kb.io,admissionReviewVersions=v1 56 57 var _ webhook.CustomDefaulter = &ClusterQueueWebhook{} 58 59 // Default implements webhook.CustomDefaulter so a webhook will be registered for the type 60 func (w *ClusterQueueWebhook) Default(ctx context.Context, obj runtime.Object) error { 61 cq := obj.(*kueue.ClusterQueue) 62 log := ctrl.LoggerFrom(ctx).WithName("clusterqueue-webhook") 63 log.V(5).Info("Applying defaults", "clusterQueue", klog.KObj(cq)) 64 if !controllerutil.ContainsFinalizer(cq, kueue.ResourceInUseFinalizerName) { 65 controllerutil.AddFinalizer(cq, kueue.ResourceInUseFinalizerName) 66 } 67 if cq.Spec.Preemption == nil { 68 cq.Spec.Preemption = &kueue.ClusterQueuePreemption{ 69 WithinClusterQueue: kueue.PreemptionPolicyNever, 70 ReclaimWithinCohort: kueue.PreemptionPolicyNever, 71 } 72 } 73 if cq.Spec.Preemption.BorrowWithinCohort == nil { 74 cq.Spec.Preemption.BorrowWithinCohort = &kueue.BorrowWithinCohort{ 75 Policy: kueue.BorrowWithinCohortPolicyNever, 76 } 77 } 78 if cq.Spec.FlavorFungibility == nil { 79 cq.Spec.FlavorFungibility = &kueue.FlavorFungibility{ 80 WhenCanBorrow: kueue.Borrow, 81 WhenCanPreempt: kueue.TryNextFlavor, 82 } 83 } 84 return nil 85 } 86 87 // +kubebuilder:webhook:path=/validate-kueue-x-k8s-io-v1beta1-clusterqueue,mutating=false,failurePolicy=fail,sideEffects=None,groups=kueue.x-k8s.io,resources=clusterqueues,verbs=create;update,versions=v1beta1,name=vclusterqueue.kb.io,admissionReviewVersions=v1 88 89 var _ webhook.CustomValidator = &ClusterQueueWebhook{} 90 91 // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type 92 func (w *ClusterQueueWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 93 cq := obj.(*kueue.ClusterQueue) 94 log := ctrl.LoggerFrom(ctx).WithName("clusterqueue-webhook") 95 log.V(5).Info("Validating create", "clusterQueue", klog.KObj(cq)) 96 allErrs := ValidateClusterQueue(cq) 97 return nil, allErrs.ToAggregate() 98 } 99 100 // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type 101 func (w *ClusterQueueWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { 102 newCQ := newObj.(*kueue.ClusterQueue) 103 oldCQ := oldObj.(*kueue.ClusterQueue) 104 105 log := ctrl.LoggerFrom(ctx).WithName("clusterqueue-webhook") 106 log.V(5).Info("Validating update", "clusterQueue", klog.KObj(newCQ)) 107 allErrs := ValidateClusterQueueUpdate(newCQ, oldCQ) 108 return nil, allErrs.ToAggregate() 109 } 110 111 // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type 112 func (w *ClusterQueueWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 113 return nil, nil 114 } 115 116 func ValidateClusterQueue(cq *kueue.ClusterQueue) field.ErrorList { 117 path := field.NewPath("spec") 118 119 var allErrs field.ErrorList 120 if len(cq.Spec.Cohort) != 0 { 121 allErrs = append(allErrs, validateNameReference(cq.Spec.Cohort, path.Child("cohort"))...) 122 } 123 allErrs = append(allErrs, validateResourceGroups(cq.Spec.ResourceGroups, cq.Spec.Cohort, path.Child("resourceGroups"))...) 124 allErrs = append(allErrs, 125 validation.ValidateLabelSelector(cq.Spec.NamespaceSelector, validation.LabelSelectorValidationOptions{}, path.Child("namespaceSelector"))...) 126 if cq.Spec.Preemption != nil { 127 allErrs = append(allErrs, validatePreemption(cq.Spec.Preemption, path.Child("preemption"))...) 128 } 129 return allErrs 130 } 131 132 // Since Kubernetes 1.25, we can use CEL validation rules to implement 133 // a few common immutability patterns directly in the manifest for a CRD. 134 // ref: https://kubernetes.io/blog/2022/09/29/enforce-immutability-using-cel/ 135 // We need to validate the spec.queueingStrategy immutable manually before Kubernetes 1.25. 136 func ValidateClusterQueueUpdate(newObj, oldObj *kueue.ClusterQueue) field.ErrorList { 137 var allErrs field.ErrorList 138 allErrs = append(allErrs, ValidateClusterQueue(newObj)...) 139 allErrs = append(allErrs, apivalidation.ValidateImmutableField(newObj.Spec.QueueingStrategy, oldObj.Spec.QueueingStrategy, field.NewPath("spec", "queueingStrategy"))...) 140 return allErrs 141 } 142 143 func validatePreemption(preemption *kueue.ClusterQueuePreemption, path *field.Path) field.ErrorList { 144 var allErrs field.ErrorList 145 if preemption.ReclaimWithinCohort == kueue.PreemptionPolicyNever && 146 preemption.BorrowWithinCohort != nil && 147 preemption.BorrowWithinCohort.Policy != kueue.BorrowWithinCohortPolicyNever { 148 allErrs = append(allErrs, field.Invalid(path, preemption, "reclaimWithinCohort=Never and borrowWithinCohort.Policy!=Never")) 149 } 150 return allErrs 151 } 152 153 func validateResourceGroups(resourceGroups []kueue.ResourceGroup, cohort string, path *field.Path) field.ErrorList { 154 var allErrs field.ErrorList 155 seenResources := sets.New[corev1.ResourceName]() 156 seenFlavors := sets.New[kueue.ResourceFlavorReference]() 157 158 for i, rg := range resourceGroups { 159 path := path.Index(i) 160 for j, name := range rg.CoveredResources { 161 path := path.Child("coveredResources").Index(j) 162 allErrs = append(allErrs, validateResourceName(name, path)...) 163 if seenResources.Has(name) { 164 allErrs = append(allErrs, field.Duplicate(path, name)) 165 } else { 166 seenResources.Insert(name) 167 } 168 } 169 for j, fqs := range rg.Flavors { 170 path := path.Child("flavors").Index(j) 171 allErrs = append(allErrs, validateFlavorQuotas(fqs, rg.CoveredResources, cohort, path)...) 172 if seenFlavors.Has(fqs.Name) { 173 allErrs = append(allErrs, field.Duplicate(path.Child("name"), fqs.Name)) 174 } else { 175 seenFlavors.Insert(fqs.Name) 176 } 177 } 178 } 179 return allErrs 180 } 181 182 func validateFlavorQuotas(flavorQuotas kueue.FlavorQuotas, coveredResources []corev1.ResourceName, cohort string, path *field.Path) field.ErrorList { 183 allErrs := validateNameReference(string(flavorQuotas.Name), path.Child("name")) 184 if len(flavorQuotas.Resources) != len(coveredResources) { 185 allErrs = append(allErrs, field.Invalid(path.Child("resources"), field.OmitValueType{}, "must have the same number of resources as the coveredResources")) 186 } 187 188 for i, rq := range flavorQuotas.Resources { 189 if i >= len(coveredResources) { 190 break 191 } 192 path := path.Child("resources").Index(i) 193 if rq.Name != coveredResources[i] { 194 allErrs = append(allErrs, field.Invalid(path.Child("name"), rq.Name, "must match the name in coveredResources")) 195 } 196 allErrs = append(allErrs, validateResourceQuantity(rq.NominalQuota, path.Child("nominalQuota"))...) 197 if rq.BorrowingLimit != nil { 198 borrowingLimitPath := path.Child("borrowingLimit") 199 allErrs = append(allErrs, validateResourceQuantity(*rq.BorrowingLimit, borrowingLimitPath)...) 200 allErrs = append(allErrs, validateLimit(*rq.BorrowingLimit, cohort, borrowingLimitPath)...) 201 } 202 if features.Enabled(features.LendingLimit) && rq.LendingLimit != nil { 203 lendingLimitPath := path.Child("lendingLimit") 204 allErrs = append(allErrs, validateResourceQuantity(*rq.LendingLimit, lendingLimitPath)...) 205 allErrs = append(allErrs, validateLimit(*rq.LendingLimit, cohort, lendingLimitPath)...) 206 allErrs = append(allErrs, validateLendingLimit(*rq.LendingLimit, rq.NominalQuota, lendingLimitPath)...) 207 } 208 } 209 return allErrs 210 } 211 212 // validateResourceQuantity enforces that specified quantity is valid for specified resource 213 func validateResourceQuantity(value resource.Quantity, fldPath *field.Path) field.ErrorList { 214 var allErrs field.ErrorList 215 if value.Cmp(resource.Quantity{}) < 0 { 216 allErrs = append(allErrs, field.Invalid(fldPath, value.String(), constants.IsNegativeErrorMsg)) 217 } 218 return allErrs 219 } 220 221 // validateLimit enforces that BorrowingLimit or LendingLimit must be nil when cohort is empty 222 func validateLimit(limit resource.Quantity, cohort string, fldPath *field.Path) field.ErrorList { 223 var allErrs field.ErrorList 224 if len(cohort) == 0 { 225 allErrs = append(allErrs, field.Invalid(fldPath, limit.String(), limitIsEmptyErrorMsg)) 226 } 227 return allErrs 228 } 229 230 // validateLendingLimit enforces that LendingLimit is not greater than NominalQuota 231 func validateLendingLimit(lend, nominal resource.Quantity, fldPath *field.Path) field.ErrorList { 232 var allErrs field.ErrorList 233 if lend.Cmp(nominal) > 0 { 234 allErrs = append(allErrs, field.Invalid(fldPath, lend.String(), lendingLimitErrorMsg)) 235 } 236 return allErrs 237 }