UNPKG

@bitblit/ratchet-aws

Version:

Common tools for use with AWS browser and node

136 lines 5.52 kB
import { BatchDeleteImageCommand, DescribeImagesCommand, DescribeRegistryCommand, DescribeRepositoriesCommand, } from '@aws-sdk/client-ecr'; import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet'; import { Logger } from '@bitblit/ratchet-common/logger/logger'; import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet'; import { RetainedImageReason } from './retained-image-reason.js'; export class EcrUnusedImageCleaner { ecr; static ECR_IMAGE_MINIMUM_AGE_DAYS = 60; static ECR_REPOSITORY_MINIMUM_IMAGE_COUNT = 600; constructor(ecr) { this.ecr = ecr; RequireRatchet.notNullOrUndefined(ecr, 'ecr'); } async findAllUsedImages(finders) { const rval = new Set(); for (const fnd of finders) { const next = await fnd.findUsedImageUris(); next.forEach((s) => rval.add(s)); } return Array.from(rval); } async performCleaning(opts) { Logger.info('Starting cleaning with options : %j', opts); Logger.info('Finding in-use images'); const usedImagesUris = await this.findAllUsedImages(opts.usedImageFinders || []); const usedImageTags = usedImagesUris.map((s) => s.substring(s.lastIndexOf(':') + 1)); Logger.info('Found %d images in use: %j', usedImageTags.length, usedImageTags); const regId = await this.fetchRegistryId(); Logger.info('Processing registry %s', regId); const repos = await this.fetchAllRepositoryDescriptors(regId); Logger.info('Found repos : %j', repos); const cleaned = []; for (let i = 0; i < repos.length; i++) { Logger.info('Processing repo %d of %d', i, repos.length); try { const next = await this.cleanRepository(repos[i], usedImageTags, opts); cleaned.push(next); } catch (err) { Logger.error('Failed to process repo : %j : %s', repos[i], err, err); } } const rval = { registryId: regId, repositories: cleaned, options: opts, }; return rval; } async cleanRepository(repo, usedImageTags, opts) { Logger.info('Cleaning repository: %j', repo); const images = await this.fetchAllImageDescriptors(repo); Logger.info('Found images: %d : %j', images.length, images); const toPurge = []; const toKeep = []; images.forEach((i) => { const matches = usedImageTags.map((tag) => i.imageTags.includes(tag)); const anyMatch = matches.find((s) => s); if (anyMatch) { toKeep.push({ image: i, reason: RetainedImageReason.InUse }); } else { toPurge.push(i); } }); Logger.info('Found %d to purge and %d to keep', toPurge.length, toKeep.length); const totalBytes = toPurge.map((p) => p.imageSizeInBytes).reduce((a, i) => a + i, 0); Logger.info('Found %s total bytes to purge : %d', StringRatchet.formatBytes(totalBytes), totalBytes); const purgeCmd = { registryId: repo.registryId, repositoryName: repo.repositoryName, imageIds: toPurge.map((p) => { return { imageDigest: p.imageDigest, imageTag: p.imageTags[0] }; }), }; Logger.info('Purge command : %j', purgeCmd); if (opts.dryRun) { Logger.info('Dry run specd, stopping'); } else { if (purgeCmd.imageIds.length > 0) { Logger.info('Purging unused images'); const output = await this.ecr.send(new BatchDeleteImageCommand(purgeCmd)); Logger.info('Response was : %j', output); } else { Logger.info('Skipping - nothing to purge in this repo'); } } const rval = { repository: repo, purged: toPurge, retained: toKeep, totalBytesRecovered: totalBytes, }; return rval; } async fetchAllImageDescriptors(repo) { RequireRatchet.notNullOrUndefined(repo, 'repo'); let rval = []; const cmd = { registryId: repo.registryId, repositoryName: repo.repositoryName, }; let resp = null; do { resp = await this.ecr.send(new DescribeImagesCommand(cmd)); rval = rval.concat(resp.imageDetails); cmd.nextToken = resp.nextToken; } while (StringRatchet.trimToNull(cmd.nextToken)); return rval; } async fetchAllRepositoryDescriptors(registryId) { let rval = []; const cmd = { registryId: registryId, }; let resp = null; do { resp = await this.ecr.send(new DescribeRepositoriesCommand(cmd)); rval = rval.concat(resp.repositories); cmd.nextToken = resp.nextToken; } while (StringRatchet.trimToNull(cmd.nextToken)); return rval; } async fetchAllRepositoryNames(registryId) { const resps = await this.fetchAllRepositoryDescriptors(registryId); const rval = resps.map((r) => r.repositoryName); return rval; } async fetchRegistryId() { const response = await this.ecr.send(new DescribeRegistryCommand({})); return response.registryId; } } //# sourceMappingURL=ecr-unused-image-cleaner.js.map