diff --git a/CHANGELOG.md b/CHANGELOG.md index f02e8f0a4..7f8c6b693 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/README.md b/README.md index 8808fb534..ffb80b3cd 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/getting-started.md b/docs/getting-started.md index 981aef5ba..7415973f9 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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
-
-
- - - + +
+
+ +
+
+ Amazon Web Services
Quick Start Guide
+
-
- Amazon Web Services
Quick Start Guide
+ + +
+
+ +
+
+ Azure Functions
Quick Start Guide
+
-
-
-
- - - + + +
+
+ +
+
+ Apache OpenWhisk
Quick Start Guide
+
-
-
-
- - - + + +
+
+ +
+
+ Kubeless
Quick Start Guide
+
-
-
- diff --git a/docs/providers/aws/cli-reference/invoke-local.md b/docs/providers/aws/cli-reference/invoke-local.md index 120aa51a4..6597cdf24 100644 --- a/docs/providers/aws/cli-reference/invoke-local.md +++ b/docs/providers/aws/cli-reference/invoke-local.md @@ -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 `=`. 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. diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md index 3449c6a83..62f2f50e0 100644 --- a/docs/providers/aws/events/apigateway.md +++ b/docs/providers/aws/events/apigateway.md @@ -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 diff --git a/docs/providers/aws/events/cloudwatch-event.md b/docs/providers/aws/events/cloudwatch-event.md index 5ffbd64bd..fc9d32dfb 100644 --- a/docs/providers/aws/events/cloudwatch-event.md +++ b/docs/providers/aws/events/cloudwatch-event.md @@ -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": , "key1": "value1"}' ``` ## Specifying a Description diff --git a/docs/providers/aws/events/schedule.md b/docs/providers/aws/events/schedule.md index a74c5a3d3..162c276a9 100644 --- a/docs/providers/aws/events/schedule.md +++ b/docs/providers/aws/events/schedule.md @@ -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": , "key1": "value1"}' ``` ## Specify Name and Description diff --git a/docs/providers/aws/events/sqs.md b/docs/providers/aws/events/sqs.md index 8ebb23f9c..77c5041f9 100644 --- a/docs/providers/aws/events/sqs.md +++ b/docs/providers/aws/events/sqs.md @@ -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 diff --git a/docs/providers/aws/events/websocket.md b/docs/providers/aws/events/websocket.md index 849cc20ae..320df6bc6 100644 --- a/docs/providers/aws/events/websocket.md +++ b/docs/providers/aws/events/websocket.md @@ -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 }; } -``` \ No newline at end of file +``` diff --git a/docs/providers/aws/guide/functions.md b/docs/providers/aws/guide/functions.md index 9a3ae2266..ecdfe09ed 100644 --- a/docs/providers/aws/guide/functions.md +++ b/docs/providers/aws/guide/functions.md @@ -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 +``` diff --git a/docs/providers/aws/guide/resources.md b/docs/providers/aws/guide/resources.md index 9841a71fd..4384818fd 100644 --- a/docs/providers/aws/guide/resources.md +++ b/docs/providers/aws/guide/resources.md @@ -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. diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index 1a44a8e0e..a2f3f49a7 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -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": , "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": , "key1": "value1"}' - cloudwatchLog: logGroup: '/aws/lambda/hello' filter: '{$.userIdentity.type = Root}' diff --git a/lib/Serverless.js b/lib/Serverless.js index a547d1001..8332e96e5 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -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) diff --git a/lib/classes/Error.js b/lib/classes/Error.js index ef18c16ff..cd66e850c 100644 --- a/lib/classes/Error.js +++ b/lib/classes/Error.js @@ -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}`)); diff --git a/lib/plugins/aws/info/getStackInfo.js b/lib/plugins/aws/info/getStackInfo.js index 1494214de..71f803fbe 100644 --- a/lib/plugins/aws/info/getStackInfo.js +++ b/lib/plugins/aws/info/getStackInfo.js @@ -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); }); diff --git a/lib/plugins/aws/info/getStackInfo.test.js b/lib/plugins/aws/info/getStackInfo.test.js index b980407b4..096e71ea5 100644 --- a/lib/plugins/aws/info/getStackInfo.test.js +++ b/lib/plugins/aws/info/getStackInfo.test.js @@ -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: [ diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js index bf32955ca..26140da9a 100644 --- a/lib/plugins/aws/invokeLocal/index.js +++ b/lib/plugins/aws/invokeLocal/index.js @@ -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) diff --git a/lib/plugins/aws/invokeLocal/index.test.js b/lib/plugins/aws/invokeLocal/index.test.js index c8bba83f4..c88c8ec31 100644 --- a/lib/plugins/aws/invokeLocal/index.test.js +++ b/lib/plugins/aws/invokeLocal/index.test.js @@ -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', + '{}', + ]]); + }) + ); + }); }); diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js index 1b747305e..4977039da 100644 --- a/lib/plugins/aws/lib/naming.js +++ b/lib/plugins/aws/lib/naming.js @@ -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 && diff --git a/lib/plugins/aws/lib/naming.test.js b/lib/plugins/aws/lib/naming.test.js index 643981306..bff6a8408 100644 --- a/lib/plugins/aws/lib/naming.test.js +++ b/lib/plugins/aws/lib/naming.test.js @@ -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'; diff --git a/lib/plugins/aws/lib/normalizeFiles.js b/lib/plugins/aws/lib/normalizeFiles.js index a8df6ac3b..e0150cc18 100644 --- a/lib/plugins/aws/lib/normalizeFiles.js +++ b/lib/plugins/aws/lib/normalizeFiles.js @@ -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 = ''; diff --git a/lib/plugins/aws/lib/normalizeFiles.test.js b/lib/plugins/aws/lib/normalizeFiles.test.js index de1020f09..8f1f25dd3 100644 --- a/lib/plugins/aws/lib/normalizeFiles.test.js +++ b/lib/plugins/aws/lib/normalizeFiles.test.js @@ -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: { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js index d88a18088..57e16cc05 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js @@ -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, }, diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js index ed9f40acc..a3354e05d 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js @@ -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( diff --git a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js index 6f7fd8acb..9f8fff08d 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js @@ -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; diff --git a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js index 0dd5123a2..e7decdb2e 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js @@ -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": , "key1": "value1"}', + }, + }, + }, + ], + }, + }; + + awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents(); + + expect(awsCompileCloudWatchEventEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1 + .Properties.Targets[0].InputTransformer + ).to.eql({ + InputTemplate: '{"time": , "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": , "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: { diff --git a/lib/plugins/aws/package/compile/events/schedule/index.js b/lib/plugins/aws/package/compile/events/schedule/index.js index 2639509ae..e44e53506 100644 --- a/lib/plugins/aws/package/compile/events/schedule/index.js +++ b/lib/plugins/aws/package/compile/events/schedule/index.js @@ -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; diff --git a/lib/plugins/aws/package/compile/events/schedule/index.test.js b/lib/plugins/aws/package/compile/events/schedule/index.test.js index 09745ef20..1be50220e 100644 --- a/lib/plugins/aws/package/compile/events/schedule/index.test.js +++ b/lib/plugins/aws/package/compile/events/schedule/index.test.js @@ -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": , "key1": "value1"}', + }, + }, + }, + ], + }, + }; + + awsCompileScheduledEvents.compileScheduledEvents(); + + expect(awsCompileScheduledEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleSchedule1 + .Properties.Targets[0].InputTransformer + ).to.eql({ + InputTemplate: '{"time": , "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": , "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: { diff --git a/lib/plugins/aws/package/compile/events/sqs/index.js b/lib/plugins/aws/package/compile/events/sqs/index.js index 3c728f06c..3a4904e27 100644 --- a/lib/plugins/aws/package/compile/events/sqs/index.js +++ b/lib/plugins/aws/package/compile/events/sqs/index.js @@ -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(); }()); diff --git a/lib/plugins/aws/package/compile/events/sqs/index.test.js b/lib/plugins/aws/package/compile/events/sqs/index.test.js index a251c9d87..2e21dfa15 100644 --- a/lib/plugins/aws/package/compile/events/sqs/index.test.js +++ b/lib/plugins/aws/package/compile/events/sqs/index.test.js @@ -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: [ diff --git a/lib/plugins/aws/package/compile/events/websockets/index.js b/lib/plugins/aws/package/compile/events/websockets/index.js index bdc395b04..194c205f1 100644 --- a/lib/plugins/aws/package/compile/events/websockets/index.js +++ b/lib/plugins/aws/package/compile/events/websockets/index.js @@ -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) diff --git a/lib/plugins/aws/package/compile/events/websockets/index.test.js b/lib/plugins/aws/package/compile/events/websockets/index.test.js index edacacb45..ad3826e25 100644 --- a/lib/plugins/aws/package/compile/events/websockets/index.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/index.test.js @@ -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); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/api.js b/lib/plugins/aws/package/compile/events/websockets/lib/api.js index 0dbd28045..f52067a6a 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/api.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/api.js @@ -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(); }, diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/api.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/api.test.js index bf31fbcbb..a3ed46155 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/api.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/api.test.js @@ -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); + }); + }); }); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.js b/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.js new file mode 100644 index 000000000..31062df82 --- /dev/null +++ b/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.js @@ -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(); + }, +}; diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.test.js new file mode 100644 index 000000000..1925ae6ce --- /dev/null +++ b/lib/plugins/aws/package/compile/events/websockets/lib/authorizers.test.js @@ -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({}); + }); + }); +}); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/permissions.js b/lib/plugins/aws/package/compile/events/websockets/lib/permissions.js index 939ce16df..fe28e2368 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/permissions.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/permissions.js @@ -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(); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/permissions.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/permissions.test.js index 4930cde80..25e6a8663 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/permissions.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/permissions.test.js @@ -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', + }, + ], + ], + }, + }, + }, + }); + }); + }); }); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/routes.js b/lib/plugins/aws/package/compile/events/websockets/lib/routes.js index d7b9b098c..ef3aa7a86 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/routes.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/routes.js @@ -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(); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/routes.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/routes.test.js index 2a1f8eea6..5a4fb203a 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/routes.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/routes.test.js @@ -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', + }, + ], + ], + }, + }, + }, + }); + }); + }); }); diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/validate.js b/lib/plugins/aws/package/compile/events/websockets/lib/validate.js index 00712ba39..f225bdc05 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/validate.js @@ -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({ diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/validate.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/validate.test.js index fbb0dbcf1..5f03b4317 100644 --- a/lib/plugins/aws/package/compile/events/websockets/lib/validate.test.js +++ b/lib/plugins/aws/package/compile/events/websockets/lib/validate.test.js @@ -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: { diff --git a/lib/plugins/aws/package/compile/functions/index.js b/lib/plugins/aws/package/compile/functions/index.js index 8a130d1c9..410480c74 100644 --- a/lib/plugins/aws/package/compile/functions/index.js +++ b/lib/plugins/aws/package/compile/functions/index.js @@ -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 diff --git a/lib/plugins/aws/package/compile/functions/index.test.js b/lib/plugins/aws/package/compile/functions/index.test.js index 389204db1..722285ba2 100644 --- a/lib/plugins/aws/package/compile/functions/index.test.js +++ b/lib/plugins/aws/package/compile/functions/index.test.js @@ -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 diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js index fec6d92e3..6b71d0831 100644 --- a/lib/plugins/aws/provider/awsProvider.js +++ b/lib/plugins/aws/provider/awsProvider.js @@ -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; diff --git a/lib/plugins/create/templates/aws-alexa-typescript/handler.ts b/lib/plugins/create/templates/aws-alexa-typescript/handler.ts index dc7cb2a77..f7c2c4226 100644 --- a/lib/plugins/create/templates/aws-alexa-typescript/handler.ts +++ b/lib/plugins/create/templates/aws-alexa-typescript/handler.ts @@ -1,4 +1,5 @@ import * as Ask from 'ask-sdk'; +import 'source-map-support/register'; export const alexa = Ask.SkillBuilders.custom() .addRequestHandlers({ diff --git a/lib/plugins/create/templates/aws-alexa-typescript/package.json b/lib/plugins/create/templates/aws-alexa-typescript/package.json index 01a04b631..ba84ae3bd 100644 --- a/lib/plugins/create/templates/aws-alexa-typescript/package.json +++ b/lib/plugins/create/templates/aws-alexa-typescript/package.json @@ -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)", diff --git a/lib/plugins/create/templates/aws-alexa-typescript/source-map-install.js b/lib/plugins/create/templates/aws-alexa-typescript/source-map-install.js deleted file mode 100644 index ef7457f72..000000000 --- a/lib/plugins/create/templates/aws-alexa-typescript/source-map-install.js +++ /dev/null @@ -1 +0,0 @@ -require('source-map-support').install(); diff --git a/lib/plugins/create/templates/aws-alexa-typescript/webpack.config.js b/lib/plugins/create/templates/aws-alexa-typescript/webpack.config.js index 482b98431..3fddde6b7 100644 --- a/lib/plugins/create/templates/aws-alexa-typescript/webpack.config.js +++ b/lib/plugins/create/templates/aws-alexa-typescript/webpack.config.js @@ -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'], diff --git a/lib/plugins/create/templates/aws-kotlin-jvm-maven/pom.xml b/lib/plugins/create/templates/aws-kotlin-jvm-maven/pom.xml index ec2d4738d..59d5cae70 100644 --- a/lib/plugins/create/templates/aws-kotlin-jvm-maven/pom.xml +++ b/lib/plugins/create/templates/aws-kotlin-jvm-maven/pom.xml @@ -8,42 +8,66 @@ hello - 1.1.4-3 + 1.3.21 + 1.8 1.8 1.8 UTF-8 + + + + com.fasterxml.jackson + jackson-bom + 2.9.8 + import + pom + + + + org.jetbrains.kotlin - kotlin-stdlib + kotlin-stdlib-jdk8 ${kotlin.version} com.amazonaws aws-lambda-java-core - 1.1.0 + 1.2.0 com.amazonaws - aws-lambda-java-log4j - 1.0.0 + aws-lambda-java-log4j2 + 1.1.0 + + + org.apache.logging.log4j + log4j-core + 2.11.1 + + + org.apache.logging.log4j + log4j-api + 2.11.1 com.fasterxml.jackson.core jackson-core - 2.8.5 com.fasterxml.jackson.core jackson-databind - 2.8.11.3 com.fasterxml.jackson.core jackson-annotations - 2.8.5 + + + com.fasterxml.jackson.module + jackson-module-kotlin @@ -66,6 +90,11 @@ 2.3 false + + + + @@ -75,6 +104,13 @@ + + + com.github.edwgiz + maven-shade-plugin.log4j2-cachefile-transformer + 2.8.1 + + kotlin-maven-plugin diff --git a/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/kotlin/com/serverless/ApiGatewayResponse.kt b/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/kotlin/com/serverless/ApiGatewayResponse.kt index 83f00bdf4..eb5b0a0af 100644 --- a/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/kotlin/com/serverless/ApiGatewayResponse.kt +++ b/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/kotlin/com/serverless/ApiGatewayResponse.kt @@ -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 diff --git a/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/kotlin/com/serverless/Handler.kt b/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/kotlin/com/serverless/Handler.kt index e14e0e8d9..a760dc2ba 100644 --- a/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/kotlin/com/serverless/Handler.kt +++ b/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/kotlin/com/serverless/Handler.kt @@ -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, ApiGatewayResponse> { override fun handleRequest(input:Map, 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, ApiGatewayResponse> { } } companion object { - private val LOG = Logger.getLogger(Handler::class.java) + private val LOG = LogManager.getLogger(Handler::class.java) } } \ No newline at end of file diff --git a/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/resources/log4j2.xml b/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/resources/log4j2.xml new file mode 100644 index 000000000..489fd2cde --- /dev/null +++ b/lib/plugins/create/templates/aws-kotlin-jvm-maven/src/main/resources/log4j2.xml @@ -0,0 +1,15 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n + + + + + + + + + diff --git a/lib/plugins/create/templates/aws-nodejs-typescript/handler.ts b/lib/plugins/create/templates/aws-nodejs-typescript/handler.ts index 03bfee491..db1831f55 100644 --- a/lib/plugins/create/templates/aws-nodejs-typescript/handler.ts +++ b/lib/plugins/create/templates/aws-nodejs-typescript/handler.ts @@ -1,4 +1,5 @@ import { APIGatewayProxyHandler } from 'aws-lambda'; +import 'source-map-support/register'; export const hello: APIGatewayProxyHandler = async (event, _context) => { return { diff --git a/lib/plugins/create/templates/aws-nodejs-typescript/package.json b/lib/plugins/create/templates/aws-nodejs-typescript/package.json index 44edc2b0c..a4ef1a31d 100644 --- a/lib/plugins/create/templates/aws-nodejs-typescript/package.json +++ b/lib/plugins/create/templates/aws-nodejs-typescript/package.json @@ -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)", diff --git a/lib/plugins/create/templates/aws-nodejs-typescript/source-map-install.js b/lib/plugins/create/templates/aws-nodejs-typescript/source-map-install.js deleted file mode 100644 index ef7457f72..000000000 --- a/lib/plugins/create/templates/aws-nodejs-typescript/source-map-install.js +++ /dev/null @@ -1 +0,0 @@ -require('source-map-support').install(); diff --git a/lib/plugins/create/templates/aws-nodejs-typescript/webpack.config.js b/lib/plugins/create/templates/aws-nodejs-typescript/webpack.config.js index 482b98431..3fddde6b7 100644 --- a/lib/plugins/create/templates/aws-nodejs-typescript/webpack.config.js +++ b/lib/plugins/create/templates/aws-nodejs-typescript/webpack.config.js @@ -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'], diff --git a/lib/plugins/invoke/invoke.js b/lib/plugins/invoke/invoke.js index b01212b82..d07db0216 100644 --- a/lib/plugins/invoke/invoke.js +++ b/lib/plugins/invoke/invoke.js @@ -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' }, }, + }, }, }, diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js index d8e9990aa..2ba7159e2 100644 --- a/lib/plugins/package/lib/packageService.js +++ b/lib/plugins/package/lib/packageService.js @@ -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); } diff --git a/lib/plugins/package/lib/packageService.test.js b/lib/plugins/package/lib/packageService.test.js index 9747566c7..cda987239 100644 --- a/lib/plugins/package/lib/packageService.test.js +++ b/lib/plugins/package/lib/packageService.test.js @@ -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, diff --git a/package-lock.json b/package-lock.json index 0c506ee20..9ed10e158 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8113de4c4..02bafc47c 100644 --- a/package.json +++ b/package.json @@ -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",