1
16
17 package imagelocality
18
19 import (
20 "context"
21 "crypto/sha256"
22 "encoding/hex"
23 "testing"
24
25 "github.com/google/go-cmp/cmp"
26 v1 "k8s.io/api/core/v1"
27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28 "k8s.io/klog/v2/ktesting"
29 "k8s.io/kubernetes/pkg/scheduler/framework"
30 "k8s.io/kubernetes/pkg/scheduler/framework/runtime"
31 "k8s.io/kubernetes/pkg/scheduler/internal/cache"
32 )
33
34 func TestImageLocalityPriority(t *testing.T) {
35 test40250 := v1.PodSpec{
36 Containers: []v1.Container{
37 {
38
39 Image: "gcr.io/40",
40 },
41 {
42 Image: "gcr.io/250",
43 },
44 },
45 }
46
47 test40300 := v1.PodSpec{
48 Containers: []v1.Container{
49 {
50 Image: "gcr.io/40",
51 },
52 {
53 Image: "gcr.io/300",
54 },
55 },
56 }
57
58 testMinMax := v1.PodSpec{
59 Containers: []v1.Container{
60 {
61 Image: "gcr.io/10",
62 },
63 {
64 Image: "gcr.io/4000",
65 },
66 },
67 }
68
69 test300600900 := v1.PodSpec{
70 Containers: []v1.Container{
71 {
72 Image: "gcr.io/300",
73 },
74 {
75 Image: "gcr.io/600",
76 },
77 {
78 Image: "gcr.io/900",
79 },
80 },
81 }
82
83 test3040 := v1.PodSpec{
84 Containers: []v1.Container{
85 {
86 Image: "gcr.io/30",
87 },
88 {
89 Image: "gcr.io/40",
90 },
91 },
92 }
93
94 test30Init300 := v1.PodSpec{
95 Containers: []v1.Container{
96 {
97 Image: "gcr.io/30",
98 },
99 },
100 InitContainers: []v1.Container{
101 {Image: "gcr.io/300"},
102 },
103 }
104
105 node403002000 := v1.NodeStatus{
106 Images: []v1.ContainerImage{
107 {
108 Names: []string{
109 "gcr.io/40:latest",
110 "gcr.io/40:v1",
111 "gcr.io/40:v1",
112 },
113 SizeBytes: int64(40 * mb),
114 },
115 {
116 Names: []string{
117 "gcr.io/300:latest",
118 "gcr.io/300:v1",
119 },
120 SizeBytes: int64(300 * mb),
121 },
122 {
123 Names: []string{
124 "gcr.io/2000:latest",
125 },
126 SizeBytes: int64(2000 * mb),
127 },
128 },
129 }
130
131 node25010 := v1.NodeStatus{
132 Images: []v1.ContainerImage{
133 {
134 Names: []string{
135 "gcr.io/250:latest",
136 },
137 SizeBytes: int64(250 * mb),
138 },
139 {
140 Names: []string{
141 "gcr.io/10:latest",
142 "gcr.io/10:v1",
143 },
144 SizeBytes: int64(10 * mb),
145 },
146 },
147 }
148
149 node60040900 := v1.NodeStatus{
150 Images: []v1.ContainerImage{
151 {
152 Names: []string{
153 "gcr.io/600:latest",
154 },
155 SizeBytes: int64(600 * mb),
156 },
157 {
158 Names: []string{
159 "gcr.io/40:latest",
160 },
161 SizeBytes: int64(40 * mb),
162 },
163 {
164 Names: []string{
165 "gcr.io/900:latest",
166 },
167 SizeBytes: int64(900 * mb),
168 },
169 },
170 }
171
172 node300600900 := v1.NodeStatus{
173 Images: []v1.ContainerImage{
174 {
175 Names: []string{
176 "gcr.io/300:latest",
177 },
178 SizeBytes: int64(300 * mb),
179 },
180 {
181 Names: []string{
182 "gcr.io/600:latest",
183 },
184 SizeBytes: int64(600 * mb),
185 },
186 {
187 Names: []string{
188 "gcr.io/900:latest",
189 },
190 SizeBytes: int64(900 * mb),
191 },
192 },
193 }
194
195 node400030 := v1.NodeStatus{
196 Images: []v1.ContainerImage{
197 {
198 Names: []string{
199 "gcr.io/4000:latest",
200 },
201 SizeBytes: int64(4000 * mb),
202 },
203 {
204 Names: []string{
205 "gcr.io/30:latest",
206 },
207 SizeBytes: int64(30 * mb),
208 },
209 },
210 }
211
212 node203040 := v1.NodeStatus{
213 Images: []v1.ContainerImage{
214 {
215 Names: []string{
216 "gcr.io/20:latest",
217 },
218 SizeBytes: int64(20 * mb),
219 },
220 {
221 Names: []string{
222 "gcr.io/30:latest",
223 },
224 SizeBytes: int64(30 * mb),
225 },
226 {
227 Names: []string{
228 "gcr.io/40:latest",
229 },
230 SizeBytes: int64(40 * mb),
231 },
232 },
233 }
234
235 nodeWithNoImages := v1.NodeStatus{}
236
237 tests := []struct {
238 pod *v1.Pod
239 pods []*v1.Pod
240 nodes []*v1.Node
241 expectedList framework.NodeScoreList
242 name string
243 }{
244 {
245
246
247
248
249
250
251
252
253
254 pod: &v1.Pod{Spec: test40250},
255 nodes: []*v1.Node{makeImageNode("node1", node403002000), makeImageNode("node2", node25010)},
256 expectedList: []framework.NodeScore{{Name: "node1", Score: 0}, {Name: "node2", Score: 5}},
257 name: "two images spread on two nodes, prefer the larger image one",
258 },
259 {
260
261
262
263
264
265
266
267
268
269 pod: &v1.Pod{Spec: test40300},
270 nodes: []*v1.Node{makeImageNode("node1", node403002000), makeImageNode("node2", node25010)},
271 expectedList: []framework.NodeScore{{Name: "node1", Score: 7}, {Name: "node2", Score: 0}},
272 name: "two images on one node, prefer this node",
273 },
274 {
275
276
277
278
279
280
281
282
283
284 pod: &v1.Pod{Spec: testMinMax},
285 nodes: []*v1.Node{makeImageNode("node1", node400030), makeImageNode("node2", node25010)},
286 expectedList: []framework.NodeScore{{Name: "node1", Score: framework.MaxNodeScore}, {Name: "node2", Score: 0}},
287 name: "if exceed limit, use limit",
288 },
289 {
290
291
292
293
294
295
296
297
298
299
300
301
302
303 pod: &v1.Pod{Spec: testMinMax},
304 nodes: []*v1.Node{makeImageNode("node1", node400030), makeImageNode("node2", node25010), makeImageNode("node3", nodeWithNoImages)},
305 expectedList: []framework.NodeScore{{Name: "node1", Score: 66}, {Name: "node2", Score: 0}, {Name: "node3", Score: 0}},
306 name: "if exceed limit, use limit (with node which has no images present)",
307 },
308 {
309
310
311
312
313
314
315
316
317
318
319
320
321
322 pod: &v1.Pod{Spec: test300600900},
323 nodes: []*v1.Node{makeImageNode("node1", node60040900), makeImageNode("node2", node300600900), makeImageNode("node3", nodeWithNoImages)},
324 expectedList: []framework.NodeScore{{Name: "node1", Score: 32}, {Name: "node2", Score: 36}, {Name: "node3", Score: 0}},
325 name: "pod with multiple large images, node2 is preferred",
326 },
327 {
328
329
330
331
332
333
334
335
336
337 pod: &v1.Pod{Spec: test3040},
338 nodes: []*v1.Node{makeImageNode("node1", node203040), makeImageNode("node2", node400030)},
339 expectedList: []framework.NodeScore{{Name: "node1", Score: 1}, {Name: "node2", Score: 0}},
340 name: "pod with multiple small images",
341 },
342 {
343
344
345
346
347
348
349
350
351
352 pod: &v1.Pod{Spec: test30Init300},
353 nodes: []*v1.Node{makeImageNode("node1", node403002000), makeImageNode("node2", node203040)},
354 expectedList: []framework.NodeScore{{Name: "node1", Score: 6}, {Name: "node2", Score: 0}},
355 name: "include InitContainers: two images spread on two nodes, prefer the larger image one",
356 },
357 }
358
359 for _, test := range tests {
360 t.Run(test.name, func(t *testing.T) {
361 _, ctx := ktesting.NewTestContext(t)
362 ctx, cancel := context.WithCancel(ctx)
363 defer cancel()
364
365 snapshot := cache.NewSnapshot(nil, test.nodes)
366 state := framework.NewCycleState()
367 fh, _ := runtime.NewFramework(ctx, nil, nil, runtime.WithSnapshotSharedLister(snapshot))
368
369 p, err := New(ctx, nil, fh)
370 if err != nil {
371 t.Fatalf("creating plugin: %v", err)
372 }
373 var gotList framework.NodeScoreList
374 for _, n := range test.nodes {
375 nodeName := n.ObjectMeta.Name
376 score, status := p.(framework.ScorePlugin).Score(ctx, state, test.pod, nodeName)
377 if !status.IsSuccess() {
378 t.Errorf("unexpected error: %v", status)
379 }
380 gotList = append(gotList, framework.NodeScore{Name: nodeName, Score: score})
381 }
382
383 if diff := cmp.Diff(test.expectedList, gotList); diff != "" {
384 t.Errorf("Unexpected node score list (-want, +got):\n%s", diff)
385 }
386 })
387 }
388 }
389
390 func TestNormalizedImageName(t *testing.T) {
391 for _, testCase := range []struct {
392 Name string
393 Input string
394 Output string
395 }{
396 {Name: "add :latest postfix 1", Input: "root", Output: "root:latest"},
397 {Name: "add :latest postfix 2", Input: "gcr.io:5000/root", Output: "gcr.io:5000/root:latest"},
398 {Name: "keep it as is 1", Input: "root:tag", Output: "root:tag"},
399 {Name: "keep it as is 2", Input: "root@" + getImageFakeDigest("root"), Output: "root@" + getImageFakeDigest("root")},
400 } {
401 t.Run(testCase.Name, func(t *testing.T) {
402 image := normalizedImageName(testCase.Input)
403 if image != testCase.Output {
404 t.Errorf("expected image reference: %q, got %q", testCase.Output, image)
405 }
406 })
407 }
408 }
409
410 func makeImageNode(node string, status v1.NodeStatus) *v1.Node {
411 return &v1.Node{
412 ObjectMeta: metav1.ObjectMeta{Name: node},
413 Status: status,
414 }
415 }
416
417 func getImageFakeDigest(fakeContent string) string {
418 hash := sha256.Sum256([]byte(fakeContent))
419 return "sha256:" + hex.EncodeToString(hash[:])
420 }
421
View as plain text