mirror of
https://github.com/serverless/serverless.git
synced 2026-01-25 15:07:39 +00:00
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:
commit
e9cfc881ec
30
CHANGELOG.md
30
CHANGELOG.md
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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}'
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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}`));
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
'{}',
|
||||
]]);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 &&
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 = '';
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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();
|
||||
}());
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
},
|
||||
};
|
||||
@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import * as Ask from 'ask-sdk';
|
||||
import 'source-map-support/register';
|
||||
|
||||
export const alexa = Ask.SkillBuilders.custom()
|
||||
.addRequestHandlers({
|
||||
|
||||
@ -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)",
|
||||
|
||||
@ -1 +0,0 @@
|
||||
require('source-map-support').install();
|
||||
@ -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'],
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -1,4 +1,5 @@
|
||||
import { APIGatewayProxyHandler } from 'aws-lambda';
|
||||
import 'source-map-support/register';
|
||||
|
||||
export const hello: APIGatewayProxyHandler = async (event, _context) => {
|
||||
return {
|
||||
|
||||
@ -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)",
|
||||
|
||||
@ -1 +0,0 @@
|
||||
require('source-map-support').install();
|
||||
@ -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'],
|
||||
|
||||
@ -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' },
|
||||
},
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
28
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user