Merge branch 'master' into feature/BET-1118

This commit is contained in:
Kyle Minor 2018-01-09 15:11:18 -05:00
commit eba004c2e6
18 changed files with 829 additions and 89 deletions

View File

@ -25,7 +25,8 @@ serverless deploy
- `--verbose` or `-v` Shows all stack events during deployment, and display any Stack Output.
- `--function` or `-f` Invoke `deploy function` (see above). Convenience shortcut - cannot be used with `--package`.
- `--conceal` Hides secrets from the output (e.g. API Gateway key values).
- `--aws-s3-accelerate` Enables S3 Transfer Acceleration making uploading artifacts much faster. You can read more about it [here](http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html). **Note: When using Transfer Acceleration, additional data transfer charges may apply**
- `--aws-s3-accelerate` Enables S3 Transfer Acceleration making uploading artifacts much faster. You can read more about it [here](http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html). It requires additional `s3:PutAccelerateConfiguration` permissions. **Note: When using Transfer Acceleration, additional data transfer charges may apply.**
- `--no-aws-s3-accelerate` Explicitly disables S3 Transfer Acceleration). It also requires additional `s3:PutAccelerateConfiguration` permissions.
## Artifacts

View File

@ -633,7 +633,39 @@ See the [api gateway documentation](https://docs.aws.amazon.com/apigateway/lates
**Notes:**
- A missing/empty request Content-Type is considered to be the API Gateway default (`application/json`)
- API Gateway docs refer to "WHEN_NO_TEMPLATE" (singular), but this will fail during creation as the actual value should be "WHEN_NO_TEMPLATES" (plural)
- [[Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/apigateway)](#read-this-on-the-main-serverless-docs-sitehttpswwwserverlesscomframeworkdocsprovidersawseventsapigateway)
- [[Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/apigateway)](#read-this-on-the-main-serverless-docs-sitehttpswwwserverlesscomframeworkdocsprovidersawseventsapigateway)
- [[Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/apigateway)](#read-this-on-the-main-serverless-docs-sitehttpswwwserverlesscomframeworkdocsprovidersawseventsapigateway)
- [[Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/apigateway)](#read-this-on-the-main-serverless-docs-sitehttpswwwserverlesscomframeworkdocsprovidersawseventsapigateway)
- [[Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/apigateway)](#read-this-on-the-main-serverless-docs-sitehttpswwwserverlesscomframeworkdocsprovidersawseventsapigateway)
- [[Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/apigateway)](#read-this-on-the-main-serverless-docs-sitehttpswwwserverlesscomframeworkdocsprovidersawseventsapigateway)
- [[Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/apigateway)](#read-this-on-the-main-serverless-docs-sitehttpswwwserverlesscomframeworkdocsprovidersawseventsapigateway)
- [API Gateway](#api-gateway)
- [Lambda Proxy Integration](#lambda-proxy-integration)
- [Simple HTTP Endpoint](#simple-http-endpoint)
- [Example "LAMBDA-PROXY" event (default)](#example-lambda-proxy-event-default)
- [HTTP Endpoint with Extended Options](#http-endpoint-with-extended-options)
- [Enabling CORS](#enabling-cors)
- [HTTP Endpoints with `AWS_IAM` Authorizers](#http-endpoints-with-awsiam-authorizers)
- [HTTP Endpoints with Custom Authorizers](#http-endpoints-with-custom-authorizers)
- [Catching Exceptions In Your Lambda Function](#catching-exceptions-in-your-lambda-function)
- [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api)
- [Request Parameters](#request-parameters)
- [Lambda Integration](#lambda-integration)
- [Example "LAMBDA" event (before customization)](#example-lambda-event-before-customization)
- [Request templates](#request-templates)
- [Default Request Templates](#default-request-templates)
- [Custom Request Templates](#custom-request-templates)
- [Pass Through Behavior](#pass-through-behavior)
- [Responses](#responses)
- [Custom Response Headers](#custom-response-headers)
- [Custom Response Templates](#custom-response-templates)
- [Status codes](#status-codes)
- [Available Status Codes](#available-status-codes)
- [Using Status Codes](#using-status-codes)
- [Custom Status Codes](#custom-status-codes)
- [Setting an HTTP Proxy on API Gateway](#setting-an-http-proxy-on-api-gateway)
- [Share API Gateway and API Resources](#share-api-gateway-and-api-resources)
### Responses
@ -828,3 +860,116 @@ endpoint of your proxy, and the URI you want to set a proxy to.
Now that you have these two CloudFormation templates defined in your `serverless.yml` file, you can simply run
`serverless deploy` and that will deploy these custom resources for you along with your service and set up a proxy on your Rest API.
## Share API Gateway and API Resources
As you application grows, you will have idea to break it out into multiple services. However, each serverless project generates new API Gateway by default. If you want to share same API Gateway for muliple projects, you 'll need to reference REST API ID and Root Resource ID into serverless.yml files
```yml
service: service-name
provider:
name: aws
apiGateway:
restApiId: xxxxxxxxxx # REST API resource ID. Default is generated by the framework
restApiRootResourceId: xxxxxxxxxx # Root resource, represent as / path
functions:
...
```
In case the application has many chilren and grandchildren paths, you also want to break them out into smaller services.
```yml
service: service-a
provider:
apiGateway:
restApiId: xxxxxxxxxx
restApiRootResourceId: xxxxxxxxxx
functions:
create:
handler: posts.create
events:
- http:
method: post
path: /posts
```
```yml
service: service-b
provider:
apiGateway:
restApiId: xxxxxxxxxx
restApiRootResourceId: xxxxxxxxxx
functions:
create:
handler: posts.createComment
events:
- http:
method: post
path: /posts/{id}/comments
```
They reference the same parent path `/posts`. Cloudformation will throw error if we try to generate existed one. To avoid that, we must reference source ID of `/posts`.
```yml
service: service-a
provider:
apiGateway:
restApiId: xxxxxxxxxx
restApiRootResourceId: xxxxxxxxxx
restApiResources:
/posts: xxxxxxxxxx
functions:
...
```
```yml
service: service-b
provider:
apiGateway:
restApiId: xxxxxxxxxx
restApiRootResourceId: xxxxxxxxxx
restApiResources:
/posts: xxxxxxxxxx
functions:
...
```
You can define more than one path resource. Otherwise, serverless will generate paths from root resource. `restApiRootResourceId` can be optional if there isn't path that need to be generated from the root
```yml
service: service-a
provider:
apiGateway:
restApiId: xxxxxxxxxx
# restApiRootResourceId: xxxxxxxxxx # Optional
restApiResources:
/posts: xxxxxxxxxx
/categories: xxxxxxxxx
functions:
listPosts:
handler: posts.list
events:
- http:
method: get
path: /posts
listCategories:
handler: categories.list
events:
- http:
method: get
path: /categories
```
For best practice and CI, CD friendly, we should define Cloudformation resources from early service, then use Cross-Stack References for another ones.

View File

@ -45,6 +45,13 @@ provider:
- myFirstKey
- ${opt:stage}-myFirstKey
- ${env:MY_API_KEY} # you can hide it in a serverless variable
apiGateway: # Optional API Gateway global config
restApiId: xxxxxxxxxx # REST API resource ID. Default is generated by the framework
restApiRootResourceId: xxxxxxxxxx # Root resource ID, represent as / path
restApiResources: # List of existing resources that were created in the REST API. This is required or the stack will be conflicted
'/users': xxxxxxxxxx
'/users/create': xxxxxxxxxx
usagePlan: # Optional usage plan configuration
quota:
limit: 5000
@ -95,6 +102,7 @@ package: # Optional deployment packaging configuration
- .travis.yml
excludeDevDependencies: false # Config if Serverless should automatically exclude dev dependencies in the deployment package. Defaults to true
functions:
usersCreate: # A Function
handler: users.create # The file and module for this specific function.

View File

@ -27,7 +27,7 @@ module.exports = {
Enabled: true,
Name: apiKey,
StageKeys: [{
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
RestApiId: this.provider.getApiGatewayRestApiId(),
StageName: this.provider.getStage(),
}],
},

View File

@ -12,7 +12,7 @@ module.exports = {
AuthorizerResultTtlInSeconds: authorizer.resultTtlInSeconds,
IdentitySource: authorizer.identitySource,
Name: authorizer.name,
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
RestApiId: this.provider.getApiGatewayRestApiId(),
};
if (typeof authorizer.identityValidationExpression === 'string') {

View File

@ -52,7 +52,7 @@ module.exports = {
IntegrationResponses: this.generateCorsIntegrationResponses(preflightHeaders),
},
ResourceId: resourceRef,
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
RestApiId: this.provider.getApiGatewayRestApiId(),
},
},
});

View File

@ -34,19 +34,28 @@ describe('#compileCors()', () => {
};
awsCompileApigEvents = new AwsCompileApigEvents(serverless, options);
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
awsCompileApigEvents.apiGatewayResourceLogicalIds = {
'users/create': 'ApiGatewayResourceUsersCreate',
'users/list': 'ApiGatewayResourceUsersList',
'users/update': 'ApiGatewayResourceUsersUpdate',
'users/delete': 'ApiGatewayResourceUsersDelete',
'users/any': 'ApiGatewayResourceUsersAny',
};
awsCompileApigEvents.apiGatewayResourceNames = {
'users/create': 'UsersCreate',
'users/list': 'UsersList',
'users/update': 'UsersUpdate',
'users/delete': 'UsersDelete',
'users/any': 'UsersAny',
awsCompileApigEvents.apiGatewayResources = {
'users/create': {
name: 'UsersCreate',
resourceLogicalId: 'ApiGatewayResourceUsersCreate',
},
'users/list': {
name: 'UsersList',
resourceLogicalId: 'ApiGatewayResourceUsersList',
},
'users/update': {
name: 'UsersUpdate',
resourceLogicalId: 'ApiGatewayResourceUsersUpdate',
},
'users/delete': {
name: 'UsersDelete',
resourceLogicalId: 'ApiGatewayResourceUsersDelete',
},
'users/any': {
name: 'UsersAny',
resourceLogicalId: 'ApiGatewayResourceUsersAny',
},
};
awsCompileApigEvents.validated = {};
});

View File

@ -12,7 +12,7 @@ module.exports = {
[this.apiGatewayDeploymentLogicalId]: {
Type: 'AWS::ApiGateway::Deployment',
Properties: {
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
RestApiId: this.provider.getApiGatewayRestApiId(),
StageName: this.provider.getStage(),
},
DependsOn: this.apiGatewayMethodLogicalIds,
@ -27,7 +27,7 @@ module.exports = {
'Fn::Join': ['',
[
'https://',
{ Ref: this.apiGatewayRestApiLogicalId },
this.provider.getApiGatewayRestApiId(),
`.execute-api.${
this.provider.getRegion()
}.amazonaws.com/${

View File

@ -20,7 +20,7 @@ module.exports = {
HttpMethod: event.http.method.toUpperCase(),
RequestParameters: requestParameters,
ResourceId: resourceId,
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
RestApiId: this.provider.getApiGatewayRestApiId(),
},
};

View File

@ -35,17 +35,24 @@ describe('#compileMethods()', () => {
awsCompileApigEvents = new AwsCompileApigEvents(serverless, options);
awsCompileApigEvents.validated = {};
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
awsCompileApigEvents.apiGatewayResourceLogicalIds = {
'users/create': 'ApiGatewayResourceUsersCreate',
'users/list': 'ApiGatewayResourceUsersList',
'users/update': 'ApiGatewayResourceUsersUpdate',
'users/delete': 'ApiGatewayResourceUsersDelete',
};
awsCompileApigEvents.apiGatewayResourceNames = {
'users/create': 'UsersCreate',
'users/list': 'UsersList',
'users/update': 'UsersUpdate',
'users/delete': 'UsersDelete',
awsCompileApigEvents.apiGatewayResources = {
'users/create': {
name: 'UsersCreate',
resourceLogicalId: 'ApiGatewayResourceUsersCreate',
},
'users/list': {
name: 'UsersList',
resourceLogicalId: 'ApiGatewayResourceUsersList',
},
'users/update': {
name: 'UsersUpdate',
resourceLogicalId: 'ApiGatewayResourceUsersUpdate',
},
'users/delete': {
name: 'UsersDelete',
resourceLogicalId: 'ApiGatewayResourceUsersDelete',
},
};
});

View File

@ -26,7 +26,7 @@ module.exports = {
':',
{ Ref: 'AWS::AccountId' },
':',
{ Ref: this.apiGatewayRestApiLogicalId },
this.provider.getApiGatewayRestApiId(),
'/*/*',
],
] },

View File

@ -6,67 +6,248 @@ const _ = require('lodash');
module.exports = {
compileResources() {
const resourcePaths = this.getResourcePaths();
this.apiGatewayResourceNames = {};
this.apiGatewayResourceLogicalIds = {};
this.apiGatewayResources = this.getResourcePaths();
// ['users', 'users/create', 'users/create/something']
resourcePaths.forEach(path => {
const pathArray = path.split('/');
const resourceName = this.provider.naming.normalizePath(path);
const resourceLogicalId = this.provider.naming.getResourceLogicalId(path);
const pathPart = pathArray.pop();
const parentPath = pathArray.join('/');
const parentRef = this.getResourceId(parentPath);
_.keys(this.apiGatewayResources).forEach((path) => {
const resource = this.apiGatewayResources[path];
if (resource.resourceId) {
return;
}
this.apiGatewayResourceNames[path] = resourceName;
this.apiGatewayResourceLogicalIds[path] = resourceLogicalId;
resource.resourceLogicalId = this.provider.naming.getResourceLogicalId(path);
resource.resourceId = { Ref: resource.resourceLogicalId };
const parentRef = resource.parent
? resource.parent.resourceId : this.getResourceId();
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[resourceLogicalId]: {
[resource.resourceLogicalId]: {
Type: 'AWS::ApiGateway::Resource',
Properties: {
ParentId: parentRef,
PathPart: pathPart,
RestApiId: { Ref: this.apiGatewayRestApiLogicalId },
PathPart: resource.pathPart,
RestApiId: this.provider.getApiGatewayRestApiId(),
},
},
});
});
return BbPromise.resolve();
},
getResourcePaths() {
const paths = _.reduce(this.validated.events, (resourcePaths, event) => {
let path = event.http.path;
combineResourceTrees(trees) {
const self = this;
while (path !== '') {
if (resourcePaths.indexOf(path) === -1) {
resourcePaths.push(path);
function getNodePaths(result, node) {
const r = result;
r[node.path] = node;
if (!node.name) {
r[node.path].name = self.provider.naming.normalizePath(node.path);
}
node.children.forEach((child) => getNodePaths(result, child));
}
return _.reduce(trees, (result, tree) => {
getNodePaths(result, tree);
return result;
}, {});
},
getResourcePaths() {
const trees = [];
const predefinedResourceNodes = [];
const methodNodes = [];
const predefinedResources = this.provider.getApiGatewayPredefinedResources();
function cutBranch(node) {
if (!node.parent) {
return;
}
const n = node;
if (node.parent.children.length <= 1) {
n.parent.children = [];
} else {
n.parent.children = node.parent.children.filter((c) => c.path !== n.path);
n.parent.isCut = true;
}
n.parent = null;
}
// organize all resource paths into N-ary tree
function applyResource(resource, isMethod) {
let root;
let parent;
let currentPath;
const path = resource.path.replace(/^\//, '').replace(/\/$/, '');
const pathParts = path.split('/');
function applyNodeResource(node, parts, index) {
const n = node;
if (index === parts.length - 1) {
n.name = resource.name;
if (resource.resourceId) {
n.resourceId = resource.resourceId;
if (_.every(predefinedResourceNodes, (iter) => iter.path !== n.path)) {
predefinedResourceNodes.push(node);
}
}
if (isMethod && !node.hasMethod) {
n.hasMethod = true;
if (_.every(methodNodes, (iter) => iter.path !== n.path)) {
methodNodes.push(node);
}
}
}
const splittedPath = path.split('/');
splittedPath.pop();
path = splittedPath.join('/');
parent = node;
}
return resourcePaths;
}, []);
// (stable) sort so that parents get processed before children
return _.sortBy(paths, path => path.split('/').length);
pathParts.forEach((pathPart, index) => {
currentPath = currentPath ? `${currentPath}/${pathPart}` : pathPart;
root = root || _.find(trees, (node) => node.path === currentPath);
parent = parent || root;
let node;
if (parent) {
if (parent.path === currentPath) {
applyNodeResource(parent, pathParts, index);
return;
} else if (parent.children.length > 0) {
node = _.find(parent.children, (n) => n.path === currentPath);
if (node) {
applyNodeResource(node, pathParts, index);
return;
}
}
}
node = {
path: currentPath,
pathPart,
parent,
level: index,
children: [],
};
if (parent) {
parent.children.push(node);
}
if (!root) {
root = node;
trees.push(root);
}
applyNodeResource(node, pathParts, index);
});
}
predefinedResources.forEach(applyResource);
this.validated.events.forEach((event) => {
if (event.http.path) {
applyResource(event.http, true);
}
});
// if predefinedResources array is empty, return all paths
if (predefinedResourceNodes.length === 0) {
return this.combineResourceTrees(trees);
}
// if all methods have resource ID already, no need to validate resource trees
if (_.every(this.validated.events, (event) =>
_.some(predefinedResourceNodes, (node) =>
node.path === event.http.path))) {
return _.reduce(predefinedResources, (resourceMap, resource) => {
const r = resourceMap;
r[resource.path] = resource;
if (!resource.name) {
r[resource.path].name = this.provider.naming.normalizePath(resource.path);
}
return r;
}, {});
}
// cut resource branches from trees
const sortedResourceNodes = _.sortBy(predefinedResourceNodes,
node => node.level);
const validatedTrees = [];
for (let i = sortedResourceNodes.length - 1; i >= 0; i--) {
const node = sortedResourceNodes[i];
let parent = node;
while (parent && parent.parent) {
if (parent.parent.hasMethod && !parent.parent.resourceId) {
throw new Error(`Resource ID for path ${parent.parent.path} is required`);
}
if (parent.parent.resourceId || parent.parent.children.length > 1) {
cutBranch(parent);
break;
}
parent = parent.parent;
}
}
// get branches that begin from root resource
methodNodes.forEach((node) => {
let iter = node;
while (iter) {
if (iter.resourceId) {
cutBranch(iter);
if (_.every(validatedTrees, (t) => t.path !== node.path)) {
validatedTrees.push(iter);
}
break;
}
if (iter.isCut || (!iter.parent && iter.level > 0)) {
throw new Error(`Resource ID for path ${iter.path} is required`);
}
if (!iter.parent) {
validatedTrees.push(iter);
break;
}
iter = iter.parent;
}
});
return this.combineResourceTrees(validatedTrees);
},
getResourceId(path) {
if (path === '') {
return { 'Fn::GetAtt': [this.apiGatewayRestApiLogicalId, 'RootResourceId'] };
if (!path) {
return this.provider.getApiGatewayRestApiRootResourceId();
}
return { Ref: this.apiGatewayResourceLogicalIds[path] };
if (!this.apiGatewayResources || !this.apiGatewayResources[path]) {
throw new Error(`Can not find API Gateway resource from path ${path}`);
}
if (!this.apiGatewayResources[path].resourceId
&& this.apiGatewayResources[path].resourceLogicalId) {
this.apiGatewayResources[path].resourceId =
{ Ref: this.apiGatewayResources[path].resourceLogicalId };
}
return this.apiGatewayResources[path].resourceId;
},
getResourceName(path) {
if (path === '') {
if (path === '' || !this.apiGatewayResources) {
return '';
}
return this.apiGatewayResourceNames[path];
return this.apiGatewayResources[path].name;
},
};

View File

@ -70,15 +70,15 @@ describe('#compileResources()', () => {
},
},
];
expect(awsCompileApigEvents.getResourcePaths()).to.deep.equal([
expect(Object.keys(awsCompileApigEvents.getResourcePaths())).to.deep.equal([
'foo',
'bar',
'foo/bar',
'bar',
'bar/-',
'bar/foo',
'bar/{id}',
'bar/{foo_id}',
'bar/{id}/foobar',
'bar/{foo_id}',
'bar/{foo_id}/foobar',
]);
});
@ -160,12 +160,16 @@ describe('#compileResources()', () => {
},
];
return awsCompileApigEvents.compileResources().then(() => {
expect(awsCompileApigEvents.apiGatewayResourceLogicalIds).to.deep.equal({
const expectedResourceLogicalIds = {
baz: 'ApiGatewayResourceBaz',
'baz/foo': 'ApiGatewayResourceBazFoo',
foo: 'ApiGatewayResourceFoo',
'foo/{foo_id}': 'ApiGatewayResourceFooFooidVar',
'foo/{foo_id}/bar': 'ApiGatewayResourceFooFooidVarBar',
};
Object.keys(expectedResourceLogicalIds).forEach((path) => {
expect(awsCompileApigEvents.apiGatewayResources[path].resourceLogicalId)
.equal(expectedResourceLogicalIds[path]);
});
});
});
@ -186,10 +190,14 @@ describe('#compileResources()', () => {
},
];
return awsCompileApigEvents.compileResources().then(() => {
expect(awsCompileApigEvents.apiGatewayResourceLogicalIds).to.deep.equal({
const expectedResourceLogicalIds = {
foo: 'ApiGatewayResourceFoo',
'foo/bar': 'ApiGatewayResourceFooBar',
'foo/{bar}': 'ApiGatewayResourceFooBarVar',
};
Object.keys(expectedResourceLogicalIds).forEach((path) => {
expect(awsCompileApigEvents.apiGatewayResources[path].resourceLogicalId)
.equal(expectedResourceLogicalIds[path]);
});
});
});
@ -242,4 +250,239 @@ describe('#compileResources()', () => {
.Resources).to.deep.equal({});
});
});
it('should create child resources only if there are predefined parent resources', () => {
awsCompileApigEvents.serverless.service.provider.apiGateway = {
restApiId: '6fyzt1pfpk',
restApiRootResourceId: 'z5d4qh4oqi',
restApiResources: {
'/foo': 'axcybf2i39',
'/users': 'zxcvbnmasd',
'/users/friends': 'fcasdoojp1',
'/groups': 'iuoyiusduo',
},
};
awsCompileApigEvents.validated.events = [
{
http: {
path: 'foo/bar',
method: 'GET',
},
},
{
http: {
path: 'foo/bar',
method: 'POST',
},
},
{
http: {
path: 'foo/bar',
method: 'DELETE',
},
},
{
http: {
path: 'bar/-',
method: 'GET',
},
},
{
http: {
path: 'bar/foo',
method: 'GET',
},
},
{
http: {
path: 'bar/{id}/foobar',
method: 'GET',
},
},
{
http: {
path: 'bar/{id}',
method: 'GET',
},
},
{
http: {
path: 'users/friends/comments',
method: 'GET',
},
},
{
http: {
path: 'users/me/posts',
method: 'GET',
},
},
{
http: {
path: 'groups/categories',
method: 'GET',
},
},
];
return awsCompileApigEvents.compileResources().then(() => {
try {
awsCompileApigEvents.getResourceId('users/{userId}');
throw new Error('Expected API Gateway resource not found error, got success');
} catch (e) {
expect(e.message).to.equal('Can not find API Gateway resource from path users/{userId}');
}
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceFoo).to.equal(undefined);
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceBar.Properties.RestApiId)
.to.equal('6fyzt1pfpk');
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceBar.Properties.ParentId)
.to.equal('z5d4qh4oqi');
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceFooBar.Properties.ParentId)
.to.equal('axcybf2i39');
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceBarIdVar.Properties.ParentId.Ref)
.to.equal('ApiGatewayResourceBar');
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceUsersMePosts).not.equal(undefined);
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceUsersFriendsComments.Properties.ParentId)
.to.equal('fcasdoojp1');
});
});
it('should not create any child resources if all resources exists', () => {
awsCompileApigEvents.serverless.service.provider.apiGateway = {
restApiId: '6fyzt1pfpk',
restApiRootResourceId: 'z5d4qh4oqi',
restApiResources: {
foo: 'axcybf2i39',
users: 'zxcvbnmasd',
'users/friends': 'fcasdoojp1',
'users/is/this/a/long/path': 'sadvgpoujk',
},
};
awsCompileApigEvents.validated.events = [
{
http: {
path: 'foo',
method: 'GET',
},
},
{
http: {
path: 'users',
method: 'GET',
},
},
{
http: {
path: 'users/friends',
method: 'GET',
},
},
{
http: {
path: 'users/is/this/a/long/path',
method: 'GET',
},
},
];
return awsCompileApigEvents.compileResources().then(() => {
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceFoo).to.equal(undefined);
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceUsers).to.equal(undefined);
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceUsersFriends).to.equal(undefined);
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayResourceUsersIsThis).to.equal(undefined);
});
});
it('should throw error if parent of existing resources is required', () => {
awsCompileApigEvents.serverless.service.provider.apiGateway = {
restApiId: '6fyzt1pfpk',
restApiRootResourceId: 'z5d4qh4oqi',
restApiResources: {
'users/friends': 'fcasdoojp1',
},
};
awsCompileApigEvents.validated.events = [
{
http: {
path: 'users',
method: 'GET',
},
},
{
http: {
path: 'users/friends/{friendId}',
method: 'GET',
},
},
];
expect(() => awsCompileApigEvents.compileResources())
.to.throw(Error, 'Resource ID for path users is required');
});
it('should named all method paths if all resources are predefined', () => {
awsCompileApigEvents.serverless.service.provider.apiGateway = {
restApiId: '6fyzt1pfpk',
restApiRootResourceId: 'z5d4qh4oqi',
restApiResources: {
'users/friends': 'fcasdoojp1',
'users/friends/{id}': 'fcasdoojp1',
},
};
awsCompileApigEvents.validated.events = [
{
http: {
path: 'users/friends',
method: 'GET',
},
},
{
http: {
path: 'users/friends',
method: 'POST',
},
},
{
http: {
path: 'users/friends',
method: 'DELETE',
},
},
{
http: {
path: 'users/friends/{id}',
method: 'GET',
},
},
{
http: {
path: 'users/friends/{id}',
method: 'POST',
},
},
];
return awsCompileApigEvents.compileResources().then(() => {
expect(Object.keys(awsCompileApigEvents.serverless
.service.provider.compiledCloudFormationTemplate
.Resources).every((k) => ['ApiGatewayMethodundefinedGet',
'ApiGatewayMethodundefinedPost'].indexOf(k) === -1))
.to.equal(true);
});
});
});

View File

@ -5,6 +5,11 @@ const BbPromise = require('bluebird');
module.exports = {
compileRestApi() {
if (this.serverless.service.provider.apiGateway &&
this.serverless.service.provider.apiGateway.restApiId) {
return BbPromise.resolve();
}
this.apiGatewayRestApiLogicalId = this.provider.naming.getRestApiLogicalId();
let endpointType = 'EDGE';

View File

@ -60,6 +60,22 @@ describe('#compileRestApi()', () => {
})
);
it('should ignore REST API resource creation if there is predefined restApi config',
() => {
awsCompileApigEvents.serverless.service.provider.apiGateway = {
restApiId: '6fyzt1pfpk',
restApiRootResourceId: 'z5d4qh4oqi',
};
return awsCompileApigEvents
.compileRestApi().then(() => {
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources
).to.deep.equal({});
});
}
);
it('throw error if endpointType property is not a string', () => {
awsCompileApigEvents.serverless.service.provider.endpointType = ['EDGE'];
expect(() => awsCompileApigEvents.compileRestApi()).to.throw(Error);

View File

@ -25,6 +25,14 @@ module.exports = {
const bucketName = this.serverless.service.provider.deploymentBucket;
const isS3TransferAccelerationEnabled = this.provider.isS3TransferAccelerationEnabled();
const isS3TransferAccelerationDisabled = this.provider.isS3TransferAccelerationDisabled();
if (isS3TransferAccelerationEnabled && isS3TransferAccelerationDisabled) {
const errorMessage = [
'You cannot enable and disable S3 Transfer Acceleration at the same time',
].join('');
return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}
if (bucketName) {
return BbPromise.bind(this)
@ -45,17 +53,25 @@ module.exports = {
});
}
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ServerlessDeploymentBucket.Properties = {
AccelerateConfiguration: {
AccelerationStatus:
isS3TransferAccelerationEnabled ? 'Enabled' : 'Suspended',
},
};
if (isS3TransferAccelerationEnabled) {
// enable acceleration via CloudFormation
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ServerlessDeploymentBucket.Properties = {
AccelerateConfiguration: {
AccelerationStatus: 'Enabled',
},
};
// keep track of acceleration status via CloudFormation Output
this.serverless.service.provider.compiledCloudFormationTemplate
.Outputs.ServerlessDeploymentBucketAccelerated = { Value: true };
} else if (isS3TransferAccelerationDisabled) {
// explicitly disable acceleration via CloudFormation
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ServerlessDeploymentBucket.Properties = {
AccelerateConfiguration: {
AccelerationStatus: 'Suspended',
},
};
}
const coreTemplateFileName = this.provider.naming.getCoreTemplateFileName();

View File

@ -61,14 +61,45 @@ describe('#generateCoreTemplate()', () => {
return expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled
.then(() => {
const template = awsPlugin.serverless.service.provider.compiledCloudFormationTemplate;
expect(
awsPlugin.serverless.service.provider.compiledCloudFormationTemplate
.Outputs.ServerlessDeploymentBucketName.Value
template.Outputs.ServerlessDeploymentBucketName.Value
).to.equal(bucketName);
// eslint-disable-next-line no-unused-expressions
expect(
awsPlugin.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ServerlessDeploymentBucket
template.Resources.ServerlessDeploymentBucket
).to.not.exist;
});
});
it('should use a custom bucket if specified, even with S3 transfer acceleration', () => {
const bucketName = 'com.serverless.deploys';
awsPlugin.serverless.service.provider.deploymentBucket = bucketName;
awsPlugin.provider.options['aws-s3-accelerate'] = true;
const coreCloudFormationTemplate = awsPlugin.serverless.utils.readFileSync(
path.join(
__dirname,
'core-cloudformation-template.json'
)
);
awsPlugin.serverless.service.provider
.compiledCloudFormationTemplate = coreCloudFormationTemplate;
return expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled
.then(() => {
const template = awsPlugin.serverless.service.provider.compiledCloudFormationTemplate;
expect(
template.Outputs.ServerlessDeploymentBucketName.Value
).to.equal(bucketName);
// eslint-disable-next-line no-unused-expressions
expect(
template.Resources.ServerlessDeploymentBucket
).to.not.exist;
// eslint-disable-next-line no-unused-expressions
expect(
template.Outputs.ServerlessDeploymentBucketAccelerated
).to.not.exist;
});
});
@ -80,11 +111,6 @@ describe('#generateCoreTemplate()', () => {
.Resources.ServerlessDeploymentBucket
).to.be.deep.equal({
Type: 'AWS::S3::Bucket',
Properties: {
AccelerateConfiguration: {
AccelerationStatus: 'Suspended',
},
},
});
})
);
@ -114,4 +140,36 @@ describe('#generateCoreTemplate()', () => {
expect(template.Outputs.ServerlessDeploymentBucketAccelerated.Value).to.equal(true);
});
});
it('should explicitly disable S3 Transfer Acceleration, if requested', () => {
sinon.stub(awsPlugin.provider, 'request').resolves();
sinon.stub(serverless.utils, 'writeFileSync').resolves();
serverless.config.servicePath = './';
awsPlugin.provider.options['no-aws-s3-accelerate'] = true;
return awsPlugin.generateCoreTemplate()
.then(() => {
const template = serverless.service.provider.coreCloudFormationTemplate;
expect(template.Resources.ServerlessDeploymentBucket).to.be.deep.equal({
Type: 'AWS::S3::Bucket',
Properties: {
AccelerateConfiguration: {
AccelerationStatus: 'Suspended',
},
},
});
});
});
it('should explode if transfer acceleration is both enabled and disabled', () => {
sinon.stub(awsPlugin.provider, 'request').resolves();
sinon.stub(serverless.utils, 'writeFileSync').resolves();
serverless.config.servicePath = './';
awsPlugin.provider.options['aws-s3-accelerate'] = true;
awsPlugin.provider.options['no-aws-s3-accelerate'] = true;
return expect(
awsPlugin.generateCoreTemplate()
).to.be.rejectedWith(serverless.classes.Error, /at the same time/);
});
});

View File

@ -317,6 +317,10 @@ class AwsProvider {
return !!this.options['aws-s3-accelerate'];
}
isS3TransferAccelerationDisabled() {
return !!this.options['no-aws-s3-accelerate'];
}
disableTransferAccelerationForCurrentDeploy() {
delete this.options['aws-s3-accelerate'];
}
@ -363,6 +367,53 @@ class AwsProvider {
return this.request('STS', 'getCallerIdentity', {})
.then((result) => result.Account);
}
/**
* Get API Gateway Rest API ID from serverless config
*/
getApiGatewayRestApiId() {
if (this.serverless.service.provider.apiGateway
&& this.serverless.service.provider.apiGateway.restApiId) {
return this.serverless.service.provider.apiGateway.restApiId;
}
return { Ref: this.naming.getRestApiLogicalId() };
}
/**
* Get Rest API Root Resource ID from serverless config
*/
getApiGatewayRestApiRootResourceId() {
if (this.serverless.service.provider.apiGateway
&& this.serverless.service.provider.apiGateway.restApiRootResourceId) {
return this.serverless.service.provider.apiGateway.restApiRootResourceId;
}
return { 'Fn::GetAtt': [this.naming.getRestApiLogicalId(), 'RootResourceId'] };
}
/**
* Get Rest API Predefined Resources from serverless config
*/
getApiGatewayPredefinedResources() {
if (!this.serverless.service.provider.apiGateway
|| !this.serverless.service.provider.apiGateway.restApiResources) {
return [];
}
if (Array.isArray(this.serverless.service.provider.apiGateway.restApiResources)) {
return this.serverless.service.provider.apiGateway.restApiResources;
}
if (typeof this.serverless.service.provider.apiGateway.restApiResources !== 'object') {
throw new Error('REST API resource must be an array of object');
}
return Object.keys(this.serverless.service.provider.apiGateway.restApiResources)
.map((key) => ({
path: key,
resourceId: this.serverless.service.provider.apiGateway.restApiResources[key],
}));
}
}
module.exports = AwsProvider;