serverless/lib/actions/EndpointRemove.js

302 lines
9.5 KiB
JavaScript

'use strict';
/**
* Action: Endpoint Remove
* - Removes Endpoints
* - 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
* - names: (Array) Array of endpoint paths to remove. Format: ['users/show#GET']
* - all: (Boolean) Indicates whether all Endpoints 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 EndpointRemove extends SPlugin {
constructor(S) {
super(S);
SUtils = S.utils
}
static getName() {
return 'serverless.core.' + this.name;
}
registerActions() {
this.S.addAction(this.endpointRemove.bind(this), {
handler: 'endpointRemove',
description: 'Removes REST API endpoints',
context: 'endpoint',
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 Endpoints'
}
],
parameters: [
{
parameter: 'names',
description: 'The names/ids of the endpoints you want to remove in this format: user/create#GET',
position: '0->'
}
]
});
return BbPromise.resolve();
}
endpointRemove(evt) {
return EndpointRemover.run(this.S, evt);
}
}
class EndpointRemover 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) {
const remover = new this(evt, S);
return remover.endpointRemove();
}
endpointRemove(evt) {
// Flow
return BbPromise
.try(() => {
// Prompt: Stage
if (!this.S.config.interactive || this.evt.options.stage) return BbPromise.resolve(this.evt.options.stage);
return this.cliPromptSelectStage('Endpoint Remove - Choose a stage: ', this.evt.options.stage, false)
.then(stage => this.evt.options.stage = stage)
})
.bind(this)
.then(this._validateAndPrepare)
.then(this._processRemoval)
.then(() => {
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) {
SCli.log(`Failed to remove endpoints in "${this.evt.options.stage}" from the following regions:`);
_.each(this.failed, (failed, region) => {
SCli.log(region + ' ------------------------');
_.each(failed, (endpoint) => {
SCli.log(` ${endpoint.endpointMethod} - ${endpoint.endpointPath}: ${endpoint.message}`);
});
});
SCli.log('');
SCli.log('Run this again with --debug to get more error information...');
}
}
_displaySuccessful() {
if (this.removed) {
SCli.log(`Successfully removed endpoints in "${this.evt.options.stage}" from the following regions:`);
_.each(this.removed, (removed, region) => {
SCli.log(region + ' ------------------------');
_.each(removed, (endpoint) => {
SCli.log(` ${endpoint.endpointMethod} - ${endpoint.endpointPath} - ${endpoint.endpointUrl}`);
});
});
}
}
/**
* 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.endpoints = _.map(this.evt.options.names, (name) => {
const endpoint = this.project.getEndpoint(name);
if (!endpoint) throw new SError(`Endpoint "${name}" doesn't exist in your project`);
return endpoint;
});
// If CLI and no paths targeted, remove from CWD if Function
if (this.S.cli && !this.endpoints.length && !this.evt.options.all) {
let functionsByCwd = SUtils.getFunctionsByCwd(_this.project.getAllFunctions());
functionsByCwd.forEach(function(func) {
func.getAllEndpoints().forEach(function(endpoint) {
_this.endpoints.push(endpoint);
});
});
}
// If --all is selected, load all paths
if (this.evt.options.all) {
this.endpoints = this.project.getAllEndpoints();
}
if (_.isEmpty(this.endpoints)) throw new SError(`You don't have any endpoints in your project`);
// Validate Stage
if (!this.evt.options.stage || !this.project.validateStageExists(this.evt.options.stage)) {
throw new SError(`Stage is required`);
}
return BbPromise.resolve();
}
_processRemoval() {
SCli.log(`Removing endpoints 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
}
/**
* Remove Endpoints By Region
* - Finds a API Gateway in the region
* - Removes all function endpoints queued in a specific region
*/
_removeByRegion(region) {
let restApiId,
stage = this.evt.options.stage;
let restApiName = this.project.getRegion(stage, region).getVariables()['apiGatewayApi'] || this.project.getName();
return this.aws.getApiByName(restApiName, stage, region)
.then((restApiData) => restApiId = restApiData.id)
.then(() => this.aws.request('APIGateway', 'getResources', {restApiId, limit: 500}, stage, region))
.then((response) => response.items)
.then((resources) => {
return BbPromise.map(this.endpoints, ((endpoint) => this._endpointRemove(endpoint, region, resources, restApiId)), {concurrency: 5});
});
}
_endpointRemove(endpoint, region, resources, restApiId) {
let stage = this.evt.options.stage,
resource;
let getResourceToRemove = function (res) {
if (!res.parentId) return res.id;
// check if parent resource has no other children
if (_.filter(resources, {parentId: res.parentId}).length > 1) {
return res.id;
} else {
let parentResource = _.find(resources, {id: res.parentId});
// Skip if it's root - We can't remove the root resource.
if(parentResource.path === '/') return res.id;
return getResourceToRemove(parentResource);
}
}
return BbPromise
.try(() => {
if (!endpoint) throw new SError(`Endpoint could not be found`);
resource = _.find(resources, {path: '/' + endpoint.path} );
if (!resource || !resource.resourceMethods[endpoint.method]) {
throw new SError(`Endpoint "${endpoint.path}#${endpoint.method}" is not deployed in "${region}"`);
}
})
.then(() => {
let params = {
restApiId: restApiId,
resourceId: resource.id
}
if (_.keys(resource.resourceMethods).length > 1) {
SUtils.sDebug(`Removing method "${endpoint.method}" of "${resource.path}" resource`);
params.httpMethod = endpoint.method;
delete resource.resourceMethods[endpoint.method];
return this.aws.request('APIGateway', 'deleteMethod', params, stage, region);
} else {
// removing resource
SUtils.sDebug(`Removing resource "${resource.path}"`);
params.resourceId = getResourceToRemove(resource);
delete resource.parentId
return this.aws.request('APIGateway', 'deleteResource', params, stage, region);
}
})
.then((result) => {
// Stash removed endpoints
if (!this.removed) this.removed = {};
if (!this.removed[region]) this.removed[region] = [];
this.removed[region].push({
endpointPath: endpoint.path,
endpointMethod: endpoint.method,
endpointUrl: resource.path
});
})
.catch((e) => {
// Stash Failed Endpoint
if (!this.failed) this.failed = {};
if (!this.failed[region]) this.failed[region] = [];
this.failed[region].push({
endpointPath: endpoint ? endpoint.path : 'unknown',
endpointMethod: endpoint ? endpoint.method : 'unknown',
message: e.message,
stack: e.stack
});
});
}
}
return EndpointRemove;
};