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  }