Skip to content

Add support for api keys and usage plans #183

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
@@ -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
@@ -10,6 +10,9 @@ extends:
- plugin:import/errors
- plugin:import/warnings

parserOptions:
ecmaVersion: 9

plugins:
- promise
- lodash
@@ -19,8 +22,7 @@ rules:
indent:
- error
- tab
-
MemberExpression: off
- MemberExpression: off
linebreak-style:
- error
- unix
@@ -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
@@ -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}
};

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

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

_.forOwn(eventStageConfig, (value, key) => {
if (!_.has(stageMethodConfigMappings, key)) {
@@ -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;
@@ -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`
}
@@ -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`
}
@@ -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);
@@ -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'));
@@ -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}');
}
}

@@ -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
@@ -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
@@ -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`},
'/*/*'
]
]
@@ -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];
@@ -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
Loading