@bitblit/ratchet-aws
Version:
Common tools for use with AWS browser and node
136 lines • 5.52 kB
JavaScript
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