k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/scheduler/framework/plugins/imagelocality/image_locality_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 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 // Pod: gcr.io/40 gcr.io/250 246 247 // Node1 248 // Image: gcr.io/40:latest 40MB 249 // Score: 0 (40M/2 < 23M, min-threshold) 250 251 // Node2 252 // Image: gcr.io/250:latest 250MB 253 // Score: 100 * (250M/2 - 23M)/(1000M * 2 - 23M) = 5 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 // Pod: gcr.io/40 gcr.io/300 261 262 // Node1 263 // Image: gcr.io/40:latest 40MB, gcr.io/300:latest 300MB 264 // Score: 100 * ((40M + 300M)/2 - 23M)/(1000M * 2 - 23M) = 7 265 266 // Node2 267 // Image: not present 268 // Score: 0 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 // Pod: gcr.io/4000 gcr.io/10 276 277 // Node1 278 // Image: gcr.io/4000:latest 2000MB 279 // Score: 100 (4000 * 1/2 >= 1000M * 2, max-threshold) 280 281 // Node2 282 // Image: gcr.io/10:latest 10MB 283 // Score: 0 (10M/2 < 23M, min-threshold) 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 // Pod: gcr.io/4000 gcr.io/10 291 292 // Node1 293 // Image: gcr.io/4000:latest 4000MB 294 // Score: 100 * (4000M/3 - 23M)/(1000M * 2 - 23M) = 66 295 296 // Node2 297 // Image: gcr.io/10:latest 10MB 298 // Score: 0 (10M*1/3 < 23M, min-threshold) 299 300 // Node3 301 // Image: 302 // Score: 0 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 // Pod: gcr.io/300 gcr.io/600 gcr.io/900 310 311 // Node1 312 // Image: gcr.io/600:latest 600MB, gcr.io/900:latest 900MB 313 // Score: 100 * (600M * 2/3 + 900M * 2/3 - 23M) / (1000M * 3 - 23M) = 32 314 315 // Node2 316 // Image: gcr.io/300:latest 300MB, gcr.io/600:latest 600MB, gcr.io/900:latest 900MB 317 // Score: 100 * (300M * 1/3 + 600M * 2/3 + 900M * 2/3 - 23M) / (1000M *3 - 23M) = 36 318 319 // Node3 320 // Image: 321 // Score: 0 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 // Pod: gcr.io/30 gcr.io/40 329 330 // Node1 331 // Image: gcr.io/20:latest 20MB, gcr.io/30:latest 30MB, gcr.io/40:latest 40MB 332 // Score: 100 * (30M + 40M * 1/2 - 23M) / (1000M * 2 - 23M) = 1 333 334 // Node2 335 // Image: 100 * (30M - 23M) / (1000M * 2 - 23M) = 0 336 // Score: 0 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 // Pod: gcr.io/30 InitContainers: gcr.io/300 344 345 // Node1 346 // Image: gcr.io/40:latest 40MB, gcr.io/300:latest 300MB, gcr.io/2000:latest 2000MB 347 // Score: 100 * (300M * 1/2 - 23M) / (1000M * 2 - 23M) = 6 348 349 // Node2 350 // Image: gcr.io/20:latest 20MB, gcr.io/30:latest 30MB, gcr.io/40:latest 40MB 351 // Score: 100 * (30M * 1/2 - 23M) / (1000M * 2 - 23M) = 0 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 }