k8s.io/kubernetes@v1.29.3/pkg/kubelet/images/image_gc_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 "fmt" 22 goruntime "runtime" 23 "testing" 24 "time" 25 26 "github.com/golang/mock/gomock" 27 "github.com/stretchr/testify/assert" 28 "github.com/stretchr/testify/require" 29 30 oteltrace "go.opentelemetry.io/otel/trace" 31 utilfeature "k8s.io/apiserver/pkg/util/feature" 32 "k8s.io/client-go/tools/record" 33 featuregatetesting "k8s.io/component-base/featuregate/testing" 34 statsapi "k8s.io/kubelet/pkg/apis/stats/v1alpha1" 35 "k8s.io/kubernetes/pkg/features" 36 "k8s.io/kubernetes/pkg/kubelet/container" 37 containertest "k8s.io/kubernetes/pkg/kubelet/container/testing" 38 stats "k8s.io/kubernetes/pkg/kubelet/server/stats" 39 statstest "k8s.io/kubernetes/pkg/kubelet/server/stats/testing" 40 testingclock "k8s.io/utils/clock/testing" 41 ) 42 43 var zero time.Time 44 var sandboxImage = "registry.k8s.io/pause-amd64:latest" 45 46 func newRealImageGCManager(policy ImageGCPolicy, mockStatsProvider stats.Provider) (*realImageGCManager, *containertest.FakeRuntime) { 47 fakeRuntime := &containertest.FakeRuntime{} 48 return &realImageGCManager{ 49 runtime: fakeRuntime, 50 policy: policy, 51 imageRecords: make(map[string]*imageRecord), 52 statsProvider: mockStatsProvider, 53 recorder: &record.FakeRecorder{}, 54 tracer: oteltrace.NewNoopTracerProvider().Tracer(""), 55 }, fakeRuntime 56 } 57 58 // Accessors used for thread-safe testing. 59 func (im *realImageGCManager) imageRecordsLen() int { 60 im.imageRecordsLock.Lock() 61 defer im.imageRecordsLock.Unlock() 62 return len(im.imageRecords) 63 } 64 func (im *realImageGCManager) getImageRecord(name string) (*imageRecord, bool) { 65 im.imageRecordsLock.Lock() 66 defer im.imageRecordsLock.Unlock() 67 v, ok := im.imageRecords[name] 68 vCopy := *v 69 return &vCopy, ok 70 } 71 72 func (im *realImageGCManager) getImageRecordWithRuntimeHandlerInImageCriAPIFeatureGate(name, runtimeHandler string) (*imageRecord, bool) { 73 im.imageRecordsLock.Lock() 74 defer im.imageRecordsLock.Unlock() 75 imageKey := getImageTuple(name, runtimeHandler) 76 v, ok := im.imageRecords[imageKey] 77 vCopy := *v 78 return &vCopy, ok 79 } 80 81 // Returns the id of the image with the given ID. 82 func imageID(id int) string { 83 return fmt.Sprintf("image-%d", id) 84 } 85 86 // Returns the name of the image with the given ID. 87 func imageName(id int) string { 88 return imageID(id) + "-name" 89 } 90 91 // Make an image with the specified ID. 92 func makeImage(id int, size int64) container.Image { 93 return container.Image{ 94 ID: imageID(id), 95 Size: size, 96 } 97 } 98 99 // Make an image with the specified ID. 100 func makeImageWithRuntimeHandler(id int, size int64, runtimeHandler string) container.Image { 101 if runtimeHandler == "" { 102 return container.Image{ 103 ID: imageID(id), 104 Size: size, 105 } 106 } else { 107 return container.Image{ 108 ID: imageID(id), 109 Size: size, 110 Spec: container.ImageSpec{ 111 RuntimeHandler: runtimeHandler, 112 }, 113 } 114 } 115 } 116 117 // Make a container with the specified ID. It will use the image with the same ID. 118 func makeContainer(id int) *container.Container { 119 return &container.Container{ 120 ID: container.ContainerID{Type: "test", ID: fmt.Sprintf("container-%d", id)}, 121 Image: imageName(id), 122 ImageID: imageID(id), 123 } 124 } 125 126 func TestDetectImagesInitialDetect(t *testing.T) { 127 ctx := context.Background() 128 mockCtrl := gomock.NewController(t) 129 defer mockCtrl.Finish() 130 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 131 132 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 133 fakeRuntime.ImageList = []container.Image{ 134 makeImage(0, 1024), 135 makeImage(1, 2048), 136 makeImage(2, 2048), 137 } 138 fakeRuntime.AllPodList = []*containertest.FakePod{ 139 {Pod: &container.Pod{ 140 Containers: []*container.Container{ 141 { 142 ID: container.ContainerID{Type: "test", ID: fmt.Sprintf("container-%d", 1)}, 143 ImageID: imageID(1), 144 // The image filed is not set to simulate a no-name image 145 }, 146 { 147 ID: container.ContainerID{Type: "test", ID: fmt.Sprintf("container-%d", 2)}, 148 Image: imageName(2), 149 ImageID: imageID(2), 150 }, 151 }, 152 }}, 153 } 154 155 startTime := time.Now().Add(-time.Millisecond) 156 _, err := manager.detectImages(ctx, zero) 157 assert := assert.New(t) 158 require.NoError(t, err) 159 assert.Equal(manager.imageRecordsLen(), 3) 160 noContainer, ok := manager.getImageRecord(imageID(0)) 161 require.True(t, ok) 162 assert.Equal(zero, noContainer.firstDetected) 163 assert.Equal(zero, noContainer.lastUsed) 164 withContainerUsingNoNameImage, ok := manager.getImageRecord(imageID(1)) 165 require.True(t, ok) 166 assert.Equal(zero, withContainerUsingNoNameImage.firstDetected) 167 assert.True(withContainerUsingNoNameImage.lastUsed.After(startTime)) 168 withContainer, ok := manager.getImageRecord(imageID(2)) 169 require.True(t, ok) 170 assert.Equal(zero, withContainer.firstDetected) 171 assert.True(withContainer.lastUsed.After(startTime)) 172 } 173 174 func TestDetectImagesInitialDetectWithRuntimeHandlerInImageCriAPIFeatureGate(t *testing.T) { 175 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RuntimeClassInImageCriAPI, true)() 176 testRuntimeHandler := "test-runtimeHandler" 177 ctx := context.Background() 178 mockCtrl := gomock.NewController(t) 179 defer mockCtrl.Finish() 180 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 181 182 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 183 fakeRuntime.ImageList = []container.Image{ 184 makeImageWithRuntimeHandler(0, 1024, testRuntimeHandler), 185 makeImageWithRuntimeHandler(1, 2048, testRuntimeHandler), 186 makeImageWithRuntimeHandler(2, 2048, ""), 187 } 188 fakeRuntime.AllPodList = []*containertest.FakePod{ 189 {Pod: &container.Pod{ 190 Containers: []*container.Container{ 191 { 192 ID: container.ContainerID{Type: "test", ID: fmt.Sprintf("container-%d", 1)}, 193 ImageID: imageID(1), 194 // The image field is not set to simulate a no-name image 195 ImageRuntimeHandler: testRuntimeHandler, 196 }, 197 { 198 ID: container.ContainerID{Type: "test", ID: fmt.Sprintf("container-%d", 2)}, 199 Image: imageName(2), 200 ImageID: imageID(2), 201 // The runtime handler field is not set to simulate the case when 202 // the feature gate "RuntimeHandlerInImageCriApi" is on and container runtime has not implemented 203 // KEP 4216, which means that runtimeHandler string is not set in the 204 // responses from the container runtime. 205 }, 206 }, 207 }}, 208 } 209 210 startTime := time.Now().Add(-time.Millisecond) 211 _, err := manager.detectImages(ctx, zero) 212 assert := assert.New(t) 213 require.NoError(t, err) 214 assert.Equal(manager.imageRecordsLen(), 3) 215 noContainer, ok := manager.getImageRecordWithRuntimeHandlerInImageCriAPIFeatureGate(imageID(0), testRuntimeHandler) 216 require.True(t, ok) 217 assert.Equal(zero, noContainer.firstDetected) 218 assert.Equal(testRuntimeHandler, noContainer.runtimeHandlerUsedToPullImage) 219 assert.Equal(zero, noContainer.lastUsed) 220 withContainerUsingNoNameImage, ok := manager.getImageRecordWithRuntimeHandlerInImageCriAPIFeatureGate(imageID(1), testRuntimeHandler) 221 require.True(t, ok) 222 assert.Equal(zero, withContainerUsingNoNameImage.firstDetected) 223 assert.True(withContainerUsingNoNameImage.lastUsed.After(startTime)) 224 assert.Equal(testRuntimeHandler, withContainerUsingNoNameImage.runtimeHandlerUsedToPullImage) 225 withContainer, ok := manager.getImageRecordWithRuntimeHandlerInImageCriAPIFeatureGate(imageID(2), "") 226 require.True(t, ok) 227 assert.Equal(zero, withContainer.firstDetected) 228 assert.True(withContainer.lastUsed.After(startTime)) 229 assert.Equal("", withContainer.runtimeHandlerUsedToPullImage) 230 } 231 232 func TestDetectImagesWithNewImage(t *testing.T) { 233 ctx := context.Background() 234 mockCtrl := gomock.NewController(t) 235 defer mockCtrl.Finish() 236 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 237 238 // Just one image initially. 239 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 240 fakeRuntime.ImageList = []container.Image{ 241 makeImage(0, 1024), 242 makeImage(1, 2048), 243 } 244 fakeRuntime.AllPodList = []*containertest.FakePod{ 245 {Pod: &container.Pod{ 246 Containers: []*container.Container{ 247 makeContainer(1), 248 }, 249 }}, 250 } 251 252 _, err := manager.detectImages(ctx, zero) 253 assert := assert.New(t) 254 require.NoError(t, err) 255 assert.Equal(manager.imageRecordsLen(), 2) 256 257 // Add a new image. 258 fakeRuntime.ImageList = []container.Image{ 259 makeImage(0, 1024), 260 makeImage(1, 1024), 261 makeImage(2, 1024), 262 } 263 264 detectedTime := zero.Add(time.Second) 265 startTime := time.Now().Add(-time.Millisecond) 266 _, err = manager.detectImages(ctx, detectedTime) 267 require.NoError(t, err) 268 assert.Equal(manager.imageRecordsLen(), 3) 269 noContainer, ok := manager.getImageRecord(imageID(0)) 270 require.True(t, ok) 271 assert.Equal(zero, noContainer.firstDetected) 272 assert.Equal(zero, noContainer.lastUsed) 273 assert.Equal("", noContainer.runtimeHandlerUsedToPullImage) 274 withContainer, ok := manager.getImageRecord(imageID(1)) 275 require.True(t, ok) 276 assert.Equal(zero, withContainer.firstDetected) 277 assert.True(withContainer.lastUsed.After(startTime)) 278 assert.Equal("", noContainer.runtimeHandlerUsedToPullImage) 279 newContainer, ok := manager.getImageRecord(imageID(2)) 280 require.True(t, ok) 281 assert.Equal(detectedTime, newContainer.firstDetected) 282 assert.Equal(zero, noContainer.lastUsed) 283 assert.Equal("", noContainer.runtimeHandlerUsedToPullImage) 284 } 285 286 func TestDeleteUnusedImagesExemptSandboxImage(t *testing.T) { 287 ctx := context.Background() 288 mockCtrl := gomock.NewController(t) 289 defer mockCtrl.Finish() 290 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 291 292 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 293 fakeRuntime.ImageList = []container.Image{ 294 { 295 ID: sandboxImage, 296 Size: 1024, 297 Pinned: true, 298 }, 299 } 300 301 err := manager.DeleteUnusedImages(ctx) 302 assert := assert.New(t) 303 assert.Len(fakeRuntime.ImageList, 1) 304 require.NoError(t, err) 305 } 306 307 func TestDeletePinnedImage(t *testing.T) { 308 ctx := context.Background() 309 mockCtrl := gomock.NewController(t) 310 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 311 312 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 313 fakeRuntime.ImageList = []container.Image{ 314 { 315 ID: sandboxImage, 316 Size: 1024, 317 Pinned: true, 318 }, 319 { 320 ID: sandboxImage, 321 Size: 1024, 322 }, 323 } 324 325 err := manager.DeleteUnusedImages(ctx) 326 assert := assert.New(t) 327 assert.Len(fakeRuntime.ImageList, 1) 328 require.NoError(t, err) 329 } 330 331 func TestDoNotDeletePinnedImage(t *testing.T) { 332 ctx := context.Background() 333 mockCtrl := gomock.NewController(t) 334 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 335 336 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 337 fakeRuntime.ImageList = []container.Image{ 338 { 339 ID: "1", 340 Size: 1024, 341 Pinned: true, 342 }, 343 { 344 ID: "2", 345 Size: 1024, 346 }, 347 } 348 349 assert := assert.New(t) 350 getImagesAndFreeSpace(ctx, t, assert, manager, fakeRuntime, 4096, 1024, 1, time.Now()) 351 } 352 353 func TestDeleteUnPinnedImage(t *testing.T) { 354 ctx := context.Background() 355 mockCtrl := gomock.NewController(t) 356 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 357 358 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 359 fakeRuntime.ImageList = []container.Image{ 360 { 361 ID: "1", 362 Size: 1024, 363 Pinned: false, 364 }, 365 { 366 ID: "2", 367 Size: 1024, 368 }, 369 } 370 371 assert := assert.New(t) 372 getImagesAndFreeSpace(ctx, t, assert, manager, fakeRuntime, 2048, 2048, 0, time.Now()) 373 } 374 375 func TestAllPinnedImages(t *testing.T) { 376 ctx := context.Background() 377 mockCtrl := gomock.NewController(t) 378 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 379 380 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 381 fakeRuntime.ImageList = []container.Image{ 382 { 383 ID: "1", 384 Size: 1024, 385 Pinned: true, 386 }, 387 { 388 ID: "2", 389 Size: 1024, 390 Pinned: true, 391 }, 392 } 393 394 assert := assert.New(t) 395 getImagesAndFreeSpace(ctx, t, assert, manager, fakeRuntime, 2048, 0, 2, time.Now()) 396 } 397 398 func TestDetectImagesContainerStopped(t *testing.T) { 399 ctx := context.Background() 400 mockCtrl := gomock.NewController(t) 401 defer mockCtrl.Finish() 402 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 403 404 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 405 fakeRuntime.ImageList = []container.Image{ 406 makeImage(0, 1024), 407 makeImage(1, 2048), 408 } 409 fakeRuntime.AllPodList = []*containertest.FakePod{ 410 {Pod: &container.Pod{ 411 Containers: []*container.Container{ 412 makeContainer(1), 413 }, 414 }}, 415 } 416 417 _, err := manager.detectImages(ctx, zero) 418 assert := assert.New(t) 419 require.NoError(t, err) 420 assert.Equal(manager.imageRecordsLen(), 2) 421 withContainer, ok := manager.getImageRecord(imageID(1)) 422 require.True(t, ok) 423 424 // Simulate container being stopped. 425 fakeRuntime.AllPodList = []*containertest.FakePod{} 426 _, err = manager.detectImages(ctx, time.Now()) 427 require.NoError(t, err) 428 assert.Equal(manager.imageRecordsLen(), 2) 429 container1, ok := manager.getImageRecord(imageID(0)) 430 require.True(t, ok) 431 assert.Equal(zero, container1.firstDetected) 432 assert.Equal(zero, container1.lastUsed) 433 container2, ok := manager.getImageRecord(imageID(1)) 434 require.True(t, ok) 435 assert.Equal(zero, container2.firstDetected) 436 assert.True(container2.lastUsed.Equal(withContainer.lastUsed)) 437 } 438 439 func TestDetectImagesWithRemovedImages(t *testing.T) { 440 ctx := context.Background() 441 mockCtrl := gomock.NewController(t) 442 defer mockCtrl.Finish() 443 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 444 445 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 446 fakeRuntime.ImageList = []container.Image{ 447 makeImage(0, 1024), 448 makeImage(1, 2048), 449 } 450 fakeRuntime.AllPodList = []*containertest.FakePod{ 451 {Pod: &container.Pod{ 452 Containers: []*container.Container{ 453 makeContainer(1), 454 }, 455 }}, 456 } 457 458 _, err := manager.detectImages(ctx, zero) 459 assert := assert.New(t) 460 require.NoError(t, err) 461 assert.Equal(manager.imageRecordsLen(), 2) 462 463 // Simulate both images being removed. 464 fakeRuntime.ImageList = []container.Image{} 465 _, err = manager.detectImages(ctx, time.Now()) 466 require.NoError(t, err) 467 assert.Equal(manager.imageRecordsLen(), 0) 468 } 469 470 func TestFreeSpaceImagesInUseContainersAreIgnored(t *testing.T) { 471 ctx := context.Background() 472 mockCtrl := gomock.NewController(t) 473 defer mockCtrl.Finish() 474 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 475 476 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 477 fakeRuntime.ImageList = []container.Image{ 478 makeImage(0, 1024), 479 makeImage(1, 2048), 480 } 481 fakeRuntime.AllPodList = []*containertest.FakePod{ 482 {Pod: &container.Pod{ 483 Containers: []*container.Container{ 484 makeContainer(1), 485 }, 486 }}, 487 } 488 489 assert := assert.New(t) 490 getImagesAndFreeSpace(ctx, t, assert, manager, fakeRuntime, 2048, 1024, 1, time.Now()) 491 } 492 493 func TestDeleteUnusedImagesRemoveAllUnusedImages(t *testing.T) { 494 ctx := context.Background() 495 mockCtrl := gomock.NewController(t) 496 defer mockCtrl.Finish() 497 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 498 499 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 500 fakeRuntime.ImageList = []container.Image{ 501 makeImage(0, 1024), 502 makeImage(1, 2048), 503 makeImage(2, 2048), 504 } 505 fakeRuntime.AllPodList = []*containertest.FakePod{ 506 {Pod: &container.Pod{ 507 Containers: []*container.Container{ 508 makeContainer(2), 509 }, 510 }}, 511 } 512 513 err := manager.DeleteUnusedImages(ctx) 514 assert := assert.New(t) 515 require.NoError(t, err) 516 assert.Len(fakeRuntime.ImageList, 1) 517 } 518 519 func TestDeleteUnusedImagesLimitByImageLiveTime(t *testing.T) { 520 ctx := context.Background() 521 mockCtrl := gomock.NewController(t) 522 defer mockCtrl.Finish() 523 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 524 525 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{ 526 MinAge: time.Second * 3, // set minAge to 3 seconds, 527 }, mockStatsProvider) 528 fakeRuntime.ImageList = []container.Image{ 529 makeImage(0, 1024), 530 makeImage(1, 2048), 531 makeImage(2, 2048), 532 } 533 fakeRuntime.AllPodList = []*containertest.FakePod{ 534 {Pod: &container.Pod{ 535 Containers: []*container.Container{ 536 makeContainer(2), 537 }, 538 }}, 539 } 540 // start to detect images 541 manager.Start() 542 // try to delete images, but images are not old enough,so no image will be deleted 543 err := manager.DeleteUnusedImages(ctx) 544 assert := assert.New(t) 545 require.NoError(t, err) 546 assert.Len(fakeRuntime.ImageList, 3) 547 // sleep 3 seconds, then images will be old enough to be deleted 548 time.Sleep(time.Second * 3) 549 err = manager.DeleteUnusedImages(ctx) 550 require.NoError(t, err) 551 assert.Len(fakeRuntime.ImageList, 1) 552 } 553 554 func TestFreeSpaceRemoveByLeastRecentlyUsed(t *testing.T) { 555 ctx := context.Background() 556 mockCtrl := gomock.NewController(t) 557 defer mockCtrl.Finish() 558 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 559 560 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 561 fakeRuntime.ImageList = []container.Image{ 562 makeImage(0, 1024), 563 makeImage(1, 2048), 564 } 565 fakeRuntime.AllPodList = []*containertest.FakePod{ 566 {Pod: &container.Pod{ 567 Containers: []*container.Container{ 568 makeContainer(0), 569 makeContainer(1), 570 }, 571 }}, 572 } 573 574 // Make 1 be more recently used than 0. 575 _, err := manager.detectImages(ctx, zero) 576 require.NoError(t, err) 577 fakeRuntime.AllPodList = []*containertest.FakePod{ 578 {Pod: &container.Pod{ 579 Containers: []*container.Container{ 580 makeContainer(1), 581 }, 582 }}, 583 } 584 // manager.detectImages uses time.Now() to update the image's lastUsed field. 585 // On Windows, consecutive time.Now() calls can return the same timestamp, which would mean 586 // that the second image is NOT newer than the first one. 587 // time.Sleep will result in the timestamp to be updated as well. 588 if goruntime.GOOS == "windows" { 589 time.Sleep(time.Millisecond) 590 } 591 _, err = manager.detectImages(ctx, time.Now()) 592 require.NoError(t, err) 593 fakeRuntime.AllPodList = []*containertest.FakePod{ 594 {Pod: &container.Pod{ 595 Containers: []*container.Container{}, 596 }}, 597 } 598 _, err = manager.detectImages(ctx, time.Now()) 599 require.NoError(t, err) 600 require.Equal(t, manager.imageRecordsLen(), 2) 601 602 // We're setting the delete time one minute in the future, so the time the image 603 // was first detected and the delete time are different. 604 assert := assert.New(t) 605 getImagesAndFreeSpace(ctx, t, assert, manager, fakeRuntime, 1024, 1024, 1, time.Now().Add(time.Minute)) 606 } 607 608 func TestFreeSpaceTiesBrokenByDetectedTime(t *testing.T) { 609 ctx := context.Background() 610 mockCtrl := gomock.NewController(t) 611 defer mockCtrl.Finish() 612 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 613 614 manager, fakeRuntime := newRealImageGCManager(ImageGCPolicy{}, mockStatsProvider) 615 fakeRuntime.ImageList = []container.Image{ 616 makeImage(0, 1024), 617 } 618 fakeRuntime.AllPodList = []*containertest.FakePod{ 619 {Pod: &container.Pod{ 620 Containers: []*container.Container{ 621 makeContainer(0), 622 }, 623 }}, 624 } 625 626 // Make 1 more recently detected but used at the same time as 0. 627 _, err := manager.detectImages(ctx, zero) 628 require.NoError(t, err) 629 fakeRuntime.ImageList = []container.Image{ 630 makeImage(0, 1024), 631 makeImage(1, 2048), 632 } 633 _, err = manager.detectImages(ctx, time.Now()) 634 require.NoError(t, err) 635 fakeRuntime.AllPodList = []*containertest.FakePod{} 636 _, err = manager.detectImages(ctx, time.Now()) 637 require.NoError(t, err) 638 require.Equal(t, manager.imageRecordsLen(), 2) 639 640 assert := assert.New(t) 641 getImagesAndFreeSpace(ctx, t, assert, manager, fakeRuntime, 1024, 2048, 1, time.Now()) 642 } 643 644 func TestGarbageCollectBelowLowThreshold(t *testing.T) { 645 ctx := context.Background() 646 policy := ImageGCPolicy{ 647 HighThresholdPercent: 90, 648 LowThresholdPercent: 80, 649 } 650 mockCtrl := gomock.NewController(t) 651 defer mockCtrl.Finish() 652 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 653 manager, _ := newRealImageGCManager(policy, mockStatsProvider) 654 655 // Expect 40% usage. 656 imageStats := &statsapi.FsStats{ 657 AvailableBytes: uint64Ptr(600), 658 CapacityBytes: uint64Ptr(1000), 659 } 660 mockStatsProvider.EXPECT().ImageFsStats(gomock.Any()).Return(imageStats, imageStats, nil) 661 662 assert.NoError(t, manager.GarbageCollect(ctx)) 663 } 664 665 func TestGarbageCollectCadvisorFailure(t *testing.T) { 666 ctx := context.Background() 667 policy := ImageGCPolicy{ 668 HighThresholdPercent: 90, 669 LowThresholdPercent: 80, 670 } 671 mockCtrl := gomock.NewController(t) 672 defer mockCtrl.Finish() 673 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 674 manager, _ := newRealImageGCManager(policy, mockStatsProvider) 675 676 mockStatsProvider.EXPECT().ImageFsStats(gomock.Any()).Return(&statsapi.FsStats{}, &statsapi.FsStats{}, fmt.Errorf("error")) 677 assert.NotNil(t, manager.GarbageCollect(ctx)) 678 } 679 680 func TestGarbageCollectBelowSuccess(t *testing.T) { 681 ctx := context.Background() 682 policy := ImageGCPolicy{ 683 HighThresholdPercent: 90, 684 LowThresholdPercent: 80, 685 } 686 687 mockCtrl := gomock.NewController(t) 688 defer mockCtrl.Finish() 689 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 690 manager, fakeRuntime := newRealImageGCManager(policy, mockStatsProvider) 691 692 // Expect 95% usage and most of it gets freed. 693 imageFs := &statsapi.FsStats{ 694 AvailableBytes: uint64Ptr(50), 695 CapacityBytes: uint64Ptr(1000), 696 } 697 mockStatsProvider.EXPECT().ImageFsStats(gomock.Any()).Return(imageFs, imageFs, nil) 698 fakeRuntime.ImageList = []container.Image{ 699 makeImage(0, 450), 700 } 701 702 assert.NoError(t, manager.GarbageCollect(ctx)) 703 } 704 705 func TestGarbageCollectNotEnoughFreed(t *testing.T) { 706 ctx := context.Background() 707 policy := ImageGCPolicy{ 708 HighThresholdPercent: 90, 709 LowThresholdPercent: 80, 710 } 711 mockCtrl := gomock.NewController(t) 712 defer mockCtrl.Finish() 713 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 714 manager, fakeRuntime := newRealImageGCManager(policy, mockStatsProvider) 715 716 // Expect 95% usage and little of it gets freed. 717 imageFs := &statsapi.FsStats{ 718 AvailableBytes: uint64Ptr(50), 719 CapacityBytes: uint64Ptr(1000), 720 } 721 mockStatsProvider.EXPECT().ImageFsStats(gomock.Any()).Return(imageFs, imageFs, nil) 722 fakeRuntime.ImageList = []container.Image{ 723 makeImage(0, 50), 724 } 725 726 assert.NotNil(t, manager.GarbageCollect(ctx)) 727 } 728 729 func TestGarbageCollectImageNotOldEnough(t *testing.T) { 730 ctx := context.Background() 731 policy := ImageGCPolicy{ 732 HighThresholdPercent: 90, 733 LowThresholdPercent: 80, 734 MinAge: time.Minute * 1, 735 } 736 fakeRuntime := &containertest.FakeRuntime{} 737 mockCtrl := gomock.NewController(t) 738 defer mockCtrl.Finish() 739 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 740 manager := &realImageGCManager{ 741 runtime: fakeRuntime, 742 policy: policy, 743 imageRecords: make(map[string]*imageRecord), 744 statsProvider: mockStatsProvider, 745 recorder: &record.FakeRecorder{}, 746 } 747 748 fakeRuntime.ImageList = []container.Image{ 749 makeImage(0, 1024), 750 makeImage(1, 2048), 751 } 752 // 1 image is in use, and another one is not old enough 753 fakeRuntime.AllPodList = []*containertest.FakePod{ 754 {Pod: &container.Pod{ 755 Containers: []*container.Container{ 756 makeContainer(1), 757 }, 758 }}, 759 } 760 761 fakeClock := testingclock.NewFakeClock(time.Now()) 762 t.Log(fakeClock.Now()) 763 _, err := manager.detectImages(ctx, fakeClock.Now()) 764 require.NoError(t, err) 765 require.Equal(t, manager.imageRecordsLen(), 2) 766 // no space freed since one image is in used, and another one is not old enough 767 assert := assert.New(t) 768 getImagesAndFreeSpace(ctx, t, assert, manager, fakeRuntime, 1024, 0, 2, fakeClock.Now()) 769 770 // move clock by minAge duration, then 1 image will be garbage collected 771 fakeClock.Step(policy.MinAge) 772 getImagesAndFreeSpace(ctx, t, assert, manager, fakeRuntime, 1024, 1024, 1, fakeClock.Now()) 773 } 774 775 func getImagesAndFreeSpace(ctx context.Context, t *testing.T, assert *assert.Assertions, im *realImageGCManager, fakeRuntime *containertest.FakeRuntime, spaceToFree, expectedSpaceFreed int64, imagesLen int, freeTime time.Time) { 776 images, err := im.imagesInEvictionOrder(ctx, freeTime) 777 require.NoError(t, err) 778 spaceFreed, err := im.freeSpace(ctx, spaceToFree, freeTime, images) 779 require.NoError(t, err) 780 assert.EqualValues(expectedSpaceFreed, spaceFreed) 781 assert.Len(fakeRuntime.ImageList, imagesLen) 782 } 783 784 func TestGarbageCollectImageTooOld(t *testing.T) { 785 ctx := context.Background() 786 policy := ImageGCPolicy{ 787 HighThresholdPercent: 90, 788 LowThresholdPercent: 80, 789 MinAge: 0, 790 MaxAge: time.Minute * 1, 791 } 792 fakeRuntime := &containertest.FakeRuntime{} 793 mockCtrl := gomock.NewController(t) 794 defer mockCtrl.Finish() 795 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 796 manager := &realImageGCManager{ 797 runtime: fakeRuntime, 798 policy: policy, 799 imageRecords: make(map[string]*imageRecord), 800 statsProvider: mockStatsProvider, 801 recorder: &record.FakeRecorder{}, 802 } 803 804 fakeRuntime.ImageList = []container.Image{ 805 makeImage(0, 1024), 806 makeImage(1, 2048), 807 } 808 // 1 image is in use, and another one is not old enough 809 fakeRuntime.AllPodList = []*containertest.FakePod{ 810 {Pod: &container.Pod{ 811 Containers: []*container.Container{ 812 makeContainer(1), 813 }, 814 }}, 815 } 816 817 fakeClock := testingclock.NewFakeClock(time.Now()) 818 t.Log(fakeClock.Now()) 819 images, err := manager.imagesInEvictionOrder(ctx, fakeClock.Now()) 820 require.NoError(t, err) 821 require.Equal(t, len(images), 1) 822 // Simulate pod having just used this image, but having been GC'd 823 images[0].lastUsed = fakeClock.Now() 824 825 // First GC round should not GC remaining image, as it was used too recently. 826 assert := assert.New(t) 827 images, err = manager.freeOldImages(ctx, images, fakeClock.Now()) 828 require.NoError(t, err) 829 assert.Len(images, 1) 830 assert.Len(fakeRuntime.ImageList, 2) 831 832 // move clock by a millisecond past maxAge duration, then 1 image will be garbage collected 833 fakeClock.Step(policy.MaxAge + 1) 834 images, err = manager.freeOldImages(ctx, images, fakeClock.Now()) 835 require.NoError(t, err) 836 assert.Len(images, 0) 837 assert.Len(fakeRuntime.ImageList, 1) 838 } 839 840 func TestGarbageCollectImageMaxAgeDisabled(t *testing.T) { 841 ctx := context.Background() 842 policy := ImageGCPolicy{ 843 HighThresholdPercent: 90, 844 LowThresholdPercent: 80, 845 MinAge: 0, 846 MaxAge: 0, 847 } 848 fakeRuntime := &containertest.FakeRuntime{} 849 mockCtrl := gomock.NewController(t) 850 defer mockCtrl.Finish() 851 mockStatsProvider := statstest.NewMockProvider(mockCtrl) 852 manager := &realImageGCManager{ 853 runtime: fakeRuntime, 854 policy: policy, 855 imageRecords: make(map[string]*imageRecord), 856 statsProvider: mockStatsProvider, 857 recorder: &record.FakeRecorder{}, 858 } 859 860 assert := assert.New(t) 861 fakeRuntime.ImageList = []container.Image{ 862 makeImage(0, 1024), 863 makeImage(1, 2048), 864 } 865 assert.Len(fakeRuntime.ImageList, 2) 866 // 1 image is in use, and another one is not old enough 867 fakeRuntime.AllPodList = []*containertest.FakePod{ 868 {Pod: &container.Pod{ 869 Containers: []*container.Container{ 870 makeContainer(1), 871 }, 872 }}, 873 } 874 875 fakeClock := testingclock.NewFakeClock(time.Now()) 876 t.Log(fakeClock.Now()) 877 images, err := manager.imagesInEvictionOrder(ctx, fakeClock.Now()) 878 require.NoError(t, err) 879 require.Equal(t, len(images), 1) 880 assert.Len(fakeRuntime.ImageList, 2) 881 882 // First GC round should not GC remaining image, as it was used too recently. 883 images, err = manager.freeOldImages(ctx, images, fakeClock.Now()) 884 require.NoError(t, err) 885 assert.Len(images, 1) 886 assert.Len(fakeRuntime.ImageList, 2) 887 888 // Move clock by a lot, and the images should continue to not be garbage colleced 889 // See https://stackoverflow.com/questions/25065055/what-is-the-maximum-time-time-in-go 890 fakeClock.SetTime(time.Unix(1<<63-62135596801, 999999999)) 891 images, err = manager.freeOldImages(ctx, images, fakeClock.Now()) 892 require.NoError(t, err) 893 assert.Len(images, 1) 894 assert.Len(fakeRuntime.ImageList, 2) 895 } 896 897 func TestValidateImageGCPolicy(t *testing.T) { 898 testCases := []struct { 899 name string 900 imageGCPolicy ImageGCPolicy 901 expectErr string 902 }{ 903 { 904 name: "Test for LowThresholdPercent < HighThresholdPercent", 905 imageGCPolicy: ImageGCPolicy{ 906 HighThresholdPercent: 2, 907 LowThresholdPercent: 1, 908 }, 909 }, 910 { 911 name: "Test for HighThresholdPercent < 0,", 912 imageGCPolicy: ImageGCPolicy{ 913 HighThresholdPercent: -1, 914 }, 915 expectErr: "invalid HighThresholdPercent -1, must be in range [0-100]", 916 }, 917 { 918 name: "Test for HighThresholdPercent > 100", 919 imageGCPolicy: ImageGCPolicy{ 920 HighThresholdPercent: 101, 921 }, 922 expectErr: "invalid HighThresholdPercent 101, must be in range [0-100]", 923 }, 924 { 925 name: "Test for LowThresholdPercent < 0", 926 imageGCPolicy: ImageGCPolicy{ 927 LowThresholdPercent: -1, 928 }, 929 expectErr: "invalid LowThresholdPercent -1, must be in range [0-100]", 930 }, 931 { 932 name: "Test for LowThresholdPercent > 100", 933 imageGCPolicy: ImageGCPolicy{ 934 LowThresholdPercent: 101, 935 }, 936 expectErr: "invalid LowThresholdPercent 101, must be in range [0-100]", 937 }, 938 { 939 name: "Test for LowThresholdPercent > HighThresholdPercent", 940 imageGCPolicy: ImageGCPolicy{ 941 HighThresholdPercent: 1, 942 LowThresholdPercent: 2, 943 }, 944 expectErr: "LowThresholdPercent 2 can not be higher than HighThresholdPercent 1", 945 }, 946 } 947 948 for _, tc := range testCases { 949 if _, err := NewImageGCManager(nil, nil, nil, nil, tc.imageGCPolicy, oteltrace.NewNoopTracerProvider()); err != nil { 950 if err.Error() != tc.expectErr { 951 t.Errorf("[%s:]Expected err:%v, but got:%v", tc.name, tc.expectErr, err.Error()) 952 } 953 } 954 } 955 } 956 957 func uint64Ptr(i uint64) *uint64 { 958 return &i 959 }