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  }