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