1
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 }
590
View as plain text