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  }