k8s.io/kubernetes@v1.29.3/pkg/kubelet/images/image_manager_test.go (about) 1 /* 2 Copyright 2015 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 images 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "sync" 24 "testing" 25 "time" 26 27 "github.com/stretchr/testify/assert" 28 v1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/types" 31 utilfeature "k8s.io/apiserver/pkg/util/feature" 32 "k8s.io/client-go/tools/record" 33 "k8s.io/client-go/util/flowcontrol" 34 featuregatetesting "k8s.io/component-base/featuregate/testing" 35 crierrors "k8s.io/cri-api/pkg/errors" 36 "k8s.io/kubernetes/pkg/features" 37 . "k8s.io/kubernetes/pkg/kubelet/container" 38 ctest "k8s.io/kubernetes/pkg/kubelet/container/testing" 39 testingclock "k8s.io/utils/clock/testing" 40 utilpointer "k8s.io/utils/pointer" 41 ) 42 43 type pullerExpects struct { 44 calls []string 45 err error 46 shouldRecordStartedPullingTime bool 47 shouldRecordFinishedPullingTime bool 48 } 49 50 type pullerTestCase struct { 51 testName string 52 containerImage string 53 policy v1.PullPolicy 54 inspectErr error 55 pullerErr error 56 qps float32 57 burst int 58 expected []pullerExpects 59 } 60 61 func pullerTestCases() []pullerTestCase { 62 return []pullerTestCase{ 63 { // pull missing image 64 testName: "image missing, pull", 65 containerImage: "missing_image", 66 policy: v1.PullIfNotPresent, 67 inspectErr: nil, 68 pullerErr: nil, 69 qps: 0.0, 70 burst: 0, 71 expected: []pullerExpects{ 72 {[]string{"GetImageRef", "PullImage"}, nil, true, true}, 73 }}, 74 75 { // image present, don't pull 76 testName: "image present, don't pull ", 77 containerImage: "present_image", 78 policy: v1.PullIfNotPresent, 79 inspectErr: nil, 80 pullerErr: nil, 81 qps: 0.0, 82 burst: 0, 83 expected: []pullerExpects{ 84 {[]string{"GetImageRef"}, nil, false, false}, 85 {[]string{"GetImageRef"}, nil, false, false}, 86 {[]string{"GetImageRef"}, nil, false, false}, 87 }}, 88 // image present, pull it 89 {containerImage: "present_image", 90 testName: "image present, pull ", 91 policy: v1.PullAlways, 92 inspectErr: nil, 93 pullerErr: nil, 94 qps: 0.0, 95 burst: 0, 96 expected: []pullerExpects{ 97 {[]string{"GetImageRef", "PullImage"}, nil, true, true}, 98 {[]string{"GetImageRef", "PullImage"}, nil, true, true}, 99 {[]string{"GetImageRef", "PullImage"}, nil, true, true}, 100 }}, 101 // missing image, error PullNever 102 {containerImage: "missing_image", 103 testName: "image missing, never pull", 104 policy: v1.PullNever, 105 inspectErr: nil, 106 pullerErr: nil, 107 qps: 0.0, 108 burst: 0, 109 expected: []pullerExpects{ 110 {[]string{"GetImageRef"}, ErrImageNeverPull, false, false}, 111 {[]string{"GetImageRef"}, ErrImageNeverPull, false, false}, 112 {[]string{"GetImageRef"}, ErrImageNeverPull, false, false}, 113 }}, 114 // missing image, unable to inspect 115 {containerImage: "missing_image", 116 testName: "image missing, pull if not present", 117 policy: v1.PullIfNotPresent, 118 inspectErr: errors.New("unknown inspectError"), 119 pullerErr: nil, 120 qps: 0.0, 121 burst: 0, 122 expected: []pullerExpects{ 123 {[]string{"GetImageRef"}, ErrImageInspect, false, false}, 124 {[]string{"GetImageRef"}, ErrImageInspect, false, false}, 125 {[]string{"GetImageRef"}, ErrImageInspect, false, false}, 126 }}, 127 // missing image, unable to fetch 128 {containerImage: "typo_image", 129 testName: "image missing, unable to fetch", 130 policy: v1.PullIfNotPresent, 131 inspectErr: nil, 132 pullerErr: errors.New("404"), 133 qps: 0.0, 134 burst: 0, 135 expected: []pullerExpects{ 136 {[]string{"GetImageRef", "PullImage"}, ErrImagePull, true, false}, 137 {[]string{"GetImageRef", "PullImage"}, ErrImagePull, true, false}, 138 {[]string{"GetImageRef"}, ErrImagePullBackOff, false, false}, 139 {[]string{"GetImageRef", "PullImage"}, ErrImagePull, true, false}, 140 {[]string{"GetImageRef"}, ErrImagePullBackOff, false, false}, 141 {[]string{"GetImageRef"}, ErrImagePullBackOff, false, false}, 142 }}, 143 // image present, non-zero qps, try to pull 144 {containerImage: "present_image", 145 testName: "image present and qps>0, pull", 146 policy: v1.PullAlways, 147 inspectErr: nil, 148 pullerErr: nil, 149 qps: 400.0, 150 burst: 600, 151 expected: []pullerExpects{ 152 {[]string{"GetImageRef", "PullImage"}, nil, true, true}, 153 {[]string{"GetImageRef", "PullImage"}, nil, true, true}, 154 {[]string{"GetImageRef", "PullImage"}, nil, true, true}, 155 }}, 156 // image present, non-zero qps, try to pull when qps exceeded 157 {containerImage: "present_image", 158 testName: "image present and excessive qps rate, pull", 159 policy: v1.PullAlways, 160 inspectErr: nil, 161 pullerErr: nil, 162 qps: 2000.0, 163 burst: 0, 164 expected: []pullerExpects{ 165 {[]string{"GetImageRef"}, ErrImagePull, true, false}, 166 {[]string{"GetImageRef"}, ErrImagePull, true, false}, 167 {[]string{"GetImageRef"}, ErrImagePullBackOff, false, false}, 168 }}, 169 // error case if image name fails validation due to invalid reference format 170 {containerImage: "FAILED_IMAGE", 171 testName: "invalid image name, no pull", 172 policy: v1.PullAlways, 173 inspectErr: nil, 174 pullerErr: nil, 175 qps: 0.0, 176 burst: 0, 177 expected: []pullerExpects{ 178 {[]string(nil), ErrInvalidImageName, false, false}, 179 }}, 180 // error case if image name contains http 181 {containerImage: "http://url", 182 testName: "invalid image name with http, no pull", 183 policy: v1.PullAlways, 184 inspectErr: nil, 185 pullerErr: nil, 186 qps: 0.0, 187 burst: 0, 188 expected: []pullerExpects{ 189 {[]string(nil), ErrInvalidImageName, false, false}, 190 }}, 191 // error case if image name contains sha256 192 {containerImage: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", 193 testName: "invalid image name with sha256, no pull", 194 policy: v1.PullAlways, 195 inspectErr: nil, 196 pullerErr: nil, 197 qps: 0.0, 198 burst: 0, 199 expected: []pullerExpects{ 200 {[]string(nil), ErrInvalidImageName, false, false}, 201 }}, 202 } 203 } 204 205 type mockPodPullingTimeRecorder struct { 206 sync.Mutex 207 startedPullingRecorded bool 208 finishedPullingRecorded bool 209 } 210 211 func (m *mockPodPullingTimeRecorder) RecordImageStartedPulling(podUID types.UID) { 212 m.Lock() 213 defer m.Unlock() 214 m.startedPullingRecorded = true 215 } 216 217 func (m *mockPodPullingTimeRecorder) RecordImageFinishedPulling(podUID types.UID) { 218 m.Lock() 219 defer m.Unlock() 220 m.finishedPullingRecorded = true 221 } 222 223 func (m *mockPodPullingTimeRecorder) reset() { 224 m.Lock() 225 defer m.Unlock() 226 m.startedPullingRecorded = false 227 m.finishedPullingRecorded = false 228 } 229 230 func pullerTestEnv(t *testing.T, c pullerTestCase, serialized bool, maxParallelImagePulls *int32) (puller ImageManager, fakeClock *testingclock.FakeClock, fakeRuntime *ctest.FakeRuntime, container *v1.Container, fakePodPullingTimeRecorder *mockPodPullingTimeRecorder) { 231 container = &v1.Container{ 232 Name: "container_name", 233 Image: c.containerImage, 234 ImagePullPolicy: c.policy, 235 } 236 237 backOff := flowcontrol.NewBackOff(time.Second, time.Minute) 238 fakeClock = testingclock.NewFakeClock(time.Now()) 239 backOff.Clock = fakeClock 240 241 fakeRuntime = &ctest.FakeRuntime{T: t} 242 fakeRecorder := &record.FakeRecorder{} 243 244 fakeRuntime.ImageList = []Image{{ID: "present_image:latest"}} 245 fakeRuntime.Err = c.pullerErr 246 fakeRuntime.InspectErr = c.inspectErr 247 248 fakePodPullingTimeRecorder = &mockPodPullingTimeRecorder{} 249 250 puller = NewImageManager(fakeRecorder, fakeRuntime, backOff, serialized, maxParallelImagePulls, c.qps, c.burst, fakePodPullingTimeRecorder) 251 return 252 } 253 254 func TestParallelPuller(t *testing.T) { 255 pod := &v1.Pod{ 256 ObjectMeta: metav1.ObjectMeta{ 257 Name: "test_pod", 258 Namespace: "test-ns", 259 UID: "bar", 260 ResourceVersion: "42", 261 }} 262 263 cases := pullerTestCases() 264 265 useSerializedEnv := false 266 for _, c := range cases { 267 t.Run(c.testName, func(t *testing.T) { 268 ctx := context.Background() 269 puller, fakeClock, fakeRuntime, container, fakePodPullingTimeRecorder := pullerTestEnv(t, c, useSerializedEnv, nil) 270 271 for _, expected := range c.expected { 272 fakeRuntime.CalledFunctions = nil 273 fakeClock.Step(time.Second) 274 275 _, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, "") 276 fakeRuntime.AssertCalls(expected.calls) 277 assert.Equal(t, expected.err, err) 278 assert.Equal(t, expected.shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded) 279 assert.Equal(t, expected.shouldRecordFinishedPullingTime, fakePodPullingTimeRecorder.finishedPullingRecorded) 280 fakePodPullingTimeRecorder.reset() 281 } 282 }) 283 } 284 } 285 286 func TestSerializedPuller(t *testing.T) { 287 pod := &v1.Pod{ 288 ObjectMeta: metav1.ObjectMeta{ 289 Name: "test_pod", 290 Namespace: "test-ns", 291 UID: "bar", 292 ResourceVersion: "42", 293 }} 294 295 cases := pullerTestCases() 296 297 useSerializedEnv := true 298 for _, c := range cases { 299 t.Run(c.testName, func(t *testing.T) { 300 ctx := context.Background() 301 puller, fakeClock, fakeRuntime, container, fakePodPullingTimeRecorder := pullerTestEnv(t, c, useSerializedEnv, nil) 302 303 for _, expected := range c.expected { 304 fakeRuntime.CalledFunctions = nil 305 fakeClock.Step(time.Second) 306 307 _, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, "") 308 fakeRuntime.AssertCalls(expected.calls) 309 assert.Equal(t, expected.err, err) 310 assert.Equal(t, expected.shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded) 311 assert.Equal(t, expected.shouldRecordFinishedPullingTime, fakePodPullingTimeRecorder.finishedPullingRecorded) 312 fakePodPullingTimeRecorder.reset() 313 } 314 }) 315 } 316 } 317 318 func TestApplyDefaultImageTag(t *testing.T) { 319 for _, testCase := range []struct { 320 testName string 321 Input string 322 Output string 323 }{ 324 {testName: "root", Input: "root", Output: "root:latest"}, 325 {testName: "root:tag", Input: "root:tag", Output: "root:tag"}, 326 {testName: "root@sha", Input: "root@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Output: "root@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, 327 {testName: "root:latest@sha", Input: "root:latest@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Output: "root:latest@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, 328 {testName: "root:latest", Input: "root:latest", Output: "root:latest"}, 329 } { 330 t.Run(testCase.testName, func(t *testing.T) { 331 image, err := applyDefaultImageTag(testCase.Input) 332 if err != nil { 333 t.Errorf("applyDefaultImageTag(%s) failed: %v", testCase.Input, err) 334 } else if image != testCase.Output { 335 t.Errorf("Expected image reference: %q, got %q", testCase.Output, image) 336 } 337 }) 338 } 339 } 340 341 func TestPullAndListImageWithPodAnnotations(t *testing.T) { 342 pod := &v1.Pod{ 343 ObjectMeta: metav1.ObjectMeta{ 344 Name: "test_pod", 345 Namespace: "test-ns", 346 UID: "bar", 347 ResourceVersion: "42", 348 Annotations: map[string]string{ 349 "kubernetes.io/runtimehandler": "handler_name", 350 }, 351 }} 352 c := pullerTestCase{ // pull missing image 353 testName: "test pull and list image with pod annotations", 354 containerImage: "missing_image", 355 policy: v1.PullIfNotPresent, 356 inspectErr: nil, 357 pullerErr: nil, 358 expected: []pullerExpects{ 359 {[]string{"GetImageRef", "PullImage"}, nil, true, true}, 360 }} 361 362 useSerializedEnv := true 363 t.Run(c.testName, func(t *testing.T) { 364 ctx := context.Background() 365 puller, fakeClock, fakeRuntime, container, fakePodPullingTimeRecorder := pullerTestEnv(t, c, useSerializedEnv, nil) 366 fakeRuntime.CalledFunctions = nil 367 fakeRuntime.ImageList = []Image{} 368 fakeClock.Step(time.Second) 369 370 _, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, "") 371 fakeRuntime.AssertCalls(c.expected[0].calls) 372 assert.Equal(t, c.expected[0].err, err, "tick=%d", 0) 373 assert.Equal(t, c.expected[0].shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded) 374 assert.Equal(t, c.expected[0].shouldRecordFinishedPullingTime, fakePodPullingTimeRecorder.finishedPullingRecorded) 375 376 images, _ := fakeRuntime.ListImages(ctx) 377 assert.Equal(t, 1, len(images), "ListImages() count") 378 379 image := images[0] 380 assert.Equal(t, "missing_image:latest", image.ID, "Image ID") 381 assert.Equal(t, "", image.Spec.RuntimeHandler, "image.Spec.RuntimeHandler not empty", "ImageID", image.ID) 382 383 expectedAnnotations := []Annotation{ 384 { 385 Name: "kubernetes.io/runtimehandler", 386 Value: "handler_name", 387 }} 388 assert.Equal(t, expectedAnnotations, image.Spec.Annotations, "image spec annotations") 389 }) 390 } 391 392 func TestPullAndListImageWithRuntimeHandlerInImageCriAPIFeatureGate(t *testing.T) { 393 runtimeHandler := "handler_name" 394 pod := &v1.Pod{ 395 ObjectMeta: metav1.ObjectMeta{ 396 Name: "test_pod", 397 Namespace: "test-ns", 398 UID: "bar", 399 ResourceVersion: "42", 400 Annotations: map[string]string{ 401 "kubernetes.io/runtimehandler": runtimeHandler, 402 }, 403 }, 404 Spec: v1.PodSpec{ 405 RuntimeClassName: &runtimeHandler, 406 }, 407 } 408 c := pullerTestCase{ // pull missing image 409 testName: "test pull and list image with pod annotations", 410 containerImage: "missing_image", 411 policy: v1.PullIfNotPresent, 412 inspectErr: nil, 413 pullerErr: nil, 414 expected: []pullerExpects{ 415 {[]string{"GetImageRef", "PullImage"}, nil, true, true}, 416 }} 417 418 useSerializedEnv := true 419 t.Run(c.testName, func(t *testing.T) { 420 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RuntimeClassInImageCriAPI, true)() 421 ctx := context.Background() 422 puller, fakeClock, fakeRuntime, container, fakePodPullingTimeRecorder := pullerTestEnv(t, c, useSerializedEnv, nil) 423 fakeRuntime.CalledFunctions = nil 424 fakeRuntime.ImageList = []Image{} 425 fakeClock.Step(time.Second) 426 427 _, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, runtimeHandler) 428 fakeRuntime.AssertCalls(c.expected[0].calls) 429 assert.Equal(t, c.expected[0].err, err, "tick=%d", 0) 430 assert.Equal(t, c.expected[0].shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded) 431 assert.Equal(t, c.expected[0].shouldRecordFinishedPullingTime, fakePodPullingTimeRecorder.finishedPullingRecorded) 432 433 images, _ := fakeRuntime.ListImages(ctx) 434 assert.Equal(t, 1, len(images), "ListImages() count") 435 436 image := images[0] 437 assert.Equal(t, "missing_image:latest", image.ID, "Image ID") 438 439 // when RuntimeClassInImageCriAPI feature gate is enabled, check runtime 440 // handler information for every image in the ListImages() response 441 assert.Equal(t, runtimeHandler, image.Spec.RuntimeHandler, "runtime handler returned not as expected", "Image ID", image) 442 443 expectedAnnotations := []Annotation{ 444 { 445 Name: "kubernetes.io/runtimehandler", 446 Value: "handler_name", 447 }} 448 assert.Equal(t, expectedAnnotations, image.Spec.Annotations, "image spec annotations") 449 }) 450 } 451 452 func TestMaxParallelImagePullsLimit(t *testing.T) { 453 ctx := context.Background() 454 pod := &v1.Pod{ 455 ObjectMeta: metav1.ObjectMeta{ 456 Name: "test_pod", 457 Namespace: "test-ns", 458 UID: "bar", 459 ResourceVersion: "42", 460 }} 461 462 testCase := &pullerTestCase{ 463 containerImage: "present_image", 464 testName: "image present, pull ", 465 policy: v1.PullAlways, 466 inspectErr: nil, 467 pullerErr: nil, 468 qps: 0.0, 469 burst: 0, 470 } 471 472 useSerializedEnv := false 473 maxParallelImagePulls := 5 474 var wg sync.WaitGroup 475 476 puller, fakeClock, fakeRuntime, container, _ := pullerTestEnv(t, *testCase, useSerializedEnv, utilpointer.Int32Ptr(int32(maxParallelImagePulls))) 477 fakeRuntime.BlockImagePulls = true 478 fakeRuntime.CalledFunctions = nil 479 fakeRuntime.T = t 480 fakeClock.Step(time.Second) 481 482 // First 5 EnsureImageExists should result in runtime calls 483 for i := 0; i < maxParallelImagePulls; i++ { 484 wg.Add(1) 485 go func() { 486 _, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, "") 487 assert.Nil(t, err) 488 wg.Done() 489 }() 490 } 491 time.Sleep(1 * time.Second) 492 fakeRuntime.AssertCallCounts("PullImage", 5) 493 494 // Next two EnsureImageExists should be blocked because maxParallelImagePulls is hit 495 for i := 0; i < 2; i++ { 496 wg.Add(1) 497 go func() { 498 _, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, "") 499 assert.Nil(t, err) 500 wg.Done() 501 }() 502 } 503 time.Sleep(1 * time.Second) 504 fakeRuntime.AssertCallCounts("PullImage", 5) 505 506 // Unblock two image pulls from runtime, and two EnsureImageExists can go through 507 fakeRuntime.UnblockImagePulls(2) 508 time.Sleep(1 * time.Second) 509 fakeRuntime.AssertCallCounts("PullImage", 7) 510 511 // Unblock the remaining 5 image pulls from runtime, and all EnsureImageExists can go through 512 fakeRuntime.UnblockImagePulls(5) 513 514 wg.Wait() 515 fakeRuntime.AssertCallCounts("PullImage", 7) 516 } 517 518 func TestEvalCRIPullErr(t *testing.T) { 519 t.Parallel() 520 for _, tc := range []struct { 521 name string 522 input error 523 assert func(string, error) 524 }{ 525 { 526 name: "fallback error", 527 input: errors.New("test"), 528 assert: func(msg string, err error) { 529 assert.ErrorIs(t, err, ErrImagePull) 530 assert.Contains(t, msg, "test") 531 }, 532 }, 533 { 534 name: "registry is unavailable", 535 input: crierrors.ErrRegistryUnavailable, 536 assert: func(msg string, err error) { 537 assert.ErrorIs(t, err, crierrors.ErrRegistryUnavailable) 538 assert.Equal(t, msg, "image pull failed for test because the registry is unavailable") 539 }, 540 }, 541 { 542 name: "registry is unavailable with additional error message", 543 input: fmt.Errorf("%v: foo", crierrors.ErrRegistryUnavailable), 544 assert: func(msg string, err error) { 545 assert.ErrorIs(t, err, crierrors.ErrRegistryUnavailable) 546 assert.Equal(t, msg, "image pull failed for test because the registry is unavailable: foo") 547 }, 548 }, 549 { 550 name: "signature is invalid", 551 input: crierrors.ErrSignatureValidationFailed, 552 assert: func(msg string, err error) { 553 assert.ErrorIs(t, err, crierrors.ErrSignatureValidationFailed) 554 assert.Equal(t, msg, "image pull failed for test because the signature validation failed") 555 }, 556 }, 557 { 558 name: "signature is invalid with additional error message (wrapped)", 559 input: fmt.Errorf("%w: bar", crierrors.ErrSignatureValidationFailed), 560 assert: func(msg string, err error) { 561 assert.ErrorIs(t, err, crierrors.ErrSignatureValidationFailed) 562 assert.Equal(t, msg, "image pull failed for test because the signature validation failed: bar") 563 }, 564 }, 565 } { 566 testInput := tc.input 567 testAssert := tc.assert 568 569 t.Run(tc.name, func(t *testing.T) { 570 t.Parallel() 571 msg, err := evalCRIPullErr(&v1.Container{Image: "test"}, testInput) 572 testAssert(msg, err) 573 }) 574 } 575 }