Merge remote-tracking branch 'upstream/master' into tagging-apigw

# Conflicts:
#	lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js
#	lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js
This commit is contained in:
exoego 2019-03-21 15:55:28 +09:00
commit e9cfc881ec
62 changed files with 2046 additions and 228 deletions

View File

@ -1,3 +1,33 @@
# 1.39.1 (2019-03-18)
- [Revert "Fixed #4188 - Package generating incorrect package artifact path in serverless-state.json"](https://github.com/serverless/serverless/pull/5936)
## Meta
- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.39.0...v1.39.1)
# 1.39.0 (2019-03-15)
- [Add support for invoke local with docker](https://github.com/serverless/serverless/pull/5863)
- [fix regression with golang check on windows ](https://github.com/serverless/serverless/pull/5899)
- [Support for Cloudwatch Event InputTransformer](https://github.com/serverless/serverless/pull/5912)
- [Allow individual packaging with TypeScript source maps](https://github.com/serverless/serverless/pull/5743)
- [Support API Gateway stage deployment description](https://github.com/serverless/serverless/pull/5509)
- [Allow Fn::Join in SQS arn builder](https://github.com/serverless/serverless/pull/5351)
- [Add AWS x-ray support for Lambda](https://github.com/serverless/serverless/pull/5860)
- [Fix CloudFormation template normalization](https://github.com/serverless/serverless/pull/5885)
- [Fix bug when using websocket events with functions with custom roles](https://github.com/serverless/serverless/pull/5880)
- [Print customized function names correctly in sls info output](https://github.com/serverless/serverless/pull/5883)
- [Added websockets authorizer support](https://github.com/serverless/serverless/pull/5867)
- [Support more route characters for websockets](https://github.com/serverless/serverless/pull/5865)
- [kotlin jvm maven updates](https://github.com/serverless/serverless/pull/5872)
- [Put `Custom Response Headers` into `[Responses]`](https://github.com/serverless/serverless/pull/5862)
- [Packaging exclude only config file being used](https://github.com/serverless/serverless/pull/5840)
## Meta
- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.38.0...v1.39.0)
# 1.38.0 (2019-02-20)
- [Set timout & others on context in python invoke local](https://github.com/serverless/serverless/pull/5796)

View File

@ -443,6 +443,7 @@ These consultants use the Serverless Framework and can help you build your serve
* [Andrew Griffiths](https://www.andrewgriffithsonline.com/) - Independent consultant specialising in serverless technology
* [Trek10](https://www.trek10.com/)
* [Parallax](https://parall.ax/) they also built the [David Guetta Campaign](https://serverlesscode.com/post/david-guetta-online-recording-with-lambda/)
* [Geniusee](https://geniusee.com)
* [SC5 Online](https://sc5.io)
* [Carrot Creative](https://carrot.is)
* [microapps](http://microapps.com)

View File

@ -6,11 +6,11 @@ menuOrder: 0
menuItems:
- {menuText: AWS Guide, path: /framework/docs/providers/aws/guide/quick-start}
- {menuText: Azure Functions Guide, path: /framework/docs/providers/azure/guide/quick-start}
- {menuText: Fn Guide, path: /framework/docs/providers/fn/guide/quick-start}
- {menuText: OpenWhisk Guide, path: /framework/docs/providers/openwhisk/guide/quick-start}
- {menuText: Apache OpenWhisk Guide, path: /framework/docs/providers/openwhisk/guide/quick-start}
- {menuText: Google Functions Guide, path: /framework/docs/providers/google/guide/quick-start}
- {menuText: Kubeless Guide, path: /framework/docs/providers/kubeless/guide/quick-start}
- {menuText: Spotinst Guide, path: /framework/docs/providers/spotinst/guide/quick-start}
- {menuText: Fn Guide, path: /framework/docs/providers/fn/guide/quick-start}
- {menuText: Cloudflare Workers Guide, path: /framework/docs/providers/cloudflare/guide/quick-start}
-->
@ -30,84 +30,92 @@ Next up, it's time to choose where you'd like your serverless service to run.
## Choose your compute provider
<div class="docsSections">
<div class="docsSection">
<div class="docsSectionHeader">
<a href="/framework/docs/providers/aws/guide/quick-start">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/images/aws-black.png" width="250" draggable="false"/>
</a>
<a href="/framework/docs/providers/aws/guide/quick-start">
<div class="docsSection">
<div class="docsSectionHeader">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/images/aws-black.png" width="250"
draggable="false" />
</div>
<div style="text-align:center;">
<span>Amazon Web Services<br />Quick Start Guide</span>
</div>
</div>
<div style="text-align:center;">
<a href="/framework/docs/providers/aws/guide/quick-start">Amazon Web Services<br/>Quick Start Guide</a>
</a>
<a href="/framework/docs/providers/azure/guide/quick-start">
<div class="docsSection">
<div class="docsSectionHeader">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/images/azure-black.png" width="250"
draggable="false" />
</div>
<div style="text-align:center;">
<span>Azure Functions<br />Quick Start Guide</span>
</div>
</div>
</div>
<div class="docsSection">
<div class="docsSectionHeader">
<a href="/framework/docs/providers/azure/guide/quick-start">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/images/azure-black.png" width="250" draggable="false"/>
</a>
</a>
<a href="/framework/docs/providers/openwhisk/guide/quick-start">
<div class="docsSection">
<div class="docsSectionHeader">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/images/openwhisk-black.png" width="250"
draggable="false" />
</div>
<div style="text-align:center;">
<span>Apache OpenWhisk<br />Quick Start Guide</span>
</div>
</div>
<div style="text-align:center;">
<a href="/framework/docs/providers/azure/guide/quick-start">Azure Functions<br/>Quick Start Guide</a>
</a>
<a href="/framework/docs/providers/google/guide/quick-start">
<div class="docsSection">
<div class="docsSectionHeader">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/images/gcf-black.png" width="250"
draggable="false" />
</div>
<div style="text-align:center;">
<span>Google Cloud Functions<br />Quick Start Guide</span>
</div>
</div>
</div>
<div class="docsSection">
<div class="docsSectionHeader">
<a href="/framework/docs/providers/openwhisk/guide/quick-start">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/images/openwhisk-black.png" width="250" draggable="false"/>
</a>
</a>
<a href="/framework/docs/providers/kubeless/guide/quick-start">
<div class="docsSection">
<div class="docsSectionHeader">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/docs/kubeless-logos-black.png"
width="250" draggable="false" />
</div>
<div style="text-align:center;">
<span>Kubeless<br />Quick Start Guide</span>
</div>
</div>
<div style="text-align:center;">
<a href="/framework/docs/providers/openwhisk/guide/quick-start">Apache OpenWhisk <br/>Quick Start Guide</a>
</a>
<a href="/framework/docs/providers/spotinst/guide/quick-start">
<div class="docsSection">
<div class="docsSectionHeader">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/docs/spotinst-logos-black-small.png"
width="250" draggable="false" />
</div>
<div style="text-align:center;">
<span>Spotinst<br />Quick Start Guide</span>
</div>
</div>
</div>
<div class="docsSection">
<div class="docsSectionHeader">
<a href="/framework/docs/providers/google/guide/quick-start">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/images/gcf-black.png" width="250" draggable="false"/>
</a>
</a>
<a href="/framework/docs/providers/fn/guide/quick-start">
<div class="docsSection">
<div class="docsSectionHeader">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/docs/fn-logo-black.png" width="250"
draggable="false" />
</div>
<div style="text-align:center;">
<span>Fn<br />Quick Start Guide</span>
</div>
</div>
<div style="text-align:center;">
<a href="/framework/docs/providers/google/guide/quick-start">Google Cloud Functions<br/>Quick Start Guide</a>
</a>
<a href="/framework/docs/providers/cloudflare/guide/quick-start">
<div class="docsSection">
<div class="docsSectionHeader">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/docs/cloudflare/cf-logo-v-dark-gray.png"
width="250" draggable="false" />
</div>
<div style="text-align:center;">
<span>Cloudflare Workers<br />Quick Start Guide</span>
</div>
</div>
</div>
<div class="docsSection">
<div class="docsSectionHeader">
<a href="/framework/docs/providers/kubeless/guide/quick-start">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/docs/kubeless-logos-black.png" width="250" draggable="false"/>
</a>
</div>
<div style="text-align:center;">
<a href="/framework/docs/providers/kubeless/guide/quick-start">Kubeless<br/>Quick Start Guide</a>
</div>
</div>
<div class="docsSection">
<div class="docsSectionHeader">
<a href="/framework/docs/providers/spotinst/guide/quick-start">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/docs/spotinst-logos-black-small.png" width="250" draggable="false"/>
</a>
</div>
<div style="text-align:center;">
<a href="/framework/docs/providers/spotinst/guide/quick-start">Spotinst<br/>Quick Start Guide</a>
</div>
</div>
<div class="docsSection">
<div class="docsSectionHeader">
<a href="/framework/docs/providers/fn/guide/quick-start">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/docs/fn-logo-black.png" width="250" draggable="false"/>
</a>
</div>
<div style="text-align:center;">
<a href="/framework/docs/providers/fn/guide/quick-start">Fn<br/>Quick Start Guide</a>
</div>
</div>
<div class="docsSection">
<div class="docsSectionHeader">
<a href="/framework/docs/providers/cloudflare/guide/quick-start">
<img src="https://s3-us-west-2.amazonaws.com/assets.site.serverless.com/docs/cloudflare/cf-logo-v-dark-gray.png" width="250" draggable="false"/>
</a>
</div>
<div style="text-align:center;">
<a href="/framework/docs/providers/cloudflare/guide/quick-start">Cloudflare Workers<br/>Quick Start Guide</a>
</div>
</div>
</a>
</div>

View File

@ -29,6 +29,8 @@ serverless invoke local --function functionName
- `--contextPath` or `-x`, The path to a json file holding input context to be passed to the invoked function. This path is relative to the root directory of the service.
- `--context` or `-c`, String data to be passed as a context to your function. Same like with `--data`, context included in `--contextPath` will overwrite the context you passed with `--context` flag.
* `--env` or `-e` String representing an environment variable to set when invoking your function, in the form `<name>=<value>`. Can be repeated for more than one environment variable.
* `--docker` Enable docker support for NodeJS/Python/Ruby/Java. Enabled by default for other
runtimes.
## Environment
@ -107,7 +109,11 @@ serverless invoke local -f functionName -e VAR1=value1 -e VAR2=value2
### Limitations
Currently, `invoke local` only supports the NodeJs, Python, Java, & Ruby runtimes.
Use of the `--docker` flag and runtimes other than NodeJs, Python, Java, & Ruby depend on having
[Docker](https://www.docker.com/) installed. On MacOS & Windows, install
[Docker Desktop](https://www.docker.com/products/docker-desktop); On Linux install
[Docker engine](https://www.docker.com/products/docker-engine) and ensure your user is in the
`docker` group so that you can invoke docker without `sudo`.
**Note:** In order to get correct output when using Java runtime, your Response class must implement `toString()` method.

View File

@ -981,6 +981,7 @@ provider:
apiGateway:
restApiId: xxxxxxxxxx # REST API resource ID. Default is generated by the framework
restApiRootResourceId: xxxxxxxxxx # Root resource, represent as / path
description: Some Description # optional - description of deployment history
functions:
...
@ -996,6 +997,7 @@ provider:
apiGateway:
restApiId: xxxxxxxxxx
restApiRootResourceId: xxxxxxxxxx
description: Some Description
functions:
create:
@ -1012,6 +1014,7 @@ provider:
apiGateway:
restApiId: xxxxxxxxxx
restApiRootResourceId: xxxxxxxxxx
description: Some Description
functions:
create:
@ -1030,6 +1033,7 @@ provider:
apiGateway:
restApiId: xxxxxxxxxx
restApiRootResourceId: xxxxxxxxxx
description: Some Description
restApiResources:
/posts: xxxxxxxxxx
@ -1044,6 +1048,7 @@ provider:
apiGateway:
restApiId: xxxxxxxxxx
restApiRootResourceId: xxxxxxxxxx
description: Some Description
restApiResources:
/posts: xxxxxxxxxx
@ -1061,6 +1066,7 @@ provider:
apiGateway:
restApiId: xxxxxxxxxx
# restApiRootResourceId: xxxxxxxxxx # Optional
description: Some Description
restApiResources:
/posts: xxxxxxxxxx
/categories: xxxxxxxxx

View File

@ -89,6 +89,19 @@ functions:
state:
- pending
inputPath: '$.stageVariables'
- cloudwatchEvent:
event:
source:
- "aws.ec2"
detail-type:
- "EC2 Instance State-change Notification"
detail:
state:
- pending
inputTransformer:
inputPathsMap:
eventTime: '$.time'
inputTemplate: '{"time": <eventTime>, "key1": "value1"}'
```
## Specifying a Description

View File

@ -47,6 +47,13 @@ functions:
rate: cron(0 12 * * ? *)
enabled: false
inputPath: '$.stageVariables'
- schedule:
rate: rate(2 hours)
enabled: true
inputTransformer:
inputPathsMap:
eventTime: '$.time'
inputTemplate: '{"time": <eventTime>, "key1": "value1"}'
```
## Specify Name and Description

View File

@ -34,6 +34,16 @@ functions:
- sqs:
arn:
Fn::ImportValue: MyExportedQueueArnId
- sqs:
arn:
Fn::Join:
- ":"
- - arn
- aws
- sqs
- Ref: AWS::Region
- Ref: AWS::AccountId
- MyOtherQueue
```
## Setting the BatchSize

View File

@ -36,7 +36,7 @@ This code will setup a websocket with a `$disconnect` route key:
```yml
functions:
disonnectHandler:
disconnectHandler:
handler: handler.disconnectHandler
events:
- websocket:
@ -61,7 +61,8 @@ service: serverless-ws-test
provider:
name: aws
runtime: nodejs8.10
websocketApiRouteSelectionExpression: $request.body.action # custom routes are selected by the value of the action property in the body
websocketsApiName: custom-websockets-api-name
websocketsApiRouteSelectionExpression: $request.body.action # custom routes are selected by the value of the action property in the body
functions:
connectionHandler:
@ -82,31 +83,73 @@ functions:
route: foo # will trigger if $request.body.action === "foo"
```
## Protect your Websocket backend
To protect your websocket connection use an authorizer function on the `$connect`-route handler. It is only possible to use an authorizer function on this route, as this is the only point in time, where it is possible to prevent the ws-client to connect to our backend at all. As the client is not able to connect, the client can also not use the other websocket routes.
## Using Authorizers
You can enable an authorizer for your connect route by specifying the `authorizer` key in the websocket event definition.
It is also possible to return a "500" in the connection handler, to prevent the ws-client from connecting.
**Note:** AWS only supports authorizers for the `$connect` route.
See this example:
```yml
functions:
connectHandler:
handler: handler.connectHandler
events:
- websocket:
route: $connect
authorizer: auth # references the auth function below
auth:
handler: handler.auth
```
```js
module.exports.connectionHandler = async (event, context) => {
Or, if your authorizer function is not managed by this service, you can provide an arn instead:
if(event.requestContext.routeKey === '$connect'){
console.log("NEW CONNECTION INCOMMING");
if (event.queryStringParameters.token !== 'abc') {
console.log('Connection blocked');
return {
statusCode: 500 // currently it is not possible to respond with a 4XX
};
}
}
```yml
functions:
connectHandler:
handler: handler.connectHandler
events:
- websocket:
route: $connect
authorizer: arn:aws:lambda:us-east-1:1234567890:function:auth
```
console.log('Connection ok');
return {
statusCode: 200
};
}
By default, the `identitySource` property is set to `route.request.header.Auth`, meaning that your request must include the auth token in the `Auth` header of the request. You can overwrite this by specifying your own `identitySource` configuration:
```yml
functions:
connectHandler:
handler: handler.connectHandler
events:
- websocket:
route: $connect
authorizer:
name: auth
identitySource:
- 'route.request.header.Auth'
- 'route.request.querystring.Auth'
auth:
handler: handler.auth
```
With the above configuration, you can now must pass the auth token in both the `Auth` query string as well as the `Auth` header.
You can also supply an ARN instead of the name when using the object syntax for the authorizer:
```yml
functions:
connectHandler:
handler: handler.connectHandler
events:
- websocket:
route: $connect
authorizer:
arn: arn:aws:lambda:us-east-1:1234567890:function:auth
identitySource:
- 'route.request.header.Auth'
- 'route.request.querystring.Auth'
auth:
handler: handler.auth
```
## Send a message to a ws-client
@ -142,4 +185,4 @@ module.exports.defaultHandler = async (event, context) => {
statusCode: 200
};
}
```
```

View File

@ -28,6 +28,8 @@ provider:
memorySize: 512 # optional, in MB, default is 1024
timeout: 10 # optional, in seconds, default is 6
versionFunctions: false # optional, default is true
tracing:
lambda: true # optional, enables tracing for all functions (can be true (true equals 'Active') 'Active' or 'PassThrough')
functions:
hello:
@ -38,6 +40,7 @@ functions:
memorySize: 512 # optional, in MB, default is 1024
timeout: 10 # optional, in seconds, default is 6
reservedConcurrency: 5 # optional, reserved concurrency limit for this function. By default, AWS uses account concurrency limit
tracing: PassThrough # optional, overwrite, can be 'Active' or 'PassThrough'
```
The `handler` property points to the file and module containing the code you want to run in your function.
@ -430,3 +433,29 @@ functions:
### Secrets using environment variables and KMS
When storing secrets in environment variables, AWS [strongly suggests](http://docs.aws.amazon.com/lambda/latest/dg/env_variables.html#env-storing-sensitive-data) encrypting sensitive information. AWS provides a [tutorial](http://docs.aws.amazon.com/lambda/latest/dg/tutorial-env_console.html) on using KMS for this purpose.
## AWS X-Ray Tracing
You can enable [AWS X-Ray Tracing](https://docs.aws.amazon.com/xray/latest/devguide/aws-xray.html) on your Lambda functions through the optional `tracing` config variable:
```yml
service: myService
provider:
name: aws
runtime: nodejs8.10
tracing:
lambda: true
```
You can also set this variable on a per-function basis. This will override the provider level setting if present:
```yml
functions:
hello:
handler: handler.hello
tracing: Active
goodbye:
handler: handler.goodbye
tracing: PassThrough
```

View File

@ -12,7 +12,7 @@ layout: Doc
# AWS - Resources
If you are using AWS as a provider for your Service, all *Resources* are other AWS infrastructure resources which the AWS Lambda functions in your *Service* depend on, like AWS DynamoDB or AWS S3.
If you are using AWS as a provider for your Service, all [*Resources*](./intro.md#resources) are other AWS infrastructure resources which the AWS Lambda functions in your [*Service*](./intro.md#services) depend on, like AWS DynamoDB or AWS S3.
Using the Serverless Framework, you can define the infrastructure resources you need in `serverless.yml`, and easily deploy them.

View File

@ -30,6 +30,8 @@ provider:
region: ${opt:region, 'us-east-1'} # Overwrite the default region used. Default is us-east-1
stackName: custom-stack-name # Use a custom name for the CloudFormation stack
apiName: custom-api-name # Use a custom name for the API Gateway API
websocketsApiName: custom-websockets-api-name # Use a custom name for the websockets API
websocketsApiRouteSelectionExpression: $request.body.route # custom route selection expression
profile: production # The default profile to use with this service
memorySize: 512 # Overwrite the default memory size. Default is 1024
timeout: 10 # The default is 6 seconds. Note: API Gateway current maximum is 30 seconds
@ -59,7 +61,7 @@ provider:
'/users/create': xxxxxxxxxx
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)
description: Some Description # optional description for the API Gateway stage deployment
usagePlan: # Optional usage plan configuration
quota:
limit: 5000
@ -118,6 +120,8 @@ provider:
tags: # Optional service wide function tags
foo: bar
baz: qux
tracing:
lambda: true # optional, can be true (true equals 'Active'), 'Active' or 'PassThrough'
package: # Optional deployment packaging configuration
include: # Specify the directories and files which should be included in the deployment package
@ -164,6 +168,7 @@ functions:
individually: true # Enables individual packaging for specific function. If true you must provide package for each function. Defaults to false
layers: # An optional list Lambda Layers to use
- arn:aws:lambda:region:XXXXXX:layer:LayerName:Y # Layer Version ARN
tracing: Active # optional, can be 'Active' or 'PassThrough' (overwrites the one defined on the provider level)
events: # The Events that trigger this Function
- http: # This creates an API Gateway HTTP endpoint which can be used to trigger this function. Learn more in "events/apigateway"
path: users/create # Path for this endpoint
@ -176,8 +181,15 @@ functions:
resultTtlInSeconds: 0
identitySource: method.request.header.Authorization
identityValidationExpression: someRegex
type: token # token or request. Determines input to the authorier function, called with the auth token or the entire request event. Defaults to token
- websocket:
route: $connect
authorizer:
# name: auth NOTE: you can either use "name" or arn" properties
arn: arn:aws:lambda:us-east-1:1234567890:function:auth
identitySource:
- 'route.request.header.Auth'
- 'route.request.querystring.Auth'
- s3:
bucket: photos
event: s3:ObjectCreated:*
@ -189,12 +201,17 @@ functions:
description: a description of my scheduled event's purpose
rate: rate(10 minutes)
enabled: false
# Note, you can use only one of input, inputPath, or inputTransformer
input:
key1: value1
key2: value2
stageParams:
stage: dev
inputPath: '$.stageVariables'
inputTransformer:
inputPathsMap:
eventTime: '$.time'
inputTemplate: '{"time": <eventTime>, "key1": "value1"}'
- sns:
topicName: aggregate
displayName: Data aggregation pipeline
@ -227,13 +244,17 @@ functions:
detail:
state:
- pending
# Note: you can either use "input" or "inputPath"
# Note, you can use only one of input, inputPath, or inputTransformer
input:
key1: value1
key2: value2
stageParams:
stage: dev
inputPath: '$.stageVariables'
inputTransformer:
inputPathsMap:
eventTime: '$.time'
inputTemplate: '{"time": <eventTime>, "key1": "value1"}'
- cloudwatchLog:
logGroup: '/aws/lambda/hello'
filter: '{$.userIdentity.type = Root}'

View File

@ -14,7 +14,6 @@ const Service = require('./classes/Service');
const Variables = require('./classes/Variables');
const ServerlessError = require('./classes/Error').ServerlessError;
const Version = require('./../package.json').version;
const _ = require('lodash');
class Serverless {
constructor(config) {
@ -91,13 +90,6 @@ class Serverless {
// populate variables after --help, otherwise help may fail to print
// (https://github.com/serverless/serverless/issues/2041)
return this.variables.populateService(this.pluginManager.cliOptions)
.then(() => {
if ((!_.includes(this.processedInput.commands, 'deploy') &&
!_.includes(this.processedInput.commands, 'remove')) || !this.config.servicePath) {
return BbPromise.resolve();
}
return BbPromise.resolve();
})
.then(() => {
// merge arrays after variables have been populated
// (https://github.com/serverless/serverless/issues/3511)

View File

@ -72,7 +72,7 @@ module.exports.logError = (e) => {
consoleLog(`${chalk.yellow(' Issues: ')}${'forum.serverless.com'}`);
consoleLog(' ');
consoleLog(chalk.yellow(' Your Environment Information -----------------------------'));
consoleLog(chalk.yellow(' Your Environment Information ---------------------------'));
consoleLog(chalk.yellow(` OS: ${platform}`));
consoleLog(chalk.yellow(` Node Version: ${nodeVersion}`));
consoleLog(chalk.yellow(` Serverless Version: ${slsVersion}`));

View File

@ -40,8 +40,7 @@ module.exports = {
this.serverless.service.getAllFunctions().forEach((func) => {
const functionInfo = {};
functionInfo.name = func;
functionInfo.deployedName = `${
this.serverless.service.service}-${this.provider.getStage()}-${func}`;
functionInfo.deployedName = this.serverless.service.getFunction(func).name;
this.gatheredData.info.functions.push(functionInfo);
});

View File

@ -20,8 +20,8 @@ describe('#getStackInfo()', () => {
serverless.setProvider('aws', new AwsProvider(serverless, options));
serverless.service.service = 'my-service';
serverless.service.functions = {
hello: {},
world: {},
hello: { name: 'my-service-dev-hello' },
world: { name: 'customized' },
};
serverless.service.layers = { test: {} };
awsInfo = new AwsInfo(serverless, options);
@ -85,7 +85,7 @@ describe('#getStackInfo()', () => {
},
{
name: 'world',
deployedName: 'my-service-dev-world',
deployedName: 'customized',
},
],
layers: [

View File

@ -4,12 +4,19 @@ const BbPromise = require('bluebird');
const _ = require('lodash');
const os = require('os');
const fs = BbPromise.promisifyAll(require('fs'));
const fse = require('fs-extra');
const path = require('path');
const validate = require('../lib/validate');
const chalk = require('chalk');
const stdin = require('get-stdin');
const spawn = require('child_process').spawn;
const inspect = require('util').inspect;
const download = require('download');
const mkdirp = require('mkdirp');
const cachedir = require('cachedir');
const jszip = require('jszip');
const cachePath = path.join(cachedir('serverless'), 'invokeLocal');
class AwsInvokeLocal {
constructor(serverless, options) {
@ -28,6 +35,12 @@ class AwsInvokeLocal {
};
}
getRuntime() {
return this.options.functionObj.runtime
|| this.serverless.service.provider.runtime
|| 'nodejs4.3';
}
validateFile(filePath, key) {
const absolutePath = path.isAbsolute(filePath) ?
filePath :
@ -126,11 +139,13 @@ class AwsInvokeLocal {
}
invokeLocal() {
const runtime = this.options.functionObj.runtime
|| this.serverless.service.provider.runtime
|| 'nodejs4.3';
const runtime = this.getRuntime();
const handler = this.options.functionObj.handler;
if (this.options.docker) {
return this.invokeLocalDocker();
}
if (runtime.startsWith('nodejs')) {
const handlerPath = handler.split('.')[0];
const handlerName = handler.split('.')[1];
@ -177,8 +192,151 @@ class AwsInvokeLocal {
this.options.context);
}
throw new this.serverless.classes
.Error('You can only invoke Node.js, Python, Java & Ruby functions locally.');
return this.invokeLocalDocker();
}
checkDockerDaemonStatus() {
return new BbPromise((resolve, reject) => {
const docker = spawn('docker', ['version']);
docker.on('exit', error => {
if (error) {
reject('Please start the Docker daemon to use the invoke local Docker integration.');
}
resolve();
});
});
}
checkDockerImage() {
const runtime = this.getRuntime();
return new BbPromise((resolve, reject) => {
const docker = spawn('docker', ['images', '-q', `lambci/lambda:${runtime}`]);
let stdout = '';
docker.stdout.on('data', (buf) => { stdout += buf.toString(); });
docker.on('exit', error => (error ? reject(error) : resolve(Boolean(stdout.trim()))));
});
}
pullDockerImage() {
const runtime = this.getRuntime();
this.serverless.cli.log('Downloading base Docker image...');
return new BbPromise((resolve, reject) => {
const docker = spawn('docker', ['pull', `lambci/lambda:${runtime}`]);
docker.on('exit', error => (error ? reject(error) : resolve()));
});
}
getLayerPaths() {
const layers = _.mapKeys(
this.serverless.service.layers,
(value, key) => this.provider.naming.getLambdaLayerLogicalId(key)
);
return BbPromise.all(
(this.options.functionObj.layers || this.serverless.service.provider.layers || [])
.map(layer => {
if (layer.Ref) {
return layers[layer.Ref].path;
}
const arnParts = layer.split(':');
const layerArn = arnParts.slice(0, -1).join(':');
const layerVersion = Number(arnParts.slice(-1)[0]);
const layerContentsPath = path.join(
'.serverless', 'layers', arnParts[6], arnParts[7]);
const layerContentsCachePath = path.join(
cachePath, 'layers', arnParts[6], arnParts[7]);
if (fs.existsSync(layerContentsPath)) {
return layerContentsPath;
}
let downloadPromise = BbPromise.resolve();
if (!fs.existsSync(layerContentsCachePath)) {
this.serverless.cli.log(`Downloading layer ${layer}...`);
mkdirp.sync(path.join(layerContentsCachePath));
downloadPromise = this.provider.request(
'Lambda', 'getLayerVersion', { LayerName: layerArn, VersionNumber: layerVersion })
.then(layerInfo => download(
layerInfo.Content.Location,
layerContentsPath,
{ extract: true }));
}
return downloadPromise
.then(() => fse.copySync(layerContentsCachePath, layerContentsPath))
.then(() => layerContentsPath);
}));
}
buildDockerImage(layerPaths) {
const runtime = this.getRuntime();
const imageName = 'sls-docker';
return new BbPromise((resolve, reject) => {
let dockerfile = `FROM lambci/lambda:${runtime}`;
for (const layerPath of layerPaths) {
dockerfile += `\nADD --chown=sbx_user1051:495 ${layerPath} /opt`;
}
mkdirp.sync(path.join('.serverless', 'invokeLocal'));
const dockerfilePath = path.join('.serverless', 'invokeLocal', 'Dockerfile');
fs.writeFileSync(dockerfilePath, dockerfile);
this.serverless.cli.log('Building Docker image...');
const docker = spawn('docker', ['build', '-t', imageName,
`${this.serverless.config.servicePath}`, '-f', dockerfilePath]);
docker.on('exit', error => (error ? reject(error) : resolve(imageName)));
});
}
extractArtifact() {
const artifact = _.get(this.options.functionObj, 'package.artifact', _.get(
this.serverless.service, 'package.artifact'
));
if (!artifact) {
return this.serverless.config.servicePath;
}
return fs.readFileAsync(artifact)
.then(jszip.loadAsync)
.then(zip => BbPromise.all(
Object.keys(zip.files)
.map(filename => zip.files[filename].async('nodebuffer').then(fileData => {
if (filename.endsWith(path.sep)) {
return BbPromise.resolve();
}
mkdirp.sync(path.join(
'.serverless', 'invokeLocal', 'artifact'));
return fs.writeFileAsync(path.join(
'.serverless', 'invokeLocal', 'artifact', filename), fileData, {
mode: zip.files[filename].unixPermissions,
});
}))))
.then(() => path.join(
this.serverless.config.servicePath, '.serverless', 'invokeLocal', 'artifact'));
}
invokeLocalDocker() {
const handler = this.options.functionObj.handler;
return BbPromise.all([
this.checkDockerDaemonStatus(),
this.checkDockerImage().then(exists => (exists ? {} : this.pullDockerImage())),
this.getLayerPaths().then(layerPaths => this.buildDockerImage(layerPaths)),
this.extractArtifact(),
])
.then((results) => new BbPromise((resolve, reject) => {
const imageName = results[2];
const artifactPath = results[3];
const dockerArgs = [
'run', '--rm', '-v', `${artifactPath}:/var/task`, imageName,
handler, JSON.stringify(this.options.data),
];
const docker = spawn('docker', dockerArgs);
docker.stdout.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString()));
docker.stderr.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString()));
docker.on('exit', error => (error ? reject(error) : resolve(imageName)));
}));
}
invokeLocalPython(runtime, handlerPath, handlerName, event, context) {
@ -360,7 +518,7 @@ class AwsInvokeLocal {
this.serverless.cli.consoleLog(JSON.stringify(result, null, 4));
}
return new Promise((resolve) => {
return new BbPromise((resolve) => {
const callback = (err, result) => {
if (!hasResponded) {
hasResponded = true;
@ -408,7 +566,7 @@ class AwsInvokeLocal {
const maybeThennable = lambda(event, context, callback);
if (!_.isUndefined(maybeThennable)) {
return Promise.resolve(maybeThennable)
return BbPromise.resolve(maybeThennable)
.then(
callback.bind(this, null),
callback.bind(this)

View File

@ -32,6 +32,7 @@ describe('AwsInvokeLocal', () => {
function: 'first',
};
serverless = new Serverless();
serverless.config.servicePath = 'servicePath';
serverless.cli = new CLI(serverless);
provider = new AwsProvider(serverless, options);
serverless.setProvider('aws', provider);
@ -334,6 +335,7 @@ describe('AwsInvokeLocal', () => {
let invokeLocalPythonStub;
let invokeLocalJavaStub;
let invokeLocalRubyStub;
let invokeLocalDockerStub;
beforeEach(() => {
invokeLocalNodeJsStub =
@ -344,6 +346,8 @@ describe('AwsInvokeLocal', () => {
sinon.stub(awsInvokeLocal, 'invokeLocalJava').resolves();
invokeLocalRubyStub =
sinon.stub(awsInvokeLocal, 'invokeLocalRuby').resolves();
invokeLocalDockerStub =
sinon.stub(awsInvokeLocal, 'invokeLocalDocker').resolves();
awsInvokeLocal.serverless.service.service = 'new-service';
awsInvokeLocal.provider.options.stage = 'dev';
@ -468,10 +472,23 @@ describe('AwsInvokeLocal', () => {
});
});
it('throw error when using runtime other than Node.js, Python, Java or Ruby', () => {
awsInvokeLocal.options.functionObj.runtime = 'invalid-runtime';
expect(() => awsInvokeLocal.invokeLocal()).to.throw(Error);
delete awsInvokeLocal.options.functionObj.runtime;
it('should call invokeLocalDocker if using runtime provided', () => {
awsInvokeLocal.options.functionObj.runtime = 'provided';
awsInvokeLocal.options.functionObj.handler = 'handler.foobar';
return awsInvokeLocal.invokeLocal().then(() => {
expect(invokeLocalDockerStub.calledOnce).to.be.equal(true);
expect(invokeLocalDockerStub.calledWithExactly()).to.be.equal(true);
});
});
it('should call invokeLocalDocker if using --docker option with nodejs8.10', () => {
awsInvokeLocal.options.functionObj.runtime = 'nodejs8.10';
awsInvokeLocal.options.functionObj.handler = 'handler.foobar';
awsInvokeLocal.options.docker = true;
return awsInvokeLocal.invokeLocal().then(() => {
expect(invokeLocalDockerStub.calledOnce).to.be.equal(true);
expect(invokeLocalDockerStub.calledWithExactly()).to.be.equal(true);
});
});
});
@ -1097,4 +1114,87 @@ describe('AwsInvokeLocal', () => {
});
});
});
describe('#invokeLocalDocker()', () => {
let awsInvokeLocalMocked;
let spawnStub;
beforeEach(() => {
awsInvokeLocal.provider.options.stage = 'dev';
awsInvokeLocal.options = {
function: 'first',
functionObj: {
handler: 'handler.hello',
name: 'hello',
timeout: 4,
},
data: {},
};
spawnStub = sinon.stub().returns({
stderr: new EventEmitter().on('data', () => {}),
stdout: new EventEmitter().on('data', () => {}),
stdin: {
write: () => {},
end: () => {},
},
on: (key, callback) => callback(),
});
mockRequire('child_process', { spawn: spawnStub });
// Remove Node.js internal "require cache" contents and re-require ./index.js
delete require.cache[require.resolve('./index')];
delete require.cache[require.resolve('child_process')];
const AwsInvokeLocalMocked = require('./index'); // eslint-disable-line global-require
serverless.setProvider('aws', new AwsProvider(serverless, options));
awsInvokeLocalMocked = new AwsInvokeLocalMocked(serverless, options);
awsInvokeLocalMocked.options = {
stage: 'dev',
function: 'first',
functionObj: {
handler: 'handler.hello',
name: 'hello',
timeout: 4,
runtime: 'nodejs8.10',
},
data: {},
};
});
afterEach(() => {
delete require.cache[require.resolve('./index')];
delete require.cache[require.resolve('child_process')];
});
it('calls docker', () =>
awsInvokeLocalMocked.invokeLocalDocker().then(() => {
expect(spawnStub.getCall(0).args).to.deep.equal(['docker', ['version']]);
expect(spawnStub.getCall(1).args).to.deep.equal(['docker',
['images', '-q', 'lambci/lambda:nodejs8.10']]);
expect(spawnStub.getCall(2).args).to.deep.equal(['docker',
['pull', 'lambci/lambda:nodejs8.10']]);
expect(spawnStub.getCall(3).args).to.deep.equal(['docker', [
'build',
'-t',
'sls-docker',
'servicePath',
'-f',
'.serverless/invokeLocal/Dockerfile',
]]);
expect(spawnStub.getCall(4).args).to.deep.equal(['docker', [
'run',
'--rm',
'-v',
'servicePath:/var/task',
'sls-docker',
'handler.hello',
'{}',
]]);
})
);
});
});

View File

@ -181,7 +181,12 @@ module.exports = {
},
getNormalizedWebsocketsRouteKey(route) {
return route.replace('$', 'S');
return route
.replace('$', 'S') // dollar sign
.replace('/', 'Slash')
.replace('-', 'Dash')
.replace('_', 'Underscore')
.replace('.', 'Period');
},
getWebsocketsRouteLogicalId(route) {
@ -196,6 +201,10 @@ module.exports = {
return 'WebsocketsDeploymentStage';
},
getWebsocketsAuthorizerLogicalId(functionName) {
return `${this.getNormalizedAuthorizerName(functionName)}WebsocketsAuthorizer`;
},
// API Gateway
getApiGatewayName() {
if (this.provider.serverless.service.provider.apiName &&

View File

@ -259,6 +259,18 @@ describe('#naming()', () => {
it('should return a normalized version of the route key', () => {
expect(sdk.naming.getNormalizedWebsocketsRouteKey('$connect'))
.to.equal('Sconnect');
expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo/bar'))
.to.equal('fooSlashbar');
expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo-bar'))
.to.equal('fooDashbar');
expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo_bar'))
.to.equal('fooUnderscorebar');
expect(sdk.naming.getNormalizedWebsocketsRouteKey('foo.bar'))
.to.equal('fooPeriodbar');
});
});
@ -283,6 +295,13 @@ describe('#naming()', () => {
});
});
describe('#getWebsocketsAuthorizerLogicalId()', () => {
it('should return the websockets authorizer logical id', () => {
expect(sdk.naming.getWebsocketsAuthorizerLogicalId('auth'))
.to.equal('AuthWebsocketsAuthorizer');
});
});
describe('#getApiGatewayName()', () => {
it('should return the composition of stage & service name if custom name not provided', () => {
serverless.service.service = 'myService';

View File

@ -6,8 +6,11 @@ module.exports = {
normalizeCloudFormationTemplate(template) {
const normalizedTemplate = _.cloneDeep(template);
// reset all the S3Keys for AWS::Lambda::Function resources
_.forEach(normalizedTemplate.Resources, (value) => {
_.forEach(normalizedTemplate.Resources, (value, key) => {
if (key.startsWith('ApiGatewayDeployment')) {
delete Object.assign(normalizedTemplate.Resources,
{ ApiGatewayDeployment: normalizedTemplate.Resources[key] })[key];
}
if (value.Type && value.Type === 'AWS::Lambda::Function') {
const newVal = value;
newVal.Properties.Code.S3Key = '';

View File

@ -35,6 +35,64 @@ describe('normalizeFiles', () => {
});
});
it('should reset the S3 content keys for Lambda layer versions', () => {
const input = {
Resources: {
MyLambdaLayer: {
Type: 'AWS::Lambda::LayerVersion',
Properties: {
Content: {
S3Key: 'some-s3-key-for-the-layer',
},
},
},
},
};
const result = normalizeFiles.normalizeCloudFormationTemplate(input);
expect(result).to.deep.equal({
Resources: {
MyLambdaLayer: {
Type: 'AWS::Lambda::LayerVersion',
Properties: {
Content: {
S3Key: '',
},
},
},
},
});
});
it('should remove the API Gateway Deployment random id', () => {
const input = {
Resources: {
ApiGatewayDeploymentR4ND0M: {
Type: 'AWS::ApiGateway::Deployment',
Properties: {
RestApiId: 'rest-api-id',
StageName: 'dev',
},
},
},
};
const result = normalizeFiles.normalizeCloudFormationTemplate(input);
expect(result).to.deep.equal({
Resources: {
ApiGatewayDeployment: {
Type: 'AWS::ApiGateway::Deployment',
Properties: {
RestApiId: 'rest-api-id',
StageName: 'dev',
},
},
},
});
});
it('should keep other resources untouched', () => {
const input = {
Resources: {

View File

@ -5,11 +5,16 @@ const BbPromise = require('bluebird');
module.exports = {
compileDeployment() {
this.apiGatewayDeploymentLogicalId = this.provider.naming
.generateApiGatewayDeploymentLogicalId();
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[this.apiGatewayDeploymentLogicalId]: {
Type: 'AWS::ApiGateway::Deployment',
Properties: {
RestApiId: this.provider.getApiGatewayRestApiId(),
StageName: this.provider.getStage(),
Description: this.provider.getApiGatewayDescription(),
},
DependsOn: this.apiGatewayMethodLogicalIds,
},

View File

@ -44,11 +44,42 @@ describe('#compileDeployment()', () => {
RestApiId: {
Ref: awsCompileApigEvents.apiGatewayRestApiLogicalId,
},
Description: undefined,
StageName: 'dev',
},
});
})
);
it('should create a deployment resource with description', () => {
awsCompileApigEvents.serverless.service.provider.apiGateway = {
description: 'Some Description',
};
return awsCompileApigEvents
.compileDeployment().then(() => {
const apiGatewayDeploymentLogicalId = Object
.keys(awsCompileApigEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources)[0];
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources[apiGatewayDeploymentLogicalId]
).to.deep.equal({
Type: 'AWS::ApiGateway::Deployment',
DependsOn: ['method-dependency1', 'method-dependency2'],
Properties: {
RestApiId: {
Ref: awsCompileApigEvents.apiGatewayRestApiLogicalId,
},
Description: 'Some Description',
StageName: 'dev',
},
});
});
}
);
it('should add service endpoint output', () =>
awsCompileApigEvents.compileDeployment().then(() => {
expect(

View File

@ -25,6 +25,7 @@ class AwsCompileCloudWatchEventEvents {
let State;
let Input;
let InputPath;
let InputTransformer;
let Description;
let Name;
@ -45,13 +46,15 @@ class AwsCompileCloudWatchEventEvents {
}
Input = event.cloudwatchEvent.input;
InputPath = event.cloudwatchEvent.inputPath;
InputTransformer = event.cloudwatchEvent.inputTransformer;
Description = event.cloudwatchEvent.description;
Name = event.cloudwatchEvent.name;
if (Input && InputPath) {
const inputOptions = [Input, InputPath, InputTransformer].filter(i => i);
if (inputOptions.length > 1) {
const errorMessage = [
'You can\'t set both input & inputPath properties at the',
'same time for cloudwatch events.',
'You can only set one of input, inputPath, or inputTransformer ',
'properties at the same time for cloudwatch events. ',
'Please check the AWS docs for more info',
].join('');
throw new this.serverless.classes.Error(errorMessage);
@ -64,6 +67,9 @@ class AwsCompileCloudWatchEventEvents {
// escape quotes to favor JSON.parse
Input = Input.replace(/\"/g, '\\"'); // eslint-disable-line
}
if (InputTransformer) {
InputTransformer = this.formatInputTransformer(InputTransformer);
}
} else {
const errorMessage = [
`CloudWatch event of function "${functionName}" is not an object`,
@ -93,6 +99,7 @@ class AwsCompileCloudWatchEventEvents {
"Targets": [{
${Input ? `"Input": "${Input.replace(/\\n|\\r/g, '')}",` : ''}
${InputPath ? `"InputPath": "${InputPath.replace(/\r?\n/g, '')}",` : ''}
${InputTransformer ? `"InputTransformer": ${InputTransformer},` : ''}
"Arn": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] },
"Id": "${cloudWatchId}"
}]
@ -128,6 +135,24 @@ class AwsCompileCloudWatchEventEvents {
}
});
}
formatInputTransformer(inputTransformer) {
if (!inputTransformer.inputTemplate) {
throw new this.serverless.classes.Error(
'The inputTemplate key is required when specifying an ' +
'inputTransformer for a cloudwatchEvent event'
);
}
const cfmOutput = {
// InputTemplate is required
InputTemplate: inputTransformer.inputTemplate,
};
// InputPathsMap is optional
if (inputTransformer.inputPathsMap) {
cfmOutput.InputPathsMap = inputTransformer.inputPathsMap;
}
return JSON.stringify(cfmOutput);
}
}
module.exports = AwsCompileCloudWatchEventEvents;

View File

@ -218,6 +218,42 @@ describe('awsCompileCloudWatchEventEvents', () => {
).to.equal('{"key":"value"}');
});
it('should respect inputTransformer variable', () => {
awsCompileCloudWatchEventEvents.serverless.service.functions = {
first: {
events: [
{
cloudwatchEvent: {
event: {
source: ['aws.ec2'],
'detail-type': ['EC2 Instance State-change Notification'],
detail: { state: ['pending'] },
},
enabled: false,
inputTransformer: {
inputPathsMap: {
eventTime: '$.time',
},
inputTemplate: '{"time": <eventTime>, "key1": "value1"}',
},
},
},
],
},
};
awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents();
expect(awsCompileCloudWatchEventEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1
.Properties.Targets[0].InputTransformer
).to.eql({
InputTemplate: '{"time": <eventTime>, "key1": "value1"}',
InputPathsMap: { eventTime: '$.time' },
});
});
it('should respect description variable', () => {
awsCompileCloudWatchEventEvents.serverless.service.functions = {
first: {
@ -328,6 +364,62 @@ describe('awsCompileCloudWatchEventEvents', () => {
expect(() => awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents()).to.throw(Error);
});
it('should throw an error when both Input and InputTransformer are set', () => {
awsCompileCloudWatchEventEvents.serverless.service.functions = {
first: {
events: [
{
cloudwatchEvent: {
event: {
source: ['aws.ec2'],
'detail-type': ['EC2 Instance State-change Notification'],
detail: { state: ['pending'] },
},
enabled: false,
input: {
key: 'value',
},
inputTransformer: {
inputPathsMap: {
eventTime: '$.time',
},
inputTemplate: '{"time": <eventTime>, "key1": "value1"}',
},
},
},
],
},
};
expect(() => awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents()).to.throw(Error);
});
it('should throw an error when inputTransformer does not have inputTemplate', () => {
awsCompileCloudWatchEventEvents.serverless.service.functions = {
first: {
events: [
{
cloudwatchEvent: {
event: {
source: ['aws.ec2'],
'detail-type': ['EC2 Instance State-change Notification'],
detail: { state: ['pending'] },
},
enabled: false,
inputTransformer: {
inputPathsMap: {
eventTime: '$.time',
},
},
},
},
],
},
};
expect(() => awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents()).to.throw(Error);
});
it('should respect variables if multi-line variables is given', () => {
awsCompileCloudWatchEventEvents.serverless.service.functions = {
first: {

View File

@ -40,6 +40,7 @@ class AwsCompileScheduledEvents {
let State;
let Input;
let InputPath;
let InputTransformer;
let Name;
let Description;
@ -56,13 +57,15 @@ class AwsCompileScheduledEvents {
}
Input = event.schedule.input;
InputPath = event.schedule.inputPath;
InputTransformer = event.schedule.inputTransformer;
Name = event.schedule.name;
Description = event.schedule.description;
if (Input && InputPath) {
const inputOptions = [Input, InputPath, InputTransformer].filter(i => i);
if (inputOptions.length > 1) {
const errorMessage = [
'You can\'t set both input & inputPath properties at the',
'same time for schedule events.',
'You can only set one of input, inputPath, or inputTransformer ',
'properties at the same time for schedule events. ',
'Please check the AWS docs for more info',
].join('');
throw new this.serverless.classes
@ -90,6 +93,9 @@ class AwsCompileScheduledEvents {
// escape quotes to favor JSON.parse
Input = Input.replace(/\"/g, '\\"'); // eslint-disable-line
}
if (InputTransformer) {
InputTransformer = this.formatInputTransformer(InputTransformer);
}
} else if (this.validateScheduleSyntax(event.schedule)) {
ScheduleExpression = event.schedule;
State = 'ENABLED';
@ -118,6 +124,7 @@ class AwsCompileScheduledEvents {
"Targets": [{
${Input ? `"Input": "${Input}",` : ''}
${InputPath ? `"InputPath": "${InputPath}",` : ''}
${InputTransformer ? `"InputTransformer": ${InputTransformer},` : ''}
"Arn": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] },
"Id": "${scheduleId}"
}]
@ -158,6 +165,24 @@ class AwsCompileScheduledEvents {
return typeof input === 'string' &&
(rateSyntaxPattern.test(input) || cronSyntaxPattern.test(input));
}
formatInputTransformer(inputTransformer) {
if (!inputTransformer.inputTemplate) {
throw new this.serverless.classes.Error(
'The inputTemplate key is required when specifying an ' +
'inputTransformer for a schedule event'
);
}
const cfmOutput = {
// InputTemplate is required
InputTemplate: inputTransformer.inputTemplate,
};
// InputPathsMap is optional
if (inputTransformer.inputPathsMap) {
cfmOutput.InputPathsMap = inputTransformer.inputPathsMap;
}
return JSON.stringify(cfmOutput);
}
}
module.exports = AwsCompileScheduledEvents;

View File

@ -405,6 +405,38 @@ describe('AwsCompileScheduledEvents', () => {
).to.equal('{"key":"value"}');
});
it('should respect inputTransformer variable', () => {
awsCompileScheduledEvents.serverless.service.functions = {
first: {
events: [
{
schedule: {
rate: 'rate(10 minutes)',
enabled: false,
inputTransformer: {
inputPathsMap: {
eventTime: '$.time',
},
inputTemplate: '{"time": <eventTime>, "key1": "value1"}',
},
},
},
],
},
};
awsCompileScheduledEvents.compileScheduledEvents();
expect(awsCompileScheduledEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule1
.Properties.Targets[0].InputTransformer
).to.eql({
InputTemplate: '{"time": <eventTime>, "key1": "value1"}',
InputPathsMap: { eventTime: '$.time' },
});
});
it('should throw an error when both Input and InputPath are set', () => {
awsCompileScheduledEvents.serverless.service.functions = {
first: {
@ -426,6 +458,32 @@ describe('AwsCompileScheduledEvents', () => {
expect(() => awsCompileScheduledEvents.compileScheduledEvents()).to.throw(Error);
});
it('should throw an error when both Input and InputTransformer are set', () => {
awsCompileScheduledEvents.serverless.service.functions = {
first: {
events: [
{
schedule: {
rate: 'rate(10 minutes)',
enabled: false,
input: {
key: 'value',
},
inputTransformer: {
inputPathsMap: {
eventTime: '$.time',
},
inputTemplate: '{"time": <eventTime>, "key1": "value1"}',
},
},
},
],
},
};
expect(() => awsCompileScheduledEvents.compileScheduledEvents()).to.throw(Error);
});
it('should not throw an error when Input body is a valid JSON string', () => {
awsCompileScheduledEvents.serverless.service.functions = {
first: {
@ -466,6 +524,28 @@ describe('AwsCompileScheduledEvents', () => {
expect(() => awsCompileScheduledEvents.compileScheduledEvents()).to.throw(Error);
});
it('should throw an error when inputTransformer does not have inputTemplate', () => {
awsCompileScheduledEvents.serverless.service.functions = {
first: {
events: [
{
schedule: {
rate: 'rate(10 minutes)',
enabled: false,
inputTransformer: {
inputPathsMap: {
eventTime: '$.time',
},
},
},
},
],
},
};
expect(() => awsCompileScheduledEvents.compileScheduledEvents()).to.throw(Error);
});
it('should not create corresponding resources when scheduled events are not given', () => {
awsCompileScheduledEvents.serverless.service.functions = {
first: {

View File

@ -49,7 +49,8 @@ class AwsCompileSQSEvents {
// for dynamic arns (GetAtt/ImportValue)
if (Object.keys(event.sqs.arn).length !== 1
|| !(_.has(event.sqs.arn, 'Fn::ImportValue')
|| _.has(event.sqs.arn, 'Fn::GetAtt'))) {
|| _.has(event.sqs.arn, 'Fn::GetAtt')
|| _.has(event.sqs.arn, 'Fn::Join'))) {
const errorMessage = [
`Bad dynamic ARN property on sqs event in function "${functionName}"`,
' If you use a dynamic "arn" (such as with Fn::GetAtt or Fn::ImportValue)',
@ -84,6 +85,9 @@ class AwsCompileSQSEvents {
return EventSourceArn['Fn::GetAtt'][0];
} else if (EventSourceArn['Fn::ImportValue']) {
return EventSourceArn['Fn::ImportValue'];
} else if (EventSourceArn['Fn::Join']) {
// [0] is the used delimiter, [1] is the array with values
return EventSourceArn['Fn::Join'][1].slice(-1).pop();
}
return EventSourceArn.split(':').pop();
}());

View File

@ -391,18 +391,29 @@ describe('AwsCompileSQSEvents', () => {
arn: { 'Fn::ImportValue': 'ForeignQueue' },
},
},
{
sqs: {
arn: {
'Fn::Join': [
':', [
'arn', 'aws', 'sqs', {
Ref: 'AWS::Region',
},
{
Ref: 'AWS::AccountId',
},
'MyQueue',
],
],
},
},
},
],
},
};
awsCompileSQSEvents.compileSQSEvents();
expect(awsCompileSQSEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.FirstEventSourceMappingSQSSomeQueue.Properties.EventSourceArn
).to.deep.equal(
{ 'Fn::GetAtt': ['SomeQueue', 'Arn'] }
);
expect(awsCompileSQSEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution
.Properties.Policies[0].PolicyDocument.Statement[0]
@ -424,18 +435,62 @@ describe('AwsCompileSQSEvents', () => {
{
'Fn::ImportValue': 'ForeignQueue',
},
{
'Fn::Join': [
':',
[
'arn',
'aws',
'sqs',
{
Ref: 'AWS::Region',
},
{
Ref: 'AWS::AccountId',
},
'MyQueue',
],
],
},
],
}
);
expect(awsCompileSQSEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.FirstEventSourceMappingSQSSomeQueue.Properties.EventSourceArn
).to.deep.equal(
{ 'Fn::GetAtt': ['SomeQueue', 'Arn'] }
);
expect(awsCompileSQSEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.FirstEventSourceMappingSQSForeignQueue.Properties.EventSourceArn
).to.deep.equal(
{ 'Fn::ImportValue': 'ForeignQueue' }
);
expect(awsCompileSQSEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.FirstEventSourceMappingSQSMyQueue.Properties.EventSourceArn
).to.deep.equal(
{
'Fn::Join': [
':',
[
'arn',
'aws',
'sqs',
{
Ref: 'AWS::Region',
},
{
Ref: 'AWS::AccountId',
},
'MyQueue',
],
],
});
});
it('fails if keys other than Fn::GetAtt/ImportValue are used for dynamic queue ARN', () => {
it('fails if keys other than Fn::GetAtt/ImportValue/Join are used for dynamic ARNs', () => {
awsCompileSQSEvents.serverless.service.functions = {
first: {
events: [

View File

@ -9,6 +9,7 @@ const compilePermissions = require('./lib/permissions');
const compileRoutes = require('./lib/routes');
const compileDeployment = require('./lib/deployment');
const compileStage = require('./lib/stage');
const compileAuthorizers = require('./lib/authorizers');
class AwsCompileWebsockets {
constructor(serverless, options) {
@ -21,6 +22,7 @@ class AwsCompileWebsockets {
validate,
compileApi,
compileIntegrations,
compileAuthorizers,
compilePermissions,
compileRoutes,
compileDeployment,
@ -38,6 +40,7 @@ class AwsCompileWebsockets {
return BbPromise.bind(this)
.then(this.compileApi)
.then(this.compileIntegrations)
.then(this.compileAuthorizers)
.then(this.compilePermissions)
.then(this.compileRoutes)
.then(this.compileDeployment)

View File

@ -35,6 +35,7 @@ describe('AwsCompileWebsocketsEvents', () => {
describe('#constructor()', () => {
let compileApiStub;
let compileIntegrationsStub;
let compileAuthorizersStub;
let compilePermissionsStub;
let compileRoutesStub;
let compileDeploymentStub;
@ -45,6 +46,8 @@ describe('AwsCompileWebsocketsEvents', () => {
.stub(awsCompileWebsocketsEvents, 'compileApi').resolves();
compileIntegrationsStub = sinon
.stub(awsCompileWebsocketsEvents, 'compileIntegrations').resolves();
compileAuthorizersStub = sinon
.stub(awsCompileWebsocketsEvents, 'compileAuthorizers').resolves();
compilePermissionsStub = sinon
.stub(awsCompileWebsocketsEvents, 'compilePermissions').resolves();
compileRoutesStub = sinon
@ -58,6 +61,7 @@ describe('AwsCompileWebsocketsEvents', () => {
afterEach(() => {
awsCompileWebsocketsEvents.compileApi.restore();
awsCompileWebsocketsEvents.compileIntegrations.restore();
awsCompileWebsocketsEvents.compileAuthorizers.restore();
awsCompileWebsocketsEvents.compilePermissions.restore();
awsCompileWebsocketsEvents.compileRoutes.restore();
awsCompileWebsocketsEvents.compileDeployment.restore();
@ -91,7 +95,8 @@ describe('AwsCompileWebsocketsEvents', () => {
expect(validateStub.calledOnce).to.be.equal(true);
expect(compileApiStub.calledAfter(validateStub)).to.be.equal(true);
expect(compileIntegrationsStub.calledAfter(compileApiStub)).to.be.equal(true);
expect(compilePermissionsStub.calledAfter(compileIntegrationsStub)).to.be.equal(true);
expect(compileAuthorizersStub.calledAfter(compileIntegrationsStub)).to.be.equal(true);
expect(compilePermissionsStub.calledAfter(compileAuthorizersStub)).to.be.equal(true);
expect(compileRoutesStub.calledAfter(compilePermissionsStub)).to.be.equal(true);
expect(compileDeploymentStub.calledAfter(compileRoutesStub)).to.be.equal(true);
expect(compileStageStub.calledAfter(compileDeploymentStub)).to.be.equal(true);

View File

@ -23,20 +23,25 @@ module.exports = {
},
});
// insert policy that allows functions to postToConnection
const websocketsPolicy = {
Effect: 'Allow',
Action: ['execute-api:ManageConnections'],
Resource: ['arn:aws:execute-api:*:*:*/@connections/*'],
};
const defaultRoleResource = this.serverless.service.provider.compiledCloudFormationTemplate
.Resources[this.provider.naming.getRoleLogicalId()];
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources[this.provider.naming.getRoleLogicalId()]
.Properties
.Policies[0]
.PolicyDocument
.Statement
.push(websocketsPolicy);
if (defaultRoleResource) {
// insert policy that allows functions to postToConnection
const websocketsPolicy = {
Effect: 'Allow',
Action: ['execute-api:ManageConnections'],
Resource: ['arn:aws:execute-api:*:*:*/@connections/*'],
};
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources[this.provider.naming.getRoleLogicalId()]
.Properties
.Policies[0]
.PolicyDocument
.Statement
.push(websocketsPolicy);
}
return BbPromise.resolve();
},

View File

@ -77,4 +77,17 @@ describe('#compileApi()', () => {
},
});
}));
it('should NOT add the websockets policy if role resource does not exist', () => {
awsCompileWebsocketsEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources = {};
return awsCompileWebsocketsEvents
.compileApi().then(() => {
const resources = awsCompileWebsocketsEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources;
expect(resources[roleLogicalId]).to.deep.equal(undefined);
});
});
});

View File

@ -0,0 +1,33 @@
'use strict';
const _ = require('lodash');
const BbPromise = require('bluebird');
module.exports = {
compileAuthorizers() {
this.validated.events.forEach(event => {
if (!event.authorizer) {
return;
}
const websocketsAuthorizerLogicalId = this.provider.naming
.getWebsocketsAuthorizerLogicalId(event.authorizer.name);
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[websocketsAuthorizerLogicalId]: {
Type: 'AWS::ApiGatewayV2::Authorizer',
Properties: {
ApiId: {
Ref: this.websocketsApiLogicalId,
},
Name: event.authorizer.name,
AuthorizerType: 'REQUEST',
AuthorizerUri: event.authorizer.uri,
IdentitySource: event.authorizer.identitySource,
},
},
});
});
return BbPromise.resolve();
},
};

View File

@ -0,0 +1,109 @@
'use strict';
const expect = require('chai').expect;
const AwsCompileWebsocketsEvents = require('../index');
const Serverless = require('../../../../../../../Serverless');
const AwsProvider = require('../../../../../provider/awsProvider');
describe('#compileAuthorizers()', () => {
let awsCompileWebsocketsEvents;
beforeEach(() => {
const serverless = new Serverless();
serverless.setProvider('aws', new AwsProvider(serverless));
serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} };
awsCompileWebsocketsEvents = new AwsCompileWebsocketsEvents(serverless);
awsCompileWebsocketsEvents.websocketsApiLogicalId
= awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId();
});
it('should create an authorizer resource for routes with authorizer definition', () => {
awsCompileWebsocketsEvents.validated = {
events: [
{
functionName: 'First',
route: '$connect',
authorizer: {
name: 'auth',
uri: {
'Fn::Join': ['',
[
'arn:',
{ Ref: 'AWS::Partition' },
':apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
{ 'Fn::GetAtt': ['AuthLambdaFunction', 'Arn'] },
'/invocations',
],
],
},
identitySource: ['route.request.header.Auth'],
},
},
],
};
return awsCompileWebsocketsEvents.compileAuthorizers().then(() => {
const resources = awsCompileWebsocketsEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources;
expect(resources).to.deep.equal({
AuthWebsocketsAuthorizer: {
Type: 'AWS::ApiGatewayV2::Authorizer',
Properties: {
ApiId: {
Ref: 'WebsocketsApi',
},
Name: 'auth',
AuthorizerType: 'REQUEST',
AuthorizerUri: {
'Fn::Join': [
'',
[
'arn:',
{
Ref: 'AWS::Partition',
},
':apigateway:',
{
Ref: 'AWS::Region',
},
':lambda:path/2015-03-31/functions/',
{
'Fn::GetAtt': [
'AuthLambdaFunction',
'Arn',
],
},
'/invocations',
],
],
},
IdentitySource: ['route.request.header.Auth'],
},
},
});
});
});
it('should NOT create an authorizer resource for routes with not authorizer definition', () => {
awsCompileWebsocketsEvents.validated = {
events: [
{
functionName: 'First',
route: '$connect',
},
],
};
return awsCompileWebsocketsEvents.compileAuthorizers().then(() => {
const resources = awsCompileWebsocketsEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources;
expect(resources).to.deep.equal({});
});
});
});

View File

@ -24,6 +24,38 @@ module.exports = {
},
},
});
if (event.authorizer) {
const websocketsAuthorizerPermissionLogicalId = this.provider.naming
.getLambdaWebsocketsPermissionLogicalId(event.authorizer.name);
const authorizerPermissionTemplate = {
[websocketsAuthorizerPermissionLogicalId]: {
Type: 'AWS::Lambda::Permission',
DependsOn: [this.websocketsApiLogicalId],
Properties: {
Action: 'lambda:InvokeFunction',
Principal: { 'Fn::Join': ['', ['apigateway.', { Ref: 'AWS::URLSuffix' }]] },
},
},
};
if (event.authorizer.permission.includes(':')) {
authorizerPermissionTemplate[websocketsAuthorizerPermissionLogicalId]
.Properties.FunctionName = event.authorizer.permission;
} else {
authorizerPermissionTemplate[websocketsAuthorizerPermissionLogicalId]
.Properties.FunctionName = {
'Fn::GetAtt': [event.authorizer.permission, 'Arn'],
};
authorizerPermissionTemplate[websocketsAuthorizerPermissionLogicalId]
.DependsOn.push(event.authorizer.permission);
}
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
authorizerPermissionTemplate);
}
});
return BbPromise.resolve();

View File

@ -94,4 +94,80 @@ describe('#compilePermissions()', () => {
});
});
});
it('should create a permission resource for authorizer function', () => {
awsCompileWebsocketsEvents.validated = {
events: [
{
functionName: 'First',
route: '$connect',
authorizer: {
name: 'auth',
permission: 'AuthLambdaPermissionWebsockets',
},
},
],
};
return awsCompileWebsocketsEvents.compilePermissions().then(() => {
const resources = awsCompileWebsocketsEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources;
expect(resources).to.deep.equal({
FirstLambdaPermissionWebsockets: {
Type: 'AWS::Lambda::Permission',
DependsOn: [
'WebsocketsApi',
'FirstLambdaFunction',
],
Properties: {
FunctionName: {
'Fn::GetAtt': [
'FirstLambdaFunction', 'Arn',
],
},
Action: 'lambda:InvokeFunction',
Principal: {
'Fn::Join': [
'',
[
'apigateway.',
{
Ref: 'AWS::URLSuffix',
},
],
],
},
},
},
AuthLambdaPermissionWebsockets: {
Type: 'AWS::Lambda::Permission',
DependsOn: [
'WebsocketsApi',
'AuthLambdaPermissionWebsockets',
],
Properties: {
FunctionName: {
'Fn::GetAtt': [
'AuthLambdaPermissionWebsockets',
'Arn',
],
},
Action: 'lambda:InvokeFunction',
Principal: {
'Fn::Join': [
'',
[
'apigateway.',
{
Ref: 'AWS::URLSuffix',
},
],
],
},
},
},
});
});
});
});

View File

@ -12,7 +12,7 @@ module.exports = {
const websocketsRouteLogicalId = this.provider.naming
.getWebsocketsRouteLogicalId(event.route);
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
const routeTemplate = {
[websocketsRouteLogicalId]: {
Type: 'AWS::ApiGatewayV2::Route',
Properties: {
@ -31,7 +31,17 @@ module.exports = {
},
},
},
});
};
if (event.authorizer) {
routeTemplate[websocketsRouteLogicalId].Properties.AuthorizationType = 'CUSTOM';
routeTemplate[websocketsRouteLogicalId].Properties.AuthorizerId = {
Ref: this.provider.naming
.getWebsocketsAuthorizerLogicalId(event.authorizer.name),
};
}
_.merge(this.serverless.service.provider
.compiledCloudFormationTemplate.Resources, routeTemplate);
});
return BbPromise.resolve();

View File

@ -82,4 +82,50 @@ describe('#compileRoutes()', () => {
});
});
});
it('should set authorizer property for the connect route', () => {
awsCompileWebsocketsEvents.validated = {
events: [
{
functionName: 'First',
route: '$connect',
authorizer: {
name: 'auth',
},
},
],
};
return awsCompileWebsocketsEvents.compileRoutes().then(() => {
const resources = awsCompileWebsocketsEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources;
expect(resources).to.deep.equal({
SconnectWebsocketsRoute: {
Type: 'AWS::ApiGatewayV2::Route',
Properties: {
ApiId: {
Ref: 'WebsocketsApi',
},
RouteKey: '$connect',
AuthorizationType: 'CUSTOM',
AuthorizerId: {
Ref: awsCompileWebsocketsEvents.provider.naming
.getWebsocketsAuthorizerLogicalId('auth'),
},
Target: {
'Fn::Join': [
'/',
[
'integrations', {
Ref: 'FirstWebsocketsIntegration',
},
],
],
},
},
},
});
});
});
});

View File

@ -6,6 +6,11 @@ module.exports = {
validate() {
const events = [];
const getAuthorizerNameFromArn = (arn) => {
const splitArn = arn.split(':');
return splitArn[splitArn.length - 1];
};
_.forEach(this.serverless.service.functions, (functionObject, functionName) => {
_.forEach(functionObject.events, (event) => {
// check if we have both, `http` and `websocket` events which is not supported
@ -20,10 +25,105 @@ module.exports = {
const errorMessage = 'You need to set the "route" when using the websocket event.';
throw new this.serverless.classes.Error(errorMessage);
}
events.push({
const websocketObj = {
functionName,
route: event.websocket.route,
});
};
// authorizers
if (_.isString(event.websocket.authorizer)) {
if (event.websocket.authorizer.includes(':')) { // arn
websocketObj.authorizer = {
name: getAuthorizerNameFromArn(event.websocket.authorizer),
uri: {
'Fn::Join': ['',
[
'arn:',
{ Ref: 'AWS::Partition' },
':apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
event.websocket.authorizer,
'/invocations',
],
],
},
identitySource: ['route.request.header.Auth'],
permission: event.websocket.authorizer,
};
} else { // reference function
const lambdaLogicalId = this.provider.naming
.getLambdaLogicalId(event.websocket.authorizer);
websocketObj.authorizer = {
name: event.websocket.authorizer,
uri: {
'Fn::Join': ['',
[
'arn:',
{ Ref: 'AWS::Partition' },
':apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
{ 'Fn::GetAtt': [lambdaLogicalId, 'Arn'] },
'/invocations',
],
],
},
identitySource: ['route.request.header.Auth'],
permission: lambdaLogicalId,
};
}
} else if (_.isObject(event.websocket.authorizer)) {
websocketObj.authorizer = {};
if (event.websocket.authorizer.arn) {
websocketObj.authorizer.name =
getAuthorizerNameFromArn(event.websocket.authorizer.arn);
websocketObj.authorizer.uri = {
'Fn::Join': ['',
[
'arn:',
{ Ref: 'AWS::Partition' },
':apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
event.websocket.authorizer.arn,
'/invocations',
],
],
};
websocketObj.authorizer.permission = event.websocket.authorizer.arn;
} else if (event.websocket.authorizer.name) {
websocketObj.authorizer.name = event.websocket.authorizer.name;
const lambdaLogicalId = this.provider.naming
.getLambdaLogicalId(event.websocket.authorizer.name);
websocketObj.authorizer.uri = {
'Fn::Join': ['',
[
'arn:',
{ Ref: 'AWS::Partition' },
':apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
{ 'Fn::GetAtt': [lambdaLogicalId, 'Arn'] },
'/invocations',
],
],
};
websocketObj.authorizer.permission = lambdaLogicalId;
} else {
const errorMessage =
'You must specify name or arn properties when using a websocket authorizer';
throw new this.serverless.classes.Error(errorMessage);
}
if (!event.websocket.authorizer.identitySource) {
websocketObj.authorizer.identitySource = ['route.request.header.Auth'];
} else {
websocketObj.authorizer.identitySource = event.websocket.authorizer.identitySource;
}
}
events.push(websocketObj);
// dealing with the simplified string representation
} else if (_.isString(event.websocket)) {
events.push({

View File

@ -59,6 +59,172 @@ describe('#validate()', () => {
]);
});
it('should add authorizer config when authorizer is specified as a string', () => {
awsCompileWebsocketsEvents.serverless.service.functions = {
first: {
events: [
{
websocket: {
route: '$connect',
authorizer: 'auth',
},
},
],
},
};
const validated = awsCompileWebsocketsEvents.validate();
expect(validated.events).to.deep.equal([
{
functionName: 'first',
route: '$connect',
authorizer: {
name: 'auth',
uri: {
'Fn::Join': ['',
[
'arn:',
{ Ref: 'AWS::Partition' },
':apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
{ 'Fn::GetAtt': ['AuthLambdaFunction', 'Arn'] },
'/invocations',
],
],
},
identitySource: ['route.request.header.Auth'],
permission: 'AuthLambdaFunction',
},
},
]);
});
it('should add authorizer config when authorizer is specified as a string with arn', () => {
awsCompileWebsocketsEvents.serverless.service.functions = {
first: {
events: [
{
websocket: {
route: '$connect',
authorizer: 'arn:aws:auth',
},
},
],
},
};
const validated = awsCompileWebsocketsEvents.validate();
expect(validated.events).to.deep.equal([
{
functionName: 'first',
route: '$connect',
authorizer: {
name: 'auth',
uri: {
'Fn::Join': ['',
[
'arn:',
{ Ref: 'AWS::Partition' },
':apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
'arn:aws:auth',
'/invocations',
],
],
},
identitySource: ['route.request.header.Auth'],
permission: 'arn:aws:auth',
},
},
]);
});
it('should add authorizer config when authorizer is specified as an object', () => {
awsCompileWebsocketsEvents.serverless.service.functions = {
first: {
events: [
{
websocket: {
route: '$connect',
authorizer: {
name: 'auth',
identitySource: ['route.request.header.Auth', 'route.request.querystring.Auth'],
},
},
},
],
},
};
const validated = awsCompileWebsocketsEvents.validate();
expect(validated.events).to.deep.equal([
{
functionName: 'first',
route: '$connect',
authorizer: {
name: 'auth',
uri: {
'Fn::Join': ['',
[
'arn:',
{ Ref: 'AWS::Partition' },
':apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
{ 'Fn::GetAtt': ['AuthLambdaFunction', 'Arn'] },
'/invocations',
],
],
},
identitySource: ['route.request.header.Auth', 'route.request.querystring.Auth'],
permission: 'AuthLambdaFunction',
},
},
]);
});
it('should add authorizer config when authorizer is specified as an object with arn', () => {
awsCompileWebsocketsEvents.serverless.service.functions = {
first: {
events: [
{
websocket: {
route: '$connect',
authorizer: {
arn: 'arn:aws:auth',
identitySource: ['route.request.header.Auth', 'route.request.querystring.Auth'],
},
},
},
],
},
};
const validated = awsCompileWebsocketsEvents.validate();
expect(validated.events).to.deep.equal([
{
functionName: 'first',
route: '$connect',
authorizer: {
name: 'auth',
uri: {
'Fn::Join': ['',
[
'arn:',
{ Ref: 'AWS::Partition' },
':apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
'arn:aws:auth',
'/invocations',
],
],
},
identitySource: ['route.request.header.Auth', 'route.request.querystring.Auth'],
permission: 'arn:aws:auth',
},
},
]);
});
it('should ignore non-websocket events', () => {
awsCompileWebsocketsEvents.serverless.service.functions = {
first: {
@ -86,6 +252,24 @@ describe('#validate()', () => {
expect(() => awsCompileWebsocketsEvents.validate()).to.throw(/set the "route"/);
});
it('should reject an authorizer definition without name nor arn', () => {
awsCompileWebsocketsEvents.serverless.service.functions = {
first: {
events: [
{
websocket: {
route: '$connect',
authorizer: {
identitySource: ['route.request.header.Auth', 'route.request.querystring.Auth'],
},
},
},
],
},
};
expect(() => awsCompileWebsocketsEvents.validate()).to.throw(/You must specify name or arn/);
});
it('should reject a usage of both, http and websocket event types', () => {
awsCompileWebsocketsEvents.serverless.service.functions = {
first: {

View File

@ -242,6 +242,48 @@ class AwsCompileFunctions {
}
}
const tracing = functionObject.tracing
|| (this.serverless.service.provider.tracing
&& this.serverless.service.provider.tracing.lambda);
if (tracing) {
if (typeof tracing === 'boolean' || typeof tracing === 'string') {
let mode = tracing;
if (typeof tracing === 'boolean') {
mode = 'Active';
}
const iamRoleLambdaExecution = this.serverless.service.provider
.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution;
newFunction.Properties.TracingConfig = {
Mode: mode,
};
const stmt = {
Effect: 'Allow',
Action: [
'xray:PutTraceSegments',
'xray:PutTelemetryRecords',
],
Resource: ['*'],
};
// update the PolicyDocument statements (if default policy is used)
if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement = _.unionWith(
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement,
[stmt],
_.isEqual
);
}
} else {
const errorMessage = 'tracing requires a boolean value or the "mode" provided as a string';
throw new this.serverless.classes.Error(errorMessage);
}
}
if (functionObject.environment || this.serverless.service.provider.environment) {
newFunction.Properties.Environment = {};
newFunction.Properties.Environment.Variables = Object.assign(
@ -319,10 +361,9 @@ class AwsCompileFunctions {
if (functionObject.layers && _.isArray(functionObject.layers)) {
newFunction.Properties.Layers = functionObject.layers;
/* TODO - is a DependsOn needed?
newLayer.DependsOn = [NEW LAYER??]
.concat(newLayer.DependsOn || []);
*/
} else if (this.serverless.service.provider.layers && _.isArray(
this.serverless.service.provider.layers)) {
newFunction.Properties.Layers = this.serverless.service.provider.layers;
}
const functionLogicalId = this.provider.naming

View File

@ -1286,6 +1286,267 @@ describe('AwsCompileFunctions', () => {
});
});
describe('when using tracing config', () => {
let s3Folder;
let s3FileName;
beforeEach(() => {
s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName;
s3FileName = awsCompileFunctions.serverless.service.package.artifact
.split(path.sep).pop();
});
it('should throw an error if config paramter is not a string', () => {
awsCompileFunctions.serverless.service.functions = {
func: {
handler: 'func.function.handler',
name: 'new-service-dev-func',
tracing: 123,
},
};
return expect(awsCompileFunctions.compileFunctions())
.to.be.rejectedWith('as a string');
});
it('should use a the provider wide tracing config if provided', () => {
Object.assign(awsCompileFunctions.serverless.service.provider, {
tracing: {
lambda: true,
},
});
awsCompileFunctions.serverless.service.functions = {
func: {
handler: 'func.function.handler',
name: 'new-service-dev-func',
},
};
const compiledFunction = {
Type: 'AWS::Lambda::Function',
DependsOn: [
'FuncLogGroup',
'IamRoleLambdaExecution',
],
Properties: {
Code: {
S3Key: `${s3Folder}/${s3FileName}`,
S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
},
FunctionName: 'new-service-dev-func',
Handler: 'func.function.handler',
MemorySize: 1024,
Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
Runtime: 'nodejs4.3',
Timeout: 6,
TracingConfig: {
Mode: 'Active',
},
},
};
return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => {
const compiledCfTemplate = awsCompileFunctions.serverless.service.provider
.compiledCloudFormationTemplate;
const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction;
expect(functionResource).to.deep.equal(compiledFunction);
});
});
it('should prefer a function tracing config over a provider config', () => {
Object.assign(awsCompileFunctions.serverless.service.provider, {
tracing: {
lambda: 'PassThrough',
},
});
awsCompileFunctions.serverless.service.functions = {
func1: {
handler: 'func1.function.handler',
name: 'new-service-dev-func1',
tracing: 'Active',
},
func2: {
handler: 'func2.function.handler',
name: 'new-service-dev-func2',
},
};
const compiledFunction1 = {
Type: 'AWS::Lambda::Function',
DependsOn: [
'Func1LogGroup',
'IamRoleLambdaExecution',
],
Properties: {
Code: {
S3Key: `${s3Folder}/${s3FileName}`,
S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
},
FunctionName: 'new-service-dev-func1',
Handler: 'func1.function.handler',
MemorySize: 1024,
Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
Runtime: 'nodejs4.3',
Timeout: 6,
TracingConfig: {
Mode: 'Active',
},
},
};
const compiledFunction2 = {
Type: 'AWS::Lambda::Function',
DependsOn: [
'Func2LogGroup',
'IamRoleLambdaExecution',
],
Properties: {
Code: {
S3Key: `${s3Folder}/${s3FileName}`,
S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
},
FunctionName: 'new-service-dev-func2',
Handler: 'func2.function.handler',
MemorySize: 1024,
Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
Runtime: 'nodejs4.3',
Timeout: 6,
TracingConfig: {
Mode: 'PassThrough',
},
},
};
return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => {
const compiledCfTemplate = awsCompileFunctions.serverless.service.provider
.compiledCloudFormationTemplate;
const function1Resource = compiledCfTemplate.Resources.Func1LambdaFunction;
const function2Resource = compiledCfTemplate.Resources.Func2LambdaFunction;
expect(function1Resource).to.deep.equal(compiledFunction1);
expect(function2Resource).to.deep.equal(compiledFunction2);
});
});
describe('when IamRoleLambdaExecution is used', () => {
beforeEach(() => {
// pretend that the IamRoleLambdaExecution is used
awsCompileFunctions.serverless.service.provider
.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = {
Properties: {
Policies: [
{
PolicyDocument: {
Statement: [],
},
},
],
},
};
});
it('should create necessary resources if a tracing config is provided', () => {
awsCompileFunctions.serverless.service.functions = {
func: {
handler: 'func.function.handler',
name: 'new-service-dev-func',
tracing: 'Active',
},
};
const compiledFunction = {
Type: 'AWS::Lambda::Function',
DependsOn: [
'FuncLogGroup',
'IamRoleLambdaExecution',
],
Properties: {
Code: {
S3Key: `${s3Folder}/${s3FileName}`,
S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
},
FunctionName: 'new-service-dev-func',
Handler: 'func.function.handler',
MemorySize: 1024,
Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
Runtime: 'nodejs4.3',
Timeout: 6,
TracingConfig: {
Mode: 'Active',
},
},
};
const compiledXrayStatement = {
Effect: 'Allow',
Action: [
'xray:PutTraceSegments',
'xray:PutTelemetryRecords',
],
Resource: ['*'],
};
return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => {
const compiledCfTemplate = awsCompileFunctions.serverless.service.provider
.compiledCloudFormationTemplate;
const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction;
const xrayStatement = compiledCfTemplate.Resources
.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement[0];
expect(functionResource).to.deep.equal(compiledFunction);
expect(xrayStatement).to.deep.equal(compiledXrayStatement);
});
});
});
describe('when IamRoleLambdaExecution is not used', () => {
it('should create necessary resources if a tracing config is provided', () => {
awsCompileFunctions.serverless.service.functions = {
func: {
handler: 'func.function.handler',
name: 'new-service-dev-func',
tracing: 'PassThrough',
},
};
const compiledFunction = {
Type: 'AWS::Lambda::Function',
DependsOn: [
'FuncLogGroup',
'IamRoleLambdaExecution',
],
Properties: {
Code: {
S3Key: `${s3Folder}/${s3FileName}`,
S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
},
FunctionName: 'new-service-dev-func',
Handler: 'func.function.handler',
MemorySize: 1024,
Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
Runtime: 'nodejs4.3',
Timeout: 6,
TracingConfig: {
Mode: 'PassThrough',
},
},
};
return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => {
const compiledCfTemplate = awsCompileFunctions.serverless.service.provider
.compiledCloudFormationTemplate;
const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction;
expect(functionResource).to.deep.equal(compiledFunction);
});
});
});
});
it('should create a function resource with environment config', () => {
const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName;
const s3FileName = awsCompileFunctions.serverless.service.package.artifact

View File

@ -485,6 +485,14 @@ class AwsProvider {
return { Ref: this.naming.getRestApiLogicalId() };
}
getApiGatewayDescription() {
if (this.serverless.service.provider.apiGateway
&& this.serverless.service.provider.apiGateway.description) {
return this.serverless.service.provider.apiGateway.description;
}
return undefined;
}
getMethodArn(accountId, apiId, method, pathParam) {
const region = this.getRegion();
let path = pathParam;

View File

@ -1,4 +1,5 @@
import * as Ask from 'ask-sdk';
import 'source-map-support/register';
export const alexa = Ask.SkillBuilders.custom()
.addRequestHandlers({

View File

@ -7,16 +7,16 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"ask-sdk": "^2.0.7"
"ask-sdk": "^2.3.0",
"source-map-support": "^0.5.10"
},
"devDependencies": {
"@types/node": "^8.0.57",
"@types/node": "^10.12.18",
"serverless-alexa-skills": "^0.1.0",
"serverless-webpack": "^5.1.1",
"source-map-support": "^0.5.6",
"ts-loader": "^4.2.0",
"typescript": "^2.9.2",
"webpack": "^4.5.0"
"serverless-webpack": "^5.2.0",
"ts-loader": "^5.3.3",
"typescript": "^3.2.4",
"webpack": "^4.29.0"
},
"author":
"The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)",

View File

@ -1 +0,0 @@
require('source-map-support').install();

View File

@ -1,15 +1,9 @@
const path = require('path');
const slsw = require('serverless-webpack');
const entries = {};
Object.keys(slsw.lib.entries).forEach(
key => (entries[key] = ['./source-map-install.js', slsw.lib.entries[key]])
);
module.exports = {
mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
entry: entries,
entry: slsw.lib.entries,
devtool: 'source-map',
resolve: {
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],

View File

@ -8,42 +8,66 @@
<name>hello</name>
<properties>
<kotlin.version>1.1.4-3</kotlin.version>
<kotlin.version>1.3.21</kotlin.version>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.9.8</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.1.0</version>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-log4j</artifactId>
<version>1.0.0</version>
<artifactId>aws-lambda-java-log4j2</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.11.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
</dependencies>
@ -66,6 +90,11 @@
<version>2.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer
implementation="com.github.edwgiz.mavenShadePlugin.log4j2CacheTransformer.PluginsCacheFileTransformer">
</transformer>
</transformers>
</configuration>
<executions>
<execution>
@ -75,6 +104,13 @@
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.github.edwgiz</groupId>
<artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId>
<version>2.8.1</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>

View File

@ -2,7 +2,8 @@ package com.serverless
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import org.apache.log4j.Logger
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import java.nio.charset.StandardCharsets
import java.util.*
@ -24,7 +25,7 @@ class ApiGatewayResponse(
}
class Builder {
var LOG: Logger = Logger.getLogger(ApiGatewayResponse.Builder::class.java)
var LOG: Logger = LogManager.getLogger(ApiGatewayResponse.Builder::class.java)
var objectMapper: ObjectMapper = ObjectMapper()
var statusCode: Int = 200

View File

@ -2,13 +2,11 @@ package com.serverless
import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
import org.apache.log4j.BasicConfigurator
import org.apache.log4j.Logger
import org.apache.logging.log4j.LogManager
import java.util.*
class Handler:RequestHandler<Map<String, Any>, ApiGatewayResponse> {
override fun handleRequest(input:Map<String, Any>, context:Context):ApiGatewayResponse {
BasicConfigurator.configure()
LOG.info("received: " + input.keys.toString())
val responseBody = Response("Go Serverless v1.x! Your Kotlin function executed successfully!", input)
@ -19,6 +17,6 @@ class Handler:RequestHandler<Map<String, Any>, ApiGatewayResponse> {
}
}
companion object {
private val LOG = Logger.getLogger(Handler::class.java)
private val LOG = LogManager.getLogger(Handler::class.java)
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration packages="com.amazonaws.services.lambda.runtime.log4j2">
<Appenders>
<Lambda name="Lambda">
<PatternLayout>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n</pattern>
</PatternLayout>
</Lambda>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Lambda"/>
</Root>
</Loggers>
</Configuration>

View File

@ -1,4 +1,5 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import 'source-map-support/register';
export const hello: APIGatewayProxyHandler = async (event, _context) => {
return {

View File

@ -7,15 +7,15 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"source-map-support": "^0.5.10"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.17",
"@types/node": "^8.0.57",
"serverless-webpack": "^5.1.1",
"source-map-support": "^0.5.6",
"ts-loader": "^4.2.0",
"typescript": "^2.9.2",
"webpack": "^4.5.0"
"@types/node": "^10.12.18",
"serverless-webpack": "^5.2.0",
"ts-loader": "^5.3.3",
"typescript": "^3.2.4",
"webpack": "^4.29.0"
},
"author":
"The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)",

View File

@ -1 +0,0 @@
require('source-map-support').install();

View File

@ -1,15 +1,9 @@
const path = require('path');
const slsw = require('serverless-webpack');
const entries = {};
Object.keys(slsw.lib.entries).forEach(
key => (entries[key] = ['./source-map-install.js', slsw.lib.entries[key]])
);
module.exports = {
mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
entry: entries,
entry: slsw.lib.entries,
devtool: 'source-map',
resolve: {
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],

View File

@ -87,7 +87,9 @@ class Invoke {
usage: 'Override environment variables. e.g. --env VAR1=val1 --env VAR2=val2',
shortcut: 'e',
},
docker: { usage: 'Flag to turn on docker use for node/python/ruby/java' },
},
},
},
},

View File

@ -97,7 +97,9 @@ module.exports = {
*/
const filesToChmodPlusX = process.platform !== 'win32' ? [] :
Object.values(this.serverless.service.functions)
.map(f => Object.assign({ runtime: this.serverless.service.provider.runtime }, f))
.map(f => Object.assign({
runtime: this.serverless.service.provider.runtime || 'node8.10',
}, f))
.filter(f => f.runtime && f.runtime.startsWith('go'))
.map(f => f.handler);
@ -137,7 +139,8 @@ module.exports = {
const zipFileName = `${functionName}.zip`;
const filesToChmodPlusX = [];
if (process.platform === 'win32') {
const runtime = functionName.runtime || this.serverless.service.provider.runtime;
const runtime = functionName.runtime || this.serverless.service.provider.runtime
|| 'node8.10';
if (runtime.startsWith('go')) {
filesToChmodPlusX.push(functionObject.handler);
}

View File

@ -347,6 +347,28 @@ describe('#packageService()', () => {
serverless.config.servicePath = servicePath;
serverless.service.provider.runtime = 'go1.x';
return expect(packagePlugin.packageService()).to.be.fulfilled
.then(() => BbPromise.all([
expect(getExcludesStub).to.be.calledOnce,
expect(getIncludesStub).to.be.calledOnce,
expect(resolveFilePathsFromPatternsStub).to.be.calledOnce,
expect(zipFilesStub).to.be.calledOnce,
expect(zipFilesStub).to.have.been.calledWithExactly(
files,
zipFileName,
undefined,
['foo']
),
]));
});
(process.platfrom === 'win32' ? it : it.skip)(
'should call zipService with settings & no binaries to chmod for non-go on win32', () => {
const servicePath = 'test';
const zipFileName = `${serverless.service.service}.zip`;
serverless.config.servicePath = servicePath;
return expect(packagePlugin.packageService()).to.be.fulfilled
.then(() => BbPromise.all([
expect(getExcludesStub).to.be.calledOnce,

28
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "serverless",
"version": "1.38.0",
"version": "1.39.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1166,6 +1166,11 @@
"unset-value": "^1.0.0"
}
},
"cachedir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.1.0.tgz",
"integrity": "sha512-xGBpPqoBvn3unBW7oxgb8aJn42K0m9m1/wyjmazah10Fq7bROGG3kRAE6OIyr3U3PIJUqGuebhCEdMk9OKJG0A=="
},
"caller-id": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/caller-id/-/caller-id-0.1.0.tgz",
@ -3735,8 +3740,7 @@
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
"dev": true
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"import-lazy": {
"version": "2.1.0",
@ -4984,7 +4988,6 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz",
"integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==",
"dev": true,
"requires": {
"core-js": "~2.3.0",
"es6-promise": "~3.0.2",
@ -4996,26 +4999,22 @@
"core-js": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
"integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=",
"dev": true
"integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU="
},
"es6-promise": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
"integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=",
"dev": true
"integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y="
},
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
"dev": true
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
},
"readable-stream": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
"integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
@ -5028,8 +5027,7 @@
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
@ -5123,7 +5121,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
"dev": true,
"requires": {
"immediate": "~3.0.5"
}
@ -6084,8 +6081,7 @@
"pako": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz",
"integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==",
"dev": true
"integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA=="
},
"parse-github-url": {
"version": "1.0.2",

View File

@ -1,6 +1,6 @@
{
"name": "serverless",
"version": "1.38.0",
"version": "1.39.1",
"engines": {
"node": ">=4.0"
},
@ -77,7 +77,6 @@
"eslint-plugin-react": "^6.1.1",
"istanbul": "^0.4.4",
"jest-cli": "^23.1.0",
"jszip": "^3.1.2",
"markdown-link": "^0.1.1",
"markdown-magic": "^0.1.19",
"markdown-table": "^1.1.1",
@ -91,10 +90,12 @@
"sinon-chai": "^2.9.0"
},
"dependencies": {
"jszip": "^3.1.2",
"archiver": "^1.1.0",
"async": "^1.5.2",
"aws-sdk": "^2.373.0",
"bluebird": "^3.5.0",
"cachedir": "^2.1.0",
"chalk": "^2.0.0",
"ci-info": "^1.1.1",
"download": "^5.0.2",
@ -112,6 +113,7 @@
"jwt-decode": "^2.2.0",
"lodash": "^4.13.1",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"moment": "^2.13.0",
"nanomatch": "^1.2.13",
"node-fetch": "^1.6.0",