volcano.sh/volcano@v1.9.0/pkg/webhooks/admission/jobs/mutate/mutate_job.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 mutate 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "strconv" 23 24 admissionv1 "k8s.io/api/admission/v1" 25 whv1 "k8s.io/api/admissionregistration/v1" 26 v1 "k8s.io/api/core/v1" 27 "k8s.io/klog/v2" 28 29 "volcano.sh/apis/pkg/apis/batch/v1alpha1" 30 "volcano.sh/volcano/pkg/controllers/job/plugins/distributed-framework/mpi" 31 "volcano.sh/volcano/pkg/controllers/job/plugins/distributed-framework/pytorch" 32 "volcano.sh/volcano/pkg/controllers/job/plugins/distributed-framework/tensorflow" 33 commonutil "volcano.sh/volcano/pkg/util" 34 "volcano.sh/volcano/pkg/webhooks/router" 35 "volcano.sh/volcano/pkg/webhooks/schema" 36 "volcano.sh/volcano/pkg/webhooks/util" 37 ) 38 39 const ( 40 // DefaultQueue constant stores the name of the queue as "default" 41 DefaultQueue = "default" 42 // DefaultMaxRetry is the default number of retries. 43 DefaultMaxRetry = 3 44 45 defaultMaxRetry int32 = 3 46 ) 47 48 func init() { 49 router.RegisterAdmission(service) 50 } 51 52 var service = &router.AdmissionService{ 53 Path: "/jobs/mutate", 54 Func: Jobs, 55 56 Config: config, 57 58 MutatingConfig: &whv1.MutatingWebhookConfiguration{ 59 Webhooks: []whv1.MutatingWebhook{{ 60 Name: "mutatejob.volcano.sh", 61 Rules: []whv1.RuleWithOperations{ 62 { 63 Operations: []whv1.OperationType{whv1.Create}, 64 Rule: whv1.Rule{ 65 APIGroups: []string{"batch.volcano.sh"}, 66 APIVersions: []string{"v1alpha1"}, 67 Resources: []string{"jobs"}, 68 }, 69 }, 70 }, 71 }}, 72 }, 73 } 74 75 var config = &router.AdmissionServiceConfig{} 76 77 type patchOperation struct { 78 Op string `json:"op"` 79 Path string `json:"path"` 80 Value interface{} `json:"value,omitempty"` 81 } 82 83 // Jobs mutate jobs. 84 func Jobs(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse { 85 klog.V(3).Infof("mutating jobs") 86 87 job, err := schema.DecodeJob(ar.Request.Object, ar.Request.Resource) 88 if err != nil { 89 return util.ToAdmissionResponse(err) 90 } 91 92 var patchBytes []byte 93 switch ar.Request.Operation { 94 case admissionv1.Create: 95 patchBytes, _ = createPatch(job) 96 default: 97 err = fmt.Errorf("expect operation to be 'CREATE' ") 98 return util.ToAdmissionResponse(err) 99 } 100 101 klog.V(3).Infof("AdmissionResponse: patch=%v", string(patchBytes)) 102 reviewResponse := admissionv1.AdmissionResponse{ 103 Allowed: true, 104 Patch: patchBytes, 105 } 106 if len(patchBytes) > 0 { 107 pt := admissionv1.PatchTypeJSONPatch 108 reviewResponse.PatchType = &pt 109 } 110 111 return &reviewResponse 112 } 113 114 func createPatch(job *v1alpha1.Job) ([]byte, error) { 115 var patch []patchOperation 116 pathQueue := patchDefaultQueue(job) 117 if pathQueue != nil { 118 patch = append(patch, *pathQueue) 119 } 120 pathScheduler := patchDefaultScheduler(job) 121 if pathScheduler != nil { 122 patch = append(patch, *pathScheduler) 123 } 124 pathMaxRetry := patchDefaultMaxRetry(job) 125 if pathMaxRetry != nil { 126 patch = append(patch, *pathMaxRetry) 127 } 128 pathSpec := mutateSpec(job.Spec.Tasks, "/spec/tasks", job) 129 if pathSpec != nil { 130 patch = append(patch, *pathSpec) 131 } 132 pathMinAvailable := patchDefaultMinAvailable(job) 133 if pathMinAvailable != nil { 134 patch = append(patch, *pathMinAvailable) 135 } 136 // Add default plugins for some distributed-framework plugin cases 137 patchPlugins := patchDefaultPlugins(job) 138 if patchPlugins != nil { 139 patch = append(patch, *patchPlugins) 140 } 141 return json.Marshal(patch) 142 } 143 144 func patchDefaultQueue(job *v1alpha1.Job) *patchOperation { 145 //Add default queue if not specified. 146 if job.Spec.Queue == "" { 147 return &patchOperation{Op: "add", Path: "/spec/queue", Value: DefaultQueue} 148 } 149 return nil 150 } 151 152 func patchDefaultScheduler(job *v1alpha1.Job) *patchOperation { 153 // Add default scheduler name if not specified. 154 if job.Spec.SchedulerName == "" { 155 return &patchOperation{Op: "add", Path: "/spec/schedulerName", Value: commonutil.GenerateSchedulerName(config.SchedulerNames)} 156 } 157 return nil 158 } 159 160 func patchDefaultMaxRetry(job *v1alpha1.Job) *patchOperation { 161 // Add default maxRetry if maxRetry is zero. 162 if job.Spec.MaxRetry == 0 { 163 return &patchOperation{Op: "add", Path: "/spec/maxRetry", Value: DefaultMaxRetry} 164 } 165 return nil 166 } 167 168 func patchDefaultMinAvailable(job *v1alpha1.Job) *patchOperation { 169 // Add default minAvailable if minAvailable is zero. 170 if job.Spec.MinAvailable == 0 { 171 var jobMinAvailable int32 172 for _, task := range job.Spec.Tasks { 173 if task.MinAvailable != nil { 174 jobMinAvailable += *task.MinAvailable 175 } else { 176 jobMinAvailable += task.Replicas 177 } 178 } 179 180 return &patchOperation{Op: "add", Path: "/spec/minAvailable", Value: jobMinAvailable} 181 } 182 return nil 183 } 184 185 func mutateSpec(tasks []v1alpha1.TaskSpec, basePath string, job *v1alpha1.Job) *patchOperation { 186 // TODO: Enable this configuration when dependOn supports coexistence with the gang plugin 187 // if _, ok := job.Spec.Plugins[mpi.MpiPluginName]; ok { 188 // mpi.AddDependsOn(job) 189 // } 190 patched := false 191 for index := range tasks { 192 // add default task name 193 taskName := tasks[index].Name 194 if len(taskName) == 0 { 195 patched = true 196 tasks[index].Name = v1alpha1.DefaultTaskSpec + strconv.Itoa(index) 197 } 198 199 if tasks[index].Template.Spec.HostNetwork && tasks[index].Template.Spec.DNSPolicy == "" { 200 patched = true 201 tasks[index].Template.Spec.DNSPolicy = v1.DNSClusterFirstWithHostNet 202 } 203 204 if tasks[index].MinAvailable == nil { 205 patched = true 206 minAvailable := tasks[index].Replicas 207 tasks[index].MinAvailable = &minAvailable 208 } 209 210 if tasks[index].MaxRetry == 0 { 211 patched = true 212 tasks[index].MaxRetry = defaultMaxRetry 213 } 214 } 215 if !patched { 216 return nil 217 } 218 return &patchOperation{ 219 Op: "replace", 220 Path: basePath, 221 Value: tasks, 222 } 223 } 224 225 func patchDefaultPlugins(job *v1alpha1.Job) *patchOperation { 226 if job.Spec.Plugins == nil { 227 return nil 228 } 229 plugins := map[string][]string{} 230 for k, v := range job.Spec.Plugins { 231 plugins[k] = v 232 } 233 234 // Because the tensorflow-plugin and mpi-plugin depends on svc-plugin. 235 // If the svc-plugin is not defined, we should add it. 236 _, hasTf := job.Spec.Plugins[tensorflow.TFPluginName] 237 _, hasMPI := job.Spec.Plugins[mpi.MPIPluginName] 238 _, hasPytorch := job.Spec.Plugins[pytorch.PytorchPluginName] 239 if hasTf || hasMPI || hasPytorch { 240 if _, ok := plugins["svc"]; !ok { 241 plugins["svc"] = []string{} 242 } 243 } 244 245 if _, ok := job.Spec.Plugins["mpi"]; ok { 246 if _, ok := plugins["ssh"]; !ok { 247 plugins["ssh"] = []string{} 248 } 249 } 250 251 return &patchOperation{ 252 Op: "replace", 253 Path: "/spec/plugins", 254 Value: plugins, 255 } 256 }