From f3f2e3f5511dc2c3221ba664a7a3cf046b9e7f03 Mon Sep 17 00:00:00 2001 From: Tim deBoer Date: Thu, 15 Feb 2024 09:07:49 -0500 Subject: [PATCH] feat: remove old builder images (#144) * feat: remove old builder images Adds a new function to delete images with a given name that do not have the same tag (nor a second tag; you can tag to keep something). Uses this to look for and clean up any old builder images, so that we'll automatically clean up whenever we pull a new one. Fixes #98. Signed-off-by: Tim deBoer * chore: only log error, don't throw Signed-off-by: Tim deBoer --------- Signed-off-by: Tim deBoer --- src/build-disk-image.ts | 3 +++ src/container-utils.spec.ts | 23 ++++++++++++++++ src/container-utils.ts | 53 +++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/src/build-disk-image.ts b/src/build-disk-image.ts index 77d69765..9dd067b6 100644 --- a/src/build-disk-image.ts +++ b/src/build-disk-image.ts @@ -150,6 +150,9 @@ export async function buildDiskImage(imageData: unknown, history: History) { progress.report({ increment: 4 }); await containerUtils.pullImage(buildImageContainer.Image); + // delete previous copies of the image (in case we have upgraded it) + await containerUtils.deleteOldImages(image.engineId, buildImageContainer.Image); + // Step 2. Check if there are any previous builds and remove them progress.report({ increment: 5 }); await containerUtils.removeContainerIfExists(image.engineId, buildImageContainer.name); diff --git a/src/container-utils.spec.ts b/src/container-utils.spec.ts index 58a75db2..c8ec1eb2 100644 --- a/src/container-utils.spec.ts +++ b/src/container-utils.spec.ts @@ -25,6 +25,7 @@ import { waitForContainerToExit, removeContainerIfExists, removeContainerAndVolumes, + deleteOldImages, } from './container-utils'; // Mocks and utilities @@ -155,3 +156,25 @@ test('removeContainerAndVolumes should remove existing container and volumes ass expect(deleteContainerMock).toBeCalled(); expect(deleteVolumeMock).toBeCalledTimes(2); }); + +// Test deleteOldImages() deletes correctly tagged images +test('deleteOldImages should remove images with other tags', async () => { + const listImagesMock = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (extensionApi.containerEngine as any).listImages = listImagesMock; + listImagesMock.mockResolvedValue([ + { engineId: 'podman', Id: 'i1', RepoTags: ['test.io/name:1'] }, + { engineId: 'podman', Id: 'i2', RepoTags: ['test.io/name:2'] }, + { engineId: 'podman', Id: 'i3', RepoTags: ['test.io/name:3'] }, + { engineId: 'podman', Id: 'i4', RepoTags: ['test.io/name:4', 'keep-me'] }, + ]); + + const deletedIds: string[] = []; + const deleteImageMock = vi.fn().mockImplementation((_engineId, id) => deletedIds.push(id)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (extensionApi.containerEngine as any).deleteImage = deleteImageMock; + + await deleteOldImages('podman', 'test.io/name:2'); + expect(deleteImageMock).toHaveBeenCalledTimes(2); + expect(deletedIds).toEqual(['i1', 'i3']); +}); diff --git a/src/container-utils.ts b/src/container-utils.ts index 7e5efce5..cd3d7cf3 100644 --- a/src/container-utils.ts +++ b/src/container-utils.ts @@ -56,6 +56,59 @@ export async function pullImage(image: string) { } } +// Delete all copies of the given image except for the current one +export async function deleteOldImages(engineId: string, currentImage: string) { + console.log('Deleting old images: ', currentImage); + try { + // List all the images and check to see if it exists + const images = await extensionApi.containerEngine.listImages(); + if (!images || images.length === 0) { + return; + } + + // We're looking to delete images that have the same name but different tags + const indexTag = currentImage.lastIndexOf(':'); + const currentName = currentImage.slice(0, indexTag); + const currentTag = currentImage.slice(indexTag + 1); + + // Build a list of images by scanning all images that have the same name, + // but do not have the current tag or other tags. + const imageIdsToRemove: string[] = []; + images.forEach(image => { + if (image.engineId === engineId && image.RepoTags) { + let found = false; + let otherTags = false; + image.RepoTags.map(repoTag => { + const indexTag = repoTag.lastIndexOf(':'); + const name = repoTag.slice(0, indexTag); + const tag = repoTag.slice(indexTag + 1); + if (name === currentName) { + if (tag !== currentTag) { + found = true; + } else { + otherTags = true; + } + } else { + otherTags = true; + } + }); + if (found && !otherTags) { + imageIdsToRemove.push(image.Id); + } + } + }); + + // Delete the images + await imageIdsToRemove.reduce((prev: Promise, imageId) => { + return prev + .then(() => extensionApi.containerEngine.deleteImage(engineId, imageId)) + .catch((e: unknown) => console.error('error while removing image', e)); + }, Promise.resolve()); + } catch (e) { + console.error(e); + } +} + // Create and start a container based upon the container create options // For functions such as start / stop / delete, we need the engineID passed in.. export async function createAndStartContainer(engineId: string, options: ContainerCreateOptions): Promise {