Skip to content

Commit

Permalink
Add an integration test for cleaning up images
Browse files Browse the repository at this point in the history
  • Loading branch information
Barry Gordon committed Mar 1, 2022
1 parent cb1fa6a commit 795abdc
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 27 deletions.
69 changes: 69 additions & 0 deletions __tests__/cleanup-integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as core from '@actions/core'
import Docker from 'dockerode'
import {ImageService} from '../src/image-service'
import {integration, delay} from './helpers'
import {run, cleanupOldImageVersions} from '../src/cleanup'

integration('run', () => {
beforeEach(async () => {
jest.spyOn(core, 'error').mockImplementation(jest.fn())
})

test('it does not log any errors interacting with Docker by default', async () => {
await run()

expect(core.error).not.toHaveBeenCalled()
})
})

integration('cleanupOldImageVersions', () => {
// We use this GitHub-hosted hello world example as a small stand-in for this test
// in order to avoid hitting the rate limit on pulling containers from docker.io
// since this test needs to remove and pull containers per run.
const testImage = 'ghcr.io/github/hello-docker'
const docker = new Docker()
const imageOptions = {
filters: {
reference: [testImage]
}
}

const currentImage = `${testImage}@sha256:f32f4412fa4b6c7ece72cb85ae652751f11ac0d075c1131df09bb24f46b2f4e3`
const oldImage = `${testImage}@sha256:8cfee63309567569d3d7d0edc05fcf8be8f9f5130f0564dacea4cfe82a9db4b7`

async function clearTestImages(): Promise<void> {
const testImages = await docker.listImages(imageOptions)

for (const image of testImages) {
await docker.getImage(image.Id).remove()
}
}

beforeAll(async () => {
// Remove any existing alpine images from other tests
await clearTestImages()
}, 10000)

beforeEach(async () => {
await ImageService.fetchImage(currentImage)
await ImageService.fetchImage(oldImage)
}, 20000)

afterEach(async () => {
await clearTestImages()
}, 10000)

test('it removes unused versions of the given image', async () => {
const imageCount = (await docker.listImages(imageOptions)).length
expect(imageCount).toEqual(2)

await cleanupOldImageVersions(docker, currentImage)
// The Docker API seems to ack the removal before it is carried out, so let's wait briefly to ensure
// the verification query doesn't race the deletion
await delay(200)

const remainingImages = await docker.listImages(imageOptions)
expect(remainingImages.length).toEqual(1)
expect(remainingImages[0].RepoDigests?.includes(currentImage))
})
})
15 changes: 0 additions & 15 deletions __tests__/cleanup.test.ts

This file was deleted.

4 changes: 4 additions & 0 deletions __tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ export const eventFixturePath = (fixtureName: string): string => {
export const integration = process.env.SKIP_INTEGRATION_TESTS
? describe.skip
: describe

export async function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
23 changes: 17 additions & 6 deletions dist/cleanup/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/cleanup/index.js.map

Large diffs are not rendered by default.

22 changes: 17 additions & 5 deletions src/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function run(cutoff = '24h'): Promise<void> {
}
}

async function cleanupOldImageVersions(
export async function cleanupOldImageVersions(
docker: Docker,
imageName: string
): Promise<void> {
Expand All @@ -42,18 +42,30 @@ async function cleanupOldImageVersions(
docker.listImages(options, async function (err, images) {
if (images && images.length > 0) {
for (const imageInfo of images) {
if (imageInfo.RepoDigests?.includes(imageName)) {
core.info(`Skipping current image ${imageInfo.RepoDigests}`)
// The given imageName is expected to be a digest, however to avoid any surprises in future
// we fail over to check for a match on tags as well.
//
// This means we won't remove any image which matches an imageName of either of these notations:
// - dependabot/image@sha256:$REF (current implementation)
// - dependabot/image:v1
//
// Without checking imageInfo.RepoTags for a match, we would actually remove the latter even if
// this was the active version.
if (
imageInfo.RepoDigests?.includes(imageName) ||
imageInfo.RepoTags?.includes(imageName)
) {
core.info(`Skipping current image ${imageInfo.Id}`)
continue
}

core.info(`Removing image ${imageInfo.RepoDigests}`)
core.info(`Removing image ${imageInfo.Id}`)
try {
await docker.getImage(imageInfo.Id).remove()
} catch (error) {
if (error.statusCode === 409) {
core.info(
`Unable to remove ${imageInfo.RepoDigests} as it is currently in use`
`Unable to remove ${imageInfo.Id} as it is currently in use`
)
}
}
Expand Down

0 comments on commit 795abdc

Please sign in to comment.