'use strict'; /** * Action: Event Remove * - Removes Event Sources from a lambda function * - Loops sequentially through each Region in specified Stage * * Options: * - stage: (String) The stage to remove from * - region: (String) The region in the stage to remove from * - paths: (Array) Array of event paths to remove. Format: ['users/show#eventName'] * - all: (Boolean) Indicates whether all Events in the project should be removed. */ module.exports = function(SPlugin, serverlessPath) { const path = require('path'), SError = require(path.join(serverlessPath, 'Error')), SCli = require(path.join(serverlessPath, 'utils/cli')), BbPromise = require('bluebird'), _ = require('lodash'); let SUtils; class EventRemove extends SPlugin { constructor(S) { super(S); SUtils = S.utils; } static getName() { return 'serverless.core.' + this.name; } registerActions() { this.S.addAction(this.eventRemove.bind(this), { handler: 'eventRemove', description: 'Removes event sources from lambdas', context: 'event', contextAction: 'remove', options: [ { option: 'stage', shortcut: 's', description: 'Optional if only one stage is defined in project' }, { option: 'region', shortcut: 'r', description: 'Optional - Target one region to remove from' }, { option: 'all', shortcut: 'a', description: 'Optional - Remove all Events' } ], parameters: [ { parameter: 'names', description: 'One or multiple event names', position: '0->' } ] }); return BbPromise.resolve(); } eventRemove(evt) { return EventRemover.run(this.S, evt); } } class EventRemover extends SPlugin { constructor(S, evt) { super(S); this.evt = evt; // Instantiate Classes this.project = this.S.getProject(); this.aws = this.S.getProvider('aws'); } static run(evt, S) { let remover = new this(evt, S); return remover.eventRemove(); } eventRemove() { return BbPromise.try(() => { // Prompt: Stage if (!this.S.config.interactive || this.evt.options.stage) return; return this.cliPromptSelectStage('Event Remover - Choose a stage: ', this.evt.options.stage, false) .then(stage => this.evt.options.stage = stage) }) .bind(this) .then(this._validateAndPrepare) .then(this._processRemoval) .then(function() { this._displaySuccessful(); this._displayFailed(); /** * Return EVT */ this.evt.data.removed = this.removed; this.evt.data.failed = this.failed; return this.evt; }); } _displayFailed() { if(!this.failed) return; SCli.log(`Failed to remove events in "${this.evt.options.stage}" from the following regions:`); _.each(this.failed, (failed, region) => { SCli.log(region + ' ------------------------'); _.each(failed, (fail) => SCli.log(` ${fail.name}: ${fail.message}`)); }); SCli.log(''); SCli.log('Run this again with --debug to get more error information...'); } _displaySuccessful() { if (!this.removed) return; SCli.log(`Successfully removed events in "${this.evt.options.stage}" from the following regions:`); _.each(this.removed, (removed, region) => { SCli.log(region + ' ------------------------'); _.each(removed, (event) => SCli.log(` ${event.name}`)); }); } /** * Validate And Prepare * - If CLI, maps CLI input to event object */ _validateAndPrepare() { let _this = this; // Set defaults this.evt.options.names = this.evt.options.names || []; this.regions = this.evt.options.region ? [this.evt.options.region] : this.project.getAllRegionNames(this.evt.options.stage); this.events = _.map(this.evt.options.names, (name) => this.project.getEvent(name)); // If CLI and no event names targeted, remove by CWD if (this.S.cli && !this.evt.options.names.length && !this.evt.options.all) { let functionsByCwd = SUtils.getFunctionsByCwd(_this.project.getAllFunctions()); functionsByCwd.forEach(function(func) { func.getAllEvents().forEach(function(event) { _this.events.push(event); }); }); } // If --all is selected, load all paths if (this.evt.options.all) { this.events = this.project.getAllEvents(); } // Validate Stage if (!this.evt.options.stage || !this.project.validateStageExists(this.evt.options.stage)) { throw new SError(`Stage is required`); } return BbPromise.resolve(); } _processRemoval() { // Status SCli.log(`\nRemoving events in "${this.evt.options.stage}" to the following regions: ${this.regions.join(', ')}`); let spinner = SCli.spinner(); spinner.start(); return BbPromise .map(this.regions, this._removeByRegion.bind(this)) .then(() => spinner.stop(true)); // Stop Spinner } _removeByRegion(region) { return BbPromise.map(this.events, ((event) => this._eventRemove(event, region)), {concurrency: 5}); } _eventRemove(event, region) { if(!event) throw new SError(`Event could not be found: ${event.name}`); let eventType = event.type.toLowerCase(); if (['dynamodbstream', 'kinesisstream'].indexOf(eventType) > -1) eventType = 'LambdaStream'; if (!this[`_remove_${eventType}`]) { SCli.log(`WARNING: Event type "${event.type}" removal is not supported yet`); return BbPromise.resolve(); } return this[`_remove_${eventType}`](event, region) .then((result) => { // Stash removed events if (!this.removed) this.removed = {}; if (!this.removed[region]) this.removed[region] = []; this.removed[region].push({ function: event.getFunction(), name: event.name }); }) .catch((e) => { // Stash Failed Events if (!this.failed) this.failed = {}; if (!this.failed[region]) this.failed[region] = []; this.failed[region].push({ function: event.getFunction() || 'unknown', name: event.name, message: e.message, stack: e.stack }); }); } _remove_LambdaStream(event, region) { const stage = this.evt.options.stage, functionName = event.getFunction().getDeployedName({stage, region}), regionVars = this.project.getRegion(stage, region).getVariables(), awsAccountId = regionVars.iamRoleArnLambda.split('::')[1].split(':')[0], arn = 'arn:aws:lambda:' + region + ':' + awsAccountId + ':function:' + functionName + ':' + stage, eventId = 'eventID:' + event.name, UUID = regionVars[eventId]; if (!UUID) return BbPromise.reject(new SError(`EventSourceMapping UUID for "${event.name}" is not found`)); return this.aws.request('Lambda', 'deleteEventSourceMapping', {UUID}, stage, region).then(() => { let regionInstance = this.project.getRegion(stage, region) delete regionInstance.getVariables()[eventId]; return regionInstance.save(); }); } _remove_s3(event, region) { const stage = this.evt.options.stage, populatedEvent = event.toObjectPopulated({stage, region}), Bucket = populatedEvent.config.bucket, regionVars = this.project.getRegion(stage, region).getVariables(), functionName = event.getFunction().getDeployedName({stage, region}), awsAccountId = regionVars.iamRoleArnLambda.split('::')[1].split(':')[0], arn = 'arn:aws:lambda:' + region + ':' + awsAccountId + ':function:' + functionName + ':' + stage, s3Region = this.project.getVariables().projectBucket.split('.')[1]; return this.aws.request('S3', 'getBucketNotificationConfiguration', {Bucket}, stage, s3Region) .then((conf) => { if (!_.find(conf.LambdaFunctionConfigurations, {LambdaFunctionArn: arn})) { return BbPromise.reject(new SError(`S3 configuration for "${event.name}" is not found`)) } conf.LambdaFunctionConfigurations = _.filter(conf.LambdaFunctionConfigurations, (item) => item.LambdaFunctionArn !== arn ); return this.aws.request('S3', 'putBucketNotificationConfiguration', {Bucket, NotificationConfiguration: conf}, stage, s3Region); }); } _remove_schedule(event, region) { const stage = this.evt.options.stage, functionName = event.getFunction().getDeployedName({stage, region}), Rule = functionName + '-' + event.name; return this.aws.request('CloudWatchEvents', 'removeTargets', {Ids: [functionName], Rule}, stage, region) .then(() => this.aws.request('CloudWatchEvents', 'deleteRule', {Name: Rule}, stage, region)); } _remove_sns(event, region) { const stage = this.evt.options.stage, functionName = event.getFunction().getDeployedName({stage, region}), regionVars = this.project.getRegion(stage, region).getVariables(), awsAccountId = regionVars.iamRoleArnLambda.split('::')[1].split(':')[0], Endpoint = 'arn:aws:lambda:' + region + ':' + awsAccountId + ':function:' + functionName + ':' + stage, populatedEvent = event.toObjectPopulated({stage, region}), TopicArn = 'arn:aws:sns:' + region + ':' + awsAccountId + ':' + populatedEvent.config.topicName; return this._SNSlistSubscriptionsByTopic(TopicArn, stage, region) .then((subscriptions) => _.filter(subscriptions, {Endpoint})) .then((subscriptions) => subscriptions.length && subscriptions || BbPromise.reject(new SError(`Subscription for "${event.name}" is not found`))) .map((subscription) => subscription.SubscriptionArn) .map((SubscriptionArn) => this.aws.request('SNS', 'unsubscribe', {SubscriptionArn}, stage, region)); } _SNSlistSubscriptionsByTopic(TopicArn, stage, region, NextToken, subscriptions) { subscriptions = subscriptions || []; return this.aws.request('SNS', 'listSubscriptionsByTopic', {TopicArn, NextToken}, stage, region) .then((reply) => { subscriptions = subscriptions.concat(reply.Subscriptions); if (reply.NextToken) { return this._SNSlistSubscriptionsByTopic(TopicArn, stage, region, reply.NextToken, subscriptions); } else { return subscriptions; } }); } } return EventRemove; };