Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for api keys and usage plans #183

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ end_of_line = lf
charset = utf-8

[*.{json,yml}]
indent_style = space
indent_style = tab
indent_size = 2
11 changes: 6 additions & 5 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ extends:
- plugin:import/errors
- plugin:import/warnings

parserOptions:
ecmaVersion: 9

plugins:
- promise
- lodash
Expand All @@ -19,8 +22,7 @@ rules:
indent:
- error
- tab
-
MemberExpression: off
- MemberExpression: off
linebreak-style:
- error
- unix
Expand All @@ -42,6 +44,5 @@ rules:
lodash/prop-shorthand: off
lodash/prefer-lodash-method:
- error
-
ignoreObjects:
- BbPromise
- ignoreObjects:
- BbPromise
144 changes: 96 additions & 48 deletions lib/stackops/apiGateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@
*/

const _ = require('lodash');
const BbPromise = require('bluebird');
const utils = require('../utils');

const stageMethodConfigMappings = {
cacheDataEncrypted: { prop: 'CacheDataEncrypted', validate: _.isBoolean, default: false },
cacheTtlInSeconds: { prop: 'CacheTtlInSeconds', validate: _.isInteger },
cachingEnabled: { prop: 'CachingEnabled', validate: _.isBoolean, default: false },
dataTraceEnabled: { prop: 'DataTraceEnabled', validate: _.isBoolean, default: false },
loggingLevel: { prop: 'LoggingLevel', validate: value => _.includes([ 'OFF', 'INFO', 'ERROR' ], value), default: 'OFF' },
metricsEnabled: { prop: 'MetricsEnabled', validate: _.isBoolean, default: false },
throttlingBurstLimit: { prop: 'ThrottlingBurstLimit', validate: _.isInteger },
throttlingRateLimit: { prop: 'ThrottlingRateLimit', validate: _.isNumber }
cacheDataEncrypted: {prop: 'CacheDataEncrypted', validate: _.isBoolean, default: false},
cacheTtlInSeconds: {prop: 'CacheTtlInSeconds', validate: _.isInteger},
cachingEnabled: {prop: 'CachingEnabled', validate: _.isBoolean, default: false},
dataTraceEnabled: {prop: 'DataTraceEnabled', validate: _.isBoolean, default: false},
loggingLevel: {prop: 'LoggingLevel', validate: value => _.includes(['OFF', 'INFO', 'ERROR'], value), default: 'OFF'},
metricsEnabled: {prop: 'MetricsEnabled', validate: _.isBoolean, default: false},
throttlingBurstLimit: {prop: 'ThrottlingBurstLimit', validate: _.isInteger},
throttlingRateLimit: {prop: 'ThrottlingRateLimit', validate: _.isNumber}
};

