volcano.sh/volcano@v1.9.0/pkg/webhooks/admission/pods/validate/admit_pod.go (about) 1 /* 2 Copyright 2019 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 v1 "k8s.io/api/core/v1" 28 apierrors "k8s.io/apimachinery/pkg/api/errors" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/util/intstr" 31 "k8s.io/klog/v2" 32 33 "volcano.sh/apis/pkg/apis/helpers" 34 vcv1beta1 "volcano.sh/apis/pkg/apis/scheduling/v1beta1" 35 commonutil "volcano.sh/volcano/pkg/util" 36 "volcano.sh/volcano/pkg/webhooks/router" 37 "volcano.sh/volcano/pkg/webhooks/schema" 38 "volcano.sh/volcano/pkg/webhooks/util" 39 ) 40 41 func init() { 42 router.RegisterAdmission(service) 43 } 44 45 var service = &router.AdmissionService{ 46 Path: "/pods/validate", 47 Func: AdmitPods, 48 49 Config: config, 50 51 ValidatingConfig: &whv1.ValidatingWebhookConfiguration{ 52 Webhooks: []whv1.ValidatingWebhook{{ 53 Name: "validatepod.volcano.sh", 54 Rules: []whv1.RuleWithOperations{ 55 { 56 Operations: []whv1.OperationType{whv1.Create}, 57 Rule: whv1.Rule{ 58 APIGroups: []string{""}, 59 APIVersions: []string{"v1"}, 60 Resources: []string{"pods"}, 61 }, 62 }, 63 }, 64 }}, 65 }, 66 } 67 68 var config = &router.AdmissionServiceConfig{} 69 70 // AdmitPods is to admit pods and return response. 71 func AdmitPods(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse { 72 klog.V(3).Infof("admitting pods -- %s", ar.Request.Operation) 73 74 pod, err := schema.DecodePod(ar.Request.Object, ar.Request.Resource) 75 if err != nil { 76 return util.ToAdmissionResponse(err) 77 } 78 79 var msg string 80 reviewResponse := admissionv1.AdmissionResponse{} 81 reviewResponse.Allowed = true 82 83 switch ar.Request.Operation { 84 case admissionv1.Create: 85 msg = validatePod(pod, &reviewResponse) 86 default: 87 err := fmt.Errorf("expect operation to be 'CREATE'") 88 return util.ToAdmissionResponse(err) 89 } 90 91 if !reviewResponse.Allowed { 92 reviewResponse.Result = &metav1.Status{Message: strings.TrimSpace(msg)} 93 } 94 return &reviewResponse 95 } 96 97 /* 98 allow pods to create when 99 1. schedulerName of pod isn't volcano 100 2. normal pods whose schedulerName is volcano don't have podgroup. 101 3. check pod budget annotations configure 102 */ 103 func validatePod(pod *v1.Pod, reviewResponse *admissionv1.AdmissionResponse) string { 104 if !commonutil.Contains(config.SchedulerNames, pod.Spec.SchedulerName) { 105 return "" 106 } 107 pgName := "" 108 msg := "" 109 110 // vc-job, SN == volcano 111 if pod.Annotations != nil { 112 pgName = pod.Annotations[vcv1beta1.KubeGroupNameAnnotationKey] 113 } 114 if pgName != "" { 115 if err := checkPG(pod, pgName, true); err != nil { 116 msg = err.Error() 117 reviewResponse.Allowed = false 118 } else if err := checkPGQueueState(pod, pgName); err != nil { 119 msg = err.Error() 120 reviewResponse.Allowed = false 121 } 122 return msg 123 } 124 if pod.Annotations != nil && pod.Annotations[vcv1beta1.QueueNameAnnotationKey] != "" { 125 queueName := pod.Annotations[vcv1beta1.QueueNameAnnotationKey] 126 if err := checkQueueState(queueName); err != nil { 127 msg = err.Error() 128 reviewResponse.Allowed = false 129 return msg 130 } 131 } 132 // normal pod, SN == volcano 133 pgName = helpers.GeneratePodgroupName(pod) 134 if err := checkPG(pod, pgName, false); err != nil { 135 msg = err.Error() 136 reviewResponse.Allowed = false 137 } 138 139 // check pod annotatations 140 if err := validateAnnotation(pod); err != nil { 141 msg = err.Error() 142 reviewResponse.Allowed = false 143 } 144 145 return msg 146 } 147 148 func checkPG(pod *v1.Pod, pgName string, isVCJob bool) error { 149 _, err := config.VolcanoClient.SchedulingV1beta1().PodGroups(pod.Namespace).Get(context.TODO(), pgName, metav1.GetOptions{}) 150 if err != nil { 151 if isVCJob || (!isVCJob && !apierrors.IsNotFound(err)) { 152 return fmt.Errorf("failed to get PodGroup for pod <%s/%s>: %v", pod.Namespace, pod.Name, err) 153 } 154 return nil 155 } 156 return nil 157 } 158 159 func checkPGQueueState(pod *v1.Pod, pgName string) error { 160 pgObj, err := config.VolcanoClient.SchedulingV1beta1().PodGroups(pod.Namespace).Get(context.TODO(), pgName, metav1.GetOptions{}) 161 if err == nil { 162 if errQueue := checkQueueState(pgObj.Spec.Queue); errQueue != nil { 163 return fmt.Errorf("failed : %v;", errQueue) 164 } 165 } 166 return nil 167 } 168 169 func checkQueueState(queueName string) error { 170 if queueName == "" { 171 return nil 172 } 173 queue, err := config.VolcanoClient.SchedulingV1beta1().Queues().Get(context.TODO(), queueName, metav1.GetOptions{}) 174 if err != nil { 175 return fmt.Errorf(" unable to find job queue: %v;", err) 176 } else if queue.Status.State != vcv1beta1.QueueStateOpen { 177 return fmt.Errorf(" can only submit job to queue with state `Open`, "+ 178 "queue `%s` status is `%s`;", queue.Name, queue.Status.State) 179 } 180 return nil 181 } 182 183 func validateAnnotation(pod *v1.Pod) error { 184 num := 0 185 if len(pod.Annotations) > 0 { 186 keys := []string{ 187 vcv1beta1.JDBMinAvailable, 188 vcv1beta1.JDBMaxUnavailable, 189 } 190 for _, key := range keys { 191 if value, found := pod.Annotations[key]; found { 192 num++ 193 if err := validateIntPercentageStr(key, value); err != nil { 194 recordEvent(err) 195 return err 196 } 197 } 198 } 199 if num > 1 { 200 return fmt.Errorf("not allow configure multiple annotations <%v> at same time", keys) 201 } 202 } 203 return nil 204 } 205 206 func recordEvent(err error) { 207 config.Recorder.Eventf(nil, v1.EventTypeWarning, "Admit", "Create pod failed due to %v", err) 208 } 209 210 func validateIntPercentageStr(key, value string) error { 211 tmp := intstr.Parse(value) 212 switch tmp.Type { 213 case intstr.Int: 214 if tmp.IntValue() <= 0 { 215 return fmt.Errorf("invalid value <%q> for %v, it must be a positive integer", value, key) 216 } 217 return nil 218 case intstr.String: 219 s := strings.Replace(tmp.StrVal, "%", "", -1) 220 v, err := strconv.Atoi(s) 221 if err != nil { 222 return fmt.Errorf("invalid value %v for %v", err, key) 223 } 224 if v <= 0 || v >= 100 { 225 return fmt.Errorf("invalid value <%q> for %v, it must be a valid percentage which between 1%% ~ 99%%", tmp.StrVal, key) 226 } 227 return nil 228 } 229 return fmt.Errorf("invalid type: neither int nor percentage for %v", key) 230 }