volcano.sh/volcano@v1.9.0/pkg/webhooks/admission/queues/validate/validate_queue.go (about) 1 /* 2 Copyright 2018 The Volcano 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 validate 18 19 import ( 20 "context" 21 "fmt" 22 "strconv" 23 "strings" 24 25 admissionv1 "k8s.io/api/admission/v1" 26 whv1 "k8s.io/api/admissionregistration/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/util/validation/field" 29 "k8s.io/klog/v2" 30 31 schedulingv1beta1 "volcano.sh/apis/pkg/apis/scheduling/v1beta1" 32 "volcano.sh/volcano/pkg/webhooks/router" 33 "volcano.sh/volcano/pkg/webhooks/schema" 34 "volcano.sh/volcano/pkg/webhooks/util" 35 ) 36 37 func init() { 38 router.RegisterAdmission(service) 39 } 40 41 var service = &router.AdmissionService{ 42 Path: "/queues/validate", 43 Func: AdmitQueues, 44 45 Config: config, 46 47 ValidatingConfig: &whv1.ValidatingWebhookConfiguration{ 48 Webhooks: []whv1.ValidatingWebhook{{ 49 Name: "validatequeue.volcano.sh", 50 Rules: []whv1.RuleWithOperations{ 51 { 52 Operations: []whv1.OperationType{whv1.Create, whv1.Update, whv1.Delete}, 53 Rule: whv1.Rule{ 54 APIGroups: []string{schedulingv1beta1.SchemeGroupVersion.Group}, 55 APIVersions: []string{schedulingv1beta1.SchemeGroupVersion.Version}, 56 Resources: []string{"queues"}, 57 }, 58 }, 59 }, 60 }}, 61 }, 62 } 63 64 var config = &router.AdmissionServiceConfig{} 65 66 // AdmitQueues is to admit queues and return response. 67 func AdmitQueues(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse { 68 klog.V(3).Infof("Admitting %s queue %s.", ar.Request.Operation, ar.Request.Name) 69 70 queue, err := schema.DecodeQueue(ar.Request.Object, ar.Request.Resource) 71 if err != nil { 72 return util.ToAdmissionResponse(err) 73 } 74 75 switch ar.Request.Operation { 76 case admissionv1.Create, admissionv1.Update: 77 err = validateQueue(queue) 78 case admissionv1.Delete: 79 err = validateQueueDeleting(ar.Request.Name) 80 default: 81 return util.ToAdmissionResponse(fmt.Errorf("invalid operation `%s`, "+ 82 "expect operation to be `CREATE`, `UPDATE` or `DELETE`", ar.Request.Operation)) 83 } 84 85 if err != nil { 86 return &admissionv1.AdmissionResponse{ 87 Allowed: false, 88 Result: &metav1.Status{Message: err.Error()}, 89 } 90 } 91 92 return &admissionv1.AdmissionResponse{ 93 Allowed: true, 94 } 95 } 96 97 func validateQueue(queue *schedulingv1beta1.Queue) error { 98 errs := field.ErrorList{} 99 resourcePath := field.NewPath("requestBody") 100 101 errs = append(errs, validateStateOfQueue(queue.Status.State, resourcePath.Child("spec").Child("state"))...) 102 errs = append(errs, validateWeightOfQueue(queue.Spec.Weight, resourcePath.Child("spec").Child("weight"))...) 103 errs = append(errs, validateHierarchicalAttributes(queue, resourcePath.Child("metadata").Child("annotations"))...) 104 105 if len(errs) > 0 { 106 return errs.ToAggregate() 107 } 108 109 return nil 110 } 111 func validateHierarchicalAttributes(queue *schedulingv1beta1.Queue, fldPath *field.Path) field.ErrorList { 112 errs := field.ErrorList{} 113 hierarchy := queue.Annotations[schedulingv1beta1.KubeHierarchyAnnotationKey] 114 hierarchicalWeights := queue.Annotations[schedulingv1beta1.KubeHierarchyWeightAnnotationKey] 115 if hierarchy != "" || hierarchicalWeights != "" { 116 paths := strings.Split(hierarchy, "/") 117 weights := strings.Split(hierarchicalWeights, "/") 118 // path length must be the same with weights length 119 if len(paths) != len(weights) { 120 return append(errs, field.Invalid(fldPath, hierarchy, 121 fmt.Sprintf("%s must have the same length with %s", 122 schedulingv1beta1.KubeHierarchyAnnotationKey, 123 schedulingv1beta1.KubeHierarchyWeightAnnotationKey, 124 ))) 125 } 126 127 // check weights format 128 for _, weight := range weights { 129 weightFloat, err := strconv.ParseFloat(weight, 64) 130 if err != nil { 131 return append(errs, field.Invalid(fldPath, hierarchicalWeights, 132 fmt.Sprintf("%s in the %s is invalid number: %v", 133 weight, hierarchicalWeights, err, 134 ))) 135 } 136 if weightFloat <= 0 { 137 return append(errs, field.Invalid(fldPath, hierarchicalWeights, 138 fmt.Sprintf("%s in the %s must be larger than 0", 139 weight, hierarchicalWeights, 140 ))) 141 } 142 } 143 144 // The node is not allowed to be in the sub path of a node. 145 // For example, a queue with "root/sci" conflicts with a queue with "root/sci/dev" 146 queueList, err := config.VolcanoClient.SchedulingV1beta1().Queues().List(context.TODO(), metav1.ListOptions{}) 147 if err != nil { 148 return append(errs, field.Invalid(fldPath, hierarchy, 149 fmt.Sprintf("checking %s, list queues failed: %v", 150 schedulingv1beta1.KubeHierarchyAnnotationKey, 151 err, 152 ))) 153 } 154 for _, queueInTree := range queueList.Items { 155 hierarchyInTree := queueInTree.Annotations[schedulingv1beta1.KubeHierarchyAnnotationKey] 156 if hierarchyInTree != "" && queue.Name != queueInTree.Name && 157 strings.HasPrefix(hierarchyInTree, hierarchy) { 158 return append(errs, field.Invalid(fldPath, hierarchy, 159 fmt.Sprintf("%s is not allowed to be in the sub path of %s of queue %s", 160 hierarchy, hierarchyInTree, queueInTree.Name))) 161 } 162 } 163 } 164 return errs 165 } 166 167 func validateStateOfQueue(value schedulingv1beta1.QueueState, fldPath *field.Path) field.ErrorList { 168 errs := field.ErrorList{} 169 170 if len(value) == 0 { 171 return errs 172 } 173 174 validQueueStates := []schedulingv1beta1.QueueState{ 175 schedulingv1beta1.QueueStateOpen, 176 schedulingv1beta1.QueueStateClosed, 177 } 178 179 for _, validQueue := range validQueueStates { 180 if value == validQueue { 181 return errs 182 } 183 } 184 185 return append(errs, field.Invalid(fldPath, value, fmt.Sprintf("queue state must be in %v", validQueueStates))) 186 } 187 188 func validateWeightOfQueue(value int32, fldPath *field.Path) field.ErrorList { 189 errs := field.ErrorList{} 190 if value > 0 { 191 return errs 192 } 193 return append(errs, field.Invalid(fldPath, value, "queue weight must be a positive integer")) 194 } 195 196 func validateQueueDeleting(queue string) error { 197 if queue == "default" { 198 return fmt.Errorf("`%s` queue can not be deleted", "default") 199 } 200 201 _, err := config.VolcanoClient.SchedulingV1beta1().Queues().Get(context.TODO(), queue, metav1.GetOptions{}) 202 if err != nil { 203 return err 204 } 205 206 return nil 207 }