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 Gateway Binary Media Types #6063

Merged
merged 4 commits into from
May 8, 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
16 changes: 16 additions & 0 deletions docs/providers/aws/events/apigateway.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ layout: Doc
- [Share Authorizer](#share-authorizer) - [Share Authorizer](#share-authorizer)
- [Resource Policy](#resource-policy) - [Resource Policy](#resource-policy)
- [Compression](#compression) - [Compression](#compression)
- [Binary Media Types](#binary-media-types)
- [Stage specific setups](#stage-specific-setups) - [Stage specific setups](#stage-specific-setups)
- [AWS X-Ray Tracing](#aws-x-ray-tracing) - [AWS X-Ray Tracing](#aws-x-ray-tracing)
- [Tags / Stack Tags](#tags--stack-tags) - [Tags / Stack Tags](#tags--stack-tags)
Expand Down Expand Up @@ -1416,6 +1417,21 @@ provider:
minimumCompressionSize: 1024 minimumCompressionSize: 1024
``` ```


## Binary Media Types

API Gateway makes it possible to return binary media such as images or files as responses.

Configuring API Gateway to return binary media can be done via the `binaryMediaTypes` config:

```yml
provider:
apiGateway:
binaryMediaTypes:
- '*/*'
```

In your Lambda function you need to ensure that the correct `content-type` header is set. Furthermore you might want to return the response body in base64 format.

## Stage specific setups ## Stage specific setups


**IMPORTANT:** Due to CloudFormation limitations it's not possible to enable API Gateway stage settings on existing deployments. Please remove your old API Gateway and re-deploy with your new stage configuration. Once done, subsequent deployments should work without any issues. **IMPORTANT:** Due to CloudFormation limitations it's not possible to enable API Gateway stage settings on existing deployments. Please remove your old API Gateway and re-deploy with your new stage configuration. Once done, subsequent deployments should work without any issues.
Expand Down
4 changes: 3 additions & 1 deletion docs/providers/aws/guide/serverless.yml.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ provider:
'/users/create': xxxxxxxxxx '/users/create': xxxxxxxxxx
apiKeySourceType: HEADER # Source of API key for usage plan. HEADER or AUTHORIZER. apiKeySourceType: HEADER # Source of API key for usage plan. HEADER or AUTHORIZER.
minimumCompressionSize: 1024 # Compress response when larger than specified size in bytes (must be between 0 and 10485760) minimumCompressionSize: 1024 # Compress response when larger than specified size in bytes (must be between 0 and 10485760)
description: Some Description # optional description for the API Gateway stage deployment description: Some Description # Optional description for the API Gateway stage deployment
logs: true # Optional configuration which specifies if API Gateway logs are used logs: true # Optional configuration which specifies if API Gateway logs are used
binaryMediaTypes: # Optional binary media types the API might return
- '*/*'
usagePlan: # Optional usage plan configuration usagePlan: # Optional usage plan configuration
quota: quota:
limit: 5000 limit: 5000
Expand Down
23 changes: 13 additions & 10 deletions lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ const BbPromise = require('bluebird');


module.exports = { module.exports = {
compileRestApi() { compileRestApi() {
if (this.serverless.service.provider.apiGateway && const apiGateway = this.serverless.service.provider.apiGateway || {};
this.serverless.service.provider.apiGateway.restApiId) {
// immediately return if we're using an external REST API id
if (apiGateway.restApiId) {
return BbPromise.resolve(); return BbPromise.resolve();
} }


this.apiGatewayRestApiLogicalId = this.provider.naming.getRestApiLogicalId(); this.apiGatewayRestApiLogicalId = this.provider.naming.getRestApiLogicalId();


let endpointType = 'EDGE'; let endpointType = 'EDGE';
let BinaryMediaTypes;
if (apiGateway.binaryMediaTypes) {
BinaryMediaTypes = apiGateway.binaryMediaTypes;
}


if (this.serverless.service.provider.endpointType) { if (this.serverless.service.provider.endpointType) {
const validEndpointTypes = ['REGIONAL', 'EDGE', 'PRIVATE']; const validEndpointTypes = ['REGIONAL', 'EDGE', 'PRIVATE'];
Expand All @@ -36,6 +42,7 @@ module.exports = {
Type: 'AWS::ApiGateway::RestApi', Type: 'AWS::ApiGateway::RestApi',
Properties: { Properties: {
Name: this.provider.naming.getApiGatewayName(), Name: this.provider.naming.getApiGatewayName(),
BinaryMediaTypes,
EndpointConfiguration: { EndpointConfiguration: {
Types: [endpointType], Types: [endpointType],
}, },
Expand All @@ -54,10 +61,8 @@ module.exports = {
}); });
} }


if (!_.isEmpty(this.serverless.service.provider.apiGateway) && if (!_.isEmpty(apiGateway.apiKeySourceType)) {
!_.isEmpty(this.serverless.service.provider.apiGateway.apiKeySourceType)) { const apiKeySourceType = apiGateway.apiKeySourceType.toUpperCase();
const apiKeySourceType =
this.serverless.service.provider.apiGateway.apiKeySourceType.toUpperCase();
const validApiKeySourceType = ['HEADER', 'AUTHORIZER']; const validApiKeySourceType = ['HEADER', 'AUTHORIZER'];


if (!_.includes(validApiKeySourceType, apiKeySourceType)) { if (!_.includes(validApiKeySourceType, apiKeySourceType)) {
Expand All @@ -74,10 +79,8 @@ module.exports = {
); );
} }


if (!_.isEmpty(this.serverless.service.provider.apiGateway) && if (!_.isUndefined(apiGateway.minimumCompressionSize)) {
!_.isUndefined(this.serverless.service.provider.apiGateway.minimumCompressionSize)) { const minimumCompressionSize = apiGateway.minimumCompressionSize;
const minimumCompressionSize =
this.serverless.service.provider.apiGateway.minimumCompressionSize;


if (!_.isInteger(minimumCompressionSize)) { if (!_.isInteger(minimumCompressionSize)) {
const message = const message =
Expand Down
121 changes: 69 additions & 52 deletions lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -9,50 +9,6 @@ describe('#compileRestApi()', () => {
let serverless; let serverless;
let awsCompileApigEvents; let awsCompileApigEvents;


const serviceResourcesAwsResourcesObjectMock = {
Resources: {
ApiGatewayRestApi: {
Type: 'AWS::ApiGateway::RestApi',
Properties: {
Name: 'dev-new-service',
EndpointConfiguration: {
Types: ['EDGE'],
},
},
},
},
};

const serviceResourcesAwsResourcesObjectWithResourcePolicyMock = {
Resources: {
ApiGatewayRestApi: {
Type: 'AWS::ApiGateway::RestApi',
Properties: {
Name: 'dev-new-service',
EndpointConfiguration: {
Types: ['EDGE'],
},
Policy: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: '*',
Action: 'execute-api:Invoke',
Resource: ['execute-api:/*/*/*'],
Condition: {
IpAddress: {
'aws:SourceIp': ['123.123.123.123'],
},
},
},
],
},
},
},
},
};

beforeEach(() => { beforeEach(() => {
const options = { const options = {
stage: 'dev', stage: 'dev',
Expand All @@ -79,10 +35,19 @@ describe('#compileRestApi()', () => {


it('should create a REST API resource', () => it('should create a REST API resource', () =>
awsCompileApigEvents.compileRestApi().then(() => { awsCompileApigEvents.compileRestApi().then(() => {
expect(awsCompileApigEvents.serverless.service const resources = awsCompileApigEvents.serverless.service.provider
.provider.compiledCloudFormationTemplate.Resources).to.deep.equal( .compiledCloudFormationTemplate.Resources;
serviceResourcesAwsResourcesObjectMock.Resources
); expect(resources.ApiGatewayRestApi).to.deep.equal({
Type: 'AWS::ApiGateway::RestApi',
Properties: {
BinaryMediaTypes: undefined,
Name: 'dev-new-service',
EndpointConfiguration: {
Types: ['EDGE'],
},
},
});
})); }));


it('should create a REST API resource with resource policy', () => { it('should create a REST API resource with resource policy', () => {
Expand All @@ -100,10 +65,35 @@ describe('#compileRestApi()', () => {
}, },
]; ];
return awsCompileApigEvents.compileRestApi().then(() => { return awsCompileApigEvents.compileRestApi().then(() => {
expect(awsCompileApigEvents.serverless.service.provider const resources = awsCompileApigEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources).to.deep.equal( .compiledCloudFormationTemplate.Resources;
serviceResourcesAwsResourcesObjectWithResourcePolicyMock.Resources
); expect(resources.ApiGatewayRestApi).to.deep.equal({
Type: 'AWS::ApiGateway::RestApi',
Properties: {
Name: 'dev-new-service',
BinaryMediaTypes: undefined,
EndpointConfiguration: {
Types: ['EDGE'],
},
Policy: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: '*',
Action: 'execute-api:Invoke',
Resource: ['execute-api:/*/*/*'],
Condition: {
IpAddress: {
'aws:SourceIp': ['123.123.123.123'],
},
},
},
],
},
},
});
}); });
}); });


Expand All @@ -120,6 +110,33 @@ describe('#compileRestApi()', () => {
}); });
}); });


it('should set binary media types if defined at the apiGateway provider config level', () => {
awsCompileApigEvents.serverless.service.provider.apiGateway = {
binaryMediaTypes: [
'*/*',
],
};
return awsCompileApigEvents.compileRestApi().then(() => {
const resources = awsCompileApigEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources;

expect(resources.ApiGatewayRestApi).to.deep.equal({
Type: 'AWS::ApiGateway::RestApi',
Properties: {
BinaryMediaTypes: [
'*/*',
],
EndpointConfiguration: {
Types: [
'EDGE',
],
},
Name: 'dev-new-service',
},
});
});
});

it('throw error if endpointType property is not a string', () => { it('throw error if endpointType property is not a string', () => {
awsCompileApigEvents.serverless.service.provider.endpointType = ['EDGE']; awsCompileApigEvents.serverless.service.provider.endpointType = ['EDGE'];
expect(() => awsCompileApigEvents.compileRestApi()).to.throw(Error); expect(() => awsCompileApigEvents.compileRestApi()).to.throw(Error);
Expand Down