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 multiple usage plans #5970

Merged
merged 1 commit into from
Apr 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/providers/aws/events/apigateway.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -543,6 +543,45 @@ Please note that those are the API keys names, not the actual values. Once you d


Clients connecting to this Rest API will then need to set any of these API keys values in the `x-api-key` header of their request. This is only necessary for functions where the `private` property is set to true. Clients connecting to this Rest API will then need to set any of these API keys values in the `x-api-key` header of their request. This is only necessary for functions where the `private` property is set to true.


You can also setup multiple usage plans for your API. In this case you need to map your usage plans to your api keys. Here's an example how this might look like:

```yml
service: my-service
provider:
name: aws
apiKeys:
- free:
- myFreeKey
- ${opt:stage}-myFreeKey
- paid:
- myPaidKey
- ${opt:stage}-myPaidKey
usagePlan:
- free:
quota:
limit: 5000
offset: 2
period: MONTH
throttle:
burstLimit: 200
rateLimit: 100
- paid:
quota:
limit: 50000
offset: 1
period: MONTH
throttle:
burstLimit: 2000
rateLimit: 1000
functions:
hello:
events:
- http:
path: user/create
method: get
private: true
```

### Configuring endpoint types ### Configuring endpoint types


API Gateway [supports regional endpoints](https://aws.amazon.com/about-aws/whats-new/2017/11/amazon-api-gateway-supports-regional-api-endpoints/) for associating your API Gateway REST APIs with a particular region. This can reduce latency if your requests originate from the same region as your REST API and can be helpful in building multi-region applications. API Gateway [supports regional endpoints](https://aws.amazon.com/about-aws/whats-new/2017/11/amazon-api-gateway-supports-regional-api-endpoints/) for associating your API Gateway REST APIs with a particular region. This can reduce latency if your requests originate from the same region as your REST API and can be helpful in building multi-region applications.
Expand Down
6 changes: 3 additions & 3 deletions docs/providers/aws/guide/resources.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ If you are unsure how a resource is named, that you want to reference from your
|ApiGateway::Method | ApiGatewayMethod{normalizedPath}{normalizedMethod} | ApiGatewayMethodUsersGet | |ApiGateway::Method | ApiGatewayMethod{normalizedPath}{normalizedMethod} | ApiGatewayMethodUsersGet |
|ApiGateway::Authorizer | {normalizedFunctionName}ApiGatewayAuthorizer | HelloApiGatewayAuthorizer | |ApiGateway::Authorizer | {normalizedFunctionName}ApiGatewayAuthorizer | HelloApiGatewayAuthorizer |
|ApiGateway::Deployment | ApiGatewayDeployment{instanceId} | ApiGatewayDeployment12356789 | |ApiGateway::Deployment | ApiGatewayDeployment{instanceId} | ApiGatewayDeployment12356789 |
|ApiGateway::ApiKey | ApiGatewayApiKey{SequentialID} | ApiGatewayApiKey1 | |ApiGateway::ApiKey | ApiGatewayApiKey{OptionalNormalizedName}{SequentialID} | ApiGatewayApiKeyFree1 |
|ApiGateway::UsagePlan | ApiGatewayUsagePlan | ApiGatewayUsagePlan | |ApiGateway::UsagePlan | ApiGatewayUsagePlan{OptionalNormalizedName} | ApiGatewayUsagePlanFree |
|ApiGateway::UsagePlanKey | ApiGatewayUsagePlanKey{SequentialID} | ApiGatewayUsagePlanKey1 | |ApiGateway::UsagePlanKey | ApiGatewayUsagePlanKey{OptionalNormalizedName}{SequentialID} | ApiGatewayUsagePlanKeyFree1 |
|ApiGateway::Stage | ApiGatewayStage | ApiGatewayStage | |ApiGateway::Stage | ApiGatewayStage | ApiGatewayStage |
|SNS::Topic | SNSTopic{normalizedTopicName} | SNSTopicSometopic | |SNS::Topic | SNSTopic{normalizedTopicName} | SNSTopicSometopic |
|SNS::Subscription | {normalizedFunctionName}SnsSubscription{normalizedTopicName} | HelloSnsSubscriptionSomeTopic | |SNS::Subscription | {normalizedFunctionName}SnsSubscription{normalizedTopicName} | HelloSnsSubscriptionSomeTopic |
Expand Down
15 changes: 14 additions & 1 deletion lib/plugins/aws/info/getApiKeyValues.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -9,7 +9,20 @@ module.exports = {
info.apiKeys = []; info.apiKeys = [];


// check if the user has set api keys // check if the user has set api keys
const apiKeyNames = this.serverless.service.provider.apiKeys || []; const apiKeyDefinitions = this.serverless.service.provider.apiKeys;
const apiKeyNames = [];
if (_.isArray(apiKeyDefinitions) && apiKeyDefinitions.length) {
_.forEach(apiKeyDefinitions, (definition) => {
// different API key types are nested in separate arrays
if (_.isObject(definition)) {
const keyTypeName = Object.keys(definition)[0];
_.forEach(definition[keyTypeName], (keyName) => apiKeyNames.push(keyName));
} else if (_.isString(definition)) {
// plain strings are simple, non-nested API keys
apiKeyNames.push(definition);
}
});
}


if (apiKeyNames.length) { if (apiKeyNames.length) {
return this.provider.request('APIGateway', return this.provider.request('APIGateway',
Expand Down
74 changes: 73 additions & 1 deletion lib/plugins/aws/info/getApiKeyValues.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('#getApiKeyValues()', () => {
awsInfo.provider.request.restore(); awsInfo.provider.request.restore();
}); });


it('should add API Key values to this.gatheredData if API key names are available', () => { it('should add API Key values to this.gatheredData if simple API key names are available', () => {
// set the API Keys for the service // set the API Keys for the service
awsInfo.serverless.service.provider.apiKeys = ['foo', 'bar']; awsInfo.serverless.service.provider.apiKeys = ['foo', 'bar'];


Expand Down Expand Up @@ -78,6 +78,78 @@ describe('#getApiKeyValues()', () => {
}); });
}); });


it('should add API Key values to this.gatheredData if typed API key names are available', () => {
// set the API Keys for the service
awsInfo.serverless.service.provider.apiKeys = [
{ free: ['foo', 'bar'] },
{ paid: ['baz', 'qux'] },
];

awsInfo.gatheredData = {
info: {},
};

const apiKeyItems = {
items: [
{
id: '4711',
name: 'SomeRandomIdInUsersAccount',
value: 'ShouldNotBeConsidered',
},
{
id: '1234',
name: 'foo',
value: 'valueForKeyFoo',
},
{
id: '5678',
name: 'bar',
value: 'valueForKeyBar',
},
{
id: '9101112',
name: 'baz',
value: 'valueForKeyBaz',
},
{
id: '13141516',
name: 'qux',
value: 'valueForKeyQux',
},
],
};

requestStub.resolves(apiKeyItems);

const expectedGatheredDataObj = {
info: {
apiKeys: [
{
name: 'foo',
value: 'valueForKeyFoo',
},
{
name: 'bar',
value: 'valueForKeyBar',
},
{
name: 'baz',
value: 'valueForKeyBaz',
},
{
name: 'qux',
value: 'valueForKeyQux',
},
],
},
};

return awsInfo.getApiKeyValues().then(() => {
expect(requestStub.calledOnce).to.equal(true);
expect(awsInfo.gatheredData).to.deep.equal(expectedGatheredDataObj);
});
});

it('should resolve if AWS does not return API key values', () => { it('should resolve if AWS does not return API key values', () => {
// set the API Keys for the service // set the API Keys for the service
awsInfo.serverless.service.provider.apiKeys = ['foo', 'bar']; awsInfo.serverless.service.provider.apiKeys = ['foo', 'bar'];
Expand Down
15 changes: 12 additions & 3 deletions lib/plugins/aws/lib/naming.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -242,16 +242,25 @@ module.exports = {
getMethodLogicalId(resourceId, methodName) { getMethodLogicalId(resourceId, methodName) {
return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}`; return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}`;
}, },
getApiKeyLogicalId(apiKeyNumber) { getApiKeyLogicalId(apiKeyNumber, apiKeyName) {
if (apiKeyName) {
return `ApiGatewayApiKey${this.normalizeName(apiKeyName)}${apiKeyNumber}`;
}
return `ApiGatewayApiKey${apiKeyNumber}`; return `ApiGatewayApiKey${apiKeyNumber}`;
}, },
getApiKeyLogicalIdRegex() { getApiKeyLogicalIdRegex() {
return /^ApiGatewayApiKey/; return /^ApiGatewayApiKey/;
}, },
getUsagePlanLogicalId() { getUsagePlanLogicalId(name) {
if (name) {
return `ApiGatewayUsagePlan${this.normalizeName(name)}`;
}
return 'ApiGatewayUsagePlan'; return 'ApiGatewayUsagePlan';
}, },
getUsagePlanKeyLogicalId(usagePlanKeyNumber) { getUsagePlanKeyLogicalId(usagePlanKeyNumber, usagePlanKeyName) {
if (usagePlanKeyName) {
return `ApiGatewayUsagePlanKey${this.normalizeName(usagePlanKeyName)}${usagePlanKeyNumber}`;
}
return `ApiGatewayUsagePlanKey${usagePlanKeyNumber}`; return `ApiGatewayUsagePlanKey${usagePlanKeyNumber}`;
}, },
getStageLogicalId() { getStageLogicalId() {
Expand Down
18 changes: 16 additions & 2 deletions lib/plugins/aws/lib/naming.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -394,6 +394,10 @@ describe('#naming()', () => {
it('should produce the given index with ApiGatewayApiKey as a prefix', () => { it('should produce the given index with ApiGatewayApiKey as a prefix', () => {
expect(sdk.naming.getApiKeyLogicalId(1)).to.equal('ApiGatewayApiKey1'); expect(sdk.naming.getApiKeyLogicalId(1)).to.equal('ApiGatewayApiKey1');
}); });

it('should support API Key names', () => {
expect(sdk.naming.getApiKeyLogicalId(1, 'free')).to.equal('ApiGatewayApiKeyFree1');
});
}); });


describe('#getApiKeyLogicalIdRegex()', () => { describe('#getApiKeyLogicalIdRegex()', () => {
Expand All @@ -414,16 +418,26 @@ describe('#naming()', () => {
}); });


describe('#getUsagePlanLogicalId()', () => { describe('#getUsagePlanLogicalId()', () => {
it('should return ApiGateway usage plan logical id', () => { it('should return the default ApiGateway usage plan logical id', () => {
expect(sdk.naming.getUsagePlanLogicalId()) expect(sdk.naming.getUsagePlanLogicalId())
.to.equal('ApiGatewayUsagePlan'); .to.equal('ApiGatewayUsagePlan');
}); });

it('should return the named ApiGateway usage plan logical id', () => {
expect(sdk.naming.getUsagePlanLogicalId('free'))
.to.equal('ApiGatewayUsagePlanFree');
});
}); });


describe('#getUsagePlanKeyLogicalId(keyIndex)', () => { describe('#getUsagePlanKeyLogicalId()', () => {
it('should produce the given index with ApiGatewayUsagePlanKey as a prefix', () => { it('should produce the given index with ApiGatewayUsagePlanKey as a prefix', () => {
expect(sdk.naming.getUsagePlanKeyLogicalId(1)).to.equal('ApiGatewayUsagePlanKey1'); expect(sdk.naming.getUsagePlanKeyLogicalId(1)).to.equal('ApiGatewayUsagePlanKey1');
}); });

it('should support API Key names', () => {
expect(sdk.naming.getUsagePlanKeyLogicalId(1, 'free'))
.to.equal('ApiGatewayUsagePlanKeyFree1');
});
}); });


describe('#getStageLogicalId()', () => { describe('#getStageLogicalId()', () => {
Expand Down
66 changes: 44 additions & 22 deletions lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -3,37 +3,59 @@
const _ = require('lodash'); const _ = require('lodash');
const BbPromise = require('bluebird'); const BbPromise = require('bluebird');


function createApiKeyResource(that, apiKey) {
const resourceTemplate = {
Type: 'AWS::ApiGateway::ApiKey',
Properties: {
Enabled: true,
Name: apiKey,
StageKeys: [{
RestApiId: that.provider.getApiGatewayRestApiId(),
StageName: that.provider.getStage(),
}],
},
DependsOn: that.apiGatewayDeploymentLogicalId,
};

return _.cloneDeep(resourceTemplate);
}

module.exports = { module.exports = {
compileApiKeys() { compileApiKeys() {
if (this.serverless.service.provider.apiKeys) { if (this.serverless.service.provider.apiKeys) {
if (!Array.isArray(this.serverless.service.provider.apiKeys)) { if (!Array.isArray(this.serverless.service.provider.apiKeys)) {
throw new this.serverless.classes.Error('apiKeys property must be an array'); throw new this.serverless.classes.Error('apiKeys property must be an array');
} }


_.forEach(this.serverless.service.provider.apiKeys, (apiKey, i) => { const resources = this.serverless.service.provider.compiledCloudFormationTemplate.Resources;
const apiKeyNumber = i + 1; let keyNumber = 0;


if (typeof apiKey !== 'string') { _.forEach(this.serverless.service.provider.apiKeys, (apiKeyDefinition) => {
throw new this.serverless.classes.Error('API Keys must be strings'); // if multiple API key types are used
if (_.isObject(apiKeyDefinition)) {
keyNumber = 0;
const name = Object.keys(apiKeyDefinition)[0];
_.forEach(apiKeyDefinition[name], (key) => {
if (!_.isString(key)) {
throw new this.serverless.classes.Error('API keys must be strings');
}
keyNumber += 1;
const apiKeyLogicalId = this.provider.naming
.getApiKeyLogicalId(keyNumber, name);
const resourceTemplate = createApiKeyResource(this, key);
_.merge(resources, {
[apiKeyLogicalId]: resourceTemplate,
});
});
} else {
keyNumber += 1;
const apiKeyLogicalId = this.provider.naming
.getApiKeyLogicalId(keyNumber);
const resourceTemplate = createApiKeyResource(this, apiKeyDefinition);
_.merge(resources, {
[apiKeyLogicalId]: resourceTemplate,
});
} }

const apiKeyLogicalId = this.provider.naming
.getApiKeyLogicalId(apiKeyNumber);

_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[apiKeyLogicalId]: {
Type: 'AWS::ApiGateway::ApiKey',
Properties: {
Enabled: true,
Name: apiKey,
StageKeys: [{
RestApiId: this.provider.getApiGatewayRestApiId(),
StageName: this.provider.getStage(),
}],
},
DependsOn: this.apiGatewayDeploymentLogicalId,
},
});
}); });
} }
return BbPromise.resolve(); return BbPromise.resolve();
Expand Down