k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/plugin/pkg/admission/runtimeclass/admission_test.go (about) 1 /* 2 Copyright 2019 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 runtimeclass 18 19 import ( 20 "context" 21 "strconv" 22 "testing" 23 24 corev1 "k8s.io/api/core/v1" 25 nodev1 "k8s.io/api/node/v1" 26 "k8s.io/apimachinery/pkg/api/resource" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/apiserver/pkg/admission" 30 "k8s.io/apiserver/pkg/authentication/user" 31 "k8s.io/client-go/informers" 32 "k8s.io/client-go/kubernetes" 33 "k8s.io/client-go/kubernetes/fake" 34 "k8s.io/kubernetes/pkg/apis/core" 35 "k8s.io/kubernetes/pkg/controller" 36 37 "github.com/stretchr/testify/assert" 38 ) 39 40 func newOverheadValidPod(name string, numContainers int, resources core.ResourceRequirements, setOverhead bool) *core.Pod { 41 pod := &core.Pod{ 42 ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"}, 43 Spec: core.PodSpec{}, 44 } 45 pod.Spec.Containers = make([]core.Container, 0, numContainers) 46 for i := 0; i < numContainers; i++ { 47 pod.Spec.Containers = append(pod.Spec.Containers, core.Container{ 48 Image: "foo:V" + strconv.Itoa(i), 49 Resources: resources, 50 Name: "foo-" + strconv.Itoa(i), 51 }) 52 } 53 54 if setOverhead { 55 pod.Spec.Overhead = core.ResourceList{ 56 core.ResourceName(core.ResourceCPU): resource.MustParse("100m"), 57 core.ResourceName(core.ResourceMemory): resource.MustParse("1"), 58 } 59 } 60 return pod 61 } 62 63 func newSchedulingValidPod(name string, nodeSelector map[string]string, tolerations []core.Toleration) *core.Pod { 64 return &core.Pod{ 65 ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"}, 66 Spec: core.PodSpec{ 67 NodeSelector: nodeSelector, 68 Tolerations: tolerations, 69 }, 70 } 71 } 72 73 func getGuaranteedRequirements() core.ResourceRequirements { 74 resources := core.ResourceList{ 75 core.ResourceName(core.ResourceCPU): resource.MustParse("1"), 76 core.ResourceName(core.ResourceMemory): resource.MustParse("10"), 77 } 78 79 return core.ResourceRequirements{Limits: resources, Requests: resources} 80 } 81 82 func TestSetOverhead(t *testing.T) { 83 tests := []struct { 84 name string 85 runtimeClass *nodev1.RuntimeClass 86 pod *core.Pod 87 expectError bool 88 expectedPod *core.Pod 89 }{ 90 { 91 name: "overhead, no container requirements", 92 runtimeClass: &nodev1.RuntimeClass{ 93 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 94 Handler: "bar", 95 Overhead: &nodev1.Overhead{ 96 PodFixed: corev1.ResourceList{ 97 corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"), 98 corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"), 99 }, 100 }, 101 }, 102 pod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, false), 103 expectError: false, 104 expectedPod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, true), 105 }, 106 { 107 name: "overhead, guaranteed pod", 108 runtimeClass: &nodev1.RuntimeClass{ 109 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 110 Handler: "bar", 111 Overhead: &nodev1.Overhead{ 112 PodFixed: corev1.ResourceList{ 113 corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"), 114 corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"), 115 }, 116 }, 117 }, 118 pod: newOverheadValidPod("guaranteed", 1, getGuaranteedRequirements(), false), 119 expectError: false, 120 expectedPod: newOverheadValidPod("guaranteed", 1, core.ResourceRequirements{}, true), 121 }, 122 { 123 name: "overhead, pod with differing overhead already set", 124 runtimeClass: &nodev1.RuntimeClass{ 125 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 126 Handler: "bar", 127 Overhead: &nodev1.Overhead{ 128 PodFixed: corev1.ResourceList{ 129 corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("10"), 130 corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("10G"), 131 }, 132 }, 133 }, 134 pod: newOverheadValidPod("empty-requiremennts-overhead", 1, core.ResourceRequirements{}, true), 135 expectError: true, 136 expectedPod: nil, 137 }, 138 { 139 name: "overhead, pod with same overhead already set", 140 runtimeClass: &nodev1.RuntimeClass{ 141 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 142 Handler: "bar", 143 Overhead: &nodev1.Overhead{ 144 PodFixed: corev1.ResourceList{ 145 corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"), 146 corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"), 147 }, 148 }, 149 }, 150 pod: newOverheadValidPod("empty-requiremennts-overhead", 1, core.ResourceRequirements{}, true), 151 expectError: false, 152 expectedPod: nil, 153 }, 154 } 155 156 for _, tc := range tests { 157 t.Run(tc.name, func(t *testing.T) { 158 159 attrs := admission.NewAttributesRecord(tc.pod, nil, core.Kind("Pod").WithVersion("version"), tc.pod.Namespace, tc.pod.Name, core.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{}) 160 161 errs := setOverhead(attrs, tc.pod, tc.runtimeClass) 162 if tc.expectError { 163 assert.NotEmpty(t, errs) 164 } else { 165 assert.Empty(t, errs) 166 } 167 }) 168 } 169 } 170 171 func TestSetScheduling(t *testing.T) { 172 tests := []struct { 173 name string 174 runtimeClass *nodev1.RuntimeClass 175 pod *core.Pod 176 expectError bool 177 expectedPod *core.Pod 178 }{ 179 { 180 name: "scheduling, nil scheduling", 181 runtimeClass: &nodev1.RuntimeClass{ 182 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 183 Handler: "bar", 184 Scheduling: nil, 185 }, 186 pod: newSchedulingValidPod("pod-with-conflict-node-selector", map[string]string{"foo": "bar"}, []core.Toleration{}), 187 expectError: false, 188 expectedPod: newSchedulingValidPod("pod-with-conflict-node-selector", map[string]string{"foo": "bar"}, []core.Toleration{}), 189 }, 190 { 191 name: "scheduling, conflict node selector", 192 runtimeClass: &nodev1.RuntimeClass{ 193 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 194 Handler: "bar", 195 Scheduling: &nodev1.Scheduling{ 196 NodeSelector: map[string]string{ 197 "foo": "conflict", 198 }, 199 }, 200 }, 201 pod: newSchedulingValidPod("pod-with-conflict-node-selector", map[string]string{"foo": "bar"}, []core.Toleration{}), 202 expectError: true, 203 }, 204 { 205 name: "scheduling, nil node selector", 206 runtimeClass: &nodev1.RuntimeClass{ 207 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 208 Handler: "bar", 209 Scheduling: &nodev1.Scheduling{ 210 NodeSelector: map[string]string{ 211 "foo": "bar", 212 }, 213 }, 214 }, 215 pod: newSchedulingValidPod("pod-with-conflict-node-selector", nil, nil), 216 expectError: false, 217 expectedPod: newSchedulingValidPod("pod-with-conflict-node-selector", map[string]string{"foo": "bar"}, nil), 218 }, 219 { 220 name: "scheduling, node selector with the same key value", 221 runtimeClass: &nodev1.RuntimeClass{ 222 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 223 Handler: "bar", 224 Scheduling: &nodev1.Scheduling{ 225 NodeSelector: map[string]string{ 226 "foo": "bar", 227 }, 228 }, 229 }, 230 pod: newSchedulingValidPod("pod-with-same-key-value-node-selector", map[string]string{"foo": "bar"}, nil), 231 expectError: false, 232 expectedPod: newSchedulingValidPod("pod-with-same-key-value-node-selector", map[string]string{"foo": "bar"}, nil), 233 }, 234 { 235 name: "scheduling, node selector with different key value", 236 runtimeClass: &nodev1.RuntimeClass{ 237 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 238 Handler: "bar", 239 Scheduling: &nodev1.Scheduling{ 240 NodeSelector: map[string]string{ 241 "foo": "bar", 242 "fizz": "buzz", 243 }, 244 }, 245 }, 246 pod: newSchedulingValidPod("pod-with-different-key-value-node-selector", map[string]string{"foo": "bar"}, nil), 247 expectError: false, 248 expectedPod: newSchedulingValidPod("pod-with-different-key-value-node-selector", map[string]string{"foo": "bar", "fizz": "buzz"}, nil), 249 }, 250 { 251 name: "scheduling, multiple tolerations", 252 runtimeClass: &nodev1.RuntimeClass{ 253 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 254 Handler: "bar", 255 Scheduling: &nodev1.Scheduling{ 256 Tolerations: []corev1.Toleration{ 257 { 258 Key: "foo", 259 Operator: corev1.TolerationOpEqual, 260 Value: "bar", 261 Effect: corev1.TaintEffectNoSchedule, 262 }, 263 { 264 Key: "fizz", 265 Operator: corev1.TolerationOpEqual, 266 Value: "buzz", 267 Effect: corev1.TaintEffectNoSchedule, 268 }, 269 }, 270 }, 271 }, 272 pod: newSchedulingValidPod("pod-with-tolerations", map[string]string{"foo": "bar"}, 273 []core.Toleration{ 274 { 275 Key: "foo", 276 Operator: core.TolerationOpEqual, 277 Value: "bar", 278 Effect: core.TaintEffectNoSchedule, 279 }, 280 }), 281 expectError: false, 282 expectedPod: newSchedulingValidPod("pod-with-tolerations", map[string]string{"foo": "bar"}, 283 []core.Toleration{ 284 { 285 Key: "foo", 286 Operator: core.TolerationOpEqual, 287 Value: "bar", 288 Effect: core.TaintEffectNoSchedule, 289 }, 290 { 291 Key: "fizz", 292 Operator: core.TolerationOpEqual, 293 Value: "buzz", 294 Effect: core.TaintEffectNoSchedule, 295 }, 296 }), 297 }, 298 } 299 300 for _, tc := range tests { 301 t.Run(tc.name, func(t *testing.T) { 302 attrs := admission.NewAttributesRecord(tc.pod, nil, core.Kind("Pod").WithVersion("version"), tc.pod.Namespace, tc.pod.Name, core.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{}) 303 304 errs := setScheduling(attrs, tc.pod, tc.runtimeClass) 305 if tc.expectError { 306 assert.NotEmpty(t, errs) 307 } else { 308 assert.Equal(t, tc.expectedPod, tc.pod) 309 assert.Empty(t, errs) 310 } 311 }) 312 } 313 } 314 315 func NewObjectInterfacesForTest() admission.ObjectInterfaces { 316 scheme := runtime.NewScheme() 317 corev1.AddToScheme(scheme) 318 return admission.NewObjectInterfacesFromScheme(scheme) 319 } 320 321 func newRuntimeClassForTest( 322 addLister bool, 323 listerObject *nodev1.RuntimeClass, 324 addClient bool, 325 clientObject *nodev1.RuntimeClass) *RuntimeClass { 326 runtimeClass := NewRuntimeClass() 327 328 if addLister { 329 informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) 330 runtimeClass.SetExternalKubeInformerFactory(informerFactory) 331 if listerObject != nil { 332 informerFactory.Node().V1().RuntimeClasses().Informer().GetStore().Add(listerObject) 333 } 334 } 335 336 if addClient { 337 var client kubernetes.Interface 338 if clientObject != nil { 339 client = fake.NewSimpleClientset(clientObject) 340 } else { 341 client = fake.NewSimpleClientset() 342 } 343 runtimeClass.SetExternalKubeClientSet(client) 344 } 345 346 return runtimeClass 347 } 348 349 func TestValidateInitialization(t *testing.T) { 350 tests := []struct { 351 name string 352 expectError bool 353 runtimeClass *RuntimeClass 354 }{ 355 { 356 name: "runtimeClass enabled, success", 357 expectError: false, 358 runtimeClass: newRuntimeClassForTest(true, nil, true, nil), 359 }, 360 { 361 name: "runtimeClass enabled, no lister", 362 expectError: true, 363 runtimeClass: newRuntimeClassForTest(false, nil, true, nil), 364 }, 365 { 366 name: "runtimeClass enabled, no client", 367 expectError: true, 368 runtimeClass: newRuntimeClassForTest(true, nil, false, nil), 369 }, 370 } 371 372 for _, tc := range tests { 373 t.Run(tc.name, func(t *testing.T) { 374 err := tc.runtimeClass.ValidateInitialization() 375 if tc.expectError { 376 assert.NotEmpty(t, err) 377 } else { 378 assert.Empty(t, err) 379 } 380 }) 381 } 382 } 383 384 func TestAdmit(t *testing.T) { 385 runtimeClassName := "runtimeClassName" 386 387 rc := &nodev1.RuntimeClass{ 388 ObjectMeta: metav1.ObjectMeta{Name: runtimeClassName}, 389 } 390 391 pod := core.Pod{ 392 ObjectMeta: metav1.ObjectMeta{Name: "podname"}, 393 Spec: core.PodSpec{ 394 RuntimeClassName: &runtimeClassName, 395 }, 396 } 397 398 attributes := admission.NewAttributesRecord(&pod, 399 nil, 400 core.Kind("kind").WithVersion("version"), 401 "", 402 "", 403 core.Resource("pods").WithVersion("version"), 404 "", 405 admission.Create, 406 nil, 407 false, 408 nil) 409 410 tests := []struct { 411 name string 412 expectError bool 413 runtimeClass *RuntimeClass 414 }{ 415 { 416 name: "runtimeClass found by lister", 417 expectError: false, 418 runtimeClass: newRuntimeClassForTest(true, rc, true, nil), 419 }, 420 { 421 name: "runtimeClass found by client", 422 expectError: false, 423 runtimeClass: newRuntimeClassForTest(true, nil, true, rc), 424 }, 425 { 426 name: "runtimeClass not found by lister nor client", 427 expectError: true, 428 runtimeClass: newRuntimeClassForTest(true, nil, true, nil), 429 }, 430 } 431 432 for _, tc := range tests { 433 t.Run(tc.name, func(t *testing.T) { 434 err := tc.runtimeClass.Admit(context.TODO(), attributes, nil) 435 if tc.expectError { 436 assert.NotEmpty(t, err) 437 } else { 438 assert.Empty(t, err) 439 } 440 }) 441 } 442 } 443 444 func TestValidate(t *testing.T) { 445 tests := []struct { 446 name string 447 runtimeClass *nodev1.RuntimeClass 448 pod *core.Pod 449 expectError bool 450 }{ 451 { 452 name: "No Overhead in RunntimeClass, Overhead set in pod", 453 runtimeClass: &nodev1.RuntimeClass{ 454 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 455 Handler: "bar", 456 }, 457 pod: newOverheadValidPod("no-resource-req-no-overhead", 1, getGuaranteedRequirements(), true), 458 expectError: true, 459 }, 460 { 461 name: "Non-matching Overheads", 462 runtimeClass: &nodev1.RuntimeClass{ 463 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 464 Handler: "bar", 465 Overhead: &nodev1.Overhead{ 466 PodFixed: corev1.ResourceList{ 467 corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("10"), 468 corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("10G"), 469 }, 470 }, 471 }, 472 pod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, true), 473 expectError: true, 474 }, 475 { 476 name: "Matching Overheads", 477 runtimeClass: &nodev1.RuntimeClass{ 478 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 479 Handler: "bar", 480 Overhead: &nodev1.Overhead{ 481 PodFixed: corev1.ResourceList{ 482 corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"), 483 corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"), 484 }, 485 }, 486 }, 487 pod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, false), 488 expectError: false, 489 }, 490 } 491 rt := NewRuntimeClass() 492 o := NewObjectInterfacesForTest() 493 for _, tc := range tests { 494 t.Run(tc.name, func(t *testing.T) { 495 496 attrs := admission.NewAttributesRecord(tc.pod, nil, core.Kind("Pod").WithVersion("version"), tc.pod.Namespace, tc.pod.Name, core.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{}) 497 498 errs := rt.Validate(context.TODO(), attrs, o) 499 if tc.expectError { 500 assert.NotEmpty(t, errs) 501 } else { 502 assert.Empty(t, errs) 503 } 504 }) 505 } 506 } 507 508 func TestValidateOverhead(t *testing.T) { 509 tests := []struct { 510 name string 511 runtimeClass *nodev1.RuntimeClass 512 pod *core.Pod 513 expectError bool 514 }{ 515 { 516 name: "Overhead part of RuntimeClass, no Overhead defined in pod", 517 runtimeClass: &nodev1.RuntimeClass{ 518 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 519 Handler: "bar", 520 Overhead: &nodev1.Overhead{ 521 PodFixed: corev1.ResourceList{ 522 corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"), 523 corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"), 524 }, 525 }, 526 }, 527 pod: newOverheadValidPod("no-requirements", 1, core.ResourceRequirements{}, false), 528 expectError: true, 529 }, 530 { 531 name: "No Overhead in RunntimeClass, Overhead set in pod", 532 runtimeClass: &nodev1.RuntimeClass{ 533 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 534 Handler: "bar", 535 }, 536 pod: newOverheadValidPod("no-resource-req-no-overhead", 1, getGuaranteedRequirements(), true), 537 expectError: true, 538 }, 539 { 540 name: "No RunntimeClass, Overhead set in pod", 541 runtimeClass: nil, 542 pod: newOverheadValidPod("no-resource-req-no-overhead", 1, getGuaranteedRequirements(), true), 543 expectError: true, 544 }, 545 { 546 name: "Non-matching Overheads", 547 runtimeClass: &nodev1.RuntimeClass{ 548 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 549 Handler: "bar", 550 Overhead: &nodev1.Overhead{ 551 PodFixed: corev1.ResourceList{ 552 corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("10"), 553 corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("10G"), 554 }, 555 }, 556 }, 557 pod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, true), 558 expectError: true, 559 }, 560 { 561 name: "Matching Overheads", 562 runtimeClass: &nodev1.RuntimeClass{ 563 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 564 Handler: "bar", 565 Overhead: &nodev1.Overhead{ 566 PodFixed: corev1.ResourceList{ 567 corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"), 568 corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"), 569 }, 570 }, 571 }, 572 pod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, true), 573 expectError: false, 574 }, 575 } 576 577 for _, tc := range tests { 578 t.Run(tc.name, func(t *testing.T) { 579 attrs := admission.NewAttributesRecord(tc.pod, nil, core.Kind("Pod").WithVersion("version"), tc.pod.Namespace, tc.pod.Name, core.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{}) 580 581 errs := validateOverhead(attrs, tc.pod, tc.runtimeClass) 582 if tc.expectError { 583 assert.NotEmpty(t, errs) 584 } else { 585 assert.Empty(t, errs) 586 } 587 }) 588 } 589 }