From 7dfcc8dacf316b7dd33ab4f5cc75a9b18dda3e56 Mon Sep 17 00:00:00 2001 From: Cam Date: Fri, 3 May 2019 15:15:46 -0700 Subject: [PATCH] image manager: cleanup dangling images Use ImageID to cleanup images rather than tag name, to guarantee that we are passing a legitimate identifier to InspectImage and RemoveImage. closes #2017 --- agent/engine/docker_image_manager.go | 112 +++++++---- agent/engine/docker_image_manager_test.go | 225 +++++++++++++++++++--- 2 files changed, 274 insertions(+), 63 deletions(-) diff --git a/agent/engine/docker_image_manager.go b/agent/engine/docker_image_manager.go index 108eae129d9..089e47be5da 100644 --- a/agent/engine/docker_image_manager.go +++ b/agent/engine/docker_image_manager.go @@ -388,70 +388,89 @@ func (imageManager *dockerImageManager) getNonECSContainerIDs(ctx context.Contex return nonECSContainersIDs, nil } -type ImageWithSize struct { - ImageName string - Size int64 +type ImageWithSizeID struct { + RepoTags []string + ImageID string + Size int64 } func (imageManager *dockerImageManager) removeNonECSImages(ctx context.Context, nonECSImagesNumToDelete int) { if nonECSImagesNumToDelete == 0 { return } - var nonECSImageNames = imageManager.getNonECSImageNames(ctx) - var nonECSImageNamesRemoveEligible []string - for _, nonECSImage := range nonECSImageNames { - if !isInExclusionList(nonECSImage, imageManager.imageCleanupExclusionList) { - nonECSImageNamesRemoveEligible = append(nonECSImageNamesRemoveEligible, nonECSImage) - } - } + nonECSImages := imageManager.getNonECSImages(ctx) - var imageWithSizeList []ImageWithSize - for _, imageName := range nonECSImageNamesRemoveEligible { - resp, iiErr := imageManager.client.InspectImage(imageName) - if iiErr != nil { - seelog.Errorf("Error inspecting non-ECS image name: %s - %v", imageName, iiErr) - continue - } - imageWithSizeList = append(imageWithSizeList, ImageWithSize{imageName, resp.Size}) - } // we want to sort images with size ascending - sort.Slice(imageWithSizeList, func(i, j int) bool { - return imageWithSizeList[i].Size < imageWithSizeList[j].Size + sort.Slice(nonECSImages, func(i, j int) bool { + return nonECSImages[i].Size < nonECSImages[j].Size }) // we will remove the remaining nonECSImages in each performPeriodicImageCleanup call() var numImagesAlreadyDeleted = 0 - for _, kv := range imageWithSizeList { + for _, image := range nonECSImages { if numImagesAlreadyDeleted == nonECSImagesNumToDelete { break } - seelog.Infof("Removing non-ECS Image: %s", kv.ImageName) - err := imageManager.client.RemoveImage(ctx, kv.ImageName, dockerclient.RemoveImageTimeout) - if err != nil { - seelog.Errorf("Error removing Image %s - %v", kv.ImageName, err) - continue + if len(image.RepoTags) > 1 { + seelog.Debugf("Non-ECS image has more than one tag Image: %s (Tags: %s)", image.ImageID, image.RepoTags) + for _, tag := range image.RepoTags { + err := imageManager.client.RemoveImage(ctx, tag, dockerclient.RemoveImageTimeout) + if err != nil { + seelog.Errorf("Error removing RepoTag (ImageID: %s, Tag: %s) %v", image.ImageID, tag, err) + } + } } else { - seelog.Infof("Image removed: %s", kv.ImageName) - numImagesAlreadyDeleted++ + seelog.Debugf("Removing non-ECS Image: %s (Tags: %s)", image.ImageID, image.RepoTags) + err := imageManager.client.RemoveImage(ctx, image.ImageID, dockerclient.RemoveImageTimeout) + if err != nil { + seelog.Errorf("Error removing Image %s (Tags: %s) - %v", image.ImageID, image.RepoTags, err) + continue + } else { + seelog.Infof("Image removed: %s (Tags: %s)", image.ImageID, image.RepoTags) + numImagesAlreadyDeleted++ + } } } } -func (imageManager *dockerImageManager) getNonECSImageNames(ctx context.Context) []string { - response := imageManager.client.ListImages(ctx, dockerclient.ListImagesTimeout) - var allImagesNames []string - for _, name := range response.RepoTags { - allImagesNames = append(allImagesNames, name) +// getNonECSImages returns type ImageWithSizeID with all fields populated. +func (imageManager *dockerImageManager) getNonECSImages(ctx context.Context) []ImageWithSizeID { + r := imageManager.client.ListImages(ctx, dockerclient.ListImagesTimeout) + var allImages []ImageWithSizeID + // inspect all images + for _, imageID := range r.ImageIDs { + resp, err := imageManager.client.InspectImage(imageID) + if err != nil { + seelog.Errorf("Error inspecting non-ECS image: (ImageID: %s), %s", imageID, err) + continue + } + allImages = append(allImages, + ImageWithSizeID{ + ImageID: imageID, + Size: resp.Size, + RepoTags: resp.RepoTags, + }) } - var ecsImageNames []string + + // get all 'ecs' image IDs + var ecsImageIDs []string for _, imageState := range imageManager.getAllImageStates() { - for _, imageName := range imageState.Image.Names { - ecsImageNames = append(ecsImageNames, imageName) - } + ecsImageIDs = append(ecsImageIDs, imageState.Image.ImageID) } - var nonECSImageNames = exclude(allImagesNames, ecsImageNames) - return nonECSImageNames + // exclude 'ecs' image IDs and image IDs with an explicitly excluded tag + var nonECSImages []ImageWithSizeID + for _, image := range allImages { + // check image ID is not excluded + if isInExclusionList(image.ImageID, ecsImageIDs) { + continue + } + // check image TAG(s) is not excluded + if !anyIsInExclusionList(image.RepoTags, imageManager.imageCleanupExclusionList) { + nonECSImages = append(nonECSImages, image) + } + } + return nonECSImages } func isInExclusionList(imageName string, imageExclusionList []string) bool { @@ -463,6 +482,16 @@ func isInExclusionList(imageName string, imageExclusionList []string) bool { return false } +// anyIsInExclusionList returns true if any name is in the exclusion list. +func anyIsInExclusionList(imageNames []string, nameExclusionList []string) bool { + for _, name := range imageNames { + if isInExclusionList(name, nameExclusionList) { + return true + } + } + return false +} + func exclude(allList []string, exclusionList []string) []string { var ret []string var allMap = make(map[string]bool) @@ -486,8 +515,9 @@ func (imageManager *dockerImageManager) imagesConsiderForDeletion(allImageStates for _, imageState := range allImageStates { if imageManager.isExcludedFromCleanup(imageState) { //imageState that we want to keep - seelog.Infof("Image excluded from deletion: [%s]", imageState.String()) + seelog.Debugf("Image excluded from deletion: [%s]", imageState.String()) } else { + seelog.Debugf("Image going to be considered for deletion: [%s]", imageState.String()) imagesConsiderForDeletionMap[imageState.Image.ImageID] = imageState } } diff --git a/agent/engine/docker_image_manager_test.go b/agent/engine/docker_image_manager_test.go index 703a5430e28..d823b6d8380 100644 --- a/agent/engine/docker_image_manager_test.go +++ b/agent/engine/docker_image_manager_test.go @@ -1022,20 +1022,201 @@ func TestNonECSImageAndContainersCleanupRemoveImage(t *testing.T) { client.EXPECT().InspectContainer(gomock.Any(), gomock.Any(), dockerclient.InspectContainerTimeout).Return(inspectContainerResponse, nil).AnyTimes() client.EXPECT().RemoveContainer(gomock.Any(), gomock.Any(), dockerclient.RemoveContainerTimeout).Return(nil).Times(1) client.EXPECT().ListImages(gomock.Any(), dockerclient.ListImagesTimeout).Return(listImagesResponse).AnyTimes() - client.EXPECT().InspectImage(listImagesResponse.RepoTags[0]).Return(inspectImageResponse, nil).Times(1) + client.EXPECT().InspectImage(listImagesResponse.ImageIDs[0]).Return(inspectImageResponse, nil).Times(1) + client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.ImageIDs[0], dockerclient.RemoveImageTimeout).Return(nil).Times(1) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + imageManager.removeUnusedImages(ctx) + + assert.Len(t, listContainersResponse.DockerIDs, 1, "Error removing container IDs") + assert.Len(t, inspectContainerResponse.ID, 1, "Error inspecting containers ids") + assert.Len(t, imageManager.imageStates, 0, "Error removing image state after the image is removed") +} + +func TestNonECSImageAndContainersCleanupRemoveImage_OneImageThreeTags(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mock_dockerapi.NewMockDockerClient(ctrl) + imageManager := &dockerImageManager{ + client: client, + state: dockerstate.NewTaskEngineState(), + minimumAgeBeforeDeletion: config.DefaultImageDeletionAge, + numImagesToDelete: config.DefaultNumImagesToDeletePerCycle, + numNonECSContainersToDelete: 10, + imageCleanupTimeInterval: config.DefaultImageCleanupTimeInterval, + deleteNonECSImagesEnabled: true, + nonECSContainerCleanupWaitDuration: time.Hour * 3, + } + imageManager.SetSaver(statemanager.NewNoopStateManager()) + + listContainersResponse := dockerapi.ListContainersResponse{ + DockerIDs: []string{"1"}, + } + + inspectContainerResponse := &types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: "1", + State: &types.ContainerState{ + Status: "exited", + FinishedAt: time.Now().AddDate(0, -2, 0).Format(time.RFC3339Nano), + }, + }, + } + + inspectImageResponse := &types.ImageInspect{ + Size: 4096, + RepoTags: []string{"tester", "foo", "bar"}, + } + + listImagesResponse := dockerapi.ListImagesResponse{ + ImageIDs: []string{"sha256:qwerty1"}, + RepoTags: []string{"tester", "foo", "bar"}, + } + + client.EXPECT().ListContainers(gomock.Any(), gomock.Any(), dockerclient.ListImagesTimeout).Return(listContainersResponse).AnyTimes() + client.EXPECT().InspectContainer(gomock.Any(), gomock.Any(), dockerclient.InspectContainerTimeout).Return(inspectContainerResponse, nil).AnyTimes() + client.EXPECT().RemoveContainer(gomock.Any(), gomock.Any(), dockerclient.RemoveContainerTimeout).Return(nil).Times(1) + client.EXPECT().ListImages(gomock.Any(), dockerclient.ListImagesTimeout).Return(listImagesResponse).AnyTimes() + client.EXPECT().InspectImage(listImagesResponse.ImageIDs[0]).Return(inspectImageResponse, nil).Times(1) client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.RepoTags[0], dockerclient.RemoveImageTimeout).Return(nil).Times(1) + client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.RepoTags[1], dockerclient.RemoveImageTimeout).Return(nil).Times(1) + client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.RepoTags[2], dockerclient.RemoveImageTimeout).Return(nil).Times(1) + client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.ImageIDs[0], dockerclient.RemoveImageTimeout).Return(nil).Times(0) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + imageManager.removeUnusedImages(ctx) + + assert.Len(t, listContainersResponse.DockerIDs, 1, "Error removing container IDs") + assert.Len(t, inspectContainerResponse.ID, 1, "Error inspecting containers ids") + assert.Len(t, imageManager.imageStates, 0, "Error removing image state after the image is removed") +} + +func TestNonECSImageAndContainersCleanupRemoveImage_DontDeleteExcludedImage(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mock_dockerapi.NewMockDockerClient(ctrl) + imageManager := &dockerImageManager{ + client: client, + state: dockerstate.NewTaskEngineState(), + minimumAgeBeforeDeletion: config.DefaultImageDeletionAge, + numImagesToDelete: config.DefaultNumImagesToDeletePerCycle, + numNonECSContainersToDelete: 10, + imageCleanupTimeInterval: config.DefaultImageCleanupTimeInterval, + deleteNonECSImagesEnabled: true, + nonECSContainerCleanupWaitDuration: time.Hour * 3, + imageCleanupExclusionList: []string{"tester"}, + } + imageManager.SetSaver(statemanager.NewNoopStateManager()) + + listContainersResponse := dockerapi.ListContainersResponse{ + DockerIDs: []string{"1"}, + } + + inspectContainerResponse := &types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: "1", + State: &types.ContainerState{ + Status: "exited", + FinishedAt: time.Now().AddDate(0, -2, 0).Format(time.RFC3339Nano), + }, + }, + } + + inspectImageResponse := &types.ImageInspect{ + Size: 4096, + RepoTags: []string{"tester"}, + } + + listImagesResponse := dockerapi.ListImagesResponse{ + ImageIDs: []string{"sha256:qwerty1"}, + RepoTags: []string{"tester"}, + } + + client.EXPECT().ListContainers(gomock.Any(), gomock.Any(), dockerclient.ListImagesTimeout).Return(listContainersResponse).AnyTimes() + client.EXPECT().InspectContainer(gomock.Any(), gomock.Any(), dockerclient.InspectContainerTimeout).Return(inspectContainerResponse, nil).AnyTimes() + client.EXPECT().RemoveContainer(gomock.Any(), gomock.Any(), dockerclient.RemoveContainerTimeout).Return(nil).Times(1) + client.EXPECT().ListImages(gomock.Any(), dockerclient.ListImagesTimeout).Return(listImagesResponse).AnyTimes() + client.EXPECT().InspectImage(listImagesResponse.ImageIDs[0]).Return(inspectImageResponse, nil).Times(1) + client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.ImageIDs[0], dockerclient.RemoveImageTimeout).Return(nil).Times(0) ctx, cancel := context.WithCancel(context.TODO()) defer cancel() imageManager.removeUnusedImages(ctx) - assert.Len(t, listContainersResponse.DockerIDs, 1, "error removing container IDs") - assert.Len(t, inspectContainerResponse.ID, 1, "error inspecting containers ids") + assert.Len(t, listContainersResponse.DockerIDs, 1, "Error removing container IDs") + assert.Len(t, inspectContainerResponse.ID, 1, "Error inspecting containers ids") assert.Len(t, imageManager.imageStates, 0, "Error removing image state after the image is removed") } -// Dead images should be cleaned up. +func TestNonECSImageAndContainersCleanupRemoveImage_DontDeleteECSImages(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mock_dockerapi.NewMockDockerClient(ctrl) + imageManager := &dockerImageManager{ + client: client, + state: dockerstate.NewTaskEngineState(), + minimumAgeBeforeDeletion: config.DefaultImageDeletionAge, + numImagesToDelete: config.DefaultNumImagesToDeletePerCycle, + numNonECSContainersToDelete: 10, + imageCleanupTimeInterval: config.DefaultImageCleanupTimeInterval, + deleteNonECSImagesEnabled: true, + nonECSContainerCleanupWaitDuration: time.Hour * 3, + } + imageState := &image.ImageState{ + Image: &image.Image{ImageID: "sha256:qwerty1"}, + PulledAt: time.Now(), + } + imageManager.addImageState(imageState) + imageManager.SetSaver(statemanager.NewNoopStateManager()) + + listContainersResponse := dockerapi.ListContainersResponse{ + DockerIDs: []string{"1"}, + } + + inspectContainerResponse := &types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: "1", + State: &types.ContainerState{ + Status: "exited", + FinishedAt: time.Now().AddDate(0, -2, 0).Format(time.RFC3339Nano), + }, + }, + } + + inspectImageResponse := &types.ImageInspect{ + Size: 4096, + RepoTags: []string{"tester"}, + ID: "sha256:qwerty1", + } + + listImagesResponse := dockerapi.ListImagesResponse{ + ImageIDs: []string{"sha256:qwerty1"}, + RepoTags: []string{"tester"}, + } + + client.EXPECT().ListContainers(gomock.Any(), gomock.Any(), dockerclient.ListImagesTimeout).Return(listContainersResponse).AnyTimes() + client.EXPECT().InspectContainer(gomock.Any(), gomock.Any(), dockerclient.InspectContainerTimeout).Return(inspectContainerResponse, nil).AnyTimes() + client.EXPECT().RemoveContainer(gomock.Any(), gomock.Any(), dockerclient.RemoveContainerTimeout).Return(nil).Times(1) + client.EXPECT().ListImages(gomock.Any(), dockerclient.ListImagesTimeout).Return(listImagesResponse).AnyTimes() + client.EXPECT().InspectImage(listImagesResponse.ImageIDs[0]).Return(inspectImageResponse, nil).Times(1) + client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.ImageIDs[0], dockerclient.RemoveImageTimeout).Return(nil).Times(0) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + imageManager.removeUnusedImages(ctx) + + assert.Len(t, listContainersResponse.DockerIDs, 1, "Error removing container IDs") + assert.Len(t, inspectContainerResponse.ID, 1, "Error inspecting containers ids") + assert.Len(t, imageManager.imageStates, 1, "there should still be an image state") +} + +// Dead containers should be cleaned up. func TestNonECSImageAndContainers_RemoveDeadContainer(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -1079,20 +1260,20 @@ func TestNonECSImageAndContainers_RemoveDeadContainer(t *testing.T) { client.EXPECT().InspectContainer(gomock.Any(), gomock.Any(), dockerclient.InspectContainerTimeout).Return(inspectContainerResponse, nil).AnyTimes() client.EXPECT().RemoveContainer(gomock.Any(), gomock.Any(), dockerclient.RemoveContainerTimeout).Return(nil).Times(1) client.EXPECT().ListImages(gomock.Any(), dockerclient.ListImagesTimeout).Return(listImagesResponse).AnyTimes() - client.EXPECT().InspectImage(listImagesResponse.RepoTags[0]).Return(inspectImageResponse, nil).Times(1) - client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.RepoTags[0], dockerclient.RemoveImageTimeout).Return(nil).Times(1) + client.EXPECT().InspectImage(listImagesResponse.ImageIDs[0]).Return(inspectImageResponse, nil).Times(1) + client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.ImageIDs[0], dockerclient.RemoveImageTimeout).Return(nil).Times(1) ctx, cancel := context.WithCancel(context.TODO()) defer cancel() imageManager.removeUnusedImages(ctx) - assert.Len(t, listContainersResponse.DockerIDs, 1, "error removing container IDs") - assert.Len(t, inspectContainerResponse.ID, 1, "error inspecting containers ids") + assert.Len(t, listContainersResponse.DockerIDs, 1, "Error removing container IDs") + assert.Len(t, inspectContainerResponse.ID, 1, "Error inspecting containers ids") assert.Len(t, imageManager.imageStates, 0, "Error removing image state after the image is removed") } -// Old 'Created' Images should be cleaned up +// Old 'Created' containers should be cleaned up func TestNonECSImageAndContainersCleanup_RemoveOldCreatedContainer(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -1136,16 +1317,16 @@ func TestNonECSImageAndContainersCleanup_RemoveOldCreatedContainer(t *testing.T) client.EXPECT().InspectContainer(gomock.Any(), gomock.Any(), dockerclient.InspectContainerTimeout).Return(inspectContainerResponse, nil).AnyTimes() client.EXPECT().RemoveContainer(gomock.Any(), "1", gomock.Any()).Return(nil).Times(1) client.EXPECT().ListImages(gomock.Any(), dockerclient.ListImagesTimeout).Return(listImagesResponse).AnyTimes() - client.EXPECT().InspectImage(listImagesResponse.RepoTags[0]).Return(inspectImageResponse, nil).Times(1) - client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.RepoTags[0], dockerclient.RemoveImageTimeout).Return(nil).Times(1) + client.EXPECT().InspectImage(listImagesResponse.ImageIDs[0]).Return(inspectImageResponse, nil).Times(1) + client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.ImageIDs[0], dockerclient.RemoveImageTimeout).Return(nil).Times(1) ctx, cancel := context.WithCancel(context.TODO()) defer cancel() imageManager.removeUnusedImages(ctx) - assert.Len(t, listContainersResponse.DockerIDs, 1, "error removing container IDs") - assert.Len(t, inspectContainerResponse.ID, 1, "error inspecting containers ids") + assert.Len(t, listContainersResponse.DockerIDs, 1, "Error removing container IDs") + assert.Len(t, inspectContainerResponse.ID, 1, "Error inspecting containers ids") assert.Len(t, imageManager.imageStates, 0, "Error removing image state after the image is removed") } @@ -1192,20 +1373,20 @@ func TestNonECSImageAndContainersCleanup_DontRemoveContainerWithInvalidFinishedT client.EXPECT().InspectContainer(gomock.Any(), gomock.Any(), dockerclient.InspectContainerTimeout).Return(inspectContainerResponse, nil).AnyTimes() client.EXPECT().RemoveContainer(gomock.Any(), "1", gomock.Any()).Return(nil).Times(0) client.EXPECT().ListImages(gomock.Any(), dockerclient.ListImagesTimeout).Return(listImagesResponse).AnyTimes() - client.EXPECT().InspectImage(listImagesResponse.RepoTags[0]).Return(inspectImageResponse, nil).Times(1) - client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.RepoTags[0], dockerclient.RemoveImageTimeout).Return(nil).Times(1) + client.EXPECT().InspectImage(listImagesResponse.ImageIDs[0]).Return(inspectImageResponse, nil).Times(1) + client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.ImageIDs[0], dockerclient.RemoveImageTimeout).Return(nil).Times(1) ctx, cancel := context.WithCancel(context.TODO()) defer cancel() imageManager.removeUnusedImages(ctx) - assert.Len(t, listContainersResponse.DockerIDs, 1, "error removing container IDs") - assert.Len(t, inspectContainerResponse.ID, 1, "error inspecting containers ids") + assert.Len(t, listContainersResponse.DockerIDs, 1, "Error removing container IDs") + assert.Len(t, inspectContainerResponse.ID, 1, "Error inspecting containers ids") assert.Len(t, imageManager.imageStates, 0, "Error removing image state after the image is removed") } -// New 'Created' Images should NOT be cleaned up. +// New 'Created' containers should NOT be cleaned up. func TestNonECSImageAndContainersCleanup_DoNotRemoveNewlyCreatedContainer(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -1249,16 +1430,16 @@ func TestNonECSImageAndContainersCleanup_DoNotRemoveNewlyCreatedContainer(t *tes client.EXPECT().InspectContainer(gomock.Any(), gomock.Any(), dockerclient.InspectContainerTimeout).Return(inspectContainerResponse, nil).AnyTimes() client.EXPECT().RemoveContainer(gomock.Any(), gomock.Any(), dockerclient.RemoveContainerTimeout).Return(nil).Times(0) client.EXPECT().ListImages(gomock.Any(), dockerclient.ListImagesTimeout).Return(listImagesResponse).AnyTimes() - client.EXPECT().InspectImage(listImagesResponse.RepoTags[0]).Return(inspectImageResponse, nil).Times(1) - client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.RepoTags[0], dockerclient.RemoveImageTimeout).Return(nil).Times(1) + client.EXPECT().InspectImage(listImagesResponse.ImageIDs[0]).Return(inspectImageResponse, nil).Times(1) + client.EXPECT().RemoveImage(gomock.Any(), listImagesResponse.ImageIDs[0], dockerclient.RemoveImageTimeout).Return(nil).Times(1) ctx, cancel := context.WithCancel(context.TODO()) defer cancel() imageManager.removeUnusedImages(ctx) - assert.Len(t, listContainersResponse.DockerIDs, 1, "error removing container IDs") - assert.Len(t, inspectContainerResponse.ID, 1, "error inspecting containers ids") + assert.Len(t, listContainersResponse.DockerIDs, 1, "Error removing container IDs") + assert.Len(t, inspectContainerResponse.ID, 1, "Error inspecting containers ids") assert.Len(t, imageManager.imageStates, 0, "Error removing image state after the image is removed") }