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  }