sigs.k8s.io/kueue@v0.6.2/pkg/controller/jobs/job/job_webhook.go (about)

     1  /*
     2  Copyright 2022 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 job
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strconv"
    23  
    24  	batchv1 "k8s.io/api/batch/v1"
    25  	apivalidation "k8s.io/apimachinery/pkg/api/validation"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/util/validation/field"
    29  	"k8s.io/klog/v2"
    30  	"k8s.io/utils/ptr"
    31  	ctrl "sigs.k8s.io/controller-runtime"
    32  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    33  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    34  
    35  	"sigs.k8s.io/kueue/pkg/controller/constants"
    36  	"sigs.k8s.io/kueue/pkg/controller/jobframework"
    37  	"sigs.k8s.io/kueue/pkg/util/kubeversion"
    38  )
    39  
    40  var (
    41  	minPodsCountAnnotationsPath   = field.NewPath("metadata", "annotations").Key(JobMinParallelismAnnotation)
    42  	syncCompletionAnnotationsPath = field.NewPath("metadata", "annotations").Key(JobCompletionsEqualParallelismAnnotation)
    43  )
    44  
    45  type JobWebhook struct {
    46  	manageJobsWithoutQueueName bool
    47  	kubeServerVersion          *kubeversion.ServerVersionFetcher
    48  }
    49  
    50  // SetupWebhook configures the webhook for batchJob.
    51  func SetupWebhook(mgr ctrl.Manager, opts ...jobframework.Option) error {
    52  	options := jobframework.ProcessOptions(opts...)
    53  	wh := &JobWebhook{
    54  		manageJobsWithoutQueueName: options.ManageJobsWithoutQueueName,
    55  		kubeServerVersion:          options.KubeServerVersion,
    56  	}
    57  	return ctrl.NewWebhookManagedBy(mgr).
    58  		For(&batchv1.Job{}).
    59  		WithDefaulter(wh).
    60  		WithValidator(wh).
    61  		Complete()
    62  }
    63  
    64  // +kubebuilder:webhook:path=/mutate-batch-v1-job,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch,resources=jobs,verbs=create,versions=v1,name=mjob.kb.io,admissionReviewVersions=v1
    65  
    66  var _ webhook.CustomDefaulter = &JobWebhook{}
    67  
    68  // Default implements webhook.CustomDefaulter so a webhook will be registered for the type
    69  func (w *JobWebhook) Default(ctx context.Context, obj runtime.Object) error {
    70  	job := fromObject(obj)
    71  	log := ctrl.LoggerFrom(ctx).WithName("job-webhook")
    72  	log.V(5).Info("Applying defaults", "job", klog.KObj(job))
    73  
    74  	// While using prebuilt workloads, the owner job may set the parent workload to a different one
    75  	// then the one generated from its name.
    76  	if owner := metav1.GetControllerOf(job); owner != nil && jobframework.IsOwnerManagedByKueue(owner) && jobframework.ParentWorkloadName(job) == "" {
    77  		if job.Annotations == nil {
    78  			job.Annotations = make(map[string]string)
    79  		}
    80  		if pwName, err := jobframework.GetWorkloadNameForOwnerRef(owner); err != nil {
    81  			return err
    82  		} else {
    83  			job.Annotations[constants.ParentWorkloadAnnotation] = pwName
    84  		}
    85  	}
    86  
    87  	jobframework.ApplyDefaultForSuspend(job, w.manageJobsWithoutQueueName)
    88  
    89  	return nil
    90  }
    91  
    92  // +kubebuilder:webhook:path=/validate-batch-v1-job,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch,resources=jobs,verbs=create;update,versions=v1,name=vjob.kb.io,admissionReviewVersions=v1
    93  
    94  var _ webhook.CustomValidator = &JobWebhook{}
    95  
    96  // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
    97  func (w *JobWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
    98  	job := fromObject(obj)
    99  	log := ctrl.LoggerFrom(ctx).WithName("job-webhook")
   100  	log.V(5).Info("Validating create", "job", klog.KObj(job))
   101  	return nil, w.validateCreate(job).ToAggregate()
   102  }
   103  
   104  func (w *JobWebhook) validateCreate(job *Job) field.ErrorList {
   105  	var allErrs field.ErrorList
   106  	allErrs = append(allErrs, jobframework.ValidateAnnotationAsCRDName(job, constants.ParentWorkloadAnnotation)...)
   107  	allErrs = append(allErrs, jobframework.ValidateCreateForQueueName(job)...)
   108  	allErrs = append(allErrs, w.validatePartialAdmissionCreate(job)...)
   109  	allErrs = append(allErrs, jobframework.ValidateCreateForParentWorkload(job)...)
   110  	return allErrs
   111  }
   112  
   113  func (w *JobWebhook) validatePartialAdmissionCreate(job *Job) field.ErrorList {
   114  	var allErrs field.ErrorList
   115  	if strVal, found := job.Annotations[JobMinParallelismAnnotation]; found {
   116  		v, err := strconv.Atoi(strVal)
   117  		if err != nil {
   118  			allErrs = append(allErrs, field.Invalid(minPodsCountAnnotationsPath, job.Annotations[JobMinParallelismAnnotation], err.Error()))
   119  		} else {
   120  			if int32(v) >= job.podsCount() || v <= 0 {
   121  				allErrs = append(allErrs, field.Invalid(minPodsCountAnnotationsPath, v, fmt.Sprintf("should be between 0 and %d", job.podsCount()-1)))
   122  			}
   123  		}
   124  	}
   125  	if strVal, found := job.Annotations[JobCompletionsEqualParallelismAnnotation]; found {
   126  		enabled, err := strconv.ParseBool(strVal)
   127  		if err != nil {
   128  			allErrs = append(allErrs, field.Invalid(syncCompletionAnnotationsPath, job.Annotations[JobCompletionsEqualParallelismAnnotation], err.Error()))
   129  		}
   130  		if enabled {
   131  			if job.Spec.CompletionMode == nil || *job.Spec.CompletionMode == batchv1.NonIndexedCompletion {
   132  				allErrs = append(allErrs, field.Invalid(syncCompletionAnnotationsPath, job.Annotations[JobCompletionsEqualParallelismAnnotation], "should not be enabled for NonIndexed jobs"))
   133  			}
   134  			if w.kubeServerVersion != nil {
   135  				version := w.kubeServerVersion.GetServerVersion()
   136  				if version.String() == "" || version.LessThan(kubeversion.KubeVersion1_27) {
   137  					allErrs = append(allErrs, field.Invalid(syncCompletionAnnotationsPath, job.Annotations[JobCompletionsEqualParallelismAnnotation], "only supported in Kubernetes 1.27 or newer"))
   138  				}
   139  			}
   140  			if ptr.Deref(job.Spec.Parallelism, 1) != ptr.Deref(job.Spec.Completions, 1) {
   141  				allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "completions"), job.Spec.Completions, fmt.Sprintf("should be equal to parallelism when %s is annotation is true", JobCompletionsEqualParallelismAnnotation)))
   142  			}
   143  		}
   144  	}
   145  	return allErrs
   146  }
   147  
   148  // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
   149  func (w *JobWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
   150  	oldJob := fromObject(oldObj)
   151  	newJob := fromObject(newObj)
   152  	log := ctrl.LoggerFrom(ctx).WithName("job-webhook")
   153  	log.V(5).Info("Validating update", "job", klog.KObj(newJob))
   154  	return nil, w.validateUpdate(oldJob, newJob).ToAggregate()
   155  }
   156  
   157  func (w *JobWebhook) validateUpdate(oldJob, newJob *Job) field.ErrorList {
   158  	allErrs := w.validateCreate(newJob)
   159  	allErrs = append(allErrs, jobframework.ValidateUpdateForParentWorkload(oldJob, newJob)...)
   160  	allErrs = append(allErrs, jobframework.ValidateUpdateForQueueName(oldJob, newJob)...)
   161  	allErrs = append(allErrs, validatePartialAdmissionUpdate(oldJob, newJob)...)
   162  	allErrs = append(allErrs, jobframework.ValidateUpdateForWorkloadPriorityClassName(oldJob, newJob)...)
   163  	return allErrs
   164  }
   165  
   166  func validatePartialAdmissionUpdate(oldJob, newJob *Job) field.ErrorList {
   167  	var allErrs field.ErrorList
   168  	if _, found := oldJob.Annotations[JobMinParallelismAnnotation]; found {
   169  		if !oldJob.IsSuspended() && ptr.Deref(oldJob.Spec.Parallelism, 1) != ptr.Deref(newJob.Spec.Parallelism, 1) {
   170  			allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "parallelism"), "cannot change when partial admission is enabled and the job is not suspended"))
   171  		}
   172  	}
   173  	if oldJob.IsSuspended() == newJob.IsSuspended() && !newJob.IsSuspended() && oldJob.syncCompletionWithParallelism() != newJob.syncCompletionWithParallelism() {
   174  		allErrs = append(allErrs, field.Forbidden(syncCompletionAnnotationsPath, fmt.Sprintf("%s while the job is not suspended", apivalidation.FieldImmutableErrorMsg)))
   175  	}
   176  	return allErrs
   177  }
   178  
   179  // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
   180  func (w *JobWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
   181  	return nil, nil
   182  }