1
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
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
42
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
99
100
101 var invalidPluginsByVersion = []invalidPlugins{
102 {
103 schemeGroupVersion: v1.SchemeGroupVersion.String(),
104 plugins: []string{},
105 },
106 }
107
108
109
110 func isPluginInvalid(apiVersion string, name string) (bool, string) {
111 for _, dp := range invalidPluginsByVersion {
112 for _, plugin := range dp.plugins {
113 if name == plugin {
114 return true, dp.schemeGroupVersion
115 }
116 }
117 if apiVersion == dp.schemeGroupVersion {
118 break
119 }
120 }
121 return false, ""
122 }
123
124 func validatePluginSetForInvalidPlugins(path *field.Path, apiVersion string, ps config.PluginSet) []error {
125 var errs []error
126 for i, plugin := range ps.Enabled {
127 if invalid, invalidVersion := isPluginInvalid(apiVersion, plugin.Name); invalid {
128 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)))
129 }
130 }
131 return errs
132 }
133
134 func validateKubeSchedulerProfile(path *field.Path, apiVersion string, profile *config.KubeSchedulerProfile) []error {
135 var errs []error
136 if len(profile.SchedulerName) == 0 {
137 errs = append(errs, field.Required(path.Child("schedulerName"), ""))
138 }
139 errs = append(errs, validatePercentageOfNodesToScore(path.Child("percentageOfNodesToScore"), profile.PercentageOfNodesToScore))
140 errs = append(errs, validatePluginConfig(path, apiVersion, profile)...)
141 return errs
142 }
143
144 func validatePluginConfig(path *field.Path, apiVersion string, profile *config.KubeSchedulerProfile) []error {
145 var errs []error
146 m := map[string]interface{}{
147 "DefaultPreemption": ValidateDefaultPreemptionArgs,
148 "InterPodAffinity": ValidateInterPodAffinityArgs,
149 "NodeAffinity": ValidateNodeAffinityArgs,
150 "NodeResourcesBalancedAllocation": ValidateNodeResourcesBalancedAllocationArgs,
151 "NodeResourcesFitArgs": ValidateNodeResourcesFitArgs,
152 "PodTopologySpread": ValidatePodTopologySpreadArgs,
153 "VolumeBinding": ValidateVolumeBindingArgs,
154 }
155
156 if profile.Plugins != nil {
157 stagesToPluginSet := map[string]config.PluginSet{
158 "preEnqueue": profile.Plugins.PreEnqueue,
159 "queueSort": profile.Plugins.QueueSort,
160 "preFilter": profile.Plugins.PreFilter,
161 "filter": profile.Plugins.Filter,
162 "postFilter": profile.Plugins.PostFilter,
163 "preScore": profile.Plugins.PreScore,
164 "score": profile.Plugins.Score,
165 "reserve": profile.Plugins.Reserve,
166 "permit": profile.Plugins.Permit,
167 "preBind": profile.Plugins.PreBind,
168 "bind": profile.Plugins.Bind,
169 "postBind": profile.Plugins.PostBind,
170 }
171
172 pluginsPath := path.Child("plugins")
173 for s, p := range stagesToPluginSet {
174 errs = append(errs, validatePluginSetForInvalidPlugins(
175 pluginsPath.Child(s), apiVersion, p)...)
176 }
177 }
178
179 seenPluginConfig := sets.New[string]()
180
181 for i := range profile.PluginConfig {
182 pluginConfigPath := path.Child("pluginConfig").Index(i)
183 name := profile.PluginConfig[i].Name
184 args := profile.PluginConfig[i].Args
185 if seenPluginConfig.Has(name) {
186 errs = append(errs, field.Duplicate(pluginConfigPath, name))
187 } else {
188 seenPluginConfig.Insert(name)
189 }
190 if invalid, invalidVersion := isPluginInvalid(apiVersion, name); invalid {
191 errs = append(errs, field.Invalid(pluginConfigPath, name, fmt.Sprintf("was invalid in version %q (KubeSchedulerConfiguration is version %q)", invalidVersion, apiVersion)))
192 } else if validateFunc, ok := m[name]; ok {
193
194 if reflect.TypeOf(args) != reflect.ValueOf(validateFunc).Type().In(1) {
195 errs = append(errs, field.Invalid(pluginConfigPath.Child("args"), args, "has to match plugin args"))
196 } else {
197 in := []reflect.Value{reflect.ValueOf(pluginConfigPath.Child("args")), reflect.ValueOf(args)}
198 res := reflect.ValueOf(validateFunc).Call(in)
199
200 if res[0].Interface() != nil {
201 errs = append(errs, res[0].Interface().(error))
202 }
203 }
204 }
205 }
206 return errs
207 }
208
209 func validateCommonQueueSort(path *field.Path, profiles []config.KubeSchedulerProfile) []error {
210 var errs []error
211 var canon config.PluginSet
212 var queueSortName string
213 var queueSortArgs runtime.Object
214 if profiles[0].Plugins != nil {
215 canon = profiles[0].Plugins.QueueSort
216 if len(profiles[0].Plugins.QueueSort.Enabled) != 0 {
217 queueSortName = profiles[0].Plugins.QueueSort.Enabled[0].Name
218 }
219 length := len(profiles[0].Plugins.QueueSort.Enabled)
220 if length > 1 {
221 errs = append(errs, field.Invalid(path.Index(0).Child("plugins", "queueSort", "Enabled"), length, "only one queue sort plugin can be enabled"))
222 }
223 }
224 for _, cfg := range profiles[0].PluginConfig {
225 if len(queueSortName) > 0 && cfg.Name == queueSortName {
226 queueSortArgs = cfg.Args
227 }
228 }
229 for i := 1; i < len(profiles); i++ {
230 var curr config.PluginSet
231 if profiles[i].Plugins != nil {
232 curr = profiles[i].Plugins.QueueSort
233 }
234 if !apiequality.Semantic.DeepEqual(canon, curr) {
235 errs = append(errs, field.Invalid(path.Index(i).Child("plugins", "queueSort"), curr, "queueSort must be the same for all profiles"))
236 }
237 for _, cfg := range profiles[i].PluginConfig {
238 if cfg.Name == queueSortName && !apiequality.Semantic.DeepEqual(queueSortArgs, cfg.Args) {
239 errs = append(errs, field.Invalid(path.Index(i).Child("pluginConfig", "args"), cfg.Args, "queueSort must be the same for all profiles"))
240 }
241 }
242 }
243 return errs
244 }
245
246
247 func validateExtenders(fldPath *field.Path, extenders []config.Extender) []error {
248 var errs []error
249 binders := 0
250 extenderManagedResources := sets.New[string]()
251 for i, extender := range extenders {
252 path := fldPath.Index(i)
253 if len(extender.PrioritizeVerb) > 0 && extender.Weight <= 0 {
254 errs = append(errs, field.Invalid(path.Child("weight"),
255 extender.Weight, "must have a positive weight applied to it"))
256 }
257 if extender.BindVerb != "" {
258 binders++
259 }
260 for j, resource := range extender.ManagedResources {
261 managedResourcesPath := path.Child("managedResources").Index(j)
262 validationErrors := validateExtendedResourceName(managedResourcesPath.Child("name"), v1.ResourceName(resource.Name))
263 errs = append(errs, validationErrors...)
264 if extenderManagedResources.Has(resource.Name) {
265 errs = append(errs, field.Invalid(managedResourcesPath.Child("name"),
266 resource.Name, "duplicate extender managed resource name"))
267 }
268 extenderManagedResources.Insert(resource.Name)
269 }
270 }
271 if binders > 1 {
272 errs = append(errs, field.Invalid(fldPath, fmt.Sprintf("found %d extenders implementing bind", binders), "only one extender can implement bind"))
273 }
274 return errs
275 }
276
277
278
279 func validateExtendedResourceName(path *field.Path, name v1.ResourceName) []error {
280 var validationErrors []error
281 for _, msg := range validation.IsQualifiedName(string(name)) {
282 validationErrors = append(validationErrors, field.Invalid(path, name, msg))
283 }
284 if len(validationErrors) != 0 {
285 return validationErrors
286 }
287 if !v1helper.IsExtendedResourceName(name) {
288 validationErrors = append(validationErrors, field.Invalid(path, string(name), "is an invalid extended resource name"))
289 }
290 return validationErrors
291 }
292
View as plain text