UNPKG

serverless-s3-cleaner

Version:

Serverless Framework plugin that empties S3 buckets before removing a stack

153 lines (152 loc) 6.44 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const prompt_1 = __importDefault(require("prompt")); class ServerlessS3Cleaner { constructor(serverless, _options, logging) { this.serverless = serverless; this.configSchema = { type: 'object', properties: { prompt: { type: 'boolean', nullable: true, default: false }, buckets: { type: 'array', uniqueItems: true, items: { type: 'string' }, nullable: true }, bucketsToCleanOnDeploy: { type: 'array', uniqueItems: true, items: { type: 'string' }, nullable: true }, }, additionalProperties: false, anyOf: [ { required: ['buckets'] }, { required: ['bucketsToCleanOnDeploy'] } ] }; this.provider = this.serverless.getProvider('aws'); this.log = logging.log; this.serverless.configSchemaHandler.defineCustomProperties({ type: 'object', properties: { 'serverless-s3-cleaner': this.configSchema } }); this.commands = { s3remove: { usage: 'Remove all files in S3 buckets', lifecycleEvents: [ 'remove' ] } }; this.hooks = { 'before:deploy:deploy': async () => this.remove(true), 'before:remove:remove': async () => this.remove(false), 's3remove:remove': async () => this.remove(false), }; } async remove(isDeploying) { const config = this.loadConfig(); let bucketsToEmpty = isDeploying ? config.bucketsToCleanOnDeploy : config.buckets; if (config.prompt) { prompt_1.default.start(); const bucketPromptResults = await prompt_1.default.get(bucketsToEmpty.map(bucket => ({ name: bucket, description: `Empty bucket ${bucket}. Are you sure? [yes/no]:`, pattern: /(yes|no)/, default: 'yes', message: 'Must respond yes or no', }))); bucketsToEmpty = []; for (const bucket of Object.keys(bucketPromptResults)) { const confirmed = bucketPromptResults[bucket].toString() === 'yes'; if (confirmed) { bucketsToEmpty.push(bucket); } else { this.log.notice(`${bucket}: remove skipped`); } } } // Filter out inaccessible buckets before doing the work; // this is so we don't log unnecessary/ugly errors in case e.g. old buckets left in bucketsToCleanOnDeploy have already been removed const existingBuckets = []; for (const bucket of bucketsToEmpty) { const exists = await this.bucketExists(bucket); if (exists) { existingBuckets.push(bucket); } else { this.log.warning(`${bucket} not found or you do not have permissions, skipping...`); } } // Parallelize the removal to speed things up const removePromises = existingBuckets.map(bucket => this .listBucketKeys(bucket) .then(keys => this.deleteObjects(bucket, keys)) .then(() => this.log.success(`${bucket} successfully emptied`)) .catch(err => this.log.error(`${bucket} cannot be emptied. ${err}`))); await Promise.all(removePromises); } async bucketExists(bucket) { const params = { Bucket: bucket }; return this.provider.request('S3', 'headBucket', params) .then(() => true) .catch(() => false); } async deleteObjects(bucket, keys) { const maxDeleteKeys = 1000; const params = []; for (let i = 0; i < keys.length; i += maxDeleteKeys) { params.push({ Bucket: bucket, Delete: { Objects: keys.slice(i, i + maxDeleteKeys), Quiet: true } }); } const deleteResults = await Promise.all(params.map(param => this.provider.request('S3', 'deleteObjects', param))); // to avoid dumping too many details in the output, we choose to list the first error message available, if any const firstErrorResult = deleteResults.find(dr => dr.Errors && dr.Errors.length > 0); if (firstErrorResult) { const errInfo = firstErrorResult.Errors[0]; throw new Error(`${errInfo.Key} - ${errInfo.Message}`); } } async listBucketKeys(bucketName) { const listParams = { Bucket: bucketName }; let bucketKeys = []; while (true) { const listResult = await this.provider.request('S3', 'listObjectVersions', listParams); if (listResult.Versions) { bucketKeys = bucketKeys.concat(listResult.Versions.map(item => ({ Key: item.Key, VersionId: item.VersionId }))); } if (listResult.DeleteMarkers) { bucketKeys = bucketKeys.concat(listResult.DeleteMarkers.map(item => ({ Key: item.Key, VersionId: item.VersionId }))); } if (!listResult.IsTruncated) { break; } listParams.VersionIdMarker = listResult.NextVersionIdMarker; listParams.KeyMarker = listResult.NextKeyMarker; } return bucketKeys; } loadConfig() { const providedConfig = this.serverless.service.custom['serverless-s3-cleaner']; if (!providedConfig.buckets && !providedConfig.bucketsToCleanOnDeploy) { throw new Error('You must configure "buckets" or "bucketsToCleanOnDeploy" parameters in custom > serverless-s3-cleaner section'); } return { buckets: providedConfig.buckets || [], prompt: providedConfig.prompt || false, bucketsToCleanOnDeploy: providedConfig.bucketsToCleanOnDeploy || [], }; } } exports.default = ServerlessS3Cleaner; module.exports = ServerlessS3Cleaner;