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 }