k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/scheduler/apis/config/validation/validation.go (about)

     1  /*
     2  Copyright 2018 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 validation
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  
    23  	v1 "k8s.io/api/core/v1"
    24  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"k8s.io/apimachinery/pkg/util/validation"
    29  	"k8s.io/apimachinery/pkg/util/validation/field"
    30  	componentbasevalidation "k8s.io/component-base/config/validation"
    31  	v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
    32  	"k8s.io/kubernetes/pkg/scheduler/apis/config"
    33  )
    34  
    35  // ValidateKubeSchedulerConfiguration ensures validation of the KubeSchedulerConfiguration struct
    36  func ValidateKubeSchedulerConfiguration(cc *config.KubeSchedulerConfiguration) utilerrors.Aggregate {
    37  	var errs []error
    38  	errs = append(errs, componentbasevalidation.ValidateClientConnectionConfiguration(&cc.ClientConnection, field.NewPath("clientConnection")).ToAggregate())
    39  	errs = append(errs, componentbasevalidation.ValidateLeaderElectionConfiguration(&cc.LeaderElection, field.NewPath("leaderElection")).ToAggregate())
    40  
    41  	// TODO: This can be removed when ResourceLock is not available
    42  	// Only ResourceLock values with leases are allowed
    43  	if cc.LeaderElection.LeaderElect && cc.LeaderElection.ResourceLock != "leases" {
    44  		leaderElectionPath := field.NewPath("leaderElection")
    45  		errs = append(errs, field.Invalid(leaderElectionPath.Child("resourceLock"), cc.LeaderElection.ResourceLock, `resourceLock value must be "leases"`))
    46  	}
    47  
    48  	profilesPath := field.NewPath("profiles")
    49  	if cc.Parallelism <= 0 {
    50  		errs = append(errs, field.Invalid(field.NewPath("parallelism"), cc.Parallelism, "should be an integer value greater than zero"))
    51  	}
    52  
    53  	if len(cc.Profiles) == 0 {
    54  		errs = append(errs, field.Required(profilesPath, ""))
    55  	} else {
    56  		existingProfiles := make(map[string]int, len(cc.Profiles))
    57  		for i := range cc.Profiles {
    58  			profile := &cc.Profiles[i]
    59  			path := profilesPath.Index(i)
    60  			errs = append(errs, validateKubeSchedulerProfile(path, cc.APIVersion, profile)...)
    61  			if idx, ok := existingProfiles[profile.SchedulerName]; ok {
    62  				errs = append(errs, field.Duplicate(path.Child("schedulerName"), profilesPath.Index(idx).Child("schedulerName")))
    63  			}
    64  			existingProfiles[profile.SchedulerName] = i
    65  		}
    66  		errs = append(errs, validateCommonQueueSort(profilesPath, cc.Profiles)...)
    67  	}
    68  
    69  	errs = append(errs, validatePercentageOfNodesToScore(field.NewPath("percentageOfNodesToScore"), cc.PercentageOfNodesToScore))
    70  
    71  	if cc.PodInitialBackoffSeconds <= 0 {
    72  		errs = append(errs, field.Invalid(field.NewPath("podInitialBackoffSeconds"),
    73  			cc.PodInitialBackoffSeconds, "must be greater than 0"))
    74  	}
    75  	if cc.PodMaxBackoffSeconds < cc.PodInitialBackoffSeconds {
    76  		errs = append(errs, field.Invalid(field.NewPath("podMaxBackoffSeconds"),
    77  			cc.PodMaxBackoffSeconds, "must be greater than or equal to PodInitialBackoffSeconds"))
    78  	}
    79  
    80  	errs = append(errs, validateExtenders(field.NewPath("extenders"), cc.Extenders)...)
    81  	return utilerrors.Flatten(utilerrors.NewAggregate(errs))
    82  }
    83  
    84  func validatePercentageOfNodesToScore(path *field.Path, percentageOfNodesToScore *int32) error {
    85  	if percentageOfNodesToScore != nil {
    86  		if *percentageOfNodesToScore < 0 || *percentageOfNodesToScore > 100 {
    87  			return field.Invalid(path, *percentageOfNodesToScore, "not in valid range [0-100]")
    88  		}
    89  	}
    90  	return nil
    91  }
    92  
    93  type invalidPlugins struct {
    94  	schemeGroupVersion string
    95  	plugins            []string
    96  }
    97  
    98  // invalidPluginsByVersion maintains a list of removed/deprecated plugins in each version.
    99  // Remember to add an entry to that list when creating a new component config
   100  // version (even if the list of invalid plugins is empty).
   101  var invalidPluginsByVersion = []invalidPlugins{
   102  	{
   103  		schemeGroupVersion: v1.SchemeGroupVersion.String(),
   104  		plugins: []string{
   105  			"AzureDiskLimits",
   106  			"CinderLimits",
   107  			"EBSLimits",
   108  			"GCEPDLimits",
   109  		},
   110  	},
   111  }
   112  
   113  // isPluginInvalid checks if a given plugin was removed/deprecated in the given component
   114  // config version or earlier.
   115  func isPluginInvalid(apiVersion string, name string) (bool, string) {
   116  	for _, dp := range invalidPluginsByVersion {
   117  		for _, plugin := range dp.plugins {
   118  			if name == plugin {
   119  				return true, dp.schemeGroupVersion
   120  			}
   121  		}
   122  		if apiVersion == dp.schemeGroupVersion {
   123  			break
   124  		}
   125  	}
   126  	return false, ""
   127  }
   128  
   129  func validatePluginSetForInvalidPlugins(path *field.Path, apiVersion string, ps config.PluginSet) []error {
   130  	var errs []error
   131  	for i, plugin := range ps.Enabled {
   132  		if invalid, invalidVersion := isPluginInvalid(apiVersion, plugin.Name); invalid {
   133  			errs = append(errs, field.Invalid(path.Child("enabled").Index(i), plugin.Name, fmt.Sprintf("was invalid in version %q (KubeSchedulerConfiguration is version %q)", invalidVersion, apiVersion)))
   134  		}
   135  	}
   136  	return errs
   137  }
   138  
   139  func validateKubeSchedulerProfile(path *field.Path, apiVersion string, profile *config.KubeSchedulerProfile) []error {
   140  	var errs []error
   141  	if len(profile.SchedulerName) == 0 {
   142  		errs = append(errs, field.Required(path.Child("schedulerName"), ""))
   143  	}
   144  	errs = append(errs, validatePercentageOfNodesToScore(path.Child("percentageOfNodesToScore"), profile.PercentageOfNodesToScore))
   145  	errs = append(errs, validatePluginConfig(path, apiVersion, profile)...)
   146  	return errs
   147  }
   148  
   149  func validatePluginConfig(path *field.Path, apiVersion string, profile *config.KubeSchedulerProfile) []error {
   150  	var errs []error
   151  	m := map[string]interface{}{
   152  		"DefaultPreemption":               ValidateDefaultPreemptionArgs,
   153  		"InterPodAffinity":                ValidateInterPodAffinityArgs,
   154  		"NodeAffinity":                    ValidateNodeAffinityArgs,
   155  		"NodeResourcesBalancedAllocation": ValidateNodeResourcesBalancedAllocationArgs,
   156  		"NodeResourcesFitArgs":            ValidateNodeResourcesFitArgs,
   157  		"PodTopologySpread":               ValidatePodTopologySpreadArgs,
   158  		"VolumeBinding":                   ValidateVolumeBindingArgs,
   159  	}
   160  
   161  	if profile.Plugins != nil {
   162  		stagesToPluginSet := map[string]config.PluginSet{
   163  			"preEnqueue": profile.Plugins.PreEnqueue,
   164  			"queueSort":  profile.Plugins.QueueSort,
   165  			"preFilter":  profile.Plugins.PreFilter,
   166  			"filter":     profile.Plugins.Filter,
   167  			"postFilter": profile.Plugins.PostFilter,
   168  			"preScore":   profile.Plugins.PreScore,
   169  			"score":      profile.Plugins.Score,
   170  			"reserve":    profile.Plugins.Reserve,
   171  			"permit":     profile.Plugins.Permit,
   172  			"preBind":    profile.Plugins.PreBind,
   173  			"bind":       profile.Plugins.Bind,
   174  			"postBind":   profile.Plugins.PostBind,
   175  		}
   176  
   177  		pluginsPath := path.Child("plugins")
   178  		for s, p := range stagesToPluginSet {
   179  			errs = append(errs, validatePluginSetForInvalidPlugins(
   180  				pluginsPath.Child(s), apiVersion, p)...)
   181  		}
   182  	}
   183  
   184  	seenPluginConfig := sets.New[string]()
   185  
   186  	for i := range profile.PluginConfig {
   187  		pluginConfigPath := path.Child("pluginConfig").Index(i)
   188  		name := profile.PluginConfig[i].Name
   189  		args := profile.PluginConfig[i].Args
   190  		if seenPluginConfig.Has(name) {
   191  			errs = append(errs, field.Duplicate(pluginConfigPath, name))
   192  		} else {
   193  			seenPluginConfig.Insert(name)
   194  		}
   195  		if invalid, invalidVersion := isPluginInvalid(apiVersion, name); invalid {
   196  			errs = append(errs, field.Invalid(pluginConfigPath, name, fmt.Sprintf("was invalid in version %q (KubeSchedulerConfiguration is version %q)", invalidVersion, apiVersion)))
   197  		} else if validateFunc, ok := m[name]; ok {
   198  			// type mismatch, no need to validate the `args`.
   199  			if reflect.TypeOf(args) != reflect.ValueOf(validateFunc).Type().In(1) {
   200  				errs = append(errs, field.Invalid(pluginConfigPath.Child("args"), args, "has to match plugin args"))
   201  			} else {
   202  				in := []reflect.Value{reflect.ValueOf(pluginConfigPath.Child("args")), reflect.ValueOf(args)}
   203  				res := reflect.ValueOf(validateFunc).Call(in)
   204  				// It's possible that validation function return a Aggregate, just append here and it will be flattened at the end of CC validation.
   205  				if res[0].Interface() != nil {
   206  					errs = append(errs, res[0].Interface().(error))
   207  				}
   208  			}
   209  		}
   210  	}
   211  	return errs
   212  }
   213  
   214  func validateCommonQueueSort(path *field.Path, profiles []config.KubeSchedulerProfile) []error {
   215  	var errs []error
   216  	var canon config.PluginSet
   217  	var queueSortName string
   218  	var queueSortArgs runtime.Object
   219  	if profiles[0].Plugins != nil {
   220  		canon = profiles[0].Plugins.QueueSort
   221  		if len(profiles[0].Plugins.QueueSort.Enabled) != 0 {
   222  			queueSortName = profiles[0].Plugins.QueueSort.Enabled[0].Name
   223  		}
   224  		length := len(profiles[0].Plugins.QueueSort.Enabled)
   225  		if length > 1 {
   226  			errs = append(errs, field.Invalid(path.Index(0).Child("plugins", "queueSort", "Enabled"), length, "only one queue sort plugin can be enabled"))
   227  		}
   228  	}
   229  	for _, cfg := range profiles[0].PluginConfig {
   230  		if len(queueSortName) > 0 && cfg.Name == queueSortName {
   231  			queueSortArgs = cfg.Args
   232  		}
   233  	}
   234  	for i := 1; i < len(profiles); i++ {
   235  		var curr config.PluginSet
   236  		if profiles[i].Plugins != nil {
   237  			curr = profiles[i].Plugins.QueueSort
   238  		}
   239  		if !apiequality.Semantic.DeepEqual(canon, curr) {
   240  			errs = append(errs, field.Invalid(path.Index(i).Child("plugins", "queueSort"), curr, "queueSort must be the same for all profiles"))
   241  		}
   242  		for _, cfg := range profiles[i].PluginConfig {
   243  			if cfg.Name == queueSortName && !apiequality.Semantic.DeepEqual(queueSortArgs, cfg.Args) {
   244  				errs = append(errs, field.Invalid(path.Index(i).Child("pluginConfig", "args"), cfg.Args, "queueSort must be the same for all profiles"))
   245  			}
   246  		}
   247  	}
   248  	return errs
   249  }
   250  
   251  // validateExtenders validates the configured extenders for the Scheduler
   252  func validateExtenders(fldPath *field.Path, extenders []config.Extender) []error {
   253  	var errs []error
   254  	binders := 0
   255  	extenderManagedResources := sets.New[string]()
   256  	for i, extender := range extenders {
   257  		path := fldPath.Index(i)
   258  		if len(extender.PrioritizeVerb) > 0 && extender.Weight <= 0 {
   259  			errs = append(errs, field.Invalid(path.Child("weight"),
   260  				extender.Weight, "must have a positive weight applied to it"))
   261  		}
   262  		if extender.BindVerb != "" {
   263  			binders++
   264  		}
   265  		for j, resource := range extender.ManagedResources {
   266  			managedResourcesPath := path.Child("managedResources").Index(j)
   267  			validationErrors := validateExtendedResourceName(managedResourcesPath.Child("name"), v1.ResourceName(resource.Name))
   268  			errs = append(errs, validationErrors...)
   269  			if extenderManagedResources.Has(resource.Name) {
   270  				errs = append(errs, field.Invalid(managedResourcesPath.Child("name"),
   271  					resource.Name, "duplicate extender managed resource name"))
   272  			}
   273  			extenderManagedResources.Insert(resource.Name)
   274  		}
   275  	}
   276  	if binders > 1 {
   277  		errs = append(errs, field.Invalid(fldPath, fmt.Sprintf("found %d extenders implementing bind", binders), "only one extender can implement bind"))
   278  	}
   279  	return errs
   280  }
   281  
   282  // validateExtendedResourceName checks whether the specified name is a valid
   283  // extended resource name.
   284  func validateExtendedResourceName(path *field.Path, name v1.ResourceName) []error {
   285  	var validationErrors []error
   286  	for _, msg := range validation.IsQualifiedName(string(name)) {
   287  		validationErrors = append(validationErrors, field.Invalid(path, name, msg))
   288  	}
   289  	if len(validationErrors) != 0 {
   290  		return validationErrors
   291  	}
   292  	if !v1helper.IsExtendedResourceName(name) {
   293  		validationErrors = append(validationErrors, field.Invalid(path, string(name), "is an invalid extended resource name"))
   294  	}
   295  	return validationErrors
   296  }