/**
Expand Down Expand Up @@ -49,7 +48,7 @@ const internal = {
SERVERLESS_STAGE: this._stage
}
},
DependsOn: [ deploymentName ]
DependsOn: [deploymentName]
};

// Set a reasonable description
Expand Down Expand Up @@ -88,7 +87,7 @@ const internal = {
'PATCH',
'POST',
'PUT'
]: [ methodType ];
] : [methodType];

_.forOwn(eventStageConfig, (value, key) => {
if (!_.has(stageMethodConfigMappings, key)) {
Expand Down Expand Up @@ -119,19 +118,50 @@ const internal = {
}
};

module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {

function movetoAliasStack(stageStack, resourceType, aliasResources, properties, depends) {
const resources = _.assign({}, _.pickBy(stageStack.Resources, ['Type', resourceType]));
if (!_.isEmpty(resources)) {
const resourceNames = _.keys(resources);
_.forEach(resourceNames, resourceName => {
let obj = resources[resourceName];

for (var i in properties) {
obj.Properties[i] = properties[i];
}

if (obj.DependsOn) {
if (!obj.DependsOn.push) {
obj.DependsOn = [obj.DependsOn];
}
} else {
obj.DependsOn = [];
}

obj.DependsOn = obj.DependsOn.concat(depends);

aliasResources.push({
[resourceName]: resources[resourceName]
});
delete stageStack.Resources[resourceName];
});
}
}


module.exports = function (currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
const stackName = this._provider.naming.getStackName();
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
const userResources = _.get(this._serverless.service, 'resources', { Resources: {}, Outputs: {} });
const userResources = _.get(this._serverless.service, 'resources', {Resources: {}, Outputs: {}});

// Check if our current deployment includes an API deployment
let exposeApi = _.includes(_.keys(stageStack.Resources), 'ApiGatewayRestApi');
const aliasResources = [];

if (!exposeApi) {
// Check if we have any aliases deployed that reference the API.
if (_.some(aliasStackTemplates, template => _.find(template.Resources, [ 'Type', 'AWS::ApiGateway::Deployment' ]))) {
if (_.some(aliasStackTemplates, template => _.find(template.Resources, ['Type', 'AWS::ApiGateway::Deployment']))) {
// Fetch the Api resource from the current stack
stageStack.Resources.ApiGatewayRestApi = currentTemplate.Resources.ApiGatewayRestApi;
exposeApi = true;
Expand All @@ -145,7 +175,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
// Export the API for the alias stacks
stageStack.Outputs.ApiGatewayRestApi = {
Description: 'API Gateway API',
Value: { Ref: 'ApiGatewayRestApi' },
Value: {Ref: 'ApiGatewayRestApi'},
Export: {
Name: `${stackName}-ApiGatewayRestApi`
}
Expand All @@ -154,7 +184,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
// Export the root resource for the API
stageStack.Outputs.ApiGatewayRestApiRootResource = {
Description: 'API Gateway API root resource',
Value: { 'Fn::GetAtt': [ 'ApiGatewayRestApi', 'RootResourceId' ] },
Value: {'Fn::GetAtt': ['ApiGatewayRestApi', 'RootResourceId']},
Export: {
Name: `${stackName}-ApiGatewayRestApiRootResource`
}
Expand All @@ -164,19 +194,19 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
if (_.some(_.reduce(aliasStackTemplates, (result, template) => {
_.merge(result, template.Resources);
return result;
}, {}), [ 'Type', 'AWS::ApiGateway::Method' ]) ||
_.find(currentAliasStackTemplate.Resources, [ 'Type', 'AWS::ApiGateway::Method' ])) {
}, {}), ['Type', 'AWS::ApiGateway::Method']) ||
_.find(currentAliasStackTemplate.Resources, ['Type', 'AWS::ApiGateway::Method'])) {
throw new this._serverless.classes.Error('ALIAS PLUGIN ALPHA CHANGE: APIG deployment had to be changed. Please remove the alias stacks and the APIG stage for the alias in CF (AWS console) and redeploy. Sorry!');
}

// Move the API deployment into the alias stack. The alias is the owner of the APIG stage.
const deployment = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Deployment' ]));
const deployment = _.assign({}, _.pickBy(stageStack.Resources, ['Type', 'AWS::ApiGateway::Deployment']));
if (!_.isEmpty(deployment)) {
const deploymentName = _.keys(deployment)[0];
const obj = deployment[deploymentName];

delete obj.Properties.StageName;
obj.Properties.RestApiId = { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi` };
obj.Properties.RestApiId = {'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`};
obj.DependsOn = [];

aliasResources.push(deployment);
Expand All @@ -185,36 +215,54 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
// Create stage resource
this.options.verbose && this._serverless.cli.log('Configuring stage');
const stageResource = internal.createStageResource.call(this, `${stackName}-ApiGatewayRestApi`, deploymentName);
aliasResources.push({ ApiGatewayStage: stageResource });
aliasResources.push({ApiGatewayStage: stageResource});

const baseMapping = _.assign({}, _.pickBy(stageStack.Resources, ['Type', 'AWS::ApiGateway::BasePathMapping']));
if (!_.isEmpty(baseMapping)) {
const baseMappingName = _.keys(baseMapping)[0];
const obj = baseMapping[baseMappingName];

obj.Properties.Stage = { Ref: 'ApiGatewayStage' };
obj.Properties.RestApiId = { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`};
//Move BasePath mappings
movetoAliasStack(stageStack, 'AWS::ApiGateway::BasePathMapping', aliasResources, {
Stage: {Ref: 'ApiGatewayStage'},
RestApiId: {'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`}
}, []);

aliasResources.push(baseMapping);
delete stageStack.Resources[baseMappingName];
}
//Move apiKeys
movetoAliasStack(stageStack, 'AWS::ApiGateway::ApiKey', aliasResources, {
StageKeys: [
{
RestApiId: {'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`},
StageName: stageResource.Properties.StageName
}
]
}, ["ApiGatewayStage"]);

//Move usageplans
movetoAliasStack(stageStack, 'AWS::ApiGateway::UsagePlan', aliasResources, {
ApiStages: [
{
ApiId: {'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`},
Stage: stageResource.Properties.StageName
}
]
}, ["ApiGatewayStage"]);

//Move usageplankeys
movetoAliasStack(stageStack, 'AWS::ApiGateway::UsagePlanKey', aliasResources, {}, []);
}

// Fetch lambda permissions, methods and resources. These have to be updated later to allow the aliased functions.
const apiLambdaPermissions =
_.assign({},
_.pickBy(_.pickBy(stageStack.Resources, [ 'Type', 'AWS::Lambda::Permission' ]),
permission => utils.hasPermissionPrincipal(permission, 'apigateway')));
_.assign({},
_.pickBy(_.pickBy(stageStack.Resources, ['Type', 'AWS::Lambda::Permission']),
permission => utils.hasPermissionPrincipal(permission, 'apigateway')));

const apiMethods = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Method' ]));
const authorizers = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Authorizer' ]));
const aliases = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Alias' ]));
const versions = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Version' ]));
const apiMethods = _.assign({}, _.pickBy(stageStack.Resources, ['Type', 'AWS::ApiGateway::Method']));
const authorizers = _.assign({}, _.pickBy(stageStack.Resources, ['Type', 'AWS::ApiGateway::Authorizer']));
const aliases = _.assign({}, _.pickBy(aliasStack.Resources, ['Type', 'AWS::Lambda::Alias']));
const versions = _.assign({}, _.pickBy(aliasStack.Resources, ['Type', 'AWS::Lambda::Version']));

// Adjust method API and target function
_.forOwn(apiMethods, (method, name) => {
// Relink to function alias in case we have a lambda endpoint
if (_.includes([ 'AWS', 'AWS_PROXY' ], _.get(method, 'Properties.Integration.Type'))) {
if (_.includes(['AWS', 'AWS_PROXY'], _.get(method, 'Properties.Integration.Type'))) {
// For methods it is a bit tricky to find the related function name. There is no direct link.
const uriParts = method.Properties.Integration.Uri['Fn::Join'][1];
const funcIndex = _.findIndex(uriParts, part => _.has(part, 'Fn::GetAtt'));
Expand Down Expand Up @@ -242,7 +290,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
const isExternalRefAuthorizer = _.some(uriParts, isExternalRefAuthorizerPredicate);
if (!isExternalRefAuthorizer) {
const funcIndex = _.findIndex(uriParts, part => _.startsWith(part, '/invocations'));
uriParts.splice(funcIndex , 0, ':${stageVariables.SERVERLESS_ALIAS}');
uriParts.splice(funcIndex, 0, ':${stageVariables.SERVERLESS_ALIAS}');
}
}

Expand All @@ -257,7 +305,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
const aliasedName = `${name}${_.replace(this._alias, /-/g, 'Dash')}`;
const authorizerRefs = utils.findReferences(stageStack.Resources, name);
_.forEach(authorizerRefs, ref => {
_.set(stageStack.Resources, ref, { Ref: aliasedName });
_.set(stageStack.Resources, ref, {Ref: aliasedName});
});

// Replace dependencies
Expand Down Expand Up @@ -285,7 +333,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac

// Adjust references and alias permissions
if (!isExternalRef) {
permission.Properties.FunctionName = { Ref: aliasName };
permission.Properties.FunctionName = {Ref: aliasName};
}
if (permission.Properties.SourceArn) {
// Authorizers do not set the SourceArn property
Expand All @@ -294,13 +342,13 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
'',
[
'arn:',
{ Ref: 'AWS::Partition' },
{Ref: 'AWS::Partition'},
':execute-api:',
{ Ref: 'AWS::Region' },
{Ref: 'AWS::Region'},
':',
{ Ref: 'AWS::AccountId' },
{Ref: 'AWS::AccountId'},
':',
{ 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi` },
{'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`},
'/*/*'
]
]
Expand All @@ -309,9 +357,9 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac

// Add dependency on function version
if (!isExternalRef) {
permission.DependsOn = [ versionName, aliasName ];
permission.DependsOn = [versionName, aliasName];
} else {
permission.DependsOn = _.compact([ versionName, aliasName ]);
permission.DependsOn = _.compact([versionName, aliasName]);
}

delete stageStack.Resources[name];
Expand All @@ -324,7 +372,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac

_.forEach(aliasResources, resource => _.assign(aliasStack.Resources, resource));

return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
return Promise.resolve([currentTemplate, aliasStackTemplates, currentAliasStackTemplate]);
};

// Exports to make internal functions available for unit tests
Expand Down
Loading