diff --git a/.appveyor.yml b/.appveyor.yml index 1c45422e22d..7cd3b0bf202 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -4,6 +4,7 @@ environment: matrix: - NODEJS_VERSION: "4" - NODEJS_VERSION: "6" + - NODEJS_VERSION: "8" install: # Get the version of Node.js diff --git a/.travis.yml b/.travis.yml index abb99c87c2c..d9dda5e9842 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,17 @@ matrix: - SLS_IGNORE_WARNING=* - secure: Ia2nYzOeYvTE6qOP7DBKX3BO7s/U7TXdsvB2nlc3kOPFi//IbTVD0/cLKCAE5XqTzrrliHINSVsFcJNSfjCwmDSRmgoIGrHj5CJkWpkI6FEPageo3mdqFQYEc8CZeAjsPBNaHe6Ewzg0Ev/sjTByLSJYVqokzDCF1QostSxx1Ss6SGt1zjxeP/Hp4yOJn52VAm9IHAKYn7Y62nMAFTaaTPUQHvW0mJj6m2Z8TWyPU+2Bx6mliO65gTPFGs+PdHGwHtmSF/4IcUO504x+HjDuwzW2itomLXZmIOFfGDcFYadKWzVMAfJzoRWOcVKF4jXdMoSCOviWpHGtK35E7K956MTXkroVoWCS7V0knQDovbRZj8c8td8mS4tdprUA+TzgZoHet2atWNtMuTh79rdmwoAO+IAWJegYj62Tdfy3ycESzY+KxSaV8kysG9sR3PRFoWjZerA7MhLZEzQMORXDGjJlgwLaZfYVqjlsGe5p5etFBUTd0WbFgSwOKLoA2U/fm7WzqItkjs3UWaHuvFVvwYixGxjEVmVczS6wa2cdGpHtVD9H7km4fPEzljHqQ26v0P5e8eylgqLF2IB6mL7UqGFrAtrMvAgN/M3gnq4dTs/wq1AJIOxEP7YW7kc0NAldk8vUz6t5GzCPNcuukxAku91Awnh0twxgUywatgJLZPY= - secure: Dgaa5XIsA5Vbw/CYQLUAuVVsDX26C8+f1XYGwsbNmFQKbKvM8iy9lGrHlfrT3jftJkJH6re8tP1RjyZjjzLe25KPk4Tps7grNteCyiIIEDsC2aHhiXHD6zNHsItpxYusaFfyQinFWnK4CAYKWb9ZNIwHIDUIB4vq807QGAhYsnoj1Lg/ajWvtEKBwYjEzDz9OjB91lw7lpCnHtmKKw5A+TNIVGpDDZ/jRBqETsPaePtiXC9UTHZQyM3gFoeVXiJw9KSU/gjIx9REihCaWWPbnuQSeIONGGlVWY9V4DTZIsJr9/uwDcbioeXDD3G1ezGtNPPRSNTtq08QlUtE4mEtKea/+ObpllKZCeZGn6AJhMn+uqMIP95FFlqBB55YzRcLZY+Igi/qm/9LJ9RinAhxRVXiwzeQ+BdVA6jshAAzr+7wklux6lZAa0xGw9pgTv7MI4RP2LJ/LMP1ppFsnv9n/qt93Ax1VEwEu3xHZe3VTYL9tbXOPTZutf6fKjUrW7wSSuy637queESjYnnPKSb1vZcPxjSFlyh+GJvxu/3PurF9aqfiBdiorIBre+pQS4lakLtoft5nsbA+4iYUwrXR58qUPVUqQ7a0A0hedOWlp6g9ixLa6nugUP5aobJzR71T8l/IjqpnY2EEd/iINEb0XfUiZtB5zHaqFWejBtmWwCI= - - node_js: '6.2' + - node_js: '8.9' + env: + SLS_IGNORE_WARNING=* + - node_js: '8.9' + env: + - INTEGRATION_TEST=true + - INTEGRATION_TEST_SUITE=simple + - SLS_IGNORE_WARNING=* + - secure: Ia2nYzOeYvTE6qOP7DBKX3BO7s/U7TXdsvB2nlc3kOPFi//IbTVD0/cLKCAE5XqTzrrliHINSVsFcJNSfjCwmDSRmgoIGrHj5CJkWpkI6FEPageo3mdqFQYEc8CZeAjsPBNaHe6Ewzg0Ev/sjTByLSJYVqokzDCF1QostSxx1Ss6SGt1zjxeP/Hp4yOJn52VAm9IHAKYn7Y62nMAFTaaTPUQHvW0mJj6m2Z8TWyPU+2Bx6mliO65gTPFGs+PdHGwHtmSF/4IcUO504x+HjDuwzW2itomLXZmIOFfGDcFYadKWzVMAfJzoRWOcVKF4jXdMoSCOviWpHGtK35E7K956MTXkroVoWCS7V0knQDovbRZj8c8td8mS4tdprUA+TzgZoHet2atWNtMuTh79rdmwoAO+IAWJegYj62Tdfy3ycESzY+KxSaV8kysG9sR3PRFoWjZerA7MhLZEzQMORXDGjJlgwLaZfYVqjlsGe5p5etFBUTd0WbFgSwOKLoA2U/fm7WzqItkjs3UWaHuvFVvwYixGxjEVmVczS6wa2cdGpHtVD9H7km4fPEzljHqQ26v0P5e8eylgqLF2IB6mL7UqGFrAtrMvAgN/M3gnq4dTs/wq1AJIOxEP7YW7kc0NAldk8vUz6t5GzCPNcuukxAku91Awnh0twxgUywatgJLZPY= + - secure: Dgaa5XIsA5Vbw/CYQLUAuVVsDX26C8+f1XYGwsbNmFQKbKvM8iy9lGrHlfrT3jftJkJH6re8tP1RjyZjjzLe25KPk4Tps7grNteCyiIIEDsC2aHhiXHD6zNHsItpxYusaFfyQinFWnK4CAYKWb9ZNIwHIDUIB4vq807QGAhYsnoj1Lg/ajWvtEKBwYjEzDz9OjB91lw7lpCnHtmKKw5A+TNIVGpDDZ/jRBqETsPaePtiXC9UTHZQyM3gFoeVXiJw9KSU/gjIx9REihCaWWPbnuQSeIONGGlVWY9V4DTZIsJr9/uwDcbioeXDD3G1ezGtNPPRSNTtq08QlUtE4mEtKea/+ObpllKZCeZGn6AJhMn+uqMIP95FFlqBB55YzRcLZY+Igi/qm/9LJ9RinAhxRVXiwzeQ+BdVA6jshAAzr+7wklux6lZAa0xGw9pgTv7MI4RP2LJ/LMP1ppFsnv9n/qt93Ax1VEwEu3xHZe3VTYL9tbXOPTZutf6fKjUrW7wSSuy637queESjYnnPKSb1vZcPxjSFlyh+GJvxu/3PurF9aqfiBdiorIBre+pQS4lakLtoft5nsbA+4iYUwrXR58qUPVUqQ7a0A0hedOWlp6g9ixLa6nugUP5aobJzR71T8l/IjqpnY2EEd/iINEb0XfUiZtB5zHaqFWejBtmWwCI= + - node_js: '8.9' env: - DISABLE_TESTS=true - LINTING=true diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b64b53eb4e..98352edea00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 1.26.0 (29.01.2018) +- [AWS Go support](https://github.com/serverless/serverless/pull/4669) +- [Support for using an existing ApiGateway and Resources](https://github.com/serverless/serverless/pull/4247) +- [Add logRetentionInDays config](https://github.com/serverless/serverless/pull/4591) +- [Add support of `serverless.js` configuration file](https://github.com/serverless/serverless/pull/4590) +- [Add "did you mean..." CLI suggestions](https://github.com/serverless/serverless/pull/4586) +- [Add `--template-path` option to `serverless create`](https://github.com/serverless/serverless/pull/4576) +- [Add support POJO input support for Java invoke local](https://github.com/serverless/serverless/pull/4596) + +## Meta +- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.25.0...v1.26.0) + + # 1.25.0 (20.12.2017) - [Improve Stage and Region Usage](https://github.com/serverless/serverless/pull/4560) - [Add API Gateway endpoint configuration](https://github.com/serverless/serverless/pull/4531) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9606d0118e9..dacac1823de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ Welcome, and thanks in advance for your help! Please follow these simple guideli **Note:** Please write a quick comment in the corresponding issue and ask if the feature is still relevant and that you want to jump into the implementation. -Check out our [help-wanted](https://github.com/serverless/serverless/labels/status/help-wanted) or [easy-pick](https://github.com/serverless/serverless/labels/status/easy-pick) labels to find issues we want to move forward on with your help. +Check out our [help wanted](https://github.com/serverless/serverless/labels/help%20wanted) or [good first issue](https://github.com/serverless/serverless/labels/good%20first%20issue) labels to find issues we want to move forward on with your help. We will do our best to respond/review/merge your PR according to priority. We hope that you stay engaged with us during this period to insure QA. Please note that the PR will be closed if there hasn't been any activity for a long time (~ 30 days) to keep us focused and keep the repo clean. diff --git a/LICENSE.txt b/LICENSE.txt index 80acf9ae420..625b94f65af 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017 Serverless, Inc. http://www.serverless.com +Copyright (c) 2018 Serverless, Inc. http://www.serverless.com The following license applies to all parts of this software except as documented below: diff --git a/README.md b/README.md index b71e98da189..044fb4b6c81 100644 --- a/README.md +++ b/README.md @@ -126,12 +126,13 @@ The following are services you can instantly install and use by running `serverl * [Ruby](https://github.com/stewartlord/serverless-ruby) - Call a Ruby function from your lambda * [Slack App](https://github.com/johnagan/serverless-slack-app) - Slack App Boilerplate with OAuth and Bot actions * [Swift](https://github.com/choefele/swift-lambda-app) - Full-featured project template to develop Lambda functions in Swift +* [Cloudwatch Alerts on Slack](https://github.com/dav009/serverless-aws-alarms-notifier) - Get AWS Cloudwatch alerts notifications on Slack **Note**: the `serverless install` command will only work on V1.0 or later. ## Features -* Supports Node.js, Python, Java, Scala, C#, F#, Groovy, Kotlin, PHP & Swift. +* Supports Node.js, Python, Java, Scala, C#, F#, Go, Groovy, Kotlin, PHP & Swift. * Manages the lifecycle of your serverless architecture (build, deploy, update, delete). * Safely deploy functions, events and their required resources together via provider resource managers (e.g., AWS CloudFormation). * Functions can be grouped ("serverless services") for easy management of code, resources & processes, across large projects & teams. @@ -321,7 +322,7 @@ This table is generated from https://github.com/serverless/examples/blob/master/ ## Contributing We love our contributors! Please read our [Contributing Document](CONTRIBUTING.md) to learn how you can start working on the Framework yourself. -Check out our [help-wanted](https://github.com/serverless/serverless/labels/status/help-wanted) or [easy-pick](https://github.com/serverless/serverless/labels/status/easy-pick) labels to find issues we want to move forward on with your help. +Check out our [help wanted](https://github.com/serverless/serverless/labels/help%20wanted) or [good first issue](https://github.com/serverless/serverless/labels/good%20first%20issue) labels to find issues we want to move forward on with your help. ## Community @@ -356,6 +357,7 @@ These consultants use the Serverless Framework and can help you build your serve * [Langa](http://langa.io/?utm_source=gh-serverless&utm_medium=github) - They built [Trails.js](http://github.com/trailsjs/trails) * [Emerging Technology Advisors](https://www.emergingtechnologyadvisors.com) * [OneSpeed](https://onespeed.io/) +* [Seraro](http://www.seraro.com/) - Who also runs Atlanta Serverless Meetup (https://www.meetup.com/Atlanta-CABI-Camp-Cloud-AI-Blockchain-IOT) and Delhi Serverless Meetup (https://www.meetup.com/Delhi-NCR-Serverless-Architecture-Meetup/) ---- # Previous Serverless Version 0.5.x diff --git a/docker-compose.yml b/docker-compose.yml index 6ffa1c77972..98c398c8f73 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,6 +63,16 @@ services: image: microsoft/dotnet:1.0.4-sdk volumes: - ./tmp/serverless-integration-test-aws-fsharp:/app + aws-go: + image: golang:1.9 + volumes: + - ./tmp/serverless-integration-test-aws-go:/app + - ./tmp/serverless-integration-test-aws-go:/go/src/app + aws-go-dep: + image: yunspace/golang:1.9 + volumes: + - ./tmp/serverless-integration-test-aws-go-dep:/app + - ./tmp/serverless-integration-test-aws-go-dep:/go/src/app aws-nodejs-typescript: image: node:6.10.3 volumes: @@ -87,6 +97,10 @@ services: image: ruby2.4.1 volumes: - ./tmp/serverless-integration-test-spotinst-ruby:/app + spotinst-java8: + image: maven:3-jdk-8 + volumes: + - ./tmp/serverless-integration-test-spotinst-java8:/app webtasks-nodejs: image: node:6.10.3 volumes: diff --git a/docs/providers/aws/cli-reference/create.md b/docs/providers/aws/cli-reference/create.md index fc38bf9cd8d..73421f72533 100644 --- a/docs/providers/aws/cli-reference/create.md +++ b/docs/providers/aws/cli-reference/create.md @@ -33,8 +33,9 @@ serverless create --template-url https://github.com/serverless/serverless/tree/m ``` ## Options -- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. -- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. +- `--template-path` The local path of your template. **Required if --template and --template-url are not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -90,6 +91,14 @@ will use the already present directory. Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. +### Creating a new service using a local template + +```bash +serverless create --template-path path/to/my/template/folder --path path/to/my/service --name my-new-service +``` + +This will copy the `path/to/my/template/folder` folder into `path/to/my/service` and rename the service to `my-new-service`. + ### Creating a new plugin ``` diff --git a/docs/providers/aws/cli-reference/deploy.md b/docs/providers/aws/cli-reference/deploy.md index 66204c690a9..8bdeced0998 100644 --- a/docs/providers/aws/cli-reference/deploy.md +++ b/docs/providers/aws/cli-reference/deploy.md @@ -25,7 +25,8 @@ serverless deploy - `--verbose` or `-v` Shows all stack events during deployment, and display any Stack Output. - `--function` or `-f` Invoke `deploy function` (see above). Convenience shortcut - cannot be used with `--package`. - `--conceal` Hides secrets from the output (e.g. API Gateway key values). -- `--aws-s3-accelerate` Enables S3 Transfer Acceleration making uploading artifacts much faster. You can read more about it [here](http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html). **Note: When using Transfer Acceleration, additional data transfer charges may apply** +- `--aws-s3-accelerate` Enables S3 Transfer Acceleration making uploading artifacts much faster. You can read more about it [here](http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html). It requires additional `s3:PutAccelerateConfiguration` permissions. **Note: When using Transfer Acceleration, additional data transfer charges may apply.** +- `--no-aws-s3-accelerate` Explicitly disables S3 Transfer Acceleration). It also requires additional `s3:PutAccelerateConfiguration` permissions. ## Artifacts diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md index 13f057c3488..7da3daf1b42 100644 --- a/docs/providers/aws/events/apigateway.md +++ b/docs/providers/aws/events/apigateway.md @@ -633,7 +633,33 @@ See the [api gateway documentation](https://docs.aws.amazon.com/apigateway/lates **Notes:** - A missing/empty request Content-Type is considered to be the API Gateway default (`application/json`) -- API Gateway docs refer to "WHEN_NO_TEMPLATE" (singular), but this will fail during creation as the actual value should be "WHEN_NO_TEMPLATES" (plural) + - [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/apigateway) +- [API Gateway](#api-gateway) + - [Lambda Proxy Integration](#lambda-proxy-integration) + - [Simple HTTP Endpoint](#simple-http-endpoint) + - [Example "LAMBDA-PROXY" event (default)](#example-lambda-proxy-event-default) + - [HTTP Endpoint with Extended Options](#http-endpoint-with-extended-options) + - [Enabling CORS](#enabling-cors) + - [HTTP Endpoints with `AWS_IAM` Authorizers](#http-endpoints-with-awsiam-authorizers) + - [HTTP Endpoints with Custom Authorizers](#http-endpoints-with-custom-authorizers) + - [Catching Exceptions In Your Lambda Function](#catching-exceptions-in-your-lambda-function) + - [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api) + - [Request Parameters](#request-parameters) + - [Lambda Integration](#lambda-integration) + - [Example "LAMBDA" event (before customization)](#example-lambda-event-before-customization) + - [Request templates](#request-templates) + - [Default Request Templates](#default-request-templates) + - [Custom Request Templates](#custom-request-templates) + - [Pass Through Behavior](#pass-through-behavior) + - [Responses](#responses) + - [Custom Response Headers](#custom-response-headers) + - [Custom Response Templates](#custom-response-templates) + - [Status codes](#status-codes) + - [Available Status Codes](#available-status-codes) + - [Using Status Codes](#using-status-codes) + - [Custom Status Codes](#custom-status-codes) + - [Setting an HTTP Proxy on API Gateway](#setting-an-http-proxy-on-api-gateway) + - [Share API Gateway and API Resources](#share-api-gateway-and-api-resources) ### Responses @@ -828,3 +854,116 @@ endpoint of your proxy, and the URI you want to set a proxy to. Now that you have these two CloudFormation templates defined in your `serverless.yml` file, you can simply run `serverless deploy` and that will deploy these custom resources for you along with your service and set up a proxy on your Rest API. + +## Share API Gateway and API Resources + +As you application grows, you will have idea to break it out into multiple services. However, each serverless project generates new API Gateway by default. If you want to share same API Gateway for muliple projects, you 'll need to reference REST API ID and Root Resource ID into serverless.yml files + +```yml +service: service-name +provider: + name: aws + apiGateway: + restApiId: xxxxxxxxxx # REST API resource ID. Default is generated by the framework + restApiRootResourceId: xxxxxxxxxx # Root resource, represent as / path + +functions: + ... + +``` + +In case the application has many chilren and grandchildren paths, you also want to break them out into smaller services. + +```yml +service: service-a +provider: + apiGateway: + restApiId: xxxxxxxxxx + restApiRootResourceId: xxxxxxxxxx + +functions: + create: + handler: posts.create + events: + - http: + method: post + path: /posts +``` + +```yml +service: service-b +provider: + apiGateway: + restApiId: xxxxxxxxxx + restApiRootResourceId: xxxxxxxxxx + +functions: + create: + handler: posts.createComment + events: + - http: + method: post + path: /posts/{id}/comments +``` + +They reference the same parent path `/posts`. Cloudformation will throw error if we try to generate existed one. To avoid that, we must reference source ID of `/posts`. + +```yml +service: service-a +provider: + apiGateway: + restApiId: xxxxxxxxxx + restApiRootResourceId: xxxxxxxxxx + restApiResources: + /posts: xxxxxxxxxx + +functions: + ... + +``` + +```yml +service: service-b +provider: + apiGateway: + restApiId: xxxxxxxxxx + restApiRootResourceId: xxxxxxxxxx + restApiResources: + /posts: xxxxxxxxxx + +functions: + ... + +``` + +You can define more than one path resource. Otherwise, serverless will generate paths from root resource. `restApiRootResourceId` can be optional if there isn't path that need to be generated from the root + +```yml +service: service-a +provider: + apiGateway: + restApiId: xxxxxxxxxx + # restApiRootResourceId: xxxxxxxxxx # Optional + restApiResources: + /posts: xxxxxxxxxx + /categories: xxxxxxxxx + + +functions: + listPosts: + handler: posts.list + events: + - http: + method: get + path: /posts + + listCategories: + handler: categories.list + events: + - http: + method: get + path: /categories + +``` + +For best practice and CI, CD friendly, we should define Cloudformation resources from early service, then use Cross-Stack References for another ones. diff --git a/docs/providers/aws/events/cloudwatch-log.md b/docs/providers/aws/events/cloudwatch-log.md index bddf59117da..2072598ab27 100644 --- a/docs/providers/aws/events/cloudwatch-log.md +++ b/docs/providers/aws/events/cloudwatch-log.md @@ -14,7 +14,7 @@ layout: Doc ## Simple event definition -This will enable your Lambda function to be called by an Log Stream. +This will enable your Lambda function to be called by a Log Stream. ```yml functions: diff --git a/docs/providers/aws/examples/hello-world/README.md b/docs/providers/aws/examples/hello-world/README.md index d1f1d070a4c..378b0567756 100644 --- a/docs/providers/aws/examples/hello-world/README.md +++ b/docs/providers/aws/examples/hello-world/README.md @@ -17,6 +17,8 @@ Pick your language of choice: * [JavaScript](./node) * [Python](./python) -* [csharp](./csharp) +* [C#](./csharp) +* [F#](./fsharp) +* [Go](./go) [View all examples](https://www.serverless.com/framework/docs/providers/aws/examples/) diff --git a/docs/providers/aws/examples/hello-world/go/README.md b/docs/providers/aws/examples/hello-world/go/README.md new file mode 100644 index 00000000000..8c522ccaecd --- /dev/null +++ b/docs/providers/aws/examples/hello-world/go/README.md @@ -0,0 +1,72 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/examples/hello-world/go/) + + +# Hello World Go Example + +Make sure `serverless` is installed. [See installation guide](../../../guide/installation.md). + +You should also have [go](https://golang.org/doc/install) and [make](https://www.gnu.org/software/make/) + +It is always good practice to organise your `go` projects within [GOPATH](https://golang.org/doc/code.html#GOPATH), to maximise the benefits of go tooling. + +## 1. Create a service +There are two templates for `go`: + +1. [aws-go](https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates/aws-go) - `serverless create --template aws-go --path myService` +2. [aws-go-dep](https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates/aws-go-dep) - `serverless create --template aws-go-dep --path myService` + +where: +- 'aws-go' fetches dependencies using standard `go get`. +- 'aws-go-dep' uses [go dep](https://github.com/golang/dep) and requires your project to be in `$GOPATH/src` +- 'myService' is a new folder to be created with template service files. + +Change directories into 'myService' folder and you can see this project has 2 handler functions: `hello` and `world` split into 2 separate go packages (folders): + +``` +. +├── hello/ +│ └── main.go +├── world/ +│ └── main.go +``` + +This because a `main()` function is required as entry point for each handler executable. + +## 2. Build using go build to create static binaries + +Run `make build` to build both functions. Successful build should generate the following binaries: + +``` +. +├── bin/ +│ |── hello +│ └── world +``` + +## 3. Deploy +`serverless deploy` or `sls deploy`. `sls` is shorthand for the Serverless CLI command + +## 4. Invoke deployed function +Invoking the both functions should return a successful results: + +```bash +serverless invoke -f hello +{ + "message": "Go Serverless v1.0! Your function executed successfully!" +} + +serverless invoke --f world +{ + "message": "Okay so your other function also executed successfully!" +} +``` + +Congrats you have just deployed and run your Hello World function! diff --git a/docs/providers/aws/guide/intro.md b/docs/providers/aws/guide/intro.md index 8f042645434..e4ea181a567 100644 --- a/docs/providers/aws/guide/intro.md +++ b/docs/providers/aws/guide/intro.md @@ -58,7 +58,7 @@ The Serverless Framework not only deploys your Functions and the Events that tri ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index 2bb376af946..c9f1c35a771 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -31,6 +31,7 @@ provider: 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 + logRetentionInDays: 14 # Set the default RetentionInDays for a CloudWatch LogGroup deploymentBucket: name: com.serverless.${self:provider.region}.deploys # Deployment bucket name. Default is generated by the framework serverSideEncryption: AES256 # when using server-side encryption @@ -44,6 +45,13 @@ provider: - myFirstKey - ${opt:stage}-myFirstKey - ${env:MY_API_KEY} # you can hide it in a serverless variable + apiGateway: # Optional API Gateway global config + restApiId: xxxxxxxxxx # REST API resource ID. Default is generated by the framework + restApiRootResourceId: xxxxxxxxxx # Root resource ID, represent as / path + restApiResources: # List of existing resources that were created in the REST API. This is required or the stack will be conflicted + '/users': xxxxxxxxxx + '/users/create': xxxxxxxxxx + usagePlan: # Optional usage plan configuration quota: limit: 5000 @@ -94,6 +102,7 @@ package: # Optional deployment packaging configuration - .travis.yml excludeDevDependencies: false # Config if Serverless should automatically exclude dev dependencies in the deployment package. Defaults to true + functions: usersCreate: # A Function handler: users.create # The file and module for this specific function. diff --git a/docs/providers/aws/guide/services.md b/docs/providers/aws/guide/services.md index 20a27a0270d..24d33dab159 100644 --- a/docs/providers/aws/guide/services.md +++ b/docs/providers/aws/guide/services.md @@ -63,6 +63,7 @@ Here are the available runtimes for AWS Lambda: * aws-scala-sbt * aws-csharp * aws-fsharp +* aws-go Check out the [create command docs](../cli-reference/create) for all the details and options. diff --git a/docs/providers/azure/cli-reference/create.md b/docs/providers/azure/cli-reference/create.md index 998a09cef3e..f9bddeeaa54 100644 --- a/docs/providers/azure/cli-reference/create.md +++ b/docs/providers/azure/cli-reference/create.md @@ -28,8 +28,9 @@ serverless create --template azure-nodejs --path myService ``` ## Options -- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. -- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. +- `--template-path` The local path of your template. **Required if --template and --template-url are not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -70,6 +71,14 @@ Serverless will use the already present directory. Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. +### Creating a new service using a local template + +```bash +serverless create --template-path path/to/my/template/folder --path path/to/my/service --name my-new-service +``` + +This will copy the `path/to/my/template/folder` folder into `path/to/my/service` and rename the service to `my-new-service`. + ### Create service in new folder using a custom template ```bash diff --git a/docs/providers/azure/guide/intro.md b/docs/providers/azure/guide/intro.md index 4f5fe67ec5f..f212424a91b 100644 --- a/docs/providers/azure/guide/intro.md +++ b/docs/providers/azure/guide/intro.md @@ -64,7 +64,7 @@ A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or -`serverless.json`). It looks like this: +`serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml diff --git a/docs/providers/google/cli-reference/create.md b/docs/providers/google/cli-reference/create.md index 232c84bdc08..27d550bdb33 100644 --- a/docs/providers/google/cli-reference/create.md +++ b/docs/providers/google/cli-reference/create.md @@ -28,8 +28,9 @@ serverless create --template google-nodejs --path my-service ## Options -- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. -- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. +- `--template-path` The local path of your template. **Required if --template and --template-url are not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -61,6 +62,14 @@ This example will generate scaffolding for a service with `google` as a provider Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. +### Creating a new service using a local template + +```bash +serverless create --template-path path/to/my/template/folder --path path/to/my/service --name my-new-service +``` + +This will copy the `path/to/my/template/folder` folder into `path/to/my/service` and rename the service to `my-new-service`. + ### Create service in new folder using a custom template ```bash diff --git a/docs/providers/google/cli-reference/invoke-local.md b/docs/providers/google/cli-reference/invoke-local.md new file mode 100644 index 00000000000..54ab1170190 --- /dev/null +++ b/docs/providers/google/cli-reference/invoke-local.md @@ -0,0 +1,56 @@ + + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/invoke-local) + + + +# Google - Invoke Local + +Invokes deployed function locally. It allows to send event data to the function, read logs and display other important information of the function invocation. + +```bash +serverless invoke -f functionName +``` + +## Options + +* `--function` or `-f` The name of the function in your service that you want to invoke. **Required**. + \_ `--data` or `-d` Data you want to pass into the function +* `--path` or `-p` Path to JSON or YAML file holding input data. This path is relative to the root directory of the service. +* `--raw` Pass data as a raw string even if it is JSON. If not set, JSON data are parsed and passed as an object. +* `--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. + +> Keep in mind that if you pass both `--path` and `--data`, the data included in the `--path` file will overwrite the data you passed with the `--data` flag. + +## Examples + +### Local function invocation + +```bash +serverless invoke local -f functionName +``` + +### Local function invocation with data + +```bash +serverless invoke local -f functionName -d '{ "data": "hello world" }' +``` + +### Local function invocation with data passing + +```bash +serverless invoke local -f functionName -p path/to/file.json + +# OR + +serverless invoke local -f functionName -p path/to/file.yaml +``` diff --git a/docs/providers/google/examples/hello-world/node/README.md b/docs/providers/google/examples/hello-world/node/README.md index 4f6f3b10f05..51f5af2044b 100644 --- a/docs/providers/google/examples/hello-world/node/README.md +++ b/docs/providers/google/examples/hello-world/node/README.md @@ -31,7 +31,7 @@ Update the `credentials` and your `project` property in the `serverless.yml` fil ## 5. Invoke deployed function -`serverless invoke --function helloWorld` +`serverless invoke --function first` In your terminal window you should see a response from the Google Cloud diff --git a/docs/providers/google/guide/intro.md b/docs/providers/google/guide/intro.md index af49d99fb03..4fbf37d6a16 100644 --- a/docs/providers/google/guide/intro.md +++ b/docs/providers/google/guide/intro.md @@ -46,7 +46,7 @@ When you define an event for your Google Cloud Function in the Serverless Framew ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml diff --git a/docs/providers/kubeless/README.md b/docs/providers/kubeless/README.md index b9bba665a2d..aaf72767710 100644 --- a/docs/providers/kubeless/README.md +++ b/docs/providers/kubeless/README.md @@ -30,6 +30,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
  • Functions
  • Events
  • Deploying
  • +
  • Packaging
  • Debugging
  • Workflow
  • diff --git a/docs/providers/kubeless/cli-reference/create.md b/docs/providers/kubeless/cli-reference/create.md index 7911ef2810c..68a1bd6b3b2 100644 --- a/docs/providers/kubeless/cli-reference/create.md +++ b/docs/providers/kubeless/cli-reference/create.md @@ -35,8 +35,9 @@ serverless create --template kubeless-nodejs --path my-service ``` ## Options -- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. -- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. +- `--template-path` The local path of your template. **Required if --template and --template-url are not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -73,3 +74,11 @@ serverless create --template kubeless-python --path my-new-service This example will generate scaffolding for a service with `kubeless` as a provider and `python2.7` as runtime. The scaffolding will be generated in the `my-new-service` directory. This directory will be created if not present. Otherwise Serverless will use the already present directory. Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. + +### Creating a new service using a local template + +```bash +serverless create --template-path path/to/my/template/folder --path path/to/my/service --name my-new-service +``` + +This will copy the `path/to/my/template/folder` folder into `path/to/my/service` and rename the service to `my-new-service`. diff --git a/docs/providers/kubeless/guide/functions.md b/docs/providers/kubeless/guide/functions.md index b85619a2694..8881110c865 100644 --- a/docs/providers/kubeless/guide/functions.md +++ b/docs/providers/kubeless/guide/functions.md @@ -25,6 +25,9 @@ service: my-service provider: name: kubeless runtime: python2.7 + memorySize: 512M # optional, maximum memory + timeout: 10 # optional, in seconds, default is 180 + namespace: funcions # optional, deployment namespace if not specified it uses "default" plugins: - serverless-kubeless @@ -34,7 +37,12 @@ functions: # and the K8s service object to get a request to call the function hello: # The function to call as a response to the HTTP event - handler: handler.hello + handler: handler.hello # required, handler set + description: Description of what the function does # optional, to set the description as an annotation + memorySize: 512M # optional, maximum memory + timeout: 10 # optional, in seconds, default is 180 + namespace: funcions # optional, deployment namespace, if not specified "default" will be used + port: 8081 # optional, deploy http-based function with a custom port, default is 8080 ``` The `handler` property points to the file and module containing the code you want to run in your function. @@ -88,3 +96,139 @@ The Kubeless provider plugin supports the following runtimes. Please see the following repository for sample projects using those runtimes: [https://github.com/serverless/serverless-kubeless/tree/master/examples](https://github.com/serverless/serverless-kubeless/tree/master/examples) + +## Installing dependencies + +For installing dependencies the standard dependency file should be placed in the function folder: + + - For Python functions, it will use the file `requirements.txt` + - For Nodejs functions, `dependencies` in the `package.json` file will be installed + - For Ruby functions, it will use the file `Gemfile.rb` + +If one of the above files is found, the depencies will be installed using a [`Init Container`](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/). + +## Environment Variables + +You can add environment variable configuration to a specific function in `serverless.yml` by adding an `environment` object property in the function configuration. This object should contain a key/value collection of strings: + +```yml +# serverless.yml +service: service-name +provider: kubeless +plugins: + - serverless-kubeless + +functions: + hello: + handler: handler.hello + environment: + TABLE_NAME: tableName +``` + +Or if you want to apply environment variable configuration to all functions in your service, you can add the configuration to the higher level `provider` object. Environment variables configured at the function level are merged with those at the provider level, so your function with specific environment variables will also have access to the environment variables defined at the provider level. If an environment variable with the same key is defined at both the function and provider levels, the function-specific value overrides the provider-level default value. For example: + +```yml +# serverless.yml +service: service-name +provider: + name: kubeless + environment: + SYSTEM_NAME: mySystem + TABLE_NAME: tableName1 + +plugins: + - serverless-kubeless + +functions: + hello: + # this function will have SYSTEM_NAME=mySystem and TABLE_NAME=tableName1 from the provider-level environment config above + handler: handler.hello + users: + # this function will have SYSTEM_NAME=mySystem from the provider-level environment config above + # but TABLE_NAME will be tableName2 because this more specific config will override the default above + handler: handler.users + environment: + TABLE_NAME: tableName2 +``` + +## Labels + +Using the `labels` configuration makes it possible to add `key` / `value` labels to your functions. + +Those labels will appear in deployments, services and pods and will make it easier to group functions by label or find functions with a common label. + +```yml +provider: + name: kubeless + +plugins: + - serverless-kubeless + +functions: + hello: + handler: handler.hello + labels: + foo: bar +``` + +## Custom hostname and path + +It is possible to define a custom hostname and path that will be used to serve a function in a specific endpoint. For doing this, it is necessary to have an [Ingress Controller](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-controllers) available in the cluster. + +```yml +provider: + name: kubeless + hostname: myhostname.io +plugins: + - serverless-kubeless + +functions: + hello: + handler: handler.hello + events: + - http: + path: /hello +``` + +In the example above, once the Ingress Rule has been processed by the Ingress controller, you can call the function using as endpoing `myhostname.io/hello`. + +If no hostname is given but a function specifies a `path`, the plugin will use the IP of the cluster followed by a DNS mapping service. By default [nip.io](http://nip.io) will be used but this can be configured with the property `defaultDNSResolution`. + +```yml +provider: + name: kubeless + defaultDNSResolution: 'xip.io' +plugins: + - serverless-kubeless + +functions: + hello: + handler: handler.hello + events: + - http: + path: /hello +``` + +The above will result in an endpoint like `1.2.3.4.xip.io/hello` where `1.2.3.4` is the IP of the cluster server. + +The final URL in which the function will be listening can be retrieved executing `serverless info`. + +## Custom images (alpha feature) + +It is possible to skip the Kubeless build system and specify a prebuilt image to run a function. This feature is useful for using Kubeless with languages that are still not supported or if the function package [is over 1MB](./packaging.md#package-maximum-size). To get more information about how to use custom images visit the [upstream documentation](https://github.com/kubeless/kubeless/blob/master/docs/runtimes.md#custom-runtime-alpha). + +```yml +service: hello + +provider: + name: kubeless + runtime: python2.7 + +plugins: + - serverless-kubeless + +functions: + hello: + handler: handler.hello + image: tuna/kubeless-python:0.0.6 +``` diff --git a/docs/providers/kubeless/guide/intro.md b/docs/providers/kubeless/guide/intro.md index a6227c9418f..4a2fdd53354 100644 --- a/docs/providers/kubeless/guide/intro.md +++ b/docs/providers/kubeless/guide/intro.md @@ -42,7 +42,7 @@ Anything that triggers an Kubeless Event to execute is regarded by the Framework ### Services -A **Service** is the Serverless Framework's unit of organization (not to be confused with [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/). You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions and the Events that trigger them, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this: +A **Service** is the Serverless Framework's unit of organization (not to be confused with [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/). You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions and the Events that trigger them, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml diff --git a/docs/providers/kubeless/guide/packaging.md b/docs/providers/kubeless/guide/packaging.md new file mode 100644 index 00000000000..2b788756c1b --- /dev/null +++ b/docs/providers/kubeless/guide/packaging.md @@ -0,0 +1,144 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/kubeless/guide/packaging) + + +# Kubeless - Packaging + +## Package CLI Command + +Using the Serverless CLI tool, you can package your project without deploying with Kubeless. This is best used with CI / CD workflows to ensure consistent deployable artifacts. + +Running the following command will build and save all of the deployment artifacts in the service's .serverless directory: + +```bash +serverless package +``` + +However, you can also use the --package option to add a destination path and Serverless will store your deployment artifacts there (./my-artifacts in the following case): + +```bash +serverless package --package my-artifacts +``` + +## Package Maximum Size + +Kubeless uses [Kubernetes ConfigMaps](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/) to store functions configuration and code. These ConfigMaps have a limitation in size of one MB since they are stored as a single entry in a `etcd` database. Due to this limitation, the maximum possible size for a Kubeless function package is no more than one MB. Note that only code and configuration files should be included in this package, dependencies will be installed during the build process. If your function package size is over one MB please [exclude some directories](#exclude-include) or create a [custom image](./functions#custom-images-alpha-feature) with your function. By default, files under the `node_modules` folder will be excluded. + +## Package Configuration + +Sometimes you might like to have more control over your function artifacts and how they are packaged. + +You can use the `package` and `exclude` configuration for more control over the packaging process. + +### Exclude / include + +Exclude and include allows you to define globs that will be excluded / included from the resulting artifact. If you wish to +include files you can use a glob pattern prefixed with `!` such as `!re-include-me/**` in `exclude` or the dedicated `include` config. +Serverless will run the glob patterns in order. + +At first it will apply the globs defined in `exclude`. After that it'll add all the globs from `include`. This way you can always re-include +previously excluded files and directories. + +### Examples + +Exclude all node_modules but then re-include a specific modules (in this case node-fetch) using `exclude` exclusively + +``` yml +package: + exclude: + - node_modules/** + - '!node_modules/node-fetch/**' +``` + +Exclude all files but `handler.js` using `exclude` and `include` + +``` yml +package: + exclude: + - src/** + include: + - src/function/handler.js +``` + +**Note:** Don't forget to use the correct glob syntax if you want to exclude directories + +``` +exclude: + - tmp/** + - .git/** +``` + +### Artifact + +For complete control over the packaging process you can specify your own artifact zip file. +Serverless won't zip your service if this is configured and therefore `exclude` and `include` will be ignored. Either you use artifact or include / exclude. + +The artifact option is especially useful in case your development environment allows you to generate a deployable artifact like Maven does for Java. + +### Example + +```yml +service: my-service +package: + artifact: path/to/my-artifact.zip +``` + +### Packaging functions separately + +If you want even more controls over your functions for deployment you can configure them to be packaged independently. This allows you more control for optimizing your deployment. To enable individual packaging set `individually` to true in the service or function wide packaging settings. + +Then for every function you can use the same `exclude`, `include` or `artifact` config options as you can service wide. The `exclude` and `include` option will be merged with the service wide options to create one `exclude` and `include` config per function during packaging. + +```yml +service: my-service +package: + individually: true + exclude: + - excluded-by-default.json +functions: + hello: + handler: handler.hello + package: + # We're including this file so it will be in the final package of this function only + include: + - excluded-by-default.json + world: + handler: handler.hello + package: + exclude: + - some-file.js +``` + +You can also select which functions to be packaged separately, and have the rest use the service package by setting the `individually` flag at the function level: + +```yml +service: my-service +functions: + hello: + handler: handler.hello + world: + handler: handler.hello + package: + individually: true +``` + +### Development dependencies + +Serverless will auto-detect and exclude development dependencies based on the runtime your service is using. + +This ensures that only the production relevant packages and modules are included in your zip file. Doing this drastically reduces the overall size of the deployment package which will be uploaded to the cloud provider. + +You can opt-out of automatic dev dependency exclusion by setting the `excludeDevDependencies` package config to `false`: + +```yml +package: + excludeDevDependencies: false +``` diff --git a/docs/providers/openwhisk/cli-reference/create.md b/docs/providers/openwhisk/cli-reference/create.md index 5c5201e47b6..952029e9652 100644 --- a/docs/providers/openwhisk/cli-reference/create.md +++ b/docs/providers/openwhisk/cli-reference/create.md @@ -27,8 +27,9 @@ serverless create --template openwhisk-nodejs --path myService ``` ## Options -- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. -- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. +- `--template-path` The local path of your template. **Required if --template and --template-url are not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -69,6 +70,14 @@ This example will generate scaffolding for a service with `openwhisk` as a provi Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. +### Creating a new service using a local template + +```bash +serverless create --template-path path/to/my/template/folder --path path/to/my/service --name my-new-service +``` + +This will copy the `path/to/my/template/folder` folder into `path/to/my/service` and rename the service to `my-new-service`. + ### Create service in new folder using a custom template ```bash diff --git a/docs/providers/openwhisk/guide/intro.md b/docs/providers/openwhisk/guide/intro.md index cbf4a6c57e8..1306d6254de 100644 --- a/docs/providers/openwhisk/guide/intro.md +++ b/docs/providers/openwhisk/guide/intro.md @@ -47,7 +47,7 @@ When you define an event for your Apache OpenWhisk Action in the Serverless Fram ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml diff --git a/docs/providers/spotinst/README.md b/docs/providers/spotinst/README.md index 50999912d7f..5fbe8fba63a 100755 --- a/docs/providers/spotinst/README.md +++ b/docs/providers/spotinst/README.md @@ -29,7 +29,10 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
  • Credentials
  • Serverless.yml Reference
  • Variables
  • -
  • Document Store
  • +
  • Document Store API
  • +
  • Document Store SDK
  • +
  • Endpoint Set Up
  • +
  • Endpoint API Documentation
  • @@ -48,6 +51,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
  • Deploy Function
  • Invoke
  • Logs
  • +
  • Stage Variable
  • Info
  • Remove
  • Plugin List
  • diff --git a/docs/providers/spotinst/cli-reference/create.md b/docs/providers/spotinst/cli-reference/create.md index 0009dea57c5..fa4421d7e60 100755 --- a/docs/providers/spotinst/cli-reference/create.md +++ b/docs/providers/spotinst/cli-reference/create.md @@ -27,8 +27,9 @@ serverless create -t spotinst-nodejs -p myService ``` ## Options -- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. -- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. +- `--template-path` The local path of your template. **Required if --template and --template-url are not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -70,3 +71,11 @@ serverless create -t spotinst-nodejs -p my-new-service This example will generate scaffolding for a service with `Spotinst` as a provider and `ruby` as runtime. The scaffolding will be generated in the `my-new-service` directory. This directory will be created if not present. Otherwise Serverless will use the already present directory. + +### Creating a new service using a local template + +```bash +serverless create --template-path path/to/my/template/folder --path path/to/my/service --name my-new-service +``` + +This will copy the `path/to/my/template/folder` folder into `path/to/my/service` and rename the service to `my-new-service`. diff --git a/docs/providers/spotinst/cli-reference/info.md b/docs/providers/spotinst/cli-reference/info.md index 14ddf4b1613..0f2fa8ade2e 100755 --- a/docs/providers/spotinst/cli-reference/info.md +++ b/docs/providers/spotinst/cli-reference/info.md @@ -1,7 +1,7 @@ diff --git a/docs/providers/spotinst/cli-reference/plugin-list.md b/docs/providers/spotinst/cli-reference/plugin-list.md index a99dbad1fb7..d706a719f50 100644 --- a/docs/providers/spotinst/cli-reference/plugin-list.md +++ b/docs/providers/spotinst/cli-reference/plugin-list.md @@ -1,7 +1,7 @@ diff --git a/docs/providers/spotinst/cli-reference/plugin-search.md b/docs/providers/spotinst/cli-reference/plugin-search.md index efcd4a1eb4f..c78e9d53d94 100644 --- a/docs/providers/spotinst/cli-reference/plugin-search.md +++ b/docs/providers/spotinst/cli-reference/plugin-search.md @@ -1,7 +1,7 @@ diff --git a/docs/providers/spotinst/cli-reference/plugin-uninstall.md b/docs/providers/spotinst/cli-reference/plugin-uninstall.md index 9784c843750..d04239f9165 100644 --- a/docs/providers/spotinst/cli-reference/plugin-uninstall.md +++ b/docs/providers/spotinst/cli-reference/plugin-uninstall.md @@ -1,7 +1,7 @@ diff --git a/docs/providers/spotinst/cli-reference/remove.md b/docs/providers/spotinst/cli-reference/remove.md index 9a1510fc201..87d2ce0d1f6 100755 --- a/docs/providers/spotinst/cli-reference/remove.md +++ b/docs/providers/spotinst/cli-reference/remove.md @@ -1,7 +1,7 @@ diff --git a/docs/providers/spotinst/cli-reference/stage.md b/docs/providers/spotinst/cli-reference/stage.md new file mode 100644 index 00000000000..e26da0f5532 --- /dev/null +++ b/docs/providers/spotinst/cli-reference/stage.md @@ -0,0 +1,35 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials) + + +# Spotinst Functions - Stage Variables + +You are able to set a stage variable in your function to distinguish between the multiple stages that your function maybe going through. The function is initially set to 'dev' for development but there are two ways you can change the stage if you so need. + +## Through Serverless Framwork +To change the stage through the serverless framework you simply need to enter the command + +```bash +serverless deploy --stage #{Your Stage Name} +``` + +## Through the .yml File + +To change the stage in the serverless.yml file you need to add the following into the provider tag then deploy your function as usual + +```bash +provider: + name: spotinst + stage: #{Your Stage Name} + spotinst: + environment: #{Your Environment ID} +``` + diff --git a/docs/providers/spotinst/examples/README.md b/docs/providers/spotinst/examples/README.md index 85c60cab568..0284afe3a30 100644 --- a/docs/providers/spotinst/examples/README.md +++ b/docs/providers/spotinst/examples/README.md @@ -17,3 +17,4 @@ Pick your language of choice: * [JavaScript](./node) * [Python](./python) * [Ruby](./ruby) +* [Java8](./java8) diff --git a/docs/providers/spotinst/examples/java8/README.md b/docs/providers/spotinst/examples/java8/README.md new file mode 100644 index 00000000000..34e9f2b30d1 --- /dev/null +++ b/docs/providers/spotinst/examples/java8/README.md @@ -0,0 +1,45 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/) + + +# Hello World Java8 Example + +Make sure `serverless` is installed. + +## 1. Create a service +`serverless create --template spotinst-java8 --path serviceName` `serviceName` is going to be a new directory there the Java8 template will be loaded. Once the download is complete change into that directory. Next you will need to install the Spotinst Serverless Functions plugin by running `npm install` in the root directory. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. Also you need to copy the Service name in the serverless.yml file and paste it into the pom.xlm file under the finalName tag. Next you will have to package the project to create a .jar file. To do this run the command `mvn package`. + +## 2. Deploy +```bash + serverless deploy +``` + +## 3. Invoke deployed function +```bash +serverless invoke --function hello +``` + +In your terminal window you should see the response + +```bash +{"hello":"null"} +``` + +Congrats you have just deployed and ran your Hello World function! + +## Short Hand Guide + +`sls` is short hand for serverless cli commands + +`-f` is short hand for `--function` + +`-t` is short hand for `--template` + +`-p` is short hang for `--path` \ No newline at end of file diff --git a/docs/providers/spotinst/examples/node/README.md b/docs/providers/spotinst/examples/node/README.md index f4e68778ebc..a8345f43017 100644 --- a/docs/providers/spotinst/examples/node/README.md +++ b/docs/providers/spotinst/examples/node/README.md @@ -14,7 +14,7 @@ layout: Doc Make sure `serverless` is installed. ## 1. Create a service -`serverless create --template spotinst-nodejs --path serviceName` `serviceName` is going to be a new directory there the python template will be loaded. Once the download is complete change into that directory. Next you will need to install the Spotinst Serverless Functions plugin by running `npm install` in the root directory +`serverless create --template spotinst-nodejs --path serviceName` `serviceName` is going to be a new directory there the JavaScript template will be loaded. Once the download is complete change into that directory. Next you will need to install the Spotinst Serverless Functions plugin by running `npm install` in the root directory. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. ## 2. Deploy ```bash diff --git a/docs/providers/spotinst/examples/python/README.md b/docs/providers/spotinst/examples/python/README.md index f85b595018b..c4f58a6284c 100644 --- a/docs/providers/spotinst/examples/python/README.md +++ b/docs/providers/spotinst/examples/python/README.md @@ -14,7 +14,7 @@ layout: Doc Make sure `serverless` is installed. ## 1. Create a service -`serverless create --template spotinst-python --path serviceName` `serviceName` is going to be a new directory there the python template will be loaded. Once the download is complete change into that directory. Next you will need to install the Spotinst Serverless Functions plugin by running `npm install` in the root directory +`serverless create --template spotinst-python --path serviceName` `serviceName` is going to be a new directory there the python template will be loaded. Once the download is complete change into that directory. Next you will need to install the Spotinst Serverless Functions plugin by running `npm install` in the root directory. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. ## 2. Deploy diff --git a/docs/providers/spotinst/examples/ruby/README.md b/docs/providers/spotinst/examples/ruby/README.md index bbf15cc1a8f..e0d7d2e2dd3 100644 --- a/docs/providers/spotinst/examples/ruby/README.md +++ b/docs/providers/spotinst/examples/ruby/README.md @@ -14,7 +14,7 @@ layout: Doc Make sure `serverless` is installed. ## 1. Create a service -`serverless create --template spotinst-ruby --path serviceName` `serviceName` is going to be a new directory there the python template will be loaded. Once the download is complete change into that directory. Next you will need to install the Spotinst Serverless Functions plugin by running `npm install` in the root directory +`serverless create --template spotinst-ruby --path serviceName` `serviceName` is going to be a new directory there the Ruby template will be loaded. Once the download is complete change into that directory. Next you will need to install the Spotinst Serverless Functions plugin by running `npm install` in the root directory. You will need to go into the serverless.yml file and add in the environment variable that you want to deploy into. ## 2. Deploy diff --git a/docs/providers/spotinst/guide/document-store-sdk.md b/docs/providers/spotinst/guide/document-store-sdk.md new file mode 100644 index 00000000000..0ffa6b13d8a --- /dev/null +++ b/docs/providers/spotinst/guide/document-store-sdk.md @@ -0,0 +1,72 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/document-store-sdk) + + +# Spotinst Functions - Document Store SDK + +We have encapsulated the Document Store API calls for retrieving your documents so you will not have to make an API call within the given function. This will allow for you as the user to access your documents using thegetDoc/get_doc method located in the context variable. Additionally this will also eliminate the need for authentication within the function for accessing your documents. + +## Node +```basg +module.exports.main = (event, context, callback) => { + context.getDoc("myKey", function(err, res) { + if(res) { + console.log('res: ' + res); //myValue + var body = { + res: res + }; + + callback(null, { + statusCode: 200, + body: JSON.stringify(body), + headers: {"Content-Type": "application/json"} + }); + } + }); +} +``` + +## Python +```bash +def main(event, context): + print ('context: %s' % context) + + doc = context.get_doc('myKey') + print(doc) #myValue + + res = { + 'statusCode': 200, + 'body': 'res: %s' % doc, + 'headers': {"Content-Type": "application/json"} + } + return res +``` + +## Java 8 +```bash +public class Java8Template implements RequestHandler { + @Override + public Response handleRequest(Request request, Context context) { + String value = context.getDoc("myKey"); + System.out.println(value); //myValue + + Response response = new Response(200, String.format("value: %s", value)); + + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + + response.setHeaders(headers); + + return response; + } +} +``` + diff --git a/docs/providers/spotinst/guide/document-store.md b/docs/providers/spotinst/guide/document-store.md index 693062262ca..f5887f5939b 100644 --- a/docs/providers/spotinst/guide/document-store.md +++ b/docs/providers/spotinst/guide/document-store.md @@ -1,8 +1,8 @@ @@ -10,7 +10,7 @@ layout: Doc ### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials) -# Spotinst Functions - Document Store +# Spotinst Functions - Document Store API Document Store is a way for you to save information across function calls within the same environment without having to access an external database. It is secured by your Spotinst account credentials and can only be accesses within a function. Because you do not need to access an external database you are able to fetch stored documents with low latency (< ~5ms) @@ -20,16 +20,29 @@ To access the document store you will need to make an API request inside a funci This is how to insert a new key/value pair into your document store in a specific environment - * **Request:** `POST` - * **Endpoint:** `https://api.spotinst.io/functions/environment/${environmentId}/userDocument?accountId=${accountId}` - * **Header:** +**HTTPS Request:** + +```bash +POST environment/${environmentId}/userDocument?accountId=${accountId} +``` + +**Host:** + +```bash +api.spotinst.io/functions/ +``` + +**Header:** + ```bash { "Content-Type": "application/json", "Authorization": "Bearer ${Spotinst API Token}" } ``` - * **Body:** + +**Body:** + ```bash { "userDocument" : { @@ -44,16 +57,29 @@ This is how to insert a new key/value pair into your document store in a specifi This is how to update a current key/value pair in your document store in a specific environment - * **Request:** `PUT` - * **Endpoint:** `https://api.spotinst.io/functions/environment/${environmentId}/userDocument/${Key}?accountId=${accountId}` - * **Header:** +**HTTPS Request:** + +```bash +PUT environment/${environmentId}/userDocument/${Key}?accountId=${accountId} +``` + +**Endpoint:** + +```bash +api.spotinst.io/functions/ +``` + +**Header:** + ```bash { "Content-Type": "application/json", "Authorization": "Bearer ${Spotinst API Token}" } ``` - * **Body:** + +**Body:** + ```bash { "userDocument" : { @@ -69,9 +95,20 @@ There are two ways to get the documents from your store, either by specifing a k ### 1. Get Sinlge Key Pair - * **Request:** `GET` - * **Endpoint:** `https://api.spotinst.io/functions/environment/${environmentId}/userDocument/${Key}?accountId=${accountId}` - * **Header:** +**HTTPS Request:** + +```bash +GET environment/${environmentId}/userDocument/${Key}?accountId=${accountId} +``` + +**Endpoint:** + +```bash +api.spotinst.io/functions/ +``` + +**Header:** + ```bash { "Content-Type": "application/json", @@ -81,9 +118,20 @@ There are two ways to get the documents from your store, either by specifing a k ### 2. Get All Keys - * **Request:** `GET` - * **Endpoint:** `https://api.spotinst.io/functions/environment/${environmentId}/userDocument?accountId=${accountId}` - * **Header:** +**HTTPS Request:** + +```bash +GET environment/${environmentId}/userDocument?accountId=${accountId} +``` + +**Endpoint:** + +```bash +api.spotinst.io/functions/ +``` + +**Header:** + ```bash { "Content-Type": "application/json", @@ -96,9 +144,20 @@ There are two ways to get the documents from your store, either by specifing a k This is how to delete a specific key value pair from your document store - * **Request:** `DELETE` - * **Endpoint:** `https://api.spotinst.io/functions/environment/${environmentId}/userDocument/${Key}?accountId=${accountId}` - * **Header:** +**HTTPS Request:** + +```bash +DELETE environment/${environmentId}/userDocument/${Key}?accountId=${accountId} +``` + +**Endpoint:** + +```bash +https://api.spotinst.io/functions/ +``` + +**Header:** + ```bash { "Content-Type": "application/json", diff --git a/docs/providers/spotinst/guide/endpoint-api.md b/docs/providers/spotinst/guide/endpoint-api.md new file mode 100644 index 00000000000..33790666268 --- /dev/null +++ b/docs/providers/spotinst/guide/endpoint-api.md @@ -0,0 +1,202 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials) + + +# Spotinst Functions - Endpoint API Documentation + +Here is the full list of API calls that you can make to set alias and patterns. Please check out the full article on Setting Up Endpoints first because it will make more sense. + +## Alias +### Create Alias +Create a new alias to point to your environment + +#### HTTPS Request +```bash +POST alias?accountId=${accountId} +``` +#### Host +```bash +api.spotinst.io/functions/ +``` +#### Body +```bash +{ + "alias": { + "host": "myAlias.com", + "environmentId": ${Environment ID} + } +} +``` +#### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json + ``` + +### Get Alias +Returns a single alias + +#### HTTPS Request +```bash +GET alias/${Alias ID}?accountId=${accountId} +``` +#### Host +```bash +api.spotinst.io/functions/ +``` +#### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json +``` + +### Get All Alias +Returns all the alias in your account + +#### HTTPS Request +```bash +GET alias?accountId=${accountId} +``` +##### Host +```bash +api.spotinst.io/functions/ +``` +#### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json +``` + +### Delete Alias +Deletes a single alias + +#### HTTPS Request +```bash +DELETE alias/${Alias ID}?accountId=${accountId} +``` +#### Host +```bash +api.spotinst.io/functions/ +``` +#### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json +``` + + +## Pattern +### Create Pattern +Create a new pattern that maps to a function + +#### HTTPS Request +```bash +POST pattern?accountId=${accountId} +``` +#### Host +```bash +api.spotinst.io/functions/ +``` +#### Body +```bash +{ + "pattern": { + "environmentId":${Environment ID}, + "method": "ALL", + "pattern": "/*", + "functionId": ${Function ID} + } +} +``` +#### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json +``` + +### Update Pattern +Update and existing pattern + +#### HTTPS Request +```bash +PUT pattern/${Pattern ID}?accountId=${accountId} +``` +#### Host +```bash +api.spotinst.io/functions/ +``` +#### Body +```bash +{ + "pattern": { + "environmentId":${Environment ID}, + "method": "ALL", + "pattern": "/*", + "functionId": ${Function ID} + } +} +``` +#### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json +``` + +### Get Pattern +Returns a single pattern + +#### HTTPS Request +```bash +GET pattern/${Pattern ID}?accountId=${accountId} +``` +#### Host +```bash +api.spotinst.io/functions/ +``` +#### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json +``` + +### Get All Patterns +Returns all the patterns your account + +#### HTTPS Request +```bash +POST pattern?accountId=${accountId} +``` +#### Host +```bash +api.spotinst.io/functions/ +``` +#### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json +``` + +### Delete Pattern +Delete a single pattern + +#### HTTPS Request +```bash +DELETE pattern/${Pattern ID}?accountId=${accountId} +``` +#### Host +```bash +api.spotinst.io/functions/ +``` +#### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json +``` diff --git a/docs/providers/spotinst/guide/endpoint-setup.md b/docs/providers/spotinst/guide/endpoint-setup.md new file mode 100644 index 00000000000..19d3c5568ac --- /dev/null +++ b/docs/providers/spotinst/guide/endpoint-setup.md @@ -0,0 +1,85 @@ + + + +### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials) + + +# Spotinst Functions - Endpoint Setup + +You are able to set an alias URL name as an endpoint for your serverless function to make it more accessible to your users. The way this works is you will point the domain of your choosing to your environment URL's then you will set paths to each of the functions in that environment you wish to bundle in together. To do this you will first need a valid domain. For this example I will be using 'myAlias.com'. + +## Set DNS Record +First you will want to create a DNS record set that will point to your environment URL. Your environment URL can be found in the Spotinst console. When you select the environment you wish to connect you will see a list of functions and their individual URL's. Like this +```bash +https://app-123xyz-raffleapp-execute-function1.spotinst.io/fx-abc987 +``` +We only want the URL starting at app and ending before the function id. Like this +```bash +app-123xyz-raffleapp-execute-function1.spotinst.io +``` +With this you will need to go to a DNS record setter and point your domain to this URL. I used AWS Route 53 to set this up. + +## Set Alias +Next you will need to set the alias in your Spotinst environment by making an API call. This does not need to be done within a function and can be set anyway you are most comfortable. The API request is connecting your domain the environment that you want. This is the API request + +### HTTPS Request +```bash +POST alias?accountId=${accountId} +``` +### Host +```bash +api.spotinst.io/functions/ +``` +### Body +```bash +{ + "alias": { + "host": "myAlias.com", + "environmentId": ${Your Environment ID} + } +} +``` +### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json +``` + +**Note:** You are able to connect multiple alias to the same environment + +## Set Up Pattern +After you have an alias set up you will need to set up pattern to connect to all the functions in the application. This is again another API call and can be done from anywhere. You specify the pattern that you want, the method that will trigger the function, the function ID and the environment ID. The pattern is what will appear after the domain. For example '/home' would point to 'myAlias.com/home'. The methods you can select are any of the usual HTTP request methods: GET, PUT, POST, DELETE , OPTIONS, PATCH, ALL where “ALL” matches every method + +### HTTPS Request +```bash +POST pattern?accountId=${accountId} +``` +### Host +```bash +api.spotinst.io/functions/ +``` +### Body +``` bash +{ + "pattern": { + "environmentId": ${Your Environment ID}, + "method": "ALL", + "pattern": "/*", + "functionId": ${Your Function ID} + } +} +``` +### Headers +```bash +Authorization: Bearer ${Spotinst API Token} +Content-Type: application/json +``` + +## API Documentation +The full API documentation has information like delete and get alias and patterns. Check it out [here](./endpoint-api.md) diff --git a/docs/providers/spotinst/guide/intro.md b/docs/providers/spotinst/guide/intro.md index fb34107c788..ea915470093 100644 --- a/docs/providers/spotinst/guide/intro.md +++ b/docs/providers/spotinst/guide/intro.md @@ -73,7 +73,7 @@ module.exports.main = function main (event, context, callback) { ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml diff --git a/docs/providers/webtasks/cli-reference/create.md b/docs/providers/webtasks/cli-reference/create.md index 56da10709cf..779f57738d2 100755 --- a/docs/providers/webtasks/cli-reference/create.md +++ b/docs/providers/webtasks/cli-reference/create.md @@ -28,8 +28,9 @@ serverless create --template webtasks-nodejs --path my-service ## Options -- `--template` or `-t` The name of one of the available templates. **Required if --template-url is not present**. -- `--template-url` or `-u` The name of one of the available templates. **Required if --template is not present**. +- `--template` or `-t` The name of one of the available templates. **Required if --template-url and --template-path are not present**. +- `--template-url` or `-u` The name of one of the available templates. **Required if --template and --template-path are not present**. +- `--template-path` The local path of your template. **Required if --template and --template-url are not present**. - `--path` or `-p` The path where the service should be created. - `--name` or `-n` the name of the service in `serverless.yml`. @@ -60,3 +61,11 @@ serverless create --template webtasks-nodejs --path my-new-service This example will generate scaffolding for a service with `webtasks` as a provider. The scaffolding will be generated in the `my-new-service` directory. This directory will be created if not present. Otherwise Serverless will use the already present directory. Additionally Serverless will rename the service according to the path you provide. In this example the service will be renamed to `my-new-service`. + +### Creating a new service using a local template + +```bash +serverless create --template-path path/to/my/template/folder --path path/to/my/service --name my-new-service +``` + +This will copy the `path/to/my/template/folder` folder into `path/to/my/service` and rename the service to `my-new-service`. diff --git a/docs/providers/webtasks/guide/intro.md b/docs/providers/webtasks/guide/intro.md index 3dce6f97b9b..9a8b18cd094 100644 --- a/docs/providers/webtasks/guide/intro.md +++ b/docs/providers/webtasks/guide/intro.md @@ -24,7 +24,7 @@ Here are the Framework's main concepts and how they pertain to Auth0 Webtasks... A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. -The Auth0 Webtasks platform was designed to be simple and easy to use with minimal configuration. Therefore, services that uses Auth0 Webtasks are just a few lines of configuration in a single file, entitled `serverless.yml` (or `serverless.json`). It looks like this: +The Auth0 Webtasks platform was designed to be simple and easy to use with minimal configuration. Therefore, services that uses Auth0 Webtasks are just a few lines of configuration in a single file, entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this: ```yml # serverless.yml diff --git a/lib/classes/CLI.js b/lib/classes/CLI.js index cb177428b83..3d7f4b5ab78 100644 --- a/lib/classes/CLI.js +++ b/lib/classes/CLI.js @@ -5,6 +5,7 @@ const minimist = require('minimist'); const _ = require('lodash'); const os = require('os'); const chalk = require('chalk'); +const getCommandSuggestion = require('../utils/getCommandSuggestion'); class CLI { constructor(serverless, inputArray) { @@ -149,7 +150,6 @@ class CLI { this.consoleLog(chalk.dim('* Documentation: https://serverless.com/framework/docs/')); this.consoleLog(''); - if (!_.isEmpty(frameworkCommands)) { _.forEach(frameworkCommands, (details, command) => { this.displayCommandUsage(details, command); @@ -252,8 +252,9 @@ class CLI { // Throw error if command not found. if (!command) { + const suggestedCommand = getCommandSuggestion(commandName, allCommands); const errorMessage = [ - `Serverless command "${commandName}" not found.`, + `Serverless command "${commandName}" not found. Did you mean "${suggestedCommand}"?`, ' Run "serverless help" for a list of all available commands.', ].join(''); throw new this.serverless.classes.Error(errorMessage); @@ -266,6 +267,7 @@ class CLI { this.displayCommandOptions(command); this.consoleLog(''); + return null; } getVersionNumber() { diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js index b2714300fc1..1325ec9a91d 100644 --- a/lib/classes/PluginManager.js +++ b/lib/classes/PluginManager.js @@ -8,6 +8,7 @@ const writeFile = require('../utils/fs/writeFile'); const getCacheFilePath = require('../utils/getCacheFilePath'); const getServerlessConfigFile = require('../utils/getServerlessConfigFile'); const crypto = require('crypto'); +const getCommandSuggestion = require('../utils/getCommandSuggestion'); /** * @private @@ -324,9 +325,12 @@ class PluginManager { return current.commands[name]; } const commandName = commandOrAlias.slice(0, index + 1).join(' '); - const errorMessage = `Serverless command "${commandName}" not found - - Run "serverless help" for a list of all available commands.`; + const suggestedCommand = getCommandSuggestion(commandName, + this.serverless.cli.loadedCommands); + const errorMessage = [ + `Serverless command "${commandName}" not found. Did you mean "${suggestedCommand}"?`, + ' Run "serverless help" for a list of all available commands.', + ].join(''); throw new this.serverless.classes.Error(errorMessage); }, { commands: this.commands }); } diff --git a/lib/classes/Service.js b/lib/classes/Service.js index c69686c81da..68991debd7e 100644 --- a/lib/classes/Service.js +++ b/lib/classes/Service.js @@ -46,6 +46,7 @@ class Service { 'serverless.yaml', 'serverless.yml', 'serverless.json', + 'serverless.js', ]; const serviceFilePaths = _.map(serviceFilenames, filename => path.join(servicePath, filename)); @@ -61,65 +62,85 @@ class Service { serviceFilenames[serviceFileIndex] : _.first(serviceFilenames); + if (serviceFilename === 'serverless.js') { + return BbPromise.try(() => { + // use require to load serverless.js file + // eslint-disable-next-line global-require + const config = require(serviceFilePath); + + if (!_.isPlainObject(config)) { + throw new Error('serverless.js must export plain object'); + } + + return that.loadServiceFileParam(serviceFilename, config); + }); + } + return that.serverless.yamlParser .parse(serviceFilePath) - .then((serverlessFileParam) => { - const serverlessFile = serverlessFileParam; - // basic service level validation - const version = this.serverless.utils.getVersion(); - const ymlVersion = serverlessFile.frameworkVersion; - if (ymlVersion && !semver.satisfies(version, ymlVersion)) { - const errorMessage = [ - `The Serverless version (${version}) does not satisfy the`, - ` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`, - ].join(''); - throw new ServerlessError(errorMessage); - } - if (!serverlessFile.service) { - throw new ServerlessError(`"service" property is missing in ${serviceFilename}`); - } - if (_.isObject(serverlessFile.service) && !serverlessFile.service.name) { - throw new ServerlessError(`"service" is missing the "name" property in ${serviceFilename}`); // eslint-disable-line max-len - } - if (!serverlessFile.provider) { - throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`); - } + .then((serverlessFileParam) => + that.loadServiceFileParam(serviceFilename, serverlessFileParam) + ); + } - if (typeof serverlessFile.provider !== 'object') { - const providerName = serverlessFile.provider; - serverlessFile.provider = { - name: providerName, - }; - } + loadServiceFileParam(serviceFilename, serverlessFileParam) { + const that = this; - if (_.isObject(serverlessFile.service)) { - that.serviceObject = serverlessFile.service; - that.service = serverlessFile.service.name; - } else { - that.serviceObject = { name: serverlessFile.service }; - that.service = serverlessFile.service; - } + const serverlessFile = serverlessFileParam; + // basic service level validation + const version = this.serverless.utils.getVersion(); + const ymlVersion = serverlessFile.frameworkVersion; + if (ymlVersion && !semver.satisfies(version, ymlVersion)) { + const errorMessage = [ + `The Serverless version (${version}) does not satisfy the`, + ` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`, + ].join(''); + throw new ServerlessError(errorMessage); + } + if (!serverlessFile.service) { + throw new ServerlessError(`"service" property is missing in ${serviceFilename}`); + } + if (_.isObject(serverlessFile.service) && !serverlessFile.service.name) { + throw new ServerlessError(`"service" is missing the "name" property in ${serviceFilename}`); // eslint-disable-line max-len + } + if (!serverlessFile.provider) { + throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`); + } - that.custom = serverlessFile.custom; - that.plugins = serverlessFile.plugins; - that.resources = serverlessFile.resources; - that.functions = serverlessFile.functions || {}; - - // merge so that the default settings are still in place and - // won't be overwritten - that.provider = _.merge(that.provider, serverlessFile.provider); - - if (serverlessFile.package) { - that.package.individually = serverlessFile.package.individually; - that.package.path = serverlessFile.package.path; - that.package.artifact = serverlessFile.package.artifact; - that.package.exclude = serverlessFile.package.exclude; - that.package.include = serverlessFile.package.include; - that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies; - } + if (!_.isObject(serverlessFile.provider)) { + const providerName = serverlessFile.provider; + serverlessFile.provider = { + name: providerName, + }; + } - return this; - }); + if (_.isObject(serverlessFile.service)) { + that.serviceObject = serverlessFile.service; + that.service = serverlessFile.service.name; + } else { + that.serviceObject = { name: serverlessFile.service }; + that.service = serverlessFile.service; + } + + that.custom = serverlessFile.custom; + that.plugins = serverlessFile.plugins; + that.resources = serverlessFile.resources; + that.functions = serverlessFile.functions || {}; + + // merge so that the default settings are still in place and + // won't be overwritten + that.provider = _.merge(that.provider, serverlessFile.provider); + + if (serverlessFile.package) { + that.package.individually = serverlessFile.package.individually; + that.package.path = serverlessFile.package.path; + that.package.artifact = serverlessFile.package.artifact; + that.package.exclude = serverlessFile.package.exclude; + that.package.include = serverlessFile.package.include; + that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies; + } + + return this; } setFunctionNames(rawOptions) { diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js index 5ea2eb39a98..96bf7529fad 100644 --- a/lib/classes/Service.test.js +++ b/lib/classes/Service.test.js @@ -287,6 +287,75 @@ describe('Service', () => { }); }); + it('should load serverless.js from filesystem', () => { + const SUtils = new Utils(); + const serverlessJSON = { + service: 'new-service', + provider: { + name: 'aws', + stage: 'dev', + region: 'us-east-1', + variableSyntax: '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}', + }, + plugins: ['testPlugin'], + functions: { + functionA: {}, + }, + resources: { + aws: { + resourcesProp: 'value', + }, + azure: {}, + google: {}, + }, + package: { + exclude: ['exclude-me'], + include: ['include-me'], + artifact: 'some/path/foo.zip', + }, + }; + + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.js'), + `module.exports = ${JSON.stringify(serverlessJSON)};`); + + const serverless = new Serverless(); + serverless.config.update({ servicePath: tmpDirPath }); + serviceInstance = new Service(serverless); + + return expect(serviceInstance.load()).to.eventually.be.fulfilled + .then(() => { + expect(serviceInstance.service).to.be.equal('new-service'); + expect(serviceInstance.provider.name).to.deep.equal('aws'); + expect(serviceInstance.provider.variableSyntax).to.equal( + '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}' + ); + expect(serviceInstance.plugins).to.deep.equal(['testPlugin']); + expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' }); + expect(serviceInstance.resources.azure).to.deep.equal({}); + expect(serviceInstance.resources.google).to.deep.equal({}); + expect(serviceInstance.package.exclude.length).to.equal(1); + expect(serviceInstance.package.exclude[0]).to.equal('exclude-me'); + expect(serviceInstance.package.include.length).to.equal(1); + expect(serviceInstance.package.include[0]).to.equal('include-me'); + expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip'); + expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined); + }); + }); + + it('should throw error if serverless.js exports invalid config', () => { + const SUtils = new Utils(); + + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.js'), + 'module.exports = function config() {};'); + + const serverless = new Serverless(); + serverless.config.update({ servicePath: tmpDirPath }); + serviceInstance = new Service(serverless); + + return expect(serviceInstance.load()) + .to.be.rejectedWith('serverless.js must export plain object'); + }); + it('should load YAML in favor of JSON', () => { const SUtils = new Utils(); const serverlessJSON = { diff --git a/lib/classes/Utils.js b/lib/classes/Utils.js index e73a5d3741b..921a5ee596e 100644 --- a/lib/classes/Utils.js +++ b/lib/classes/Utils.js @@ -105,6 +105,8 @@ class Utils { servicePath = process.cwd(); } else if (fileExistsSync(path.join(process.cwd(), 'serverless.json'))) { servicePath = process.cwd(); + } else if (fileExistsSync(path.join(process.cwd(), 'serverless.js'))) { + servicePath = process.cwd(); } return servicePath; diff --git a/lib/classes/Utils.test.js b/lib/classes/Utils.test.js index 890b40191d6..4f8745e1245 100644 --- a/lib/classes/Utils.test.js +++ b/lib/classes/Utils.test.js @@ -281,6 +281,18 @@ describe('Utils', () => { expect(servicePath).to.not.equal(null); }); + it('should detect if the CWD is a service directory when using Serverless .js files', () => { + const tmpDirPath = testUtils.getTmpDirPath(); + const tmpFilePath = path.join(tmpDirPath, 'serverless.js'); + + serverless.utils.writeFileSync(tmpFilePath, 'foo'); + process.chdir(tmpDirPath); + + const servicePath = serverless.utils.findServicePath(); + + expect(servicePath).to.not.equal(null); + }); + it('should detect if the CWD is not a service directory', () => { // just use the root of the tmpdir because findServicePath will // also check parent directories (and may find matching tmp dirs diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js index f69a810caea..738a0347cc8 100644 --- a/lib/classes/Variables.js +++ b/lib/classes/Variables.js @@ -22,7 +22,7 @@ class Variables { this.selfRefSyntax = RegExp(/^self:/g); this.cfRefSyntax = RegExp(/^cf:/g); this.s3RefSyntax = RegExp(/^s3:(.+?)\/(.+)$/); - this.stringRefSynax = RegExp(/('.*')|(".*")/g); + this.stringRefSyntax = RegExp(/('.*')|(".*")/g); this.ssmRefSyntax = RegExp(/^ssm:([a-zA-Z0-9_.\-/]+)[~]?(true|false)?/); } @@ -199,7 +199,7 @@ class Variables { value = this.getValueFromCf(variableString); } else if (variableString.match(this.s3RefSyntax)) { value = this.getValueFromS3(variableString); - } else if (variableString.match(this.stringRefSynax)) { + } else if (variableString.match(this.stringRefSyntax)) { value = this.getValueFromString(variableString); } else if (variableString.match(this.ssmRefSyntax)) { value = this.getValueFromSsm(variableString); diff --git a/lib/plugins/aws/deploy/lib/createStack.js b/lib/plugins/aws/deploy/lib/createStack.js index 303b3e44102..de10370e8b8 100644 --- a/lib/plugins/aws/deploy/lib/createStack.js +++ b/lib/plugins/aws/deploy/lib/createStack.js @@ -58,7 +58,12 @@ module.exports = { { StackName: stackName } ) .then((data) => { - if (this.provider.isS3TransferAccelerationEnabled()) { + const shouldCheckStackOutput = + // check stack output only if acceleration is requested + this.provider.isS3TransferAccelerationEnabled() && + // custom deployment bucket won't generate any output (no check) + !this.serverless.service.provider.deploymentBucket; + if (shouldCheckStackOutput) { const isAlreadyAccelerated = !!_.find(data.Stacks[0].Outputs, { OutputKey: 'ServerlessDeploymentBucketAccelerated' }); if (!isAlreadyAccelerated) { @@ -66,7 +71,7 @@ module.exports = { this.provider.disableTransferAccelerationForCurrentDeploy(); } } - BbPromise.resolve('alreadyCreated'); + return BbPromise.resolve('alreadyCreated'); })) .catch((e) => { if (e.message.indexOf('does not exist') > -1) { diff --git a/lib/plugins/aws/deploy/lib/createStack.test.js b/lib/plugins/aws/deploy/lib/createStack.test.js index a75e8e8f6e2..28d5820e99b 100644 --- a/lib/plugins/aws/deploy/lib/createStack.test.js +++ b/lib/plugins/aws/deploy/lib/createStack.test.js @@ -87,6 +87,15 @@ describe('createStack', () => { }); }); + it('should throw error if invalid stack name', () => { + sandbox.stub(awsDeploy, 'create').resolves(); + sandbox.stub(awsDeploy.provider, 'request').resolves(); + awsDeploy.serverless.service.service = 'service-name'.repeat(100); + + return expect(awsDeploy.createStack.bind(awsDeploy)) + .to.throw(awsDeploy.serverless.classes.Error, /not valid/); + }); + it('should set the createLater flag and resolve if deployment bucket is provided', () => { awsDeploy.serverless.service.provider.deploymentBucket = 'serverless'; sandbox.stub(awsDeploy.provider, 'request') @@ -104,7 +113,7 @@ describe('createStack', () => { sandbox.stub(awsDeploy.provider, 'request').rejects(errorMock); - const createStub = sinon + const createStub = sandbox .stub(awsDeploy, 'create').resolves(); return awsDeploy.createStack().catch((e) => { @@ -121,7 +130,7 @@ describe('createStack', () => { sandbox.stub(awsDeploy.provider, 'request').rejects(errorMock); - const createStub = sinon + const createStub = sandbox .stub(awsDeploy, 'create').resolves(); return awsDeploy.createStack().then(() => { @@ -149,5 +158,27 @@ describe('createStack', () => { expect(disableTransferAccelerationStub.called).to.be.equal(true); }); }); + + it('should not disable S3 Transfer Acceleration if custom bucket is used', () => { + const disableTransferAccelerationStub = sandbox + .stub(awsDeploy.provider, + 'disableTransferAccelerationForCurrentDeploy').resolves(); + + const describeStacksOutput = { + Stacks: [ + { + Outputs: [], + }, + ], + }; + sandbox.stub(awsDeploy.provider, 'request').resolves(describeStacksOutput); + + awsDeploy.provider.options['aws-s3-accelerate'] = true; + awsDeploy.serverless.service.provider.deploymentBucket = 'my-custom-bucket'; + + return awsDeploy.createStack().then(() => { + expect(disableTransferAccelerationStub.called).to.be.equal(false); + }); + }); }); }); diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js index f73c5cb10fe..ecc355cf91d 100644 --- a/lib/plugins/aws/invokeLocal/index.js +++ b/lib/plugins/aws/invokeLocal/index.js @@ -118,10 +118,10 @@ class AwsInvokeLocal { || this.serverless.service.provider.runtime || 'nodejs4.3'; const handler = this.options.functionObj.handler; - const handlerPath = handler.split('.')[0]; - const handlerName = handler.split('.')[1]; if (runtime.startsWith('nodejs')) { + const handlerPath = handler.split('.')[0]; + const handlerName = handler.split('.')[1]; return this.invokeLocalNodeJs( handlerPath, handlerName, @@ -130,6 +130,8 @@ class AwsInvokeLocal { } if (runtime === 'python2.7' || runtime === 'python3.6') { + const handlerPath = handler.split('.')[0]; + const handlerName = handler.split('.')[1]; return this.invokeLocalPython( process.platform === 'win32' ? 'python.exe' : runtime, handlerPath, @@ -139,9 +141,12 @@ class AwsInvokeLocal { } if (runtime === 'java8') { + const className = handler.split('::')[0]; + const handlerName = handler.split('::')[1] || 'handleRequest'; return this.invokeLocalJava( 'java', - handler, + className, + handlerName, this.serverless.service.package.artifact, this.options.data, this.options.context); @@ -168,7 +173,7 @@ class AwsInvokeLocal { return new BbPromise(resolve => { const python = spawn(runtime, - [path.join(__dirname, 'invoke.py'), handlerPath, handlerName], { env: process.env }); + ['-u', path.join(__dirname, 'invoke.py'), handlerPath, handlerName], { env: process.env }); python.stdout.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); python.stderr.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); python.stdin.write(input); @@ -177,11 +182,12 @@ class AwsInvokeLocal { }); } - callJavaBridge(artifactPath, className, input) { + callJavaBridge(artifactPath, className, handlerName, input) { return new BbPromise((resolve) => fs.statAsync(artifactPath).then(() => { const java = spawn('java', [ `-DartifactPath=${artifactPath}`, `-DclassName=${className}`, + `-DhandlerName=${handlerName}`, '-jar', path.join(__dirname, 'java', 'target', 'invoke-bridge-1.0.jar'), ]); @@ -201,7 +207,7 @@ class AwsInvokeLocal { })); } - invokeLocalJava(runtime, className, artifactPath, event, customContext) { + invokeLocalJava(runtime, className, handlerName, artifactPath, event, customContext) { const timeout = Number(this.options.functionObj.timeout) || Number(this.serverless.service.provider.timeout) || 6; @@ -220,7 +226,7 @@ class AwsInvokeLocal { const executablePath = path.join(javaBridgePath, 'target'); return new BbPromise(resolve => fs.statAsync(executablePath) - .then(() => this.callJavaBridge(artifactPath, className, input)) + .then(() => this.callJavaBridge(artifactPath, className, handlerName, input)) .then(resolve) .catch(() => { const mvn = spawn('mvn', [ @@ -235,7 +241,8 @@ class AwsInvokeLocal { mvn.stderr.on('data', (buf) => this.serverless.cli.consoleLog(`mvn - ${buf.toString()}`)); mvn.stdin.end(); - mvn.on('close', () => this.callJavaBridge(artifactPath, className, input).then(resolve)); + mvn.on('close', () => this.callJavaBridge(artifactPath, className, handlerName, input) + .then(resolve)); })); } diff --git a/lib/plugins/aws/invokeLocal/index.test.js b/lib/plugins/aws/invokeLocal/index.test.js index d2542acaeb6..c2e83743507 100644 --- a/lib/plugins/aws/invokeLocal/index.test.js +++ b/lib/plugins/aws/invokeLocal/index.test.js @@ -379,6 +379,7 @@ describe('AwsInvokeLocal', () => { expect(invokeLocalJavaStub.calledWithExactly( 'java', 'handler.hello', + 'handleRequest', undefined, {}, undefined @@ -588,6 +589,7 @@ describe('AwsInvokeLocal', () => { awsInvokeLocalMocked.callJavaBridge( __dirname, 'com.serverless.Handler', + 'handleRequest', '{}' ).then(() => { expect(writeChildStub.calledOnce).to.be.equal(true); @@ -625,6 +627,7 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.invokeLocalJava( 'java', 'com.serverless.Handler', + 'handleRequest', __dirname, {} ).then(() => { @@ -632,6 +635,7 @@ describe('AwsInvokeLocal', () => { expect(callJavaBridgeStub.calledWithExactly( __dirname, 'com.serverless.Handler', + 'handleRequest', JSON.stringify({ event: {}, context: { @@ -694,6 +698,7 @@ describe('AwsInvokeLocal', () => { awsInvokeLocalMocked.invokeLocalJava( 'java', 'com.serverless.Handler', + 'handleRequest', __dirname, {} ).then(() => { @@ -701,6 +706,7 @@ describe('AwsInvokeLocal', () => { expect(callJavaBridgeMockedStub.calledWithExactly( __dirname, 'com.serverless.Handler', + 'handleRequest', JSON.stringify({ event: {}, context: { diff --git a/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java index ab962942669..879c52f2fc5 100644 --- a/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java +++ b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; import java.io.BufferedReader; import java.io.File; import java.io.IOException; @@ -16,12 +18,14 @@ public class InvokeBridge { private File artifact; private String className; + private String handlerName; private Object instance; private Class clazz; private InvokeBridge() { this.artifact = new File(new File("."), System.getProperty("artifactPath")); this.className = System.getProperty("className"); + this.handlerName = System.getProperty("handlerName"); try { HashMap parsedInput = parseInput(getInput()); @@ -56,9 +60,52 @@ private Object getInstance() throws Exception { } private Object invoke(HashMap event, Context context) throws Exception { - Method[] methods = this.clazz.getDeclaredMethods(); + Method method = findHandlerMethod(this.clazz, this.handlerName); + Class requestClass = method.getParameterTypes()[0]; + + Object request = event; + if (!requestClass.isAssignableFrom(event.getClass())) { + request = requestClass.newInstance(); + PropertyDescriptor[] properties = Introspector.getBeanInfo(requestClass).getPropertyDescriptors(); + for(int i=0; i < properties.length; i++) { + if (properties[i].getWriteMethod() == null) continue; + String propertyName = properties[i].getName(); + if (event.containsKey(propertyName)) { + properties[i].getWriteMethod().invoke(request, event.get(propertyName)); + } + } + } + + if (method.getParameterCount() == 1) { + return method.invoke(this.instance, request); + } else if (method.getParameterCount() == 2) { + return method.invoke(this.instance, request, context); + } else { + throw new NoSuchMethodException("Handler should take 1 or 2 arguments: " + method); + } + } + + private Method findHandlerMethod(Class clazz, String handlerName) throws Exception { + Method candidateMethod = null; + for(Method method: clazz.getDeclaredMethods()) { + if (method.getName().equals(handlerName) && !method.isBridge()) { + // Select the method with the largest number of parameters + // If two or more methods have the same number of parameters, AWS Lambda selects the method that has + // the Context as the last parameter. + // If none or all of these methods have the Context parameter, then the behavior is undefined. + int paramCount = method.getParameterCount(); + boolean lastParamIsContext = paramCount >= 1 && method.getParameterTypes()[paramCount-1].getName().equals("com.amazonaws.services.lambda.runtime.Context"); + if (candidateMethod == null || paramCount > candidateMethod.getParameterCount() || (paramCount == candidateMethod.getParameterCount() && lastParamIsContext)) { + candidateMethod = method; + } + } + } + + if (candidateMethod == null) { + throw new NoSuchMethodException("Could not find handler for " + handlerName + " in " + clazz.getName()); + } - return methods[1].invoke(this.instance, event, context); + return candidateMethod; } private HashMap parseInput(String input) throws IOException { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.js index fe635fd84a6..069455a3e4b 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/apiKeys.js @@ -27,7 +27,7 @@ module.exports = { Enabled: true, Name: apiKey, StageKeys: [{ - RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, + RestApiId: this.provider.getApiGatewayRestApiId(), StageName: this.provider.getStage(), }], }, diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js index e041f45fbbe..5d15ad887ef 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js @@ -12,7 +12,7 @@ module.exports = { AuthorizerResultTtlInSeconds: authorizer.resultTtlInSeconds, IdentitySource: authorizer.identitySource, Name: authorizer.name, - RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, + RestApiId: this.provider.getApiGatewayRestApiId(), }; if (typeof authorizer.identityValidationExpression === 'string') { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js index cb8a5fd91e3..167d113fd0a 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js @@ -47,7 +47,7 @@ module.exports = { IntegrationResponses: this.generateCorsIntegrationResponses(preflightHeaders), }, ResourceId: resourceRef, - RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, + RestApiId: this.provider.getApiGatewayRestApiId(), }, }, }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js index 59858a73039..db2d06c876b 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js @@ -34,19 +34,28 @@ describe('#compileCors()', () => { }; awsCompileApigEvents = new AwsCompileApigEvents(serverless, options); awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'; - awsCompileApigEvents.apiGatewayResourceLogicalIds = { - 'users/create': 'ApiGatewayResourceUsersCreate', - 'users/list': 'ApiGatewayResourceUsersList', - 'users/update': 'ApiGatewayResourceUsersUpdate', - 'users/delete': 'ApiGatewayResourceUsersDelete', - 'users/any': 'ApiGatewayResourceUsersAny', - }; - awsCompileApigEvents.apiGatewayResourceNames = { - 'users/create': 'UsersCreate', - 'users/list': 'UsersList', - 'users/update': 'UsersUpdate', - 'users/delete': 'UsersDelete', - 'users/any': 'UsersAny', + awsCompileApigEvents.apiGatewayResources = { + 'users/create': { + name: 'UsersCreate', + resourceLogicalId: 'ApiGatewayResourceUsersCreate', + }, + + 'users/list': { + name: 'UsersList', + resourceLogicalId: 'ApiGatewayResourceUsersList', + }, + 'users/update': { + name: 'UsersUpdate', + resourceLogicalId: 'ApiGatewayResourceUsersUpdate', + }, + 'users/delete': { + name: 'UsersDelete', + resourceLogicalId: 'ApiGatewayResourceUsersDelete', + }, + 'users/any': { + name: 'UsersAny', + resourceLogicalId: 'ApiGatewayResourceUsersAny', + }, }; awsCompileApigEvents.validated = {}; }); 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 0f6b6b83c89..dd1d804a9a0 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js @@ -12,7 +12,7 @@ module.exports = { [this.apiGatewayDeploymentLogicalId]: { Type: 'AWS::ApiGateway::Deployment', Properties: { - RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, + RestApiId: this.provider.getApiGatewayRestApiId(), StageName: this.provider.getStage(), }, DependsOn: this.apiGatewayMethodLogicalIds, @@ -27,7 +27,7 @@ module.exports = { 'Fn::Join': ['', [ 'https://', - { Ref: this.apiGatewayRestApiLogicalId }, + this.provider.getApiGatewayRestApiId(), `.execute-api.${ this.provider.getRegion() }.amazonaws.com/${ diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js index ac0147d428c..c0c084852fe 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.js @@ -20,7 +20,7 @@ module.exports = { HttpMethod: event.http.method.toUpperCase(), RequestParameters: requestParameters, ResourceId: resourceId, - RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, + RestApiId: this.provider.getApiGatewayRestApiId(), }, }; diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js index 3ccddc8100f..2f47e5c50e4 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js @@ -35,17 +35,24 @@ describe('#compileMethods()', () => { awsCompileApigEvents = new AwsCompileApigEvents(serverless, options); awsCompileApigEvents.validated = {}; awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'; - awsCompileApigEvents.apiGatewayResourceLogicalIds = { - 'users/create': 'ApiGatewayResourceUsersCreate', - 'users/list': 'ApiGatewayResourceUsersList', - 'users/update': 'ApiGatewayResourceUsersUpdate', - 'users/delete': 'ApiGatewayResourceUsersDelete', - }; - awsCompileApigEvents.apiGatewayResourceNames = { - 'users/create': 'UsersCreate', - 'users/list': 'UsersList', - 'users/update': 'UsersUpdate', - 'users/delete': 'UsersDelete', + awsCompileApigEvents.apiGatewayResources = { + 'users/create': { + name: 'UsersCreate', + resourceLogicalId: 'ApiGatewayResourceUsersCreate', + }, + + 'users/list': { + name: 'UsersList', + resourceLogicalId: 'ApiGatewayResourceUsersList', + }, + 'users/update': { + name: 'UsersUpdate', + resourceLogicalId: 'ApiGatewayResourceUsersUpdate', + }, + 'users/delete': { + name: 'UsersDelete', + resourceLogicalId: 'ApiGatewayResourceUsersDelete', + }, }; }); @@ -111,6 +118,25 @@ describe('#compileMethods()', () => { }); }); + it('should not have integration RequestParameters when no request parameters are set', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + path: 'users/create', + method: 'post', + integration: 'AWS', + }, + }, + ]; + return awsCompileApigEvents.compileMethods().then(() => { + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration + ).to.not.have.key('RequestParameters'); + }); + }); + it('should create method resources when http events given', () => { awsCompileApigEvents.validated.events = [ { @@ -148,6 +174,16 @@ describe('#compileMethods()', () => { path: 'users/create', method: 'post', integration: 'AWS', + request: { + parameters: { + 'method.request.querystring.foo': true, + 'method.request.querystring.bar': false, + 'method.request.path.foo': true, + 'method.request.path.bar': false, + 'method.request.header.foo': true, + 'method.request.header.bar': false, + }, + }, }, }, ]; @@ -156,6 +192,17 @@ describe('#compileMethods()', () => { awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type ).to.equal('AWS'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters + ).to.deep.equal({ + 'integration.request.querystring.foo': 'method.request.querystring.foo', + 'integration.request.querystring.bar': 'method.request.querystring.bar', + 'integration.request.path.foo': 'method.request.path.foo', + 'integration.request.path.bar': 'method.request.path.bar', + 'integration.request.header.foo': 'method.request.header.foo', + 'integration.request.header.bar': 'method.request.header.bar', + }); }); }); @@ -188,6 +235,14 @@ describe('#compileMethods()', () => { integration: 'HTTP', request: { uri: 'https://example.com', + parameters: { + 'method.request.querystring.foo': true, + 'method.request.querystring.bar': false, + 'method.request.path.foo': true, + 'method.request.path.bar': false, + 'method.request.header.foo': true, + 'method.request.header.bar': false, + }, }, }, }, @@ -209,6 +264,17 @@ describe('#compileMethods()', () => { awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestTemplates ).to.equal(undefined); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters + ).to.deep.equal({ + 'integration.request.querystring.foo': 'method.request.querystring.foo', + 'integration.request.querystring.bar': 'method.request.querystring.bar', + 'integration.request.path.foo': 'method.request.path.foo', + 'integration.request.path.bar': 'method.request.path.bar', + 'integration.request.header.foo': 'method.request.header.foo', + 'integration.request.header.bar': 'method.request.header.bar', + }); }); }); @@ -254,6 +320,14 @@ describe('#compileMethods()', () => { request: { uri: 'https://example.com', method: 'patch', + parameters: { + 'method.request.querystring.foo': true, + 'method.request.querystring.bar': false, + 'method.request.path.foo': true, + 'method.request.path.bar': false, + 'method.request.header.foo': true, + 'method.request.header.bar': false, + }, }, }, }, @@ -271,6 +345,17 @@ describe('#compileMethods()', () => { awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.IntegrationHttpMethod ).to.equal('PATCH'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.RequestParameters + ).to.deep.equal({ + 'integration.request.querystring.foo': 'method.request.querystring.foo', + 'integration.request.querystring.bar': 'method.request.querystring.bar', + 'integration.request.path.foo': 'method.request.path.foo', + 'integration.request.path.bar': 'method.request.path.bar', + 'integration.request.header.foo': 'method.request.header.foo', + 'integration.request.header.bar': 'method.request.header.bar', + }); }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js index 28aa0d7a717..1a900bd9d9b 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js @@ -88,6 +88,13 @@ module.exports = { }); } + if ((type === 'AWS' || type === 'HTTP' || type === 'HTTP_PROXY') && + (http.request && !_.isEmpty(http.request.parameters))) { + _.assign(integration, { + RequestParameters: this.getIntegrationRequestParameters(http), + }); + } + return { Properties: { Integration: integration, @@ -174,6 +181,16 @@ module.exports = { return !_.isEmpty(integrationRequestTemplates) ? integrationRequestTemplates : undefined; }, + getIntegrationRequestParameters(http) { + const parameters = {}; + if (http.request && http.request.parameters) { + _.each(http.request.parameters, (value, key) => { + parameters[`integration.${key.substring('method.'.length)}`] = key; + }); + } + return parameters; + }, + DEFAULT_JSON_REQUEST_TEMPLATE: ` #set( $body = $input.json("$") ) diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js index a256405810a..2c09c0c7ba4 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js @@ -26,7 +26,7 @@ module.exports = { ':', { Ref: 'AWS::AccountId' }, ':', - { Ref: this.apiGatewayRestApiLogicalId }, + this.provider.getApiGatewayRestApiId(), '/*/*', ], ] }, diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.js index ea714f1707c..fac0b36cada 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.js @@ -6,67 +6,248 @@ const _ = require('lodash'); module.exports = { compileResources() { - const resourcePaths = this.getResourcePaths(); - - this.apiGatewayResourceNames = {}; - this.apiGatewayResourceLogicalIds = {}; + this.apiGatewayResources = this.getResourcePaths(); // ['users', 'users/create', 'users/create/something'] - resourcePaths.forEach(path => { - const pathArray = path.split('/'); - const resourceName = this.provider.naming.normalizePath(path); - const resourceLogicalId = this.provider.naming.getResourceLogicalId(path); - const pathPart = pathArray.pop(); - const parentPath = pathArray.join('/'); - const parentRef = this.getResourceId(parentPath); + _.keys(this.apiGatewayResources).forEach((path) => { + const resource = this.apiGatewayResources[path]; + if (resource.resourceId) { + return; + } - this.apiGatewayResourceNames[path] = resourceName; - this.apiGatewayResourceLogicalIds[path] = resourceLogicalId; + resource.resourceLogicalId = this.provider.naming.getResourceLogicalId(path); + resource.resourceId = { Ref: resource.resourceLogicalId }; + + const parentRef = resource.parent + ? resource.parent.resourceId : this.getResourceId(); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { - [resourceLogicalId]: { + [resource.resourceLogicalId]: { Type: 'AWS::ApiGateway::Resource', Properties: { ParentId: parentRef, - PathPart: pathPart, - RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, + PathPart: resource.pathPart, + RestApiId: this.provider.getApiGatewayRestApiId(), }, }, }); }); + return BbPromise.resolve(); }, + combineResourceTrees(trees) { + const self = this; + + function getNodePaths(result, node) { + const r = result; + r[node.path] = node; + if (!node.name) { + r[node.path].name = self.provider.naming.normalizePath(node.path); + } + + node.children.forEach((child) => getNodePaths(result, child)); + } + + return _.reduce(trees, (result, tree) => { + getNodePaths(result, tree); + return result; + }, {}); + }, + getResourcePaths() { - const paths = _.reduce(this.validated.events, (resourcePaths, event) => { - let path = event.http.path; + const trees = []; + const predefinedResourceNodes = []; + const methodNodes = []; + const predefinedResources = this.provider.getApiGatewayPredefinedResources(); + + + function cutBranch(node) { + if (!node.parent) { + return; + } + + const n = node; + if (node.parent.children.length <= 1) { + n.parent.children = []; + } else { + n.parent.children = node.parent.children.filter((c) => c.path !== n.path); + n.parent.isCut = true; + } + n.parent = null; + } + + // organize all resource paths into N-ary tree + function applyResource(resource, isMethod) { + let root; + let parent; + let currentPath; + const path = resource.path.replace(/^\//, '').replace(/\/$/, ''); + const pathParts = path.split('/'); + + function applyNodeResource(node, parts, index) { + const n = node; + if (index === parts.length - 1) { + n.name = resource.name; + if (resource.resourceId) { + n.resourceId = resource.resourceId; + if (_.every(predefinedResourceNodes, (iter) => iter.path !== n.path)) { + predefinedResourceNodes.push(node); + } + } + if (isMethod && !node.hasMethod) { + n.hasMethod = true; + if (_.every(methodNodes, (iter) => iter.path !== n.path)) { + methodNodes.push(node); + } + } + } + + parent = node; + } + + pathParts.forEach((pathPart, index) => { + currentPath = currentPath ? `${currentPath}/${pathPart}` : pathPart; + root = root || _.find(trees, (node) => node.path === currentPath); + parent = parent || root; + + let node; + if (parent) { + if (parent.path === currentPath) { + applyNodeResource(parent, pathParts, index); + return; + } else if (parent.children.length > 0) { + node = _.find(parent.children, (n) => n.path === currentPath); + if (node) { + applyNodeResource(node, pathParts, index); + return; + } + } + } + + node = { + path: currentPath, + pathPart, + parent, + + level: index, + children: [], + }; + + if (parent) { + parent.children.push(node); + } + + if (!root) { + root = node; + trees.push(root); + } + + applyNodeResource(node, pathParts, index); + }); + } + + predefinedResources.forEach(applyResource); + this.validated.events.forEach((event) => { + if (event.http.path) { + applyResource(event.http, true); + } + }); + + // if predefinedResources array is empty, return all paths + if (predefinedResourceNodes.length === 0) { + return this.combineResourceTrees(trees); + } + + // if all methods have resource ID already, no need to validate resource trees + if (_.every(this.validated.events, (event) => + _.some(predefinedResourceNodes, (node) => + node.path === event.http.path))) { + return _.reduce(predefinedResources, (resourceMap, resource) => { + const r = resourceMap; + r[resource.path] = resource; + + if (!resource.name) { + r[resource.path].name = this.provider.naming.normalizePath(resource.path); + } + return r; + }, {}); + } + + // cut resource branches from trees + const sortedResourceNodes = _.sortBy(predefinedResourceNodes, + node => node.level); + const validatedTrees = []; + + for (let i = sortedResourceNodes.length - 1; i >= 0; i--) { + const node = sortedResourceNodes[i]; + let parent = node; + + while (parent && parent.parent) { + if (parent.parent.hasMethod && !parent.parent.resourceId) { + throw new Error(`Resource ID for path ${parent.parent.path} is required`); + } + + if (parent.parent.resourceId || parent.parent.children.length > 1) { + cutBranch(parent); + break; + } + + parent = parent.parent; + } + } + + // get branches that begin from root resource + methodNodes.forEach((node) => { + let iter = node; + while (iter) { + if (iter.resourceId) { + cutBranch(iter); + if (_.every(validatedTrees, (t) => t.path !== node.path)) { + validatedTrees.push(iter); + } + + break; + } + + if (iter.isCut || (!iter.parent && iter.level > 0)) { + throw new Error(`Resource ID for path ${iter.path} is required`); + } - while (path !== '') { - if (resourcePaths.indexOf(path) === -1) { - resourcePaths.push(path); + if (!iter.parent) { + validatedTrees.push(iter); + break; } - const splittedPath = path.split('/'); - splittedPath.pop(); - path = splittedPath.join('/'); + iter = iter.parent; } - return resourcePaths; - }, []); - // (stable) sort so that parents get processed before children - return _.sortBy(paths, path => path.split('/').length); + }); + + return this.combineResourceTrees(validatedTrees); }, getResourceId(path) { - if (path === '') { - return { 'Fn::GetAtt': [this.apiGatewayRestApiLogicalId, 'RootResourceId'] }; + if (!path) { + return this.provider.getApiGatewayRestApiRootResourceId(); + } + + if (!this.apiGatewayResources || !this.apiGatewayResources[path]) { + throw new Error(`Can not find API Gateway resource from path ${path}`); + } + + if (!this.apiGatewayResources[path].resourceId + && this.apiGatewayResources[path].resourceLogicalId) { + this.apiGatewayResources[path].resourceId = + { Ref: this.apiGatewayResources[path].resourceLogicalId }; } - return { Ref: this.apiGatewayResourceLogicalIds[path] }; + return this.apiGatewayResources[path].resourceId; }, getResourceName(path) { - if (path === '') { + if (path === '' || !this.apiGatewayResources) { return ''; } - return this.apiGatewayResourceNames[path]; + + return this.apiGatewayResources[path].name; }, }; diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.test.js index 61c4eb46a75..18daad469c8 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/resources.test.js @@ -70,15 +70,15 @@ describe('#compileResources()', () => { }, }, ]; - expect(awsCompileApigEvents.getResourcePaths()).to.deep.equal([ + expect(Object.keys(awsCompileApigEvents.getResourcePaths())).to.deep.equal([ 'foo', - 'bar', 'foo/bar', + 'bar', 'bar/-', 'bar/foo', 'bar/{id}', - 'bar/{foo_id}', 'bar/{id}/foobar', + 'bar/{foo_id}', 'bar/{foo_id}/foobar', ]); }); @@ -160,12 +160,16 @@ describe('#compileResources()', () => { }, ]; return awsCompileApigEvents.compileResources().then(() => { - expect(awsCompileApigEvents.apiGatewayResourceLogicalIds).to.deep.equal({ + const expectedResourceLogicalIds = { baz: 'ApiGatewayResourceBaz', 'baz/foo': 'ApiGatewayResourceBazFoo', foo: 'ApiGatewayResourceFoo', 'foo/{foo_id}': 'ApiGatewayResourceFooFooidVar', 'foo/{foo_id}/bar': 'ApiGatewayResourceFooFooidVarBar', + }; + Object.keys(expectedResourceLogicalIds).forEach((path) => { + expect(awsCompileApigEvents.apiGatewayResources[path].resourceLogicalId) + .equal(expectedResourceLogicalIds[path]); }); }); }); @@ -186,10 +190,14 @@ describe('#compileResources()', () => { }, ]; return awsCompileApigEvents.compileResources().then(() => { - expect(awsCompileApigEvents.apiGatewayResourceLogicalIds).to.deep.equal({ + const expectedResourceLogicalIds = { foo: 'ApiGatewayResourceFoo', 'foo/bar': 'ApiGatewayResourceFooBar', 'foo/{bar}': 'ApiGatewayResourceFooBarVar', + }; + Object.keys(expectedResourceLogicalIds).forEach((path) => { + expect(awsCompileApigEvents.apiGatewayResources[path].resourceLogicalId) + .equal(expectedResourceLogicalIds[path]); }); }); }); @@ -242,4 +250,239 @@ describe('#compileResources()', () => { .Resources).to.deep.equal({}); }); }); + + it('should create child resources only if there are predefined parent resources', () => { + awsCompileApigEvents.serverless.service.provider.apiGateway = { + restApiId: '6fyzt1pfpk', + restApiRootResourceId: 'z5d4qh4oqi', + restApiResources: { + '/foo': 'axcybf2i39', + '/users': 'zxcvbnmasd', + '/users/friends': 'fcasdoojp1', + '/groups': 'iuoyiusduo', + }, + }; + + awsCompileApigEvents.validated.events = [ + { + http: { + path: 'foo/bar', + method: 'GET', + }, + }, + { + http: { + path: 'foo/bar', + method: 'POST', + }, + }, + { + http: { + path: 'foo/bar', + method: 'DELETE', + }, + }, + { + http: { + path: 'bar/-', + method: 'GET', + }, + }, + { + http: { + path: 'bar/foo', + method: 'GET', + }, + }, + { + http: { + path: 'bar/{id}/foobar', + method: 'GET', + }, + }, + { + http: { + path: 'bar/{id}', + method: 'GET', + }, + }, + { + http: { + path: 'users/friends/comments', + method: 'GET', + }, + }, + { + http: { + path: 'users/me/posts', + method: 'GET', + }, + }, + { + http: { + path: 'groups/categories', + method: 'GET', + }, + }, + ]; + return awsCompileApigEvents.compileResources().then(() => { + try { + awsCompileApigEvents.getResourceId('users/{userId}'); + throw new Error('Expected API Gateway resource not found error, got success'); + } catch (e) { + expect(e.message).to.equal('Can not find API Gateway resource from path users/{userId}'); + } + + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceFoo).to.equal(undefined); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceBar.Properties.RestApiId) + .to.equal('6fyzt1pfpk'); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceBar.Properties.ParentId) + .to.equal('z5d4qh4oqi'); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceFooBar.Properties.ParentId) + .to.equal('axcybf2i39'); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceBarIdVar.Properties.ParentId.Ref) + .to.equal('ApiGatewayResourceBar'); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceUsersMePosts).not.equal(undefined); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceUsersFriendsComments.Properties.ParentId) + .to.equal('fcasdoojp1'); + }); + }); + + it('should not create any child resources if all resources exists', () => { + awsCompileApigEvents.serverless.service.provider.apiGateway = { + restApiId: '6fyzt1pfpk', + restApiRootResourceId: 'z5d4qh4oqi', + restApiResources: { + foo: 'axcybf2i39', + users: 'zxcvbnmasd', + 'users/friends': 'fcasdoojp1', + 'users/is/this/a/long/path': 'sadvgpoujk', + }, + }; + + awsCompileApigEvents.validated.events = [ + { + http: { + path: 'foo', + method: 'GET', + }, + }, + { + http: { + path: 'users', + method: 'GET', + }, + }, + { + http: { + path: 'users/friends', + method: 'GET', + }, + }, + { + http: { + path: 'users/is/this/a/long/path', + method: 'GET', + }, + }, + ]; + + return awsCompileApigEvents.compileResources().then(() => { + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceFoo).to.equal(undefined); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceUsers).to.equal(undefined); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceUsersFriends).to.equal(undefined); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceUsersIsThis).to.equal(undefined); + }); + }); + + it('should throw error if parent of existing resources is required', () => { + awsCompileApigEvents.serverless.service.provider.apiGateway = { + restApiId: '6fyzt1pfpk', + restApiRootResourceId: 'z5d4qh4oqi', + restApiResources: { + 'users/friends': 'fcasdoojp1', + }, + }; + + awsCompileApigEvents.validated.events = [ + { + http: { + path: 'users', + method: 'GET', + }, + }, + { + http: { + path: 'users/friends/{friendId}', + method: 'GET', + }, + }, + ]; + + expect(() => awsCompileApigEvents.compileResources()) + .to.throw(Error, 'Resource ID for path users is required'); + }); + + it('should named all method paths if all resources are predefined', () => { + awsCompileApigEvents.serverless.service.provider.apiGateway = { + restApiId: '6fyzt1pfpk', + restApiRootResourceId: 'z5d4qh4oqi', + restApiResources: { + 'users/friends': 'fcasdoojp1', + 'users/friends/{id}': 'fcasdoojp1', + }, + }; + + awsCompileApigEvents.validated.events = [ + { + http: { + path: 'users/friends', + method: 'GET', + }, + }, + { + http: { + path: 'users/friends', + method: 'POST', + }, + }, + { + http: { + path: 'users/friends', + method: 'DELETE', + }, + }, + { + http: { + path: 'users/friends/{id}', + method: 'GET', + }, + }, + { + http: { + path: 'users/friends/{id}', + method: 'POST', + }, + }, + ]; + + return awsCompileApigEvents.compileResources().then(() => { + expect(Object.keys(awsCompileApigEvents.serverless + .service.provider.compiledCloudFormationTemplate + .Resources).every((k) => ['ApiGatewayMethodundefinedGet', + 'ApiGatewayMethodundefinedPost'].indexOf(k) === -1)) + .to.equal(true); + }); + }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js index 50cbeb1f8a7..fc3dd0af46f 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js @@ -5,6 +5,11 @@ const BbPromise = require('bluebird'); module.exports = { compileRestApi() { + if (this.serverless.service.provider.apiGateway && + this.serverless.service.provider.apiGateway.restApiId) { + return BbPromise.resolve(); + } + this.apiGatewayRestApiLogicalId = this.provider.naming.getRestApiLogicalId(); let endpointType = 'EDGE'; diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js index 5dbbcfc668b..eb3aa7e8a9b 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js @@ -60,6 +60,22 @@ describe('#compileRestApi()', () => { }) ); + it('should ignore REST API resource creation if there is predefined restApi config', + () => { + awsCompileApigEvents.serverless.service.provider.apiGateway = { + restApiId: '6fyzt1pfpk', + restApiRootResourceId: 'z5d4qh4oqi', + }; + return awsCompileApigEvents + .compileRestApi().then(() => { + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources + ).to.deep.equal({}); + }); + } + ); + it('throw error if endpointType property is not a string', () => { awsCompileApigEvents.serverless.service.provider.endpointType = ['EDGE']; expect(() => awsCompileApigEvents.compileRestApi()).to.throw(Error); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js index 247d07367c1..bbafbb75163 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js @@ -66,49 +66,37 @@ module.exports = { http.integration = this.getIntegration(http, functionName); - if (http.integration === 'AWS') { - if (http.request) { - http.request = this.getRequest(http); - - if (http.request.parameters) { - http.request.parameters = this.getRequestParameters(http.request); - } - } else { - http.request = {}; - } + if ((http.integration === 'HTTP' || http.integration === 'HTTP_PROXY') && + (!http.request || !http.request.uri)) { + const errorMessage = [ + `You need to set the request uri when using the ${http.integration} integration.`, + ]; + throw new this.serverless.classes.Error(errorMessage); + } + if (http.integration === 'AWS' || http.integration === 'HTTP') { + http.request = this.getRequest(http); http.request.passThrough = this.getRequestPassThrough(http); - - if (http.response) { - http.response = this.getResponse(http); - } else { - http.response = {}; - } - - if (http.response.statusCodes) { - http.response.statusCodes = _.assign({}, http.response.statusCodes); - - if (!_.some(http.response.statusCodes, code => code.pattern === '')) { - http.response.statusCodes['200'] = DEFAULT_STATUS_CODES['200']; - } - } else { - http.response.statusCodes = DEFAULT_STATUS_CODES; - } - } else if (http.integration === 'AWS_PROXY') { + http.response = this.getResponse(http); + } else if (http.integration === 'AWS_PROXY' || http.integration === 'HTTP_PROXY') { // show a warning when request / response config is used with AWS_PROXY (LAMBDA-PROXY) if (http.request) { const keys = Object.keys(http.request); - if (!(keys.length === 1 && keys[0] === 'parameters')) { + const allowedKeys = + http.integration === 'AWS_PROXY' ? ['parameters'] : ['parameters', 'uri']; + + if (!_.isEmpty(_.difference(keys, allowedKeys))) { const requestWarningMessage = [ - 'Warning! You\'re using the LAMBDA-PROXY in combination with a request', - ` configuration in your function "${functionName}". Only the`, - ' \'request.parameters\' configs are available in conjunction with', - ' LAMBDA-PROXY. Serverless will remove this configuration automatically', + `Warning! You're using the ${http.integration} in combination with a request`, + ` configuration in your function "${functionName}". Only the `, + _.map(allowedKeys, value => `request.${value}`).join(', '), + ` configs are available in conjunction with ${http.integration}.`, + ' Serverless will remove this configuration automatically', ' before deployment.', ].join(''); this.serverless.cli.log(requestWarningMessage); for (const key of keys) { - if (key !== 'parameters') { + if (!_.includes(allowedKeys, key)) { delete http.request[key]; } } @@ -118,15 +106,11 @@ module.exports = { delete http.request; } else { http.request = this.getRequest(http); - - if (http.request.parameters) { - http.request.parameters = this.getRequestParameters(http.request); - } } } if (http.response) { const warningMessage = [ - 'Warning! You\'re using the LAMBDA-PROXY in combination with response', + `Warning! You're using the ${http.integration} in combination with response`, ` configuration in your function "${functionName}".`, ' Serverless will remove this configuration automatically before deployment.', ].join(''); @@ -134,13 +118,6 @@ module.exports = { delete http.response; } - } else if (http.integration === 'HTTP' || http.integration === 'HTTP_PROXY') { - if (!http.request || !http.request.uri) { - const errorMessage = [ - `You need to set the request uri when using the ${http.integration} integration.`, - ]; - throw new this.serverless.classes.Error(errorMessage); - } } events.push({ @@ -243,7 +220,11 @@ module.exports = { type = 'AWS_IAM'; } else if (authorizer.arn) { arn = authorizer.arn; - name = this.provider.naming.extractAuthorizerNameFromArn(arn); + if (_.isString(authorizer.name)) { + name = authorizer.name; + } else { + name = this.provider.naming.extractAuthorizerNameFromArn(arn); + } } else if (authorizer.name) { name = authorizer.name; arn = this.getLambdaArn(name); @@ -381,22 +362,32 @@ module.exports = { }, getRequest(http) { - if (typeof http.request !== 'object') { - const errorMessage = [ - 'Request config must be provided as an object.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - if (http.request.template && typeof http.request.template !== 'object') { - const errorMessage = [ - 'Template config must be provided as an object.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); + if (http.request) { + const request = http.request; + + if (typeof http.request !== 'object') { + const errorMessage = [ + 'Request config must be provided as an object.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + if (http.request.template && typeof http.request.template !== 'object') { + const errorMessage = [ + 'Template config must be provided as an object.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + + if (request.parameters) { + request.parameters = this.getRequestParameters(request); + } + + return request; } - return http.request; + return {}; }, getRequestParameters(httpRequest) { @@ -435,25 +426,48 @@ module.exports = { // Validate() sets the passThrough default to NEVER. This is inappropriate // for HTTP and MOCK integrations, where there is no default request template defined. const type = http.integration || 'AWS_PROXY'; - return type === 'AWS' ? requestPassThroughBehaviors[0] : 'WHEN_NO_MATCH'; + if (type === 'AWS') { + return requestPassThroughBehaviors[0]; + } else if (type === 'HTTP' || type === 'MOCK') { + return undefined; + } + + return 'WHEN_NO_MATCH'; }, getResponse(http) { - if (typeof http.response !== 'object') { - const errorMessage = [ - 'Response config must be provided as an object.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - if (http.response.headers && typeof http.response.headers !== 'object') { - const errorMessage = [ - 'Response headers must be provided as an object.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); + if (http.response) { + const response = http.response; + + if (typeof response !== 'object') { + const errorMessage = [ + 'Response config must be provided as an object.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + if (response.headers && typeof response.headers !== 'object') { + const errorMessage = [ + 'Response headers must be provided as an object.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + + if (response.statusCodes) { + response.statusCodes = _.assign({}, response.statusCodes); + + if (!_.some(response.statusCodes, code => code.pattern === '')) { + response.statusCodes['200'] = DEFAULT_STATUS_CODES['200']; + } + } else { + response.statusCodes = DEFAULT_STATUS_CODES; + } + + return response; } - return http.response; + + return {}; }, getLambdaArn(name) { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js index d9cb2f70a34..a2132f048e0 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js @@ -934,6 +934,30 @@ describe('#validate()', () => { expect(validated.events[0].http.authorizer.arn).to.equal('xxx:dev-authorizer'); }); + it('should handle an authorizer.arn with an explicit authorizer.name object', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: 'foo/bar', + method: 'GET', + authorizer: { + arn: 'xxx:dev-authorizer', + name: 'custom-name', + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.authorizer.name).to.equal('custom-name'); + expect(validated.events[0].http.authorizer.arn).to.equal('xxx:dev-authorizer'); + }); + it('should throw an error if the provided config is not an object', () => { awsCompileApigEvents.serverless.service.functions = { first: { @@ -1223,6 +1247,50 @@ describe('#validate()', () => { expect(validated.events[0].http.integration).to.equal('HTTP'); }); + it('should process request parameters for HTTP integration', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'HTTP', + request: { + uri: 'https://example.com', + parameters: { + querystrings: { + foo: true, + bar: false, + }, + paths: { + foo: true, + bar: false, + }, + headers: { + foo: true, + bar: false, + }, + }, + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.request.parameters).to.deep.equal({ + 'method.request.querystring.foo': true, + 'method.request.querystring.bar': false, + 'method.request.path.foo': true, + 'method.request.path.bar': false, + 'method.request.header.foo': true, + 'method.request.header.bar': false, + }); + }); + it('should throw if no uri is set in HTTP integration', () => { awsCompileApigEvents.serverless.service.functions = { first: { @@ -1264,6 +1332,50 @@ describe('#validate()', () => { expect(validated.events[0].http.integration).to.equal('HTTP_PROXY'); }); + it('should process request parameters for HTTP_PROXY integration', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'HTTP_PROXY', + request: { + uri: 'https://example.com', + parameters: { + querystrings: { + foo: true, + bar: false, + }, + paths: { + foo: true, + bar: false, + }, + headers: { + foo: true, + bar: false, + }, + }, + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.request.parameters).to.deep.equal({ + 'method.request.querystring.foo': true, + 'method.request.querystring.bar': false, + 'method.request.path.foo': true, + 'method.request.path.bar': false, + 'method.request.header.foo': true, + 'method.request.header.bar': false, + }); + }); + it('should throw if no uri is set in HTTP_PROXY integration', () => { awsCompileApigEvents.serverless.service.functions = { first: { @@ -1282,6 +1394,123 @@ describe('#validate()', () => { expect(() => awsCompileApigEvents.validate()).to.throw(Error); }); + it('should show a warning message when using request / response config with HTTP-PROXY', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'http-proxy', + request: { + uri: 'http://www.example.com', + template: { + 'template/1': '{ "stage" : "$context.stage" }', + 'template/2': '{ "httpMethod" : "$context.httpMethod" }', + }, + }, + response: { + template: "$input.path('$.foo')", + }, + }, + }, + ], + }, + }; + // initialize so we get the log method from the CLI in place + serverless.init(); + + const logStub = sinon.stub(serverless.cli, 'log'); + + awsCompileApigEvents.validate(); + + expect(logStub.calledTwice).to.be.equal(true); + expect(logStub.args[0][0].length).to.be.at.least(1); + }); + + it('should not show a warning message when using request.parameter with HTTP-PROXY', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'http-proxy', + request: { + uri: 'http://www.example.com', + parameters: { + querystrings: { + foo: true, + bar: false, + }, + paths: { + foo: true, + bar: false, + }, + headers: { + foo: true, + bar: false, + }, + }, + }, + }, + }, + ], + }, + }; + // initialize so we get the log method from the CLI in place + serverless.init(); + + const logStub = sinon.stub(serverless.cli, 'log'); + + awsCompileApigEvents.validate(); + + expect(logStub.called).to.be.equal(false); + }); + + it('should remove non-parameter or uri request/response config with HTTP-PROXY', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'http-proxy', + request: { + uri: 'http://www.example.com', + template: { + 'template/1': '{ "stage" : "$context.stage" }', + }, + parameters: { + paths: { + foo: true, + }, + }, + }, + response: {}, + }, + }, + ], + }, + }; + // initialize so we get the log method from the CLI in place + serverless.init(); + + // don't want to print the logs in this test + sinon.stub(serverless.cli, 'log'); + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.response).to.equal(undefined); + expect(validated.events[0].http.request.uri).to.equal('http://www.example.com'); + expect(validated.events[0].http.request.parameters).to.deep.equal({ + 'method.request.path.foo': true, + }); + }); + it('should support MOCK integration', () => { awsCompileApigEvents.serverless.service.functions = { first: { diff --git a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js index 400a6fae0a4..697d9736d09 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js @@ -78,13 +78,20 @@ class AwsCompileCloudWatchLogEvents { .getLambdaCloudWatchLogPermissionLogicalId(functionName, cloudWatchLogNumberInFunction); + // unescape quotes once when the first quote is detected escaped + const idxFirstSlash = FilterPattern.indexOf('\\'); + const idxFirstQuote = FilterPattern.indexOf('"'); + if (idxFirstSlash >= 0 && idxFirstQuote >= 0 && idxFirstQuote > idxFirstSlash) { + FilterPattern = FilterPattern.replace(/\\("|\\|')/g, (match, g) => g); + } + const cloudWatchLogRuleTemplate = ` { "Type": "AWS::Logs::SubscriptionFilter", "DependsOn": "${lambdaPermissionLogicalId}", "Properties": { "LogGroupName": "${LogGroupName}", - "FilterPattern": "${FilterPattern}", + "FilterPattern": ${JSON.stringify(FilterPattern)}, "DestinationArn": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] } } } diff --git a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.test.js b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.test.js index 7777188d239..bfc3263cd35 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.test.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.test.js @@ -138,6 +138,50 @@ describe('AwsCompileCloudWatchLogEvents', () => { ).to.equal('{$.userIdentity.type = Root}'); }); + it('should respect "filter" variable of plain text', () => { + awsCompileCloudWatchLogEvents.serverless.service.functions = { + first: { + events: [ + { + cloudwatchLog: { + logGroup: '/aws/lambda/hello1', + filter: '"Total amount" -"level=Debug"', + }, + }, + ], + }, + }; + + awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents(); + + expect(awsCompileCloudWatchLogEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 + .Properties.FilterPattern + ).to.equal('"Total amount" -"level=Debug"'); + }); + + it('should respect escaped "filter" variable of plain text', () => { + awsCompileCloudWatchLogEvents.serverless.service.functions = { + first: { + events: [ + { + cloudwatchLog: { + logGroup: '/aws/lambda/hello1', + filter: "\\\"Total amount\\\" -\\\"level=Debug\\\"", // eslint-disable-line quotes + }, + }, + ], + }, + }; + + awsCompileCloudWatchLogEvents.compileCloudWatchLogEvents(); + + expect(awsCompileCloudWatchLogEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources.FirstLogsSubscriptionFilterCloudWatchLog1 + .Properties.FilterPattern + ).to.equal('"Total amount" -"level=Debug"'); + }); + it('should set an empty string for FilterPattern statement when "filter" variable is not given' , () => { awsCompileCloudWatchLogEvents.serverless.service.functions = { diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.js b/lib/plugins/aws/package/lib/generateCoreTemplate.js index 09a20d4ddee..fae3c200071 100644 --- a/lib/plugins/aws/package/lib/generateCoreTemplate.js +++ b/lib/plugins/aws/package/lib/generateCoreTemplate.js @@ -25,6 +25,14 @@ module.exports = { const bucketName = this.serverless.service.provider.deploymentBucket; const isS3TransferAccelerationEnabled = this.provider.isS3TransferAccelerationEnabled(); + const isS3TransferAccelerationDisabled = this.provider.isS3TransferAccelerationDisabled(); + + if (isS3TransferAccelerationEnabled && isS3TransferAccelerationDisabled) { + const errorMessage = [ + 'You cannot enable and disable S3 Transfer Acceleration at the same time', + ].join(''); + return BbPromise.reject(new this.serverless.classes.Error(errorMessage)); + } if (bucketName) { return BbPromise.bind(this) @@ -45,17 +53,25 @@ module.exports = { }); } - this.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ServerlessDeploymentBucket.Properties = { - AccelerateConfiguration: { - AccelerationStatus: - isS3TransferAccelerationEnabled ? 'Enabled' : 'Suspended', - }, - }; - if (isS3TransferAccelerationEnabled) { + // enable acceleration via CloudFormation + this.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ServerlessDeploymentBucket.Properties = { + AccelerateConfiguration: { + AccelerationStatus: 'Enabled', + }, + }; + // keep track of acceleration status via CloudFormation Output this.serverless.service.provider.compiledCloudFormationTemplate .Outputs.ServerlessDeploymentBucketAccelerated = { Value: true }; + } else if (isS3TransferAccelerationDisabled) { + // explicitly disable acceleration via CloudFormation + this.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ServerlessDeploymentBucket.Properties = { + AccelerateConfiguration: { + AccelerationStatus: 'Suspended', + }, + }; } const coreTemplateFileName = this.provider.naming.getCoreTemplateFileName(); diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.test.js b/lib/plugins/aws/package/lib/generateCoreTemplate.test.js index a089d536950..e4cb5eba22f 100644 --- a/lib/plugins/aws/package/lib/generateCoreTemplate.test.js +++ b/lib/plugins/aws/package/lib/generateCoreTemplate.test.js @@ -61,14 +61,45 @@ describe('#generateCoreTemplate()', () => { return expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled .then(() => { + const template = awsPlugin.serverless.service.provider.compiledCloudFormationTemplate; expect( - awsPlugin.serverless.service.provider.compiledCloudFormationTemplate - .Outputs.ServerlessDeploymentBucketName.Value + template.Outputs.ServerlessDeploymentBucketName.Value ).to.equal(bucketName); // eslint-disable-next-line no-unused-expressions expect( - awsPlugin.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ServerlessDeploymentBucket + template.Resources.ServerlessDeploymentBucket + ).to.not.exist; + }); + }); + + it('should use a custom bucket if specified, even with S3 transfer acceleration', () => { + const bucketName = 'com.serverless.deploys'; + + awsPlugin.serverless.service.provider.deploymentBucket = bucketName; + awsPlugin.provider.options['aws-s3-accelerate'] = true; + + const coreCloudFormationTemplate = awsPlugin.serverless.utils.readFileSync( + path.join( + __dirname, + 'core-cloudformation-template.json' + ) + ); + awsPlugin.serverless.service.provider + .compiledCloudFormationTemplate = coreCloudFormationTemplate; + + return expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled + .then(() => { + const template = awsPlugin.serverless.service.provider.compiledCloudFormationTemplate; + expect( + template.Outputs.ServerlessDeploymentBucketName.Value + ).to.equal(bucketName); + // eslint-disable-next-line no-unused-expressions + expect( + template.Resources.ServerlessDeploymentBucket + ).to.not.exist; + // eslint-disable-next-line no-unused-expressions + expect( + template.Outputs.ServerlessDeploymentBucketAccelerated ).to.not.exist; }); }); @@ -80,11 +111,6 @@ describe('#generateCoreTemplate()', () => { .Resources.ServerlessDeploymentBucket ).to.be.deep.equal({ Type: 'AWS::S3::Bucket', - Properties: { - AccelerateConfiguration: { - AccelerationStatus: 'Suspended', - }, - }, }); }) ); @@ -114,4 +140,36 @@ describe('#generateCoreTemplate()', () => { expect(template.Outputs.ServerlessDeploymentBucketAccelerated.Value).to.equal(true); }); }); + + it('should explicitly disable S3 Transfer Acceleration, if requested', () => { + sinon.stub(awsPlugin.provider, 'request').resolves(); + sinon.stub(serverless.utils, 'writeFileSync').resolves(); + serverless.config.servicePath = './'; + awsPlugin.provider.options['no-aws-s3-accelerate'] = true; + + return awsPlugin.generateCoreTemplate() + .then(() => { + const template = serverless.service.provider.coreCloudFormationTemplate; + expect(template.Resources.ServerlessDeploymentBucket).to.be.deep.equal({ + Type: 'AWS::S3::Bucket', + Properties: { + AccelerateConfiguration: { + AccelerationStatus: 'Suspended', + }, + }, + }); + }); + }); + + it('should explode if transfer acceleration is both enabled and disabled', () => { + sinon.stub(awsPlugin.provider, 'request').resolves(); + sinon.stub(serverless.utils, 'writeFileSync').resolves(); + serverless.config.servicePath = './'; + awsPlugin.provider.options['aws-s3-accelerate'] = true; + awsPlugin.provider.options['no-aws-s3-accelerate'] = true; + + return expect( + awsPlugin.generateCoreTemplate() + ).to.be.rejectedWith(serverless.classes.Error, /at the same time/); + }); }); diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.js b/lib/plugins/aws/package/lib/mergeIamTemplates.js index 7276853b002..a17e2dffd8a 100644 --- a/lib/plugins/aws/package/lib/mergeIamTemplates.js +++ b/lib/plugins/aws/package/lib/mergeIamTemplates.js @@ -29,6 +29,18 @@ module.exports = { }, }, }; + + if (_.has(this.serverless.service.provider, 'logRetentionInDays')) { + if (_.isInteger(this.serverless.service.provider.logRetentionInDays) && + this.serverless.service.provider.logRetentionInDays > 0) { + newLogGroup[logGroupLogicalId].Properties.RetentionInDays + = this.serverless.service.provider.logRetentionInDays; + } else { + const errorMessage = 'logRetentionInDays should be an integer over 0'; + throw new this.serverless.classes.Error(errorMessage); + } + } + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, newLogGroup); }); diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js index f5c9c6be7a9..1407989251f 100644 --- a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js +++ b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js @@ -238,6 +238,41 @@ describe('#mergeIamTemplates()', () => { }); }); + it('should add RetentionInDays to a CloudWatch LogGroup resource if logRetentionInDays is given' + , () => { + awsPackage.serverless.service.provider.logRetentionInDays = 5; + const normalizedName = awsPackage.provider.naming.getLogGroupLogicalId(functionName); + return awsPackage.mergeIamTemplates().then(() => { + expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate + .Resources[normalizedName] + ).to.deep.equal( + { + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: awsPackage.provider.naming.getLogGroupName(functionName), + RetentionInDays: 5, + }, + } + ); + }); + }); + + it('should throw error if RetentionInDays is 0 or not an integer' + , () => { + awsPackage.serverless.service.provider.logRetentionInDays = 0; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = 'string'; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = []; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = {}; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = undefined; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = null; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + }); + it('should add a CloudWatch LogGroup resource if all functions use custom roles', () => { awsPackage.serverless.service.functions[functionName].role = 'something'; awsPackage.serverless.service.functions = { diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js index 7f5c453bd29..6eae516f637 100644 --- a/lib/plugins/aws/provider/awsProvider.js +++ b/lib/plugins/aws/provider/awsProvider.js @@ -317,6 +317,10 @@ class AwsProvider { return !!this.options['aws-s3-accelerate']; } + isS3TransferAccelerationDisabled() { + return !!this.options['no-aws-s3-accelerate']; + } + disableTransferAccelerationForCurrentDeploy() { delete this.options['aws-s3-accelerate']; } @@ -324,8 +328,6 @@ class AwsProvider { enableS3TransferAcceleration(credentials) { this.serverless.cli.log('Using S3 Transfer Acceleration Endpoint...'); credentials.useAccelerateEndpoint = true; // eslint-disable-line no-param-reassign - credentials.signatureVersion = 'v2'; // eslint-disable-line no-param-reassign - // see https://github.com/aws/aws-sdk-js/issues/281 } getRegion() { @@ -363,6 +365,53 @@ class AwsProvider { return this.request('STS', 'getCallerIdentity', {}) .then((result) => result.Account); } + + /** + * Get API Gateway Rest API ID from serverless config + */ + getApiGatewayRestApiId() { + if (this.serverless.service.provider.apiGateway + && this.serverless.service.provider.apiGateway.restApiId) { + return this.serverless.service.provider.apiGateway.restApiId; + } + + return { Ref: this.naming.getRestApiLogicalId() }; + } + + /** + * Get Rest API Root Resource ID from serverless config + */ + getApiGatewayRestApiRootResourceId() { + if (this.serverless.service.provider.apiGateway + && this.serverless.service.provider.apiGateway.restApiRootResourceId) { + return this.serverless.service.provider.apiGateway.restApiRootResourceId; + } + return { 'Fn::GetAtt': [this.naming.getRestApiLogicalId(), 'RootResourceId'] }; + } + + /** + * Get Rest API Predefined Resources from serverless config + */ + getApiGatewayPredefinedResources() { + if (!this.serverless.service.provider.apiGateway + || !this.serverless.service.provider.apiGateway.restApiResources) { + return []; + } + + if (Array.isArray(this.serverless.service.provider.apiGateway.restApiResources)) { + return this.serverless.service.provider.apiGateway.restApiResources; + } + + if (typeof this.serverless.service.provider.apiGateway.restApiResources !== 'object') { + throw new Error('REST API resource must be an array of object'); + } + + return Object.keys(this.serverless.service.provider.apiGateway.restApiResources) + .map((key) => ({ + path: key, + resourceId: this.serverless.service.provider.apiGateway.restApiResources[key], + })); + } } module.exports = AwsProvider; diff --git a/lib/plugins/create/create.js b/lib/plugins/create/create.js index eaa3a67833f..84368002a70 100644 --- a/lib/plugins/create/create.js +++ b/lib/plugins/create/create.js @@ -5,8 +5,12 @@ const path = require('path'); const fse = require('fs-extra'); const _ = require('lodash'); +const ServerlessError = require('../../classes/Error').ServerlessError; const userStats = require('../../utils/userStats'); const download = require('../../utils/downloadTemplateFromRepo'); +const renameService = require('../../utils/renameService').renameService; +const copyDirContentsSync = require('../../utils/fs/copyDirContentsSync'); +const dirExistsSync = require('../../utils/fs/dirExistsSync'); // class wide constants const validTemplates = [ @@ -24,6 +28,8 @@ const validTemplates = [ 'aws-scala-sbt', 'aws-csharp', 'aws-fsharp', + 'aws-go', + 'aws-go-dep', 'azure-nodejs', 'google-nodejs', 'kubeless-python', @@ -35,6 +41,7 @@ const validTemplates = [ 'spotinst-nodejs', 'spotinst-python', 'spotinst-ruby', + 'spotinst-java8', 'webtasks-nodejs', 'plugin', @@ -66,6 +73,9 @@ class Create { usage: 'Template URL for the service. Supports: GitHub, BitBucket', shortcut: 'u', }, + 'template-path': { + usage: 'Template local path for the service.', + }, path: { usage: 'The path where the service should be created (e.g. --path my-service)', shortcut: 'p', @@ -111,10 +121,23 @@ class Create { .catch(err => { throw new this.serverless.classes.Error(err); }); + } else if ('template-path' in this.options) { + // Copying template from a local directory + const servicePath = this.options.path || path.join(process.cwd(), this.options.name); + if (dirExistsSync(servicePath)) { + const errorMessage = `A folder named "${servicePath}" already exists.`; + throw new ServerlessError(errorMessage); + } + copyDirContentsSync(this.options['template-path'], servicePath, { + noLinks: true, + }); + if (this.options.name) { + renameService(this.options.name, servicePath); + } } else { const errorMessage = [ - 'You must either pass a template name (--template) or a ', - 'a URL (--template-url).', + 'You must either pass a template name (--template), ', + 'a URL (--template-url) or a local path (--template-path).', ].join(''); throw new this.serverless.classes.Error(errorMessage); } diff --git a/lib/plugins/create/create.test.js b/lib/plugins/create/create.test.js index df578b49347..c0bf9d726da 100644 --- a/lib/plugins/create/create.test.js +++ b/lib/plugins/create/create.test.js @@ -497,6 +497,23 @@ describe('Create', () => { }); }); + it('should generate scaffolding for "spotinst-java8" template', () => { + process.chdir(tmpDir); + create.options.template = 'spotinst-java8'; + + return create.create().then(() => { + const dirContent = walkDirSync(tmpDir) + .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + + expect(dirContent).to.include('package.json'); + expect(dirContent).to.include('serverless.yml'); + expect(dirContent).to.include('pom.xml'); + expect(dirContent).to.include(path.join('src', 'main', 'java', + 'com', 'serverless', 'Handler.java')); + expect(dirContent).to.include('.gitignore'); + }); + }); + it('should generate scaffolding for "webtasks-nodejs" template', () => { process.chdir(tmpDir); create.options.template = 'webtasks-nodejs'; @@ -666,5 +683,82 @@ describe('Create', () => { return create.create().catch(() => download.downloadTemplateFromRepo.restore()); }); + + it('should copy "aws-nodejs" template from local path', () => { + process.chdir(tmpDir); + const distDir = path.join(tmpDir, 'my-awesome-service'); + create.options = {}; + create.options.path = distDir; + create.options['template-path'] = path.join(__dirname, 'templates/aws-nodejs'); + + return create.create().then(() => { + const dirContent = fs.readdirSync(distDir); + + expect(dirContent).to.include('serverless.yml'); + expect(dirContent).to.include('handler.js'); + expect(dirContent).to.include('gitignore'); + + // check if the service was renamed + const serverlessYmlfileContent = fse + .readFileSync(path.join(distDir, 'serverless.yml')).toString(); + + expect((/service: aws-nodejs/).test(serverlessYmlfileContent)).to.equal(true); + }); + }); + + it('should copy "aws-nodejs" template from local path with a custom name', () => { + process.chdir(tmpDir); + create.options = {}; + create.options['template-path'] = path.join(__dirname, 'templates/aws-nodejs'); + create.options.name = 'my-awesome-service'; + + return create.create().then(() => { + const dirContent = fs.readdirSync(path.join(tmpDir, 'my-awesome-service')); + + expect(dirContent).to.include('serverless.yml'); + expect(dirContent).to.include('handler.js'); + expect(dirContent).to.include('gitignore'); + + // check if the service was renamed + const serverlessYmlfileContent = fse + .readFileSync(path.join(tmpDir, 'my-awesome-service', 'serverless.yml')).toString(); + + expect((/service: my-awesome-service/).test(serverlessYmlfileContent)).to.equal(true); + }); + }); + + it('should generate scaffolding for "aws-go" template', () => { + process.chdir(tmpDir); + create.options.template = 'aws-go'; + + return create.create().then(() => { + const dirContent = walkDirSync(tmpDir) + .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + + expect(dirContent).to.include('serverless.yml'); + expect(dirContent).to.include(path.join('hello', 'main.go')); + expect(dirContent).to.include(path.join('world', 'main.go')); + expect(dirContent).to.include('Makefile'); + expect(dirContent).to.include('.gitignore'); + }); + }); + + it('should generate scaffolding for "aws-go-dep" template', () => { + process.chdir(tmpDir); + create.options.template = 'aws-go-dep'; + + return create.create().then(() => { + const dirContent = walkDirSync(tmpDir) + .map(elem => elem.replace(path.join(tmpDir, path.sep), '')); + + expect(dirContent).to.include('serverless.yml'); + expect(dirContent).to.include(path.join('hello', 'main.go')); + expect(dirContent).to.include(path.join('world', 'main.go')); + expect(dirContent).to.include('Gopkg.toml'); + expect(dirContent).to.include('Gopkg.lock'); + expect(dirContent).to.include('Makefile'); + expect(dirContent).to.include('.gitignore'); + }); + }); }); }); diff --git a/lib/plugins/create/templates/aws-go-dep/Gopkg.lock b/lib/plugins/create/templates/aws-go-dep/Gopkg.lock new file mode 100644 index 00000000000..f93855f76bc --- /dev/null +++ b/lib/plugins/create/templates/aws-go-dep/Gopkg.lock @@ -0,0 +1,19 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/aws/aws-lambda-go" + packages = [ + "lambda", + "lambda/messages", + "lambdacontext" + ] + revision = "6e2e37798efbb1dfd8e9c6681702e683a6046517" + version = "v1.0.1" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "85fa166cc59d0fa113a1517ffbb5dee0f1fa4a6795239936afb18c64364af759" + solver-name = "gps-cdcl" + solver-version = 1 \ No newline at end of file diff --git a/lib/plugins/create/templates/aws-go-dep/Gopkg.toml b/lib/plugins/create/templates/aws-go-dep/Gopkg.toml new file mode 100644 index 00000000000..8fce8929ead --- /dev/null +++ b/lib/plugins/create/templates/aws-go-dep/Gopkg.toml @@ -0,0 +1,25 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" + + +[[constraint]] + name = "github.com/aws/aws-lambda-go" + version = "^1.0.1" diff --git a/lib/plugins/create/templates/aws-go-dep/Makefile b/lib/plugins/create/templates/aws-go-dep/Makefile new file mode 100644 index 00000000000..46a24c449e1 --- /dev/null +++ b/lib/plugins/create/templates/aws-go-dep/Makefile @@ -0,0 +1,4 @@ +build: + dep ensure + env GOOS=linux go build -ldflags="-s -w" -o bin/hello hello/main.go + env GOOS=linux go build -ldflags="-s -w" -o bin/world world/main.go \ No newline at end of file diff --git a/lib/plugins/create/templates/aws-go-dep/gitignore b/lib/plugins/create/templates/aws-go-dep/gitignore new file mode 100644 index 00000000000..99a966a94de --- /dev/null +++ b/lib/plugins/create/templates/aws-go-dep/gitignore @@ -0,0 +1,8 @@ +# Serverless directories +.serverless + +# golang output binary directory +bin + +# golang vendor (dependencies) directory +vendor \ No newline at end of file diff --git a/lib/plugins/create/templates/aws-go-dep/hello/main.go b/lib/plugins/create/templates/aws-go-dep/hello/main.go new file mode 100644 index 00000000000..e75c5af0407 --- /dev/null +++ b/lib/plugins/create/templates/aws-go-dep/hello/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/aws/aws-lambda-go/lambda" +) + +type Response struct { + Message string `json:"message"` +} + +func Handler() (Response, error) { + return Response{ + Message: "Go Serverless v1.0! Your function executed successfully!", + }, nil +} + +func main() { + lambda.Start(Handler) +} diff --git a/lib/plugins/create/templates/aws-go-dep/serverless.yml b/lib/plugins/create/templates/aws-go-dep/serverless.yml new file mode 100644 index 00000000000..67f92c77f2e --- /dev/null +++ b/lib/plugins/create/templates/aws-go-dep/serverless.yml @@ -0,0 +1,104 @@ +# Welcome to Serverless! +# +# This file is the main config file for your service. +# It's very minimal at this point and uses default values. +# You can always add more config options for more control. +# We've included some commented out config examples here. +# Just uncomment any of them to get that config option. +# +# For full config options, check the docs: +# docs.serverless.com +# +# Happy Coding! + +service: aws-go-dep # NOTE: update this with your service name + +# You can pin your service to only deploy with a specific Serverless version +# Check out our docs for more details +# frameworkVersion: "=X.X.X" + +provider: + name: aws + runtime: go1.x + +# you can overwrite defaults here +# stage: dev +# region: us-east-1 + +# you can add statements to the Lambda function's IAM Role here +# iamRoleStatements: +# - Effect: "Allow" +# Action: +# - "s3:ListBucket" +# Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } +# - Effect: "Allow" +# Action: +# - "s3:PutObject" +# Resource: +# Fn::Join: +# - "" +# - - "arn:aws:s3:::" +# - "Ref" : "ServerlessDeploymentBucket" +# - "/*" + +# you can define service wide environment variables here +# environment: +# variable1: value1 + +package: + exclude: + - ./** + include: + - ./bin/** + +functions: + hello: + handler: bin/hello + world: + handler: bin/world + +# The following are a few example events you can configure +# NOTE: Please make sure to change your handler code to work with those events +# Check the event documentation for details +# events: +# events: +# - http: +# path: users/create +# method: get +# - s3: ${env:BUCKET} +# - schedule: rate(10 minutes) +# - sns: greeter-topic +# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 +# - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx +# - iot: +# sql: "SELECT * FROM 'some_topic'" +# - cloudwatchEvent: +# event: +# source: +# - "aws.ec2" +# detail-type: +# - "EC2 Instance State-change Notification" +# detail: +# state: +# - pending +# - cloudwatchLog: '/aws/lambda/hello' +# - cognitoUserPool: +# pool: MyUserPool +# trigger: PreSignUp + +# Define function environment variables here +# environment: +# variable2: value2 + +# you can add CloudFormation resource templates here +#resources: +# Resources: +# NewResource: +# Type: AWS::S3::Bucket +# Properties: +# BucketName: my-new-bucket +# Outputs: +# NewOutput: +# Description: "Description for the output" +# Value: "Some output value" diff --git a/lib/plugins/create/templates/aws-go-dep/world/main.go b/lib/plugins/create/templates/aws-go-dep/world/main.go new file mode 100644 index 00000000000..bf46aa6fbcb --- /dev/null +++ b/lib/plugins/create/templates/aws-go-dep/world/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/aws/aws-lambda-go/lambda" +) + +type Response struct { + Message string `json:"message"` +} + +func Handler() (Response, error) { + return Response{ + Message: "Okay so your other function also executed successfully!", + }, nil +} + +func main() { + lambda.Start(Handler) +} diff --git a/lib/plugins/create/templates/aws-go/Makefile b/lib/plugins/create/templates/aws-go/Makefile new file mode 100644 index 00000000000..3b697426e39 --- /dev/null +++ b/lib/plugins/create/templates/aws-go/Makefile @@ -0,0 +1,4 @@ +build: + go get github.com/aws/aws-lambda-go/lambda + env GOOS=linux go build -ldflags="-s -w" -o bin/hello hello/main.go + env GOOS=linux go build -ldflags="-s -w" -o bin/world world/main.go \ No newline at end of file diff --git a/lib/plugins/create/templates/aws-go/gitignore b/lib/plugins/create/templates/aws-go/gitignore new file mode 100644 index 00000000000..f5b4c36adc3 --- /dev/null +++ b/lib/plugins/create/templates/aws-go/gitignore @@ -0,0 +1,5 @@ +# Serverless directories +.serverless + +# golang output binary directory +bin \ No newline at end of file diff --git a/lib/plugins/create/templates/aws-go/hello/main.go b/lib/plugins/create/templates/aws-go/hello/main.go new file mode 100644 index 00000000000..e75c5af0407 --- /dev/null +++ b/lib/plugins/create/templates/aws-go/hello/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/aws/aws-lambda-go/lambda" +) + +type Response struct { + Message string `json:"message"` +} + +func Handler() (Response, error) { + return Response{ + Message: "Go Serverless v1.0! Your function executed successfully!", + }, nil +} + +func main() { + lambda.Start(Handler) +} diff --git a/lib/plugins/create/templates/aws-go/serverless.yml b/lib/plugins/create/templates/aws-go/serverless.yml new file mode 100644 index 00000000000..c3a1e2c95d4 --- /dev/null +++ b/lib/plugins/create/templates/aws-go/serverless.yml @@ -0,0 +1,104 @@ +# Welcome to Serverless! +# +# This file is the main config file for your service. +# It's very minimal at this point and uses default values. +# You can always add more config options for more control. +# We've included some commented out config examples here. +# Just uncomment any of them to get that config option. +# +# For full config options, check the docs: +# docs.serverless.com +# +# Happy Coding! + +service: aws-go # NOTE: update this with your service name + +# You can pin your service to only deploy with a specific Serverless version +# Check out our docs for more details +# frameworkVersion: "=X.X.X" + +provider: + name: aws + runtime: go1.x + +# you can overwrite defaults here +# stage: dev +# region: us-east-1 + +# you can add statements to the Lambda function's IAM Role here +# iamRoleStatements: +# - Effect: "Allow" +# Action: +# - "s3:ListBucket" +# Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } +# - Effect: "Allow" +# Action: +# - "s3:PutObject" +# Resource: +# Fn::Join: +# - "" +# - - "arn:aws:s3:::" +# - "Ref" : "ServerlessDeploymentBucket" +# - "/*" + +# you can define service wide environment variables here +# environment: +# variable1: value1 + +package: + exclude: + - ./** + include: + - ./bin/** + +functions: + hello: + handler: bin/hello + world: + handler: bin/world + +# The following are a few example events you can configure +# NOTE: Please make sure to change your handler code to work with those events +# Check the event documentation for details +# events: +# events: +# - http: +# path: users/create +# method: get +# - s3: ${env:BUCKET} +# - schedule: rate(10 minutes) +# - sns: greeter-topic +# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 +# - alexaSkill +# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx +# - iot: +# sql: "SELECT * FROM 'some_topic'" +# - cloudwatchEvent: +# event: +# source: +# - "aws.ec2" +# detail-type: +# - "EC2 Instance State-change Notification" +# detail: +# state: +# - pending +# - cloudwatchLog: '/aws/lambda/hello' +# - cognitoUserPool: +# pool: MyUserPool +# trigger: PreSignUp + +# Define function environment variables here +# environment: +# variable2: value2 + +# you can add CloudFormation resource templates here +#resources: +# Resources: +# NewResource: +# Type: AWS::S3::Bucket +# Properties: +# BucketName: my-new-bucket +# Outputs: +# NewOutput: +# Description: "Description for the output" +# Value: "Some output value" diff --git a/lib/plugins/create/templates/aws-go/world/main.go b/lib/plugins/create/templates/aws-go/world/main.go new file mode 100644 index 00000000000..bf46aa6fbcb --- /dev/null +++ b/lib/plugins/create/templates/aws-go/world/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/aws/aws-lambda-go/lambda" +) + +type Response struct { + Message string `json:"message"` +} + +func Handler() (Response, error) { + return Response{ + Message: "Okay so your other function also executed successfully!", + }, nil +} + +func main() { + lambda.Start(Handler) +} 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 b15fcd6d494..41c1876cd1f 100644 --- a/lib/plugins/create/templates/aws-nodejs-typescript/webpack.config.js +++ b/lib/plugins/create/templates/aws-nodejs-typescript/webpack.config.js @@ -3,6 +3,7 @@ const slsw = require('serverless-webpack'); module.exports = { entry: slsw.lib.entries, + devtool: 'source-map', resolve: { extensions: [ '.js', diff --git a/lib/plugins/create/templates/aws-scala-sbt/project/assembly.sbt b/lib/plugins/create/templates/aws-scala-sbt/project/assembly.sbt index 5b7f17a1c45..24ec05fe5b1 100644 --- a/lib/plugins/create/templates/aws-scala-sbt/project/assembly.sbt +++ b/lib/plugins/create/templates/aws-scala-sbt/project/assembly.sbt @@ -1,3 +1,3 @@ resolvers += Resolver.sonatypeRepo("public") -addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.0") \ No newline at end of file +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") diff --git a/lib/plugins/create/templates/aws-scala-sbt/project/plugins.sbt b/lib/plugins/create/templates/aws-scala-sbt/project/plugins.sbt index dd405c41fdf..ff351a68c3f 100644 --- a/lib/plugins/create/templates/aws-scala-sbt/project/plugins.sbt +++ b/lib/plugins/create/templates/aws-scala-sbt/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0") \ No newline at end of file +addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.7") diff --git a/lib/plugins/create/templates/kubeless-nodejs/package.json b/lib/plugins/create/templates/kubeless-nodejs/package.json index d09d445ffc9..039292997e2 100644 --- a/lib/plugins/create/templates/kubeless-nodejs/package.json +++ b/lib/plugins/create/templates/kubeless-nodejs/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "Example function for serverless kubeless", "dependencies": { - "serverless-kubeless": "^0.1.8", + "serverless-kubeless": "^0.3.1", "lodash": "^4.1.0" }, "devDependencies": {}, diff --git a/lib/plugins/create/templates/kubeless-python/package.json b/lib/plugins/create/templates/kubeless-python/package.json index 58b82e5249d..2eacb76727a 100644 --- a/lib/plugins/create/templates/kubeless-python/package.json +++ b/lib/plugins/create/templates/kubeless-python/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "Sample Kubeless Python serverless framework service.", "dependencies": { - "serverless-kubeless": "^0.1.8" + "serverless-kubeless": "^0.3.1" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/lib/plugins/create/templates/spotinst-java8/.gitignore b/lib/plugins/create/templates/spotinst-java8/.gitignore new file mode 100644 index 00000000000..2b48c8bd543 --- /dev/null +++ b/lib/plugins/create/templates/spotinst-java8/.gitignore @@ -0,0 +1,6 @@ +# package directories +node_modules +jspm_packages + +# Serverless directories +.serverless \ No newline at end of file diff --git a/lib/plugins/create/templates/spotinst-java8/package.json b/lib/plugins/create/templates/spotinst-java8/package.json new file mode 100644 index 00000000000..38e3e7f407b --- /dev/null +++ b/lib/plugins/create/templates/spotinst-java8/package.json @@ -0,0 +1,13 @@ +{ + "name": "spotionst-java8", + "version": "1.0.0", + "description": "Spotinst Functions Java8 sample for serverless framework service.", + "main": "com.serverless.Handler.java", + "keywords": [ + "serverless", + "spotinst" + ], + "devDependencies": { + "serverless-spotinst-functions": "*" + } +} diff --git a/lib/plugins/create/templates/spotinst-java8/pom.xml b/lib/plugins/create/templates/spotinst-java8/pom.xml new file mode 100644 index 00000000000..ee750fa2f5e --- /dev/null +++ b/lib/plugins/create/templates/spotinst-java8/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + com.serverless + hello + dev + jar + + UTF-8 + 1.8 + 1.8 + + + + + com.spotinst + spotinst-functions-java-core + 1.1 + + + + com.google.code.gson + gson + 2.3.1 + + + + + + spotinst-java8 + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + false + + + + package + + shade + + + + + + + + diff --git a/lib/plugins/create/templates/spotinst-java8/serverless.yml b/lib/plugins/create/templates/spotinst-java8/serverless.yml new file mode 100644 index 00000000000..088814abc58 --- /dev/null +++ b/lib/plugins/create/templates/spotinst-java8/serverless.yml @@ -0,0 +1,39 @@ +# Welcome to Serverless! +# +# This file is the main config file for your service. +# It's very minimal at this point and uses default values. +# You can always add more config options for more control. +# We've included some commented out config examples here. +# Just uncomment any of them to get that config option. +# +# For full config options, check the docs: +# docs.serverless.com +# +# Happy Coding! + +service: spotinst-java8 + +provider: + name: spotinst + #stage: #Optional setting. By default it is set to 'dev' + spotinst: + #environment: # NOTE: Remember to add the environment ID + +functions: + hello: + runtime: java8 + handler: com.serverless.Handler + memory: 128 + timeout: 30 + access: private +# cron: # Setup scheduled trigger with cron expression +# active: true +# value: '* * * * *' +# environmentVariables: { +# Key: "Value", +# } + +# extend the framework using plugins listed here: +# https://github.com/serverless/plugins +plugins: + - serverless-spotinst-functions diff --git a/lib/plugins/create/templates/spotinst-java8/src/main/java/com/serverless/Handler.java b/lib/plugins/create/templates/spotinst-java8/src/main/java/com/serverless/Handler.java new file mode 100644 index 00000000000..ac090fa9ab7 --- /dev/null +++ b/lib/plugins/create/templates/spotinst-java8/src/main/java/com/serverless/Handler.java @@ -0,0 +1,35 @@ +package com.serverless; + +import com.google.gson.JsonObject; +import com.spotinst.functions.runtime.Context; +import com.spotinst.functions.runtime.Request; +import com.spotinst.functions.runtime.RequestHandler; +import com.spotinst.functions.runtime.Response; + +import java.util.HashMap; +import java.util.Map; + +/** + * Please make sure your class implements the "RequestHandler" interface + * The return value should be of type "Response" + **/ +public class Handler implements RequestHandler { + + @Override + public Response handleRequest(Request request, Context context) { + Map queryParams = request.getQueryParams(); + + String name = queryParams.get("name"); + String responseBody = String.format("{\"hello\":\"%s\"}", name); + + Response response = new Response(200, responseBody); + + //Build response headers + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + + response.setHeaders(headers); + + return response; + } +} \ No newline at end of file diff --git a/lib/plugins/create/templates/spotinst-nodejs/serverless.yml b/lib/plugins/create/templates/spotinst-nodejs/serverless.yml index fbb13eae268..b0079fcde84 100644 --- a/lib/plugins/create/templates/spotinst-nodejs/serverless.yml +++ b/lib/plugins/create/templates/spotinst-nodejs/serverless.yml @@ -15,6 +15,7 @@ service: spotinst-nodejs # NOTE: update this with your service name provider: name: spotinst + #stage: #Optional setting. By default it is set to 'dev' spotinst: #environment: # NOTE: Remember to add the environment ID diff --git a/lib/plugins/create/templates/spotinst-python/serverless.yml b/lib/plugins/create/templates/spotinst-python/serverless.yml index 866d5a8fca1..a78c753b7ef 100644 --- a/lib/plugins/create/templates/spotinst-python/serverless.yml +++ b/lib/plugins/create/templates/spotinst-python/serverless.yml @@ -15,6 +15,7 @@ service: spotinst-python # NOTE: update this with your service name provider: name: spotinst + #stage: #Optional setting. By default it is set to 'dev' spotinst: #environment: # NOTE: Remember to add the environment ID diff --git a/lib/plugins/create/templates/spotinst-ruby/serverless.yml b/lib/plugins/create/templates/spotinst-ruby/serverless.yml index c76c951fece..0a1f291023f 100644 --- a/lib/plugins/create/templates/spotinst-ruby/serverless.yml +++ b/lib/plugins/create/templates/spotinst-ruby/serverless.yml @@ -15,6 +15,7 @@ service: spotinst-ruby # NOTE: update this with your service name provider: name: spotinst + #stage: #Optional setting. By default it is set to 'dev' spotinst: #environment: # NOTE: Remember to add the environment ID diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js index 0c43ff0b484..67a73208aca 100644 --- a/lib/plugins/package/lib/packageService.js +++ b/lib/plugins/package/lib/packageService.js @@ -14,6 +14,7 @@ module.exports = { 'serverless.yml', 'serverless.yaml', 'serverless.json', + 'serverless.js', '.serverless/**', '.serverless_plugins/**', ], diff --git a/lib/plugins/package/lib/packageService.test.js b/lib/plugins/package/lib/packageService.test.js index 6a1c1d5f925..a625e0f3099 100644 --- a/lib/plugins/package/lib/packageService.test.js +++ b/lib/plugins/package/lib/packageService.test.js @@ -82,7 +82,7 @@ describe('#packageService()', () => { expect(exclude).to.deep.equal([ '.git/**', '.gitignore', '.DS_Store', 'npm-debug.log', 'serverless.yml', - 'serverless.yaml', 'serverless.json', + 'serverless.yaml', 'serverless.json', 'serverless.js', '.serverless/**', '.serverless_plugins/**', 'dir', 'file.js', ]); @@ -102,7 +102,7 @@ describe('#packageService()', () => { expect(exclude).to.deep.equal([ '.git/**', '.gitignore', '.DS_Store', 'npm-debug.log', 'serverless.yml', - 'serverless.yaml', 'serverless.json', + 'serverless.yaml', 'serverless.json', 'serverless.js', '.serverless/**', '.serverless_plugins/**', 'dir', 'file.js', 'lib', 'other.js', ]); diff --git a/lib/plugins/plugin/install/install.js b/lib/plugins/plugin/install/install.js index 932a6cc612b..c688128bba8 100644 --- a/lib/plugins/plugin/install/install.js +++ b/lib/plugins/plugin/install/install.js @@ -109,6 +109,14 @@ class PluginInstall { addPluginToServerlessFile() { return this.getServerlessFilePath().then(serverlessFilePath => { + if (_.last(_.split(serverlessFilePath, '.')) === 'js') { + this.serverless.cli.log(` + Can't automatically add plugin into "serverless.js" file. + Please make it manually. + `); + return BbPromise.resolve(); + } + if (_.last(_.split(serverlessFilePath, '.')) === 'json') { return fse.readJsonAsync(serverlessFilePath).then(serverlessFileObj => { const newServerlessFileObj = serverlessFileObj; diff --git a/lib/plugins/plugin/install/install.test.js b/lib/plugins/plugin/install/install.test.js index 5faf7ba9a7d..396dd603a16 100644 --- a/lib/plugins/plugin/install/install.test.js +++ b/lib/plugins/plugin/install/install.test.js @@ -357,6 +357,24 @@ describe('PluginInstall', () => { }); }); }); + + it('should not modify serverless .js file', () => { + const serverlessJsFilePath = path.join(servicePath, 'serverless.js'); + const serverlessJson = { + service: 'plugin-service', + provider: 'aws', + plugins: [], + }; + serverless.utils + .writeFileSync(serverlessJsFilePath, `module.exports = ${JSON.stringify(serverlessJson)};`); + pluginInstall.options.pluginName = 'serverless-plugin-1'; + return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { + // use require to load serverless.js + // eslint-disable-next-line global-require + expect(require(serverlessJsFilePath).plugins) + .to.be.deep.equal([]); + }); + }); }); describe('#installPeerDependencies()', () => { diff --git a/lib/plugins/plugin/lib/utils.js b/lib/plugins/plugin/lib/utils.js index 6f74ec36bb7..2bed10b7fc6 100644 --- a/lib/plugins/plugin/lib/utils.js +++ b/lib/plugins/plugin/lib/utils.js @@ -19,22 +19,31 @@ module.exports = { getServerlessFilePath() { const servicePath = this.serverless.config.servicePath; - const serverlessYmlFilePath = path.join(servicePath, 'serverless.yml'); - const serverlessYamlFilePath = path.join(servicePath, 'serverless.yaml'); - const serverlessJsonFilePath = path.join(servicePath, 'serverless.json'); + const ymlFilePath = path.join(servicePath, 'serverless.yml'); + const yamlFilePath = path.join(servicePath, 'serverless.yaml'); + const jsonFilePath = path.join(servicePath, 'serverless.json'); + const jsFilePath = path.join(servicePath, 'serverless.js'); - return fileExists(serverlessYmlFilePath) - .then(ymlExists => { - if (!ymlExists) { - return fileExists(serverlessYamlFilePath) - .then(yamlExists => { - if (!yamlExists) { - return serverlessJsonFilePath; - } - return serverlessYamlFilePath; - }); + return BbPromise.props({ + json: fileExists(jsonFilePath), + yml: fileExists(ymlFilePath), + yaml: fileExists(yamlFilePath), + js: fileExists(jsFilePath), + }).then((exists) => { + if (exists.yml) { + return ymlFilePath; + } else if (exists.yaml) { + return yamlFilePath; + } else if (exists.json) { + return jsonFilePath; + } else if (exists.js) { + return jsFilePath; } - return serverlessYmlFilePath; + return BbPromise.reject( + new this.serverless.classes.Error( + 'Could not find any serverless service definition file.' + ) + ); }); }, diff --git a/lib/plugins/plugin/lib/utils.test.js b/lib/plugins/plugin/lib/utils.test.js index d89a21b52b2..018883a2f57 100644 --- a/lib/plugins/plugin/lib/utils.test.js +++ b/lib/plugins/plugin/lib/utils.test.js @@ -99,6 +99,20 @@ describe('PluginUtils', () => { expect(serverlessFilePath).to.equal(serverlessJsonFilePath); }); }); + + it('should return the correct serverless file path for a .js file', () => { + const serverlessJsFilePath = path.join(servicePath, 'serverless.js'); + fse.ensureFileSync(serverlessJsFilePath); + + return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled + .then(serverlessFilePath => { + expect(serverlessFilePath).to.equal(serverlessJsFilePath); + }); + }); + + it('should reject if no configuration file exists', () => + expect(pluginUtils.getServerlessFilePath()) + .to.be.rejectedWith('Could not find any serverless service definition file.')); }); describe('#getPlugins()', () => { diff --git a/lib/plugins/plugin/uninstall/uninstall.js b/lib/plugins/plugin/uninstall/uninstall.js index 55f55adba05..05580a98ab8 100644 --- a/lib/plugins/plugin/uninstall/uninstall.js +++ b/lib/plugins/plugin/uninstall/uninstall.js @@ -78,6 +78,14 @@ class PluginUninstall { removePluginFromServerlessFile() { return this.getServerlessFilePath().then(serverlessFilePath => { + if (_.last(_.split(serverlessFilePath, '.')) === 'js') { + this.serverless.cli.log(` + Can't automatically remove plugin from "serverless.js" file. + Please make it manually. + `); + return BbPromise.resolve(); + } + if (_.last(_.split(serverlessFilePath, '.')) === 'json') { return fse.readJsonAsync(serverlessFilePath).then(serverlessFileObj => { if (serverlessFileObj.plugins) { diff --git a/lib/plugins/plugin/uninstall/uninstall.test.js b/lib/plugins/plugin/uninstall/uninstall.test.js index 9b86b85a89e..a119fbbc0ea 100644 --- a/lib/plugins/plugin/uninstall/uninstall.test.js +++ b/lib/plugins/plugin/uninstall/uninstall.test.js @@ -339,6 +339,31 @@ describe('PluginUninstall', () => { .to.not.have.property('plugins'); })); }); + + it('should not modify serverless .js file', () => { + it('should not modify serverless .js file', () => { + const serverlessJsFilePath = path.join(servicePath, 'serverless.js'); + const serverlessJson = { + service: 'plugin-service', + provider: 'aws', + plugins: [ + 'serverless-plugin-1', + 'serverless-plugin-2', + ], + }; + serverless.utils.writeFileSync( + serverlessJsFilePath, + `module.exports = ${JSON.stringify(serverlessJson)};` + ); + pluginUninstall.options.pluginName = 'serverless-plugin-1'; + return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => { + // use require to load serverless.js + // eslint-disable-next-line global-require + expect(require(serverlessJsFilePath).plugins) + .to.be.deep.equal(serverlessJson.plugins); + }); + }); + }); }); describe('#uninstallPeerDependencies()', () => { diff --git a/lib/plugins/print/print.js b/lib/plugins/print/print.js index e4263aa8b54..b48f26e36f8 100644 --- a/lib/plugins/print/print.js +++ b/lib/plugins/print/print.js @@ -2,6 +2,7 @@ const BbPromise = require('bluebird'); const getServerlessConfigFile = require('../../utils/getServerlessConfigFile'); +const jc = require('json-cycle'); const YAML = require('js-yaml'); class Print { @@ -34,15 +35,17 @@ class Print { variableSyntax = conf.provider.variableSyntax; delete conf.provider.variableSyntax; } - return conf; + this.serverless.variables.service = conf; + this.serverless.variables.cache = {}; + return this.serverless.variables.populateObject(conf); }) - .then((data) => this.serverless.variables.populateObject(data)) .then((data) => { const conf = data; if (variableSyntax !== undefined) { conf.provider.variableSyntax = variableSyntax; } - this.serverless.cli.consoleLog(YAML.dump(conf, { noRefs: true })); + const out = JSON.parse(jc.stringify(conf)); + this.serverless.cli.consoleLog(YAML.dump(out, { noRefs: true })); }); } diff --git a/lib/plugins/print/print.test.js b/lib/plugins/print/print.test.js index 95fc05935d5..ec4dc1950e0 100644 --- a/lib/plugins/print/print.test.js +++ b/lib/plugins/print/print.test.js @@ -157,4 +157,36 @@ describe('Print', () => { expect(message).to.equal(YAML.dump(expected)); }); }); + + it('should resolve self references', () => { + const conf = { + custom: { + me: '${self:}', + }, + provider: {}, + }; + getServerlessConfigFileStub.resolves(conf); + + serverless.processedInput = { + commands: ['print'], + options: {}, + }; + + const expected = { + custom: { + me: { + $ref: '$', + }, + }, + provider: {}, + }; + + return print.print().then(() => { + const message = print.serverless.cli.consoleLog.args.join(); + + expect(getServerlessConfigFileStub.calledOnce).to.equal(true); + expect(print.serverless.cli.consoleLog.called).to.be.equal(true); + expect(message).to.equal(YAML.dump(expected)); + }); + }); }); diff --git a/lib/plugins/remove/remove.js b/lib/plugins/remove/remove.js index 1f098b38ac2..eb330ddd061 100644 --- a/lib/plugins/remove/remove.js +++ b/lib/plugins/remove/remove.js @@ -2,11 +2,17 @@ const BbPromise = require('bluebird'); const userStats = require('../../utils/userStats'); +const validate = require('../lib/validate'); class Remove { constructor(serverless) { this.serverless = serverless; + Object.assign( + this, + validate + ); + this.commands = { remove: { usage: 'Remove Serverless service and all resources', @@ -31,6 +37,7 @@ class Remove { }; this.hooks = { + 'before:remove:remove': () => BbPromise.bind(this).then(this.validate), 'after:remove:remove': () => BbPromise.bind(this).then(this.track), }; } diff --git a/lib/plugins/remove/remove.test.js b/lib/plugins/remove/remove.test.js index 598ad30b363..08a9d05e938 100644 --- a/lib/plugins/remove/remove.test.js +++ b/lib/plugins/remove/remove.test.js @@ -3,6 +3,7 @@ const expect = require('chai').expect; const Remove = require('./remove'); const Serverless = require('../../Serverless'); +const sinon = require('sinon'); describe('Remove', () => { let remove; @@ -20,4 +21,20 @@ describe('Remove', () => { it('should have commands', () => expect(remove.commands).to.be.not.empty); }); + + describe('"before:remove:remove" hook', () => { + let validateStub; + + beforeEach(() => { + validateStub = sinon.stub(remove, 'validate').resolves(); + }); + + afterEach(() => { + remove.validate.restore(); + }); + + it('should run the validation', () => expect(remove.hooks['before:remove:remove']()) + .to.be.fulfilled.then(() => expect(validateStub).to.be.called) + ); + }); }); diff --git a/lib/utils/autocomplete.js b/lib/utils/autocomplete.js index c2b4c530135..6b744aa8fe7 100644 --- a/lib/utils/autocomplete.js +++ b/lib/utils/autocomplete.js @@ -1,39 +1,33 @@ 'use strict'; +const path = require('path'); + const Serverless = require('../Serverless'); const crypto = require('crypto'); const getCacheFile = require('./getCacheFile'); const getServerlessConfigFile = require('./getServerlessConfigFile'); -const tab = require('tabtab')({ - name: 'serverless', -}); - -const tabSls = require('tabtab')({ - name: 'sls', -}); +const name = path.basename(process.argv[0]); -const getSugestions = (commands) => { - tab.on('serverless', (data, done) => { - done(null, Object.keys(commands)); - }); +const tab = require('tabtab')({ name }); - tabSls.on('sls', (data, done) => { - done(null, Object.keys(commands)); +const getSuggestions = (commands) => { + tab.on(name, (data, done) => { + if (data.words === 1) { + done(null, Object.keys(commands)); + } else { + done(null, []); + } }); Object.keys(commands).forEach(command => { tab.on(command, (data, done) => { done(null, commands[command]); }); - tabSls.on(command, (data, done) => { - done(null, commands[command]); - }); }); tab.start(); - tabSls.start(); }; const cacheFileValid = (serverlessConfigFile, validationHash) => { @@ -64,7 +58,7 @@ const autocomplete = () => { if (!cacheFile || !cacheFileValid(serverlessConfigFile, cacheFile.validationHash)) { return; } - return getSugestions(cacheFile.commands); // eslint-disable-line consistent-return + return getSuggestions(cacheFile.commands); // eslint-disable-line consistent-return }); }); }; diff --git a/lib/utils/fs/copyDirContentsSync.js b/lib/utils/fs/copyDirContentsSync.js index 5c38378b264..dc75de7a653 100644 --- a/lib/utils/fs/copyDirContentsSync.js +++ b/lib/utils/fs/copyDirContentsSync.js @@ -4,8 +4,8 @@ const path = require('path'); const fse = require('./fse'); const walkDirSync = require('./walkDirSync'); -function fileExists(srcDir, destDir) { - const fullFilesPaths = walkDirSync(srcDir); +function fileExists(srcDir, destDir, options) { + const fullFilesPaths = walkDirSync(srcDir, options); fullFilesPaths.forEach(fullFilePath => { const relativeFilePath = fullFilePath.replace(srcDir, ''); diff --git a/lib/utils/fs/walkDirSync.js b/lib/utils/fs/walkDirSync.js index 0e4615272b1..150e856b065 100644 --- a/lib/utils/fs/walkDirSync.js +++ b/lib/utils/fs/walkDirSync.js @@ -3,15 +3,21 @@ const path = require('path'); const fs = require('fs'); -function walkDirSync(dirPath) { +function walkDirSync(dirPath, opts) { + const options = Object.assign({ + noLinks: false, + }, opts); let filePaths = []; const list = fs.readdirSync(dirPath); list.forEach((filePathParam) => { let filePath = filePathParam; filePath = path.join(dirPath, filePath); - const stat = fs.statSync(filePath); - if (stat && stat.isDirectory()) { - filePaths = filePaths.concat(walkDirSync(filePath)); + const stat = options.noLinks ? fs.lstatSync(filePath) : fs.statSync(filePath); + // skipping symbolic links when noLinks option + if (options.noLinks && stat && stat.isSymbolicLink()) { + return; + } else if (stat && stat.isDirectory()) { + filePaths = filePaths.concat(walkDirSync(filePath, opts)); } else { filePaths.push(filePath); } diff --git a/lib/utils/fs/walkDirSync.test.js b/lib/utils/fs/walkDirSync.test.js index 991e9d06db0..2ef3a2600a9 100644 --- a/lib/utils/fs/walkDirSync.test.js +++ b/lib/utils/fs/walkDirSync.test.js @@ -1,5 +1,6 @@ 'use strict'; +const fs = require('fs'); const path = require('path'); const expect = require('chai').expect; const testUtils = require('../../../tests/utils'); @@ -28,4 +29,21 @@ describe('#walkDirSync()', () => { expect(filePaths).to.include(tmpFilePath2); expect(filePaths).to.include(tmpFilePath3); }); + + it('should check noLinks option', () => { + const tmpDirPath = testUtils.getTmpDirPath(); + + const realFile = path.join(tmpDirPath, 'real'); + writeFileSync(realFile, 'content'); + + const symLink = path.join(tmpDirPath, 'sym'); + fs.symlinkSync(realFile, symLink); + + const filePaths = walkDirSync(tmpDirPath, { + noLinks: true, + }); + + expect(filePaths).to.include(realFile); + expect(filePaths).not.to.include(symLink); + }); }); diff --git a/lib/utils/getCommandSuggestion.js b/lib/utils/getCommandSuggestion.js new file mode 100644 index 00000000000..3e4604ed84d --- /dev/null +++ b/lib/utils/getCommandSuggestion.js @@ -0,0 +1,36 @@ +'use strict'; +const _ = require('lodash'); +const levenshtein = require('fast-levenshtein'); + +const getCollectCommandWords = (commandObject, commandWordsArray) => { + let wordsArray = _.isArray(commandWordsArray) + && !_.isEmpty(commandWordsArray) ? commandWordsArray : []; + _.forEach(commandObject, (commandChildObject, commandChildName) => { + wordsArray.push(commandChildName); + if (commandChildObject.commands) { + wordsArray = getCollectCommandWords(commandChildObject.commands, wordsArray); + } + }); + return _.uniq(wordsArray); +}; + +const getCommandSuggestion = (inputCommand, allCommandsObject) => { + let suggestion; + const commandWordsArray = getCollectCommandWords(allCommandsObject); + let minValue = 0; + _.forEach(commandWordsArray, correctCommand => { + const distance = levenshtein.get(inputCommand, correctCommand); + if (minValue === 0) { + suggestion = correctCommand; + minValue = distance; + } + + if (minValue > distance) { + suggestion = correctCommand; + minValue = distance; + } + }); + return suggestion; +}; + +module.exports = getCommandSuggestion; diff --git a/lib/utils/getCommandSuggestion.test.js b/lib/utils/getCommandSuggestion.test.js new file mode 100644 index 00000000000..56ea4633781 --- /dev/null +++ b/lib/utils/getCommandSuggestion.test.js @@ -0,0 +1,25 @@ +'use strict'; + +const expect = require('chai').expect; +const getCommandSuggestion = require('./getCommandSuggestion'); +const Serverless = require('../../lib/Serverless'); + +const serverless = new Serverless(); +serverless.init(); + +describe('#getCommandSuggestion', () => { + it('should return "package" as a suggested command if you input "pekage"', () => { + const suggestedCommand = getCommandSuggestion('pekage', serverless.cli.loadedCommands); + expect(suggestedCommand).to.be.equal('package'); + }); + + it('should return "deploy" as a suggested command if you input "deploi"', () => { + const suggestedCommand = getCommandSuggestion('deploi', serverless.cli.loadedCommands); + expect(suggestedCommand).to.be.equal('deploy'); + }); + + it('should return "invoke" as a suggested command if you input "lnvoke"', () => { + const suggestedCommand = getCommandSuggestion('lnvoke', serverless.cli.loadedCommands); + expect(suggestedCommand).to.be.equal('invoke'); + }); +}); diff --git a/lib/utils/getServerlessConfigFile.js b/lib/utils/getServerlessConfigFile.js index 06e566033cb..a85a1b67b6f 100644 --- a/lib/utils/getServerlessConfigFile.js +++ b/lib/utils/getServerlessConfigFile.js @@ -10,11 +10,13 @@ const getServerlessConfigFile = _.memoize((servicePath) => { const jsonPath = path.join(servicePath, 'serverless.json'); const ymlPath = path.join(servicePath, 'serverless.yml'); const yamlPath = path.join(servicePath, 'serverless.yaml'); + const jsPath = path.join(servicePath, 'serverless.js'); return BbPromise.props({ json: fileExists(jsonPath), yml: fileExists(ymlPath), yaml: fileExists(yamlPath), + js: fileExists(jsPath), }).then((exists) => { if (exists.json) { return readFile(jsonPath); @@ -22,6 +24,17 @@ const getServerlessConfigFile = _.memoize((servicePath) => { return readFile(ymlPath); } else if (exists.yaml) { return readFile(yamlPath); + } else if (exists.js) { + return BbPromise.try(() => { + // use require to load serverless.js + // eslint-disable-next-line global-require + const config = require(jsPath); + + if (_.isPlainObject(config)) { + return config; + } + throw new Error('serverless.js must export plain object'); + }); } return ''; }); diff --git a/lib/utils/getServerlessConfigFile.test.js b/lib/utils/getServerlessConfigFile.test.js index 3be22a09cb5..2d6c0fa488e 100644 --- a/lib/utils/getServerlessConfigFile.test.js +++ b/lib/utils/getServerlessConfigFile.test.js @@ -48,4 +48,29 @@ describe('#getServerlessConfigFile()', () => { expect(result).to.deep.equal({ service: 'my-json-service' }); }); }); + + it('should return the file content if a serverless.js file found', () => { + const serverlessFilePath = path.join(tmpDirPath, 'serverless.js'); + writeFileSync( + serverlessFilePath, + 'module.exports = {"service": "my-json-service"};' + ); + + return expect(getServerlessConfigFile(tmpDirPath)).to.be.fulfilled.then( + (result) => { + expect(result).to.deep.equal({ service: 'my-json-service' }); + } + ); + }); + + it('should throw an error, if serverless.js export not a plain object', () => { + const serverlessFilePath = path.join(tmpDirPath, 'serverless.js'); + writeFileSync( + serverlessFilePath, + 'module.exports = function config() {};' + ); + + return expect(getServerlessConfigFile(tmpDirPath)) + .to.be.rejectedWith('serverless.js must export plain object'); + }); }); diff --git a/package-lock.json b/package-lock.json index a4d0568e6af..a238c631184 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "serverless", - "version": "1.25.0", + "version": "1.26.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -33,9 +33,9 @@ "dev": true }, "acorn": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz", - "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.3.0.tgz", + "integrity": "sha512-Yej+zOJ1Dm/IMZzzj78OntP/r3zHEaKcyNoU2lAaxPtrseM6rF0xwqoz5Q5ysAiED9hTjI2hgtvLXitlCN1/Ug==", "dev": true }, "acorn-globals": { @@ -213,7 +213,7 @@ "graphql-anywhere": "3.1.0", "graphql-tag": "2.6.1", "redux": "3.7.2", - "symbol-observable": "1.1.0", + "symbol-observable": "1.2.0", "whatwg-fetch": "2.0.3" } }, @@ -367,9 +367,9 @@ "dev": true }, "assertion-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", - "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, "async": { @@ -389,12 +389,11 @@ "dev": true }, "aws-sdk": { - "version": "2.172.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.172.0.tgz", - "integrity": "sha1-R9+3mQeXbrvVOFYupaJYNbWz810=", + "version": "2.188.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.188.0.tgz", + "integrity": "sha1-kGKrx9umOTRZ+i80I89dKU8ARhE=", "requires": { "buffer": "4.9.1", - "crypto-browserify": "1.0.9", "events": "1.1.1", "jmespath": "0.15.0", "querystring": "0.2.0", @@ -887,7 +886,7 @@ "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", "dev": true, "requires": { - "assertion-error": "1.0.2", + "assertion-error": "1.1.0", "deep-eql": "0.1.3", "type-detect": "1.0.0" } @@ -951,13 +950,13 @@ } }, "cli-usage": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/cli-usage/-/cli-usage-0.1.4.tgz", - "integrity": "sha1-fAHg3HBsI0s5yTODjI4gshdXduI=", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/cli-usage/-/cli-usage-0.1.7.tgz", + "integrity": "sha512-x/Q52iLSZsRrRb2ePmTsVYXrGcrPQ8G4yRAY7QpMlumxAfPVrnDOH2X6Z5s8qsAX7AA7YuIi8AXFrvH0wWEesA==", "dev": true, "requires": { - "marked": "0.3.7", - "marked-terminal": "1.7.0" + "marked": "0.3.12", + "marked-terminal": "2.0.0" } }, "cli-width": { @@ -1202,11 +1201,6 @@ "boom": "2.10.1" } }, - "crypto-browserify": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", - "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=" - }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -1233,7 +1227,7 @@ "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "dev": true, "requires": { - "es5-ext": "0.10.37" + "es5-ext": "0.10.38" } }, "damerau-levenshtein": { @@ -1481,9 +1475,9 @@ "dev": true }, "doctrine": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.2.tgz", - "integrity": "sha512-y0tm5Pq6ywp3qSTZ1vPgVdAnbDEoeoc5wlOHXoY1c4Wug/a7JvqHIl7BTvwodaHmejWkK/9dSb3sCYfyo/om8A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "requires": { "esutils": "2.0.2" @@ -1535,9 +1529,9 @@ } }, "end-of-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", - "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", "requires": { "once": "1.4.0" } @@ -1585,9 +1579,9 @@ } }, "es5-ext": { - "version": "0.10.37", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.37.tgz", - "integrity": "sha1-DudB0Ui4AGm6J9AgOTdWryV978M=", + "version": "0.10.38", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.38.tgz", + "integrity": "sha512-jCMyePo7AXbUESwbl8Qi01VSH2piY9s/a3rSU/5w/MlTIx8HPL1xn2InGN8ejt/xulcJgnTO7vqNtOAxzYd2Kg==", "dev": true, "requires": { "es6-iterator": "2.0.3", @@ -1601,7 +1595,7 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.37", + "es5-ext": "0.10.38", "es6-symbol": "3.1.1" } }, @@ -1612,7 +1606,7 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.37", + "es5-ext": "0.10.38", "es6-iterator": "2.0.3", "es6-set": "0.1.5", "es6-symbol": "3.1.1", @@ -1632,7 +1626,7 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.37", + "es5-ext": "0.10.38", "es6-iterator": "2.0.3", "es6-symbol": "3.1.1", "event-emitter": "0.3.5" @@ -1645,7 +1639,7 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.37" + "es5-ext": "0.10.38" } }, "es6-weak-map": { @@ -1655,7 +1649,7 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.37", + "es5-ext": "0.10.38", "es6-iterator": "2.0.3", "es6-symbol": "3.1.1" } @@ -1714,7 +1708,7 @@ "chalk": "1.1.3", "concat-stream": "1.6.0", "debug": "2.6.9", - "doctrine": "2.0.2", + "doctrine": "2.1.0", "escope": "3.6.0", "espree": "3.5.2", "esquery": "1.0.0", @@ -1727,7 +1721,7 @@ "imurmurhash": "0.1.4", "inquirer": "0.12.0", "is-my-json-valid": "2.17.1", - "is-resolvable": "1.0.1", + "is-resolvable": "1.1.0", "js-yaml": "3.10.0", "json-stable-stringify": "1.0.1", "levn": "0.3.0", @@ -1887,7 +1881,7 @@ "doctrine": "1.5.0", "has": "1.0.1", "jsx-ast-utils": "1.4.1", - "object.assign": "4.0.4" + "object.assign": "4.1.0" }, "dependencies": { "doctrine": { @@ -1908,7 +1902,7 @@ "integrity": "sha512-sadKeYwaR/aJ3stC2CdvgXu1T16TdYN+qwCpcWbMnGJ8s0zNWemzrvb2GbD4OhmJ/fwpJjudThAlLobGbWZbCQ==", "dev": true, "requires": { - "acorn": "5.2.1", + "acorn": "5.3.0", "acorn-jsx": "3.0.1" } }, @@ -1955,7 +1949,7 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.37" + "es5-ext": "0.10.38" } }, "events": { @@ -2051,8 +2045,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "fb-watchman": { "version": "1.9.2", @@ -2405,9 +2398,9 @@ "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" }, "graphlib": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.1.tgz", - "integrity": "sha1-QjUsUrovTQNctWbrkfc5X3bryVE=", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.5.tgz", + "integrity": "sha512-XvtbqCcw+EM5SqQrIetIKKD+uZVNQtDPD1goIg7K73RuRZtVI5rYMdcCVSHm/AS1sCBZ7vt0p5WgXouucHQaOA==", "requires": { "lodash": "4.17.4" } @@ -2417,7 +2410,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.10.5.tgz", "integrity": "sha512-Q7cx22DiLhwHsEfUnUip1Ww/Vfx7FS0w6+iHItNuN61+XpegHSa3k5U0+6M5BcpavQImBwFiy0z3uYwY7cXMLQ==", "requires": { - "iterall": "1.1.3" + "iterall": "1.1.4" } }, "graphql-anywhere": { @@ -2485,7 +2478,7 @@ "dev": true, "requires": { "chalk": "1.1.3", - "commander": "2.12.2", + "commander": "2.13.0", "is-my-json-valid": "2.17.1", "pinkie-promise": "2.0.1" }, @@ -2510,9 +2503,9 @@ } }, "commander": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", - "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", "dev": true }, "supports-color": { @@ -2550,6 +2543,12 @@ "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.1.tgz", "integrity": "sha512-JkaetveU7hFbqnAC1EV1sF4rlojU2D4Usc5CmS69l6NfmPDnpnFUegzFg33eDkkpNCxZ0mQp65HwUDrNFS/8MA==" }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, "has-to-string-tag-x": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", @@ -2795,9 +2794,9 @@ "dev": true }, "is-ci": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.0.10.tgz", - "integrity": "sha1-9zkzayYyNlBhqdSCcM1WrjNpMY4=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", + "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", "dev": true, "requires": { "ci-info": "1.1.2" @@ -3001,9 +3000,9 @@ } }, "is-resolvable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.1.tgz", - "integrity": "sha512-y5CXYbzvB3jTnWAZH1Nl7ykUWb6T3BcTs56HUruwBf8MhF56n1HWqhDWnVFo8GHrUPDgvUUNVhrc2U8W7iqz5g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", "dev": true }, "is-retry-allowed": { @@ -3194,7 +3193,7 @@ "babel-types": "6.26.0", "babylon": "6.18.0", "istanbul-lib-coverage": "1.1.1", - "semver": "5.4.1" + "semver": "5.5.0" } }, "istanbul-lib-report": { @@ -3275,9 +3274,9 @@ } }, "iterall": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.1.3.tgz", - "integrity": "sha512-Cu/kb+4HiNSejAPhSaN1VukdNTTi/r4/e+yykqjlG/IW+1gZH5b4+Bq3whDX4tvbYugta3r8KTMUiqT3fIGxuQ==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.1.4.tgz", + "integrity": "sha512-eaDsM/PY8D/X5mYQhecVc5/9xvSHED7yPON+ffQroBeTuqUVm7dfphMkK8NksXuImqZlVRoKtrNfMIVCYIqaUQ==" }, "jest-changed-files": { "version": "17.0.2", @@ -3295,7 +3294,7 @@ "callsites": "2.0.0", "chalk": "1.1.3", "graceful-fs": "4.1.11", - "is-ci": "1.0.10", + "is-ci": "1.1.0", "istanbul-api": "1.2.1", "istanbul-lib-coverage": "1.1.1", "istanbul-lib-instrument": "1.9.1", @@ -3824,8 +3823,8 @@ "resolved": "https://registry.npmjs.org/json-refs/-/json-refs-2.1.7.tgz", "integrity": "sha1-uesB/in16j6Sh48VrqEK04taz4k=", "requires": { - "commander": "2.12.2", - "graphlib": "2.1.1", + "commander": "2.13.0", + "graphlib": "2.1.5", "js-yaml": "3.10.0", "native-promise-only": "0.8.1", "path-loader": "1.0.4", @@ -3834,9 +3833,9 @@ }, "dependencies": { "commander": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", - "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==" + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==" } } }, @@ -4355,12 +4354,12 @@ "dev": true }, "markdown-magic": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/markdown-magic/-/markdown-magic-0.1.19.tgz", - "integrity": "sha1-IKnWWdqgx7DOt64DCVxuLK41KgM=", + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/markdown-magic/-/markdown-magic-0.1.20.tgz", + "integrity": "sha1-Xw73k0L6G0O7pCr+Y9MCMobj7sM=", "dev": true, "requires": { - "commander": "2.12.2", + "commander": "2.13.0", "deepmerge": "1.5.2", "find-up": "2.1.0", "fs-extra": "1.0.0", @@ -4371,9 +4370,9 @@ }, "dependencies": { "commander": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", - "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", "dev": true }, "find-up": { @@ -4436,15 +4435,15 @@ } }, "marked": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.7.tgz", - "integrity": "sha512-zBEP4qO1YQp5aXHt8S5wTiOv9i2X74V/LQL0zhUNvVaklt6Ywa6lChxIvS+ibYlCGgADwKwZFhjC3+XfpsvQvQ==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.12.tgz", + "integrity": "sha512-k4NaW+vS7ytQn6MgJn3fYpQt20/mOgYM5Ft9BYMfQJDz2QT6yEeS9XJ8k2Nw8JTeWK/znPPW2n3UJGzyYEiMoA==", "dev": true }, "marked-terminal": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-1.7.0.tgz", - "integrity": "sha1-yMRgiBx3LHYEtkNnAH7l938SWQQ=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-2.0.0.tgz", + "integrity": "sha1-Xq9Wi+ZvaGVBr6UqVYKAMQox3i0=", "dev": true, "requires": { "cardinal": "1.0.0", @@ -4743,11 +4742,11 @@ "integrity": "sha1-BW0UJE89zBzq3+aK+c/wxUc6M/M=", "dev": true, "requires": { - "cli-usage": "0.1.4", + "cli-usage": "0.1.7", "growly": "1.3.0", "lodash.clonedeep": "3.0.2", "minimist": "1.2.0", - "semver": "5.4.1", + "semver": "5.5.0", "shellwords": "0.1.1", "which": "1.3.0" } @@ -4769,7 +4768,7 @@ "requires": { "hosted-git-info": "2.5.0", "is-builtin-module": "1.0.0", - "semver": "5.4.1", + "semver": "5.5.0", "validate-npm-package-license": "3.0.1" } }, @@ -4849,13 +4848,14 @@ "dev": true }, "object.assign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.0.4.tgz", - "integrity": "sha1-scnMBE7xuf5jYG/BQau7MuFHMMw=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", "dev": true, "requires": { "define-properties": "1.1.2", "function-bind": "1.1.1", + "has-symbols": "1.0.0", "object-keys": "1.0.11" } }, @@ -4900,9 +4900,9 @@ "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" }, "opn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.1.0.tgz", - "integrity": "sha512-iPNl7SyM8L30Rm1sjGdLLheyHVw5YXVfi3SKWJzBI7efxRwHojfRFjwE/OLM6qp9xJYMgab8WicTU1cPoY+Hpg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.2.0.tgz", + "integrity": "sha512-Jd/GpzPyHF4P2/aNOVmS3lfMSWV9J7cOhCG1s08XCEAsPkB7lp6ddiU0J7XzyQRDUh8BqJ7PchfINjR8jyofRQ==", "requires": { "is-wsl": "1.1.0" } @@ -4976,10 +4976,13 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-limit": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", - "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "dev": true, + "requires": { + "p-try": "1.0.0" + } }, "p-locate": { "version": "2.0.0", @@ -4987,18 +4990,24 @@ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { - "p-limit": "1.1.0" + "p-limit": "1.2.0" } }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, "package-json": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", "requires": { "got": "6.7.1", - "registry-auth-token": "3.3.1", + "registry-auth-token": "3.3.2", "registry-url": "3.1.0", - "semver": "5.4.1" + "semver": "5.5.0" } }, "pako": { @@ -5198,9 +5207,9 @@ } }, "promise-queue": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/promise-queue/-/promise-queue-2.2.3.tgz", - "integrity": "sha1-hTTXa/RnPDuqOoK7oBvSlcww8U8=" + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/promise-queue/-/promise-queue-2.2.5.tgz", + "integrity": "sha1-L29ffA9tCBCelnZZx5uIqe1ek7Q=" }, "proto-list": { "version": "1.2.4", @@ -5323,9 +5332,9 @@ } }, "rc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.2.tgz", - "integrity": "sha1-2M6ctX6NZNnHut2YdsfDTL48cHc=", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.4.tgz", + "integrity": "sha1-oPYGyq4qO4YrvQ74VILAElsxX6M=", "requires": { "deep-extend": "0.4.2", "ini": "1.3.5", @@ -5421,7 +5430,7 @@ "lodash": "4.17.4", "lodash-es": "4.17.4", "loose-envify": "1.3.1", - "symbol-observable": "1.1.0" + "symbol-observable": "1.2.0" } }, "regenerator-runtime": { @@ -5440,11 +5449,11 @@ } }, "registry-auth-token": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.1.tgz", - "integrity": "sha1-+w0yie4Nmtosu1KvXf5mywcNMAY=", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", "requires": { - "rc": "1.2.2", + "rc": "1.2.4", "safe-buffer": "5.1.1" } }, @@ -5453,7 +5462,7 @@ "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", "requires": { - "rc": "1.2.2" + "rc": "1.2.4" } }, "remarkable": { @@ -5534,7 +5543,7 @@ "stringstream": "0.0.5", "tough-cookie": "2.3.3", "tunnel-agent": "0.4.3", - "uuid": "3.1.0" + "uuid": "3.2.1" }, "dependencies": { "form-data": { @@ -5561,9 +5570,9 @@ "dev": true }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", "dev": true } } @@ -5695,16 +5704,16 @@ } }, "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "semver-diff": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", "requires": { - "semver": "5.4.1" + "semver": "5.5.0" } }, "semver-regex": { @@ -6003,9 +6012,9 @@ } }, "symbol-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.1.0.tgz", - "integrity": "sha512-dQoid9tqQ+uotGhuTKEY11X4xhyYePVnqGSoSm3OGKh2E8LZ6RPULp1uXTctk33IeERlrRJYoVSBglsL05F5Uw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, "symbol-tree": { "version": "3.2.2", @@ -6119,7 +6128,7 @@ "integrity": "sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg==", "requires": { "bl": "1.2.1", - "end-of-stream": "1.4.0", + "end-of-stream": "1.4.1", "readable-stream": "2.3.3", "xtend": "4.0.1" } diff --git a/package.json b/package.json index 79552314d73..ec049036b40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless", - "version": "1.25.0", + "version": "1.26.0", "engines": { "node": ">=4.0" }, @@ -100,6 +100,7 @@ "chalk": "^2.0.0", "ci-info": "^1.1.1", "download": "^5.0.2", + "fast-levenshtein": "^2.0.6", "filesize": "^3.3.0", "fs-extra": "^0.26.7", "get-stdin": "^5.0.1", diff --git a/tests/templates/integration-test-template b/tests/templates/integration-test-template index 468532a571b..4bbd065d859 100755 --- a/tests/templates/integration-test-template +++ b/tests/templates/integration-test-template @@ -37,5 +37,10 @@ serverless deploy -v echo "Invoking Service" serverless invoke --function hello +if [ $template == "aws-go" ] || [ $template == "aws-go-dep" ] +then + serverless invoke --function world +fi + echo "Removing Service" serverless remove -v diff --git a/tests/templates/test_all_templates b/tests/templates/test_all_templates index 74625209e2f..fd88105abf8 100755 --- a/tests/templates/test_all_templates +++ b/tests/templates/test_all_templates @@ -10,6 +10,8 @@ function integration-test { integration-test aws-csharp 'apt-get -qq update && apt-get -qq -y install zip && dotnet restore && dotnet lambda package --configuration release --framework netcoreapp1.0 --output-package bin/release/netcoreapp1.0/deploy-package.zip' integration-test aws-fsharp 'apt-get -qq update && apt-get -qq -y install zip && dotnet restore && dotnet lambda package --configuration release --framework netcoreapp1.0 --output-package bin/release/netcoreapp1.0/deploy-package.zip' +integration-test aws-go 'cd /go/src/app && make build' +integration-test aws-go-dep 'cd /go/src/app && make build' integration-test aws-groovy-gradle ./gradlew build integration-test aws-java-gradle ./gradlew build integration-test aws-java-maven mvn package @@ -26,4 +28,5 @@ integration-test google-nodejs integration-test spotinst-nodejs integration-test spotinst-python integration-test spotinst-ruby +integration-test spotinst-java8 mvn package integration-test webtasks-nodejs