Skip to content

Commit

Permalink
feat: remove old builder images (containers#144)
Browse files Browse the repository at this point in the history
* 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 containers#98.

Signed-off-by: Tim deBoer <git@tdeboer.ca>

* chore: only log error, don't throw

Signed-off-by: Tim deBoer <git@tdeboer.ca>

---------

Signed-off-by: Tim deBoer <git@tdeboer.ca>
  • Loading branch information
deboer-tim authored Feb 15, 2024
1 parent d5d0410 commit f3f2e3f
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/build-disk-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
23 changes: 23 additions & 0 deletions src/container-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
waitForContainerToExit,
removeContainerIfExists,
removeContainerAndVolumes,
deleteOldImages,
} from './container-utils';

// Mocks and utilities
Expand Down Expand Up @@ -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']);
});
53 changes: 53 additions & 0 deletions src/container-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>, 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<string> {
Expand Down

0 comments on commit f3f2e3f

Please sign in to comment.