sigs.k8s.io/kueue@v0.6.2/pkg/webhooks/clusterqueue_webhook_test.go (about) 1 /* 2 Copyright 2021 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 webhooks 18 19 import ( 20 "testing" 21 22 "github.com/google/go-cmp/cmp" 23 "github.com/google/go-cmp/cmp/cmpopts" 24 corev1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/util/validation/field" 27 "k8s.io/utils/ptr" 28 29 kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1" 30 "sigs.k8s.io/kueue/pkg/features" 31 testingutil "sigs.k8s.io/kueue/pkg/util/testing" 32 ) 33 34 func TestValidateClusterQueue(t *testing.T) { 35 specPath := field.NewPath("spec") 36 resourceGroupsPath := specPath.Child("resourceGroups") 37 preemptionPath := specPath.Child("preemption") 38 39 testcases := []struct { 40 name string 41 clusterQueue *kueue.ClusterQueue 42 wantErr field.ErrorList 43 enableLendingLimit bool 44 }{ 45 { 46 name: "built-in resources with qualified names", 47 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 48 ResourceGroup(*testingutil.MakeFlavorQuotas("default").Resource("cpu").Obj()). 49 Obj(), 50 }, 51 { 52 name: "invalid resource name", 53 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 54 ResourceGroup(*testingutil.MakeFlavorQuotas("default").Resource("@cpu").Obj()). 55 Obj(), 56 wantErr: field.ErrorList{ 57 field.Invalid(resourceGroupsPath.Index(0).Child("coveredResources").Index(0), "@cpu", ""), 58 }, 59 }, 60 { 61 name: "in cohort", 62 clusterQueue: testingutil.MakeClusterQueue("cluster-queue").Cohort("prod").Obj(), 63 }, 64 { 65 name: "invalid cohort", 66 clusterQueue: testingutil.MakeClusterQueue("cluster-queue").Cohort("@prod").Obj(), 67 wantErr: field.ErrorList{ 68 field.Invalid(specPath.Child("cohort"), "@prod", ""), 69 }, 70 }, 71 { 72 name: "extended resources with qualified names", 73 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 74 ResourceGroup(*testingutil.MakeFlavorQuotas("default").Resource("example.com/gpu").Obj()). 75 Obj(), 76 }, 77 { 78 name: "flavor with qualified names", 79 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 80 ResourceGroup(*testingutil.MakeFlavorQuotas("x86").Obj()). 81 Obj(), 82 }, 83 { 84 name: "flavor with unqualified names", 85 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 86 ResourceGroup(*testingutil.MakeFlavorQuotas("invalid_name").Obj()). 87 Obj(), 88 wantErr: field.ErrorList{ 89 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("name"), "invalid_name", ""), 90 }, 91 }, 92 { 93 name: "flavor quota with negative value", 94 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 95 ResourceGroup( 96 *testingutil.MakeFlavorQuotas("x86").Resource("cpu", "-1").Obj()). 97 Obj(), 98 wantErr: field.ErrorList{ 99 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("nominalQuota"), "-1", ""), 100 }, 101 }, 102 { 103 name: "flavor quota with zero value", 104 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 105 ResourceGroup( 106 *testingutil.MakeFlavorQuotas("x86").Resource("cpu", "0").Obj()). 107 Obj(), 108 }, 109 { 110 name: "flavor quota with borrowingLimit 0", 111 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 112 ResourceGroup( 113 *testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "0").Obj()). 114 Cohort("cohort"). 115 Obj(), 116 }, 117 { 118 name: "flavor quota with negative borrowingLimit", 119 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 120 ResourceGroup( 121 *testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "-1").Obj()). 122 Cohort("cohort"). 123 Obj(), 124 wantErr: field.ErrorList{ 125 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("borrowingLimit"), "-1", ""), 126 }, 127 }, 128 { 129 name: "flavor quota with borrowingLimit and empty cohort", 130 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 131 ResourceGroup( 132 *testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "1").Obj()). 133 Obj(), 134 wantErr: field.ErrorList{ 135 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("borrowingLimit"), "1", limitIsEmptyErrorMsg), 136 }, 137 }, 138 { 139 name: "flavor quota with lendingLimit 0", 140 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 141 ResourceGroup( 142 *testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "", "0").Obj()). 143 Cohort("cohort"). 144 Obj(), 145 enableLendingLimit: true, 146 }, 147 { 148 name: "flavor quota with negative lendingLimit", 149 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 150 ResourceGroup( 151 *testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "", "-1").Obj()). 152 Cohort("cohort"). 153 Obj(), 154 wantErr: field.ErrorList{ 155 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("lendingLimit"), "-1", ""), 156 }, 157 enableLendingLimit: true, 158 }, 159 { 160 name: "flavor quota with lendingLimit and empty cohort", 161 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 162 ResourceGroup( 163 *testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "", "1").Obj()). 164 Obj(), 165 wantErr: field.ErrorList{ 166 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("lendingLimit"), "1", limitIsEmptyErrorMsg), 167 }, 168 enableLendingLimit: true, 169 }, 170 { 171 name: "flavor quota with lendingLimit greater than nominalQuota", 172 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 173 ResourceGroup( 174 *testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "", "2").Obj()). 175 Cohort("cohort"). 176 Obj(), 177 wantErr: field.ErrorList{ 178 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("lendingLimit"), "2", lendingLimitErrorMsg), 179 }, 180 enableLendingLimit: true, 181 }, 182 { 183 name: "empty queueing strategy is supported", 184 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 185 QueueingStrategy(""). 186 Obj(), 187 }, 188 { 189 name: "namespaceSelector with invalid labels", 190 clusterQueue: testingutil.MakeClusterQueue("cluster-queue").NamespaceSelector(&metav1.LabelSelector{ 191 MatchLabels: map[string]string{"nospecialchars^=@": "bar"}, 192 }).Obj(), 193 wantErr: field.ErrorList{ 194 field.Invalid(specPath.Child("namespaceSelector", "matchLabels"), "nospecialchars^=@", ""), 195 }, 196 }, 197 { 198 name: "namespaceSelector with invalid expressions", 199 clusterQueue: testingutil.MakeClusterQueue("cluster-queue").NamespaceSelector(&metav1.LabelSelector{ 200 MatchExpressions: []metav1.LabelSelectorRequirement{ 201 { 202 Key: "key", 203 Operator: "In", 204 }, 205 }, 206 }).Obj(), 207 wantErr: field.ErrorList{ 208 field.Required(specPath.Child("namespaceSelector", "matchExpressions").Index(0).Child("values"), ""), 209 }, 210 }, 211 { 212 name: "multiple resource groups", 213 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 214 ResourceGroup( 215 *testingutil.MakeFlavorQuotas("alpha"). 216 Resource("cpu", "0"). 217 Resource("memory", "0"). 218 Obj(), 219 *testingutil.MakeFlavorQuotas("beta"). 220 Resource("cpu", "0"). 221 Resource("memory", "0"). 222 Obj(), 223 ). 224 ResourceGroup( 225 *testingutil.MakeFlavorQuotas("gamma"). 226 Resource("example.com/gpu", "0"). 227 Obj(), 228 *testingutil.MakeFlavorQuotas("omega"). 229 Resource("example.com/gpu", "0"). 230 Obj(), 231 ). 232 Obj(), 233 }, 234 { 235 name: "resources in a flavor in different order", 236 clusterQueue: &kueue.ClusterQueue{ 237 ObjectMeta: metav1.ObjectMeta{ 238 Name: "cluster-queue", 239 }, 240 Spec: kueue.ClusterQueueSpec{ 241 ResourceGroups: []kueue.ResourceGroup{ 242 { 243 CoveredResources: []corev1.ResourceName{"cpu", "memory"}, 244 Flavors: []kueue.FlavorQuotas{ 245 *testingutil.MakeFlavorQuotas("alpha"). 246 Resource("cpu", "0"). 247 Resource("memory", "0"). 248 Obj(), 249 *testingutil.MakeFlavorQuotas("beta"). 250 Resource("memory", "0"). 251 Resource("cpu", "0"). 252 Obj(), 253 }, 254 }, 255 }, 256 }, 257 }, 258 wantErr: field.ErrorList{ 259 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(1).Child("resources").Index(0).Child("name"), nil, ""), 260 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(1).Child("resources").Index(1).Child("name"), nil, ""), 261 }, 262 }, 263 { 264 name: "missing resources in a flavor", 265 clusterQueue: &kueue.ClusterQueue{ 266 ObjectMeta: metav1.ObjectMeta{ 267 Name: "cluster-queue", 268 }, 269 Spec: kueue.ClusterQueueSpec{ 270 ResourceGroups: []kueue.ResourceGroup{ 271 { 272 CoveredResources: []corev1.ResourceName{"cpu", "memory"}, 273 Flavors: []kueue.FlavorQuotas{ 274 *testingutil.MakeFlavorQuotas("alpha"). 275 Resource("cpu", "0"). 276 Obj(), 277 }, 278 }, 279 }, 280 }, 281 }, 282 wantErr: field.ErrorList{ 283 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources"), nil, ""), 284 }, 285 }, 286 { 287 name: "missing resources in a flavor", 288 clusterQueue: &kueue.ClusterQueue{ 289 ObjectMeta: metav1.ObjectMeta{ 290 Name: "cluster-queue", 291 }, 292 Spec: kueue.ClusterQueueSpec{ 293 ResourceGroups: []kueue.ResourceGroup{ 294 { 295 CoveredResources: []corev1.ResourceName{"cpu"}, 296 Flavors: []kueue.FlavorQuotas{ 297 *testingutil.MakeFlavorQuotas("alpha"). 298 Resource("cpu", "0"). 299 Resource("memory", "0"). 300 Obj(), 301 }, 302 }, 303 }, 304 }, 305 }, 306 wantErr: field.ErrorList{ 307 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources"), nil, ""), 308 }, 309 }, 310 { 311 name: "missing resources in a flavor and mismatch", 312 clusterQueue: &kueue.ClusterQueue{ 313 ObjectMeta: metav1.ObjectMeta{ 314 Name: "cluster-queue", 315 }, 316 Spec: kueue.ClusterQueueSpec{ 317 ResourceGroups: []kueue.ResourceGroup{ 318 { 319 CoveredResources: []corev1.ResourceName{"blah"}, 320 Flavors: []kueue.FlavorQuotas{ 321 *testingutil.MakeFlavorQuotas("alpha"). 322 Resource("cpu", "0"). 323 Resource("memory", "0"). 324 Obj(), 325 }, 326 }, 327 }, 328 }, 329 }, 330 wantErr: field.ErrorList{ 331 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources"), nil, ""), 332 field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("name"), nil, ""), 333 }, 334 }, 335 { 336 name: "resource in more than one resource group", 337 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 338 ResourceGroup( 339 *testingutil.MakeFlavorQuotas("alpha"). 340 Resource("cpu", "0"). 341 Resource("memory", "0"). 342 Obj(), 343 ). 344 ResourceGroup( 345 *testingutil.MakeFlavorQuotas("beta"). 346 Resource("memory", "0"). 347 Obj(), 348 ). 349 Obj(), 350 wantErr: field.ErrorList{ 351 field.Duplicate(resourceGroupsPath.Index(1).Child("coveredResources").Index(0), nil), 352 }, 353 }, 354 { 355 name: "flavor in more than one resource group", 356 clusterQueue: testingutil.MakeClusterQueue("cluster-queue"). 357 ResourceGroup( 358 *testingutil.MakeFlavorQuotas("alpha").Resource("cpu").Obj(), 359 *testingutil.MakeFlavorQuotas("beta").Resource("cpu").Obj(), 360 ). 361 ResourceGroup( 362 *testingutil.MakeFlavorQuotas("beta").Resource("memory").Obj(), 363 ). 364 Obj(), 365 wantErr: field.ErrorList{ 366 field.Duplicate(resourceGroupsPath.Index(1).Child("flavors").Index(0).Child("name"), nil), 367 }, 368 }, 369 { 370 name: "invalid preemption due to reclaimWithinCohort=Never, while borrowWithinCohort!=nil", 371 clusterQueue: &kueue.ClusterQueue{ 372 ObjectMeta: metav1.ObjectMeta{ 373 Name: "cluster-queue", 374 }, 375 Spec: kueue.ClusterQueueSpec{ 376 Preemption: &kueue.ClusterQueuePreemption{ 377 ReclaimWithinCohort: kueue.PreemptionPolicyNever, 378 BorrowWithinCohort: &kueue.BorrowWithinCohort{ 379 Policy: kueue.BorrowWithinCohortPolicyLowerPriority, 380 }, 381 }, 382 }, 383 }, 384 wantErr: field.ErrorList{ 385 field.Invalid(preemptionPath, nil, ""), 386 }, 387 }, 388 { 389 name: "valid preemption with borrowWithinCohort", 390 clusterQueue: &kueue.ClusterQueue{ 391 ObjectMeta: metav1.ObjectMeta{ 392 Name: "cluster-queue", 393 }, 394 Spec: kueue.ClusterQueueSpec{ 395 Preemption: &kueue.ClusterQueuePreemption{ 396 ReclaimWithinCohort: kueue.PreemptionPolicyLowerPriority, 397 BorrowWithinCohort: &kueue.BorrowWithinCohort{ 398 Policy: kueue.BorrowWithinCohortPolicyLowerPriority, 399 MaxPriorityThreshold: ptr.To[int32](10), 400 }, 401 }, 402 }, 403 }, 404 }, 405 { 406 name: "existing cluster queue created with older Kueue version that has a nil borrowWithinCohort field", 407 clusterQueue: &kueue.ClusterQueue{ 408 ObjectMeta: metav1.ObjectMeta{ 409 Name: "cluster-queue", 410 }, 411 Spec: kueue.ClusterQueueSpec{ 412 Preemption: &kueue.ClusterQueuePreemption{ 413 ReclaimWithinCohort: kueue.PreemptionPolicyNever, 414 }, 415 }, 416 }, 417 }, 418 } 419 420 for _, tc := range testcases { 421 t.Run(tc.name, func(t *testing.T) { 422 defer features.SetFeatureGateDuringTest(t, features.LendingLimit, tc.enableLendingLimit)() 423 gotErr := ValidateClusterQueue(tc.clusterQueue) 424 if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" { 425 t.Errorf("ValidateResources() mismatch (-want +got):\n%s", diff) 426 } 427 }) 428 } 429 } 430 431 func TestValidateClusterQueueUpdate(t *testing.T) { 432 testcases := []struct { 433 name string 434 newClusterQueue *kueue.ClusterQueue 435 oldClusterQueue *kueue.ClusterQueue 436 wantErr field.ErrorList 437 }{ 438 { 439 name: "queueingStrategy cannot be updated", 440 newClusterQueue: testingutil.MakeClusterQueue("cluster-queue").QueueingStrategy("BestEffortFIFO").Obj(), 441 oldClusterQueue: testingutil.MakeClusterQueue("cluster-queue").QueueingStrategy("StrictFIFO").Obj(), 442 wantErr: field.ErrorList{ 443 field.Invalid(field.NewPath("spec", "queueingStrategy"), nil, ""), 444 }, 445 }, 446 { 447 name: "same queueingStrategy", 448 newClusterQueue: testingutil.MakeClusterQueue("cluster-queue").QueueingStrategy("BestEffortFIFO").Obj(), 449 oldClusterQueue: testingutil.MakeClusterQueue("cluster-queue").QueueingStrategy("BestEffortFIFO").Obj(), 450 wantErr: nil, 451 }, 452 } 453 454 for _, tc := range testcases { 455 t.Run(tc.name, func(t *testing.T) { 456 gotErr := ValidateClusterQueueUpdate(tc.newClusterQueue, tc.oldClusterQueue) 457 if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" { 458 t.Errorf("ValidateResources() mismatch (-want +got):\n%s", diff) 459 } 460 }) 461 } 462 }