diff --git a/docs/providers/kubeless/events/http.md b/docs/providers/kubeless/events/http.md
index 8196f46f2b4..3bb72182ed2 100644
--- a/docs/providers/kubeless/events/http.md
+++ b/docs/providers/kubeless/events/http.md
@@ -80,10 +80,10 @@ functions:
```
-If the events HTTP definitions contain a `path` attribute, when deploying this Serverless YAML definition, Kubeless will create the needed [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) rules to redirect each of the requests to the right service:
+If the events HTTP definitions contain a `path` attribute, when deploying this Serverless YAML definition, Kubeless will create the needed [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) rules to redirect each of the requests to the right service. You will need to create an [Ingress Controller](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-controllers) to make use of your Ingress rule(s):
```
kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
ingress-1506350705094 192.168.99.100.nip.io 80 28s
-```
\ No newline at end of file
+```
diff --git a/docs/providers/kubeless/events/pubsub.md b/docs/providers/kubeless/events/pubsub.md
index d3ef2065b74..cd2f707aeef 100644
--- a/docs/providers/kubeless/events/pubsub.md
+++ b/docs/providers/kubeless/events/pubsub.md
@@ -49,4 +49,4 @@ serverless logs -f hello
hello world!
```
-You can install the Kubeless CLI tool following the [../guide/installation](installation guide).
+You can install the Kubeless CLI tool following the [installation guide](../guide/installation.md).
diff --git a/docs/providers/kubeless/guide/debugging.md b/docs/providers/kubeless/guide/debugging.md
index 1822efdeeac..4dc5b4eb312 100644
--- a/docs/providers/kubeless/guide/debugging.md
+++ b/docs/providers/kubeless/guide/debugging.md
@@ -20,8 +20,8 @@ Let's imagine that we have deployed the following Python code as a Kubeless func
import urllib2
import json
-def find(request):
- term = request.json["term"]
+def find(event, context):
+ term = event['data']['term']
url = "https://feeds.capitalbikeshare.com/stations/stations.json"
response = urllib2.urlopen(url)
stations = json.loads(response.read())
@@ -126,7 +126,7 @@ Traceback (most recent call last):
File "/kubeless.py", line 35, in handler
return func(bottle.request)
File "/kubeless/handler.py", line 5, in find
- term = request.json["term"]
+ term = event['data']['term']
KeyError: 'term'
172.17.0.1 - - [25/Aug/2017:08:46:16 +0000] "POST / HTTP/1.1" 500 746 "" "" 0/6703
172.17.0.1 - - [25/Aug/2017:08:46:34 +0000] "GET /healthz HTTP/1.1" 200 2 "" "Go-http-client/1.1" 0/122
@@ -145,7 +145,7 @@ Traceback (most recent call last):
File "/kubeless.py", line 35, in handler
return func(bottle.request)
File "/kubeless/handler.py", line 5, in find
- term = request.json["term"]
+ term = event['data']['term']
KeyError: 'term'
```
diff --git a/docs/providers/kubeless/guide/functions.md b/docs/providers/kubeless/guide/functions.md
index 8881110c865..7dbf3aea87f 100644
--- a/docs/providers/kubeless/guide/functions.md
+++ b/docs/providers/kubeless/guide/functions.md
@@ -85,6 +85,25 @@ functions:
handler: handler.hello_two
```
+You can specify an array of functions, which is useful if you separate your functions in to different files:
+
+```yml
+# serverless.yml
+...
+
+functions:
+ - ${file(./foo-functions.yml)}
+ - ${file(./bar-functions.yml)}
+```
+
+```yml
+# foo-functions.yml
+getFoo:
+ handler: handler.foo
+deleteFoo:
+ handler: handler.foo
+```
+
## Runtimes
The Kubeless provider plugin supports the following runtimes.
diff --git a/docs/providers/kubeless/guide/quick-start.md b/docs/providers/kubeless/guide/quick-start.md
index 141dc194f7a..bcbb80944bb 100644
--- a/docs/providers/kubeless/guide/quick-start.md
+++ b/docs/providers/kubeless/guide/quick-start.md
@@ -17,11 +17,11 @@ layout: Doc
## Create a new service
-Create a new service using the Python template, specifying a unique name and an optional path for your service.
+Create a new service using the NodeJS template, specifying a unique name and an optional path for your service.
```bash
# Create a new Serverless Service/Project
-$ serverless create --template kubeless-python --path new-project
+$ serverless create --template kubeless-nodejs --path new-project
# Change into the newly created directory
$ cd new-project
# Install npm dependencies
diff --git a/docs/providers/openwhisk/README.md b/docs/providers/openwhisk/README.md
index 73330b2b5f7..f6da72ed7ec 100644
--- a/docs/providers/openwhisk/README.md
+++ b/docs/providers/openwhisk/README.md
@@ -12,7 +12,7 @@ layout: Doc
Welcome to the Serverless Apache OpenWhisk documentation!
-If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](https://forum.serverless.com/)
+If you have any questions, [search the forums](https://forum.serverless.com?utm_source=framework-docs) or [start your own thread](https://forum.serverless.com?utm_source=framework-docs)
**Note:** [Apache OpenWhisk system credentials](./guide/credentials.md) are required for using serverless + openwhisk.
diff --git a/docs/providers/openwhisk/cli-reference/print.md b/docs/providers/openwhisk/cli-reference/print.md
index e10ced46519..b48882404f9 100644
--- a/docs/providers/openwhisk/cli-reference/print.md
+++ b/docs/providers/openwhisk/cli-reference/print.md
@@ -26,7 +26,9 @@ serverless print
## Options
-- None
+- `format` Print configuration in given format ("yaml", "json", "text"). Default: yaml
+- `path` Period-separated path to print a sub-value (eg: "provider.name")
+- `transform` Transform-function to apply to the value (currently only "keys" is supported)
## Examples:
@@ -68,3 +70,15 @@ functions:
events:
- schedule: cron(0 * * * *) # <-- Resolved
```
+
+This prints the provider name:
+
+```bash
+sls print --path provider --format text
+```
+
+And this prints all function names:
+
+```bash
+sls print --path functions --transform keys --format text
+```
diff --git a/docs/providers/openwhisk/events/apigateway.md b/docs/providers/openwhisk/events/apigateway.md
index 91192614724..e448ceb0141 100644
--- a/docs/providers/openwhisk/events/apigateway.md
+++ b/docs/providers/openwhisk/events/apigateway.md
@@ -124,3 +124,31 @@ HTTP event configuration supports the following parameters.
**Note:** All HTTP endpoints defined in this manner have cross-site requests
enabled for all source domains.
+
+### URL Path Parameters
+
+The API Gateway service [supports path parameters](https://github.com/apache/incubator-openwhisk/blob/master/docs/apigateway.md#exposing-multiple-web-actions) in user-defined HTTP paths. This allows functions to handle URL paths which include templated values, like resource identifiers.
+
+Path parameters are identified using the `{param_name}` format in the URL path. The API Gateway sends the full matched path value in the `__ow_path` field of the event parameters.
+
+```yaml
+functions:
+ retrieve_users:
+ handler: users.get
+ events:
+ - http:
+ method: GET
+ path: /users/{id}
+ resp: http
+```
+
+This feature comes with the following restrictions:
+
+- *Path parameters are only supported when `resp` is configured as`http`.*
+- *Individual path parameter values are not included as separate event parameters. Users have to manually parse values from the full `__ow_path` value.*
+
+### Security
+
+Functions exposed through the API Gateway service are automatically converted
+into Web Actions during deployment. The framework [secures Web Actions for HTTP endpoints](https://github.com/apache/incubator-openwhisk/blob/master/docs/webactions.md#securing-web-actions) using the `require-whisk-auth` annotation. If the `require-whisk-auth`
+annotation is manually configured, the existing annotation value is used, otherwise a random token is automatically generated.
diff --git a/docs/providers/openwhisk/examples/hello-world/node/README.md b/docs/providers/openwhisk/examples/hello-world/node/README.md
index f9a6d399900..c2d1ab8cee2 100644
--- a/docs/providers/openwhisk/examples/hello-world/node/README.md
+++ b/docs/providers/openwhisk/examples/hello-world/node/README.md
@@ -17,7 +17,7 @@ Make sure `serverless` is installed. [See installation guide](../../../guide/ins
`serverless create --template openwhisk-nodejs --path myService` or `sls create --template openwhisk-nodejs --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder.
## 2. Install Provider Plugin
-`npm install -g serverless-openwhisk` followed by `npm install` in the service directory.
+`npm install ` in the service directory.
## 3. Deploy
`serverless deploy` or `sls deploy`. `sls` is shorthand for the Serverless CLI command
@@ -35,4 +35,4 @@ In your terminal window you should see the response from Apache OpenWhisk
}
```
-Congrats you have just deployed and run your Hello World function!
+Congrats you have deployed and ran your Hello World function!
diff --git a/docs/providers/openwhisk/examples/hello-world/node/package.json b/docs/providers/openwhisk/examples/hello-world/node/package.json
index d41fc10e851..c2c6bf950d4 100644
--- a/docs/providers/openwhisk/examples/hello-world/node/package.json
+++ b/docs/providers/openwhisk/examples/hello-world/node/package.json
@@ -2,8 +2,7 @@
"name": "serverless-openwhisk-hello-world",
"version": "0.1.0",
"description": "Hello World example for OpenWhisk provider with Serverless Framework.",
- "scripts": {
- "postinstall": "npm link serverless-openwhisk",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "devDependencies": {
+ "serverless-openwhisk": ">=0.13.0"
}
}
diff --git a/docs/providers/openwhisk/examples/hello-world/php/README.md b/docs/providers/openwhisk/examples/hello-world/php/README.md
index c254887bf06..944b0e6e3dc 100644
--- a/docs/providers/openwhisk/examples/hello-world/php/README.md
+++ b/docs/providers/openwhisk/examples/hello-world/php/README.md
@@ -17,7 +17,7 @@ Make sure `serverless` is installed. [See installation guide](../../../guide/ins
`serverless create --template openwhisk-php --path myService` or `sls create --template openwhisk-php --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder.
## 2. Install Provider Plugin
-`npm install -g serverless-openwhisk` followed by `npm install` in the service directory.
+Run `npm install` in the service directory.
## 3. Deploy
`serverless deploy` or `sls deploy`. `sls` is shorthand for the Serverless CLI command
@@ -35,4 +35,4 @@ In your terminal window you should see the response from Apache OpenWhisk
}
```
-Congrats you have just deployed and run your Hello World function!
+Congrats you have deployed and ran your Hello World function!
diff --git a/docs/providers/openwhisk/examples/hello-world/php/package.json b/docs/providers/openwhisk/examples/hello-world/php/package.json
index d41fc10e851..c2c6bf950d4 100644
--- a/docs/providers/openwhisk/examples/hello-world/php/package.json
+++ b/docs/providers/openwhisk/examples/hello-world/php/package.json
@@ -2,8 +2,7 @@
"name": "serverless-openwhisk-hello-world",
"version": "0.1.0",
"description": "Hello World example for OpenWhisk provider with Serverless Framework.",
- "scripts": {
- "postinstall": "npm link serverless-openwhisk",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "devDependencies": {
+ "serverless-openwhisk": ">=0.13.0"
}
}
diff --git a/docs/providers/openwhisk/examples/hello-world/python/README.md b/docs/providers/openwhisk/examples/hello-world/python/README.md
index 1056a80600d..dd09ceed738 100644
--- a/docs/providers/openwhisk/examples/hello-world/python/README.md
+++ b/docs/providers/openwhisk/examples/hello-world/python/README.md
@@ -17,7 +17,7 @@ Make sure `serverless` is installed. [See installation guide](../../../guide/ins
`serverless create --template openwhisk-python --path myService` or `sls create --template openwhisk-python --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder.
## 2. Install Provider Plugin
-`npm install -g serverless-openwhisk` followed by `npm install` in the service directory.
+`npm install` in the service directory.
## 3. Deploy
`serverless deploy` or `sls deploy`. `sls` is shorthand for the Serverless CLI command
@@ -35,4 +35,4 @@ In your terminal window you should see the response from Apache OpenWhisk
}
```
-Congrats you have just deployed and run your Hello World function!
+Congrats you have deployed and ran your Hello World function!
diff --git a/docs/providers/openwhisk/examples/hello-world/python/package.json b/docs/providers/openwhisk/examples/hello-world/python/package.json
index d41fc10e851..c2c6bf950d4 100644
--- a/docs/providers/openwhisk/examples/hello-world/python/package.json
+++ b/docs/providers/openwhisk/examples/hello-world/python/package.json
@@ -2,8 +2,7 @@
"name": "serverless-openwhisk-hello-world",
"version": "0.1.0",
"description": "Hello World example for OpenWhisk provider with Serverless Framework.",
- "scripts": {
- "postinstall": "npm link serverless-openwhisk",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "devDependencies": {
+ "serverless-openwhisk": ">=0.13.0"
}
}
diff --git a/docs/providers/openwhisk/examples/hello-world/swift/README.md b/docs/providers/openwhisk/examples/hello-world/swift/README.md
index 93dec3d3087..55904219f2b 100644
--- a/docs/providers/openwhisk/examples/hello-world/swift/README.md
+++ b/docs/providers/openwhisk/examples/hello-world/swift/README.md
@@ -17,7 +17,7 @@ Make sure `serverless` is installed. [See installation guide](../../../guide/ins
`serverless create --template openwhisk-swift --path myService` or `sls create --template openwhisk-swift --path myService`, where 'myService' is a new folder to be created with template service files. Change directories into this new folder.
## 2. Install Provider Plugin
-`npm install -g serverless-openwhisk` followed by `npm install` in the service directory.
+`npm install` in the service directory.
## 3. Deploy
`serverless deploy` or `sls deploy`. `sls` is shorthand for the Serverless CLI command
@@ -35,4 +35,4 @@ In your terminal window you should see the response from Apache OpenWhisk
}
```
-Congrats you have just deployed and run your Hello World function!
+Congrats you have deployed and ran your Hello World function!
diff --git a/docs/providers/openwhisk/examples/hello-world/swift/package.json b/docs/providers/openwhisk/examples/hello-world/swift/package.json
index d41fc10e851..c2c6bf950d4 100644
--- a/docs/providers/openwhisk/examples/hello-world/swift/package.json
+++ b/docs/providers/openwhisk/examples/hello-world/swift/package.json
@@ -2,8 +2,7 @@
"name": "serverless-openwhisk-hello-world",
"version": "0.1.0",
"description": "Hello World example for OpenWhisk provider with Serverless Framework.",
- "scripts": {
- "postinstall": "npm link serverless-openwhisk",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "devDependencies": {
+ "serverless-openwhisk": ">=0.13.0"
}
}
diff --git a/docs/providers/openwhisk/guide/credentials.md b/docs/providers/openwhisk/guide/credentials.md
index 1d8f50a30a2..5ae174e3e9c 100644
--- a/docs/providers/openwhisk/guide/credentials.md
+++ b/docs/providers/openwhisk/guide/credentials.md
@@ -18,45 +18,69 @@ OpenWhisk is an open-source serverless platform. This means you can either choos
Here we'll provide setup instructions for both options, just pick the one that you're using.
-## Register with OpenWhisk platform (IBM Bluemix)
+## Register with IBM Cloud Functions
-IBM's Bluemix cloud platform provides a hosted serverless solution based upon Apache OpenWhisk.
+[IBM's Cloud platform](https://console.bluemix.net/) provides a hosted serverless solution ([IBM Cloud Functions](https://console.bluemix.net/openwhisk/)) based upon Apache OpenWhisk.
Here's how to get started…
-- Sign up for a free account @ [https://bluemix.net](https://console.ng.bluemix.net/registration/)
+- Sign up for a free account @ [IBM Cloud](https://console.bluemix.net/)
-IBM Bluemix comes with a [free trial](https://www.ibm.com/cloud-computing/bluemix/pricing?cm_mc_uid=22424350960514851832143&cm_mc_sid_50200000=1485183214) that doesn't need credit card details for the first 30 days. Following the trial, developers have to enrol using a credit card but get a free tier for the platform and services.
+IBM Cloud comes with a [lite account](https://console.bluemix.net/registration/) that does not need credit card details to register. Lite accounts provide free access to certain platform services and do not expire after a limited time period.
-**All IBM Bluemix users get access to the [Free Tier for OpenWhisk](https://console.ng.bluemix.net/openwhisk/learn/pricing). This includes 400,000 GB-seconds of serverless function compute time per month.**
+**All IBM Cloud users get access to the [Free Tier for IBM Cloud Functions](https://console.ng.bluemix.net/openwhisk/learn/pricing). This includes 400,000 GB-seconds of serverless function compute time per month.**
Additional execution time is charged at $0.000017 per GB-second of execution, rounded to the nearest 100ms.
-### Access Account Credentials
+### Install the IBM Cloud CLI
-Once you have signed up for IBM Bluemix, we need to retrieve your account credentials. These are available on [the page](https://console.ng.bluemix.net/openwhisk/learn/cli) about installing the command-line tool from the [service homepage](https://console.ng.bluemix.net/openwhisk/).
+Following the [instructions on this page](https://console.bluemix.net/docs/cli/index.html#overview) to download and install the IBM Cloud CLI.
-The second point in the instructions contains a command-line which includes the platform endpoint and authentication keys.
+*On Linux, you can run this command:*
```
-wsk property set --apihost openwhisk.ng.bluemix.net --auth XXX:YYY
+curl -fsSL https://clis.ng.bluemix.net/install/linux | sh
```
-**Make a note of the `apihost` and `auth` command flag values.**
+*On OS X, you can run this command:*
-### (optional) Install command-line utility
+```
+curl -fsSL https://clis.ng.bluemix.net/install/osx | sh
+```
+
+### Install the IBM Cloud Functions Plugin
-The command-line utility is linked from [the previous page](https://console.ng.bluemix.net/openwhisk/learn/cli). Download and install the binary into a location in your [shell path](http://unix.stackexchange.com/questions/26047/how-to-correctly-add-a-path-to-path).
+```
+ibmcloud plugin install Cloud-Functions -r Bluemix
+```
-### (optional) Authenticate with API gateway
+### Authenticate with the CLI
-OpenWhisk on IBM Bluemix uses a third-party API gateway service. An access token is needed to add HTTP endpoints to your functions. This can be retrieved automatically using the `wsk` command-line.
+Log into the CLI to create local authentication credentials. The framework plugin automatically uses these credentials when interacting with IBM Cloud Functions.
```
-wsk bluemix login
+ibmcloud login -a
-o -s
```
-After running the login command, you will be prompted to enter your authentication credentials. The access token will be stored in the `.wskprops` file under your home directory, using the key (`APIGW_ACCESS_TOKEN`).
+**Replace `<..>` values with your [platform region endpoint, account organisation and space](https://console.bluemix.net/docs/account/orgs_spaces.html#orgsspacesusers).**
+
+For example....
+
+```
+ibmcloud login -a api.ng.bluemix.net -o user@email_host.com -s dev
+```
+
+#### regions
+
+Cloud Functions is available with the following regions US-South (`api.ng.bluemix.net`), London (`api.eu-gb.bluemix.net`), Frankfurt (` api.eu-de.bluemix.net`). Use the appropriate [API endpoint](https://console.bluemix.net/docs/overview/ibm-cloud.html#ov_intro_reg) to target Cloud Functions in that region.
+
+#### organisations and spaces
+
+Organisations and spaces for your account can be viewed on this page: [https://console.bluemix.net/account/organizations](https://console.bluemix.net/account/organizations)
+
+Accounts normally have a default organisation using the account email address. Default space name is usually `dev`.
+
+*After running the login command, authentication credentials will be stored in the `.wskprops` file under your home directory.*
## Register with OpenWhisk platform (Self-Hosted)
@@ -103,15 +127,17 @@ Executables for other operating system, and CPU architectures are located in the
Download and install the correct binary into a location in your [shell path](http://unix.stackexchange.com/questions/26047/how-to-correctly-add-a-path-to-path).
+## Using Account Credentials
+You can configure the Serverless Framework to use your OpenWhisk credentials in a few ways:
-## Using Account Credentials
+#### IBM Cloud Functions
-You can configure the Serverless Framework to use your OpenWhisk credentials in two ways:
+Provided you have logged into the IBM Cloud CLI, authenticated credentials will be already stored in the `~/.wskprops` file. If this file is available, the provider plugin will automatically read those credentials and you don't need to do anything else!
-#### Quick Setup
+#### Environment Variables Setup
-As a quick setup to get started you can export them as environment variables so they would be accessible to Serverless Framework:
+Access credentials can be provided as environment variables.
```bash
# mandatory parameters
@@ -125,16 +151,14 @@ serverless deploy
#### Using Configuration File
-For a more permanent solution you can also set up credentials through a configuration file. Here are different methods you can use to do so.
+Credentials can be stored in a local configuration file, using either the CLI or manually creating the file.
##### Setup with the `wsk` cli
-If you have followed the instructions above to install the `wsk` command-line utility, run the following command to create the configuration file.
+If you are using a self-hosted platform and have followed the instructions above to install the `wsk` command-line utility, run the following command to create the configuration file.
```bash
$ wsk property set --apihost PLATFORM_API_HOST --auth USER_AUTH_KEY
-// followed by this command if you want to use the api gateway on bluemix
-$ wsk bluemix login
```
Credentials are stored in `~/.wskprops`, which you can edit directly if needed.
diff --git a/docs/providers/openwhisk/guide/functions.md b/docs/providers/openwhisk/guide/functions.md
index bae71fa5568..2f0aacbda2f 100644
--- a/docs/providers/openwhisk/guide/functions.md
+++ b/docs/providers/openwhisk/guide/functions.md
@@ -35,6 +35,8 @@ functions:
runtime: nodejs # optional overwrite, default is provider runtime
memory: 512 # optional overwrite, default is 256
timeout: 10 # optional overwrite, default is 60
+ parameters:
+ foo: bar // default parameters
```
The `handler` property points to the file and module containing the code you want to run in your function.
@@ -94,14 +96,122 @@ functions:
functionOne:
handler: handler.functionOne
memory: 512 # function specific
+ parameters:
+ foo: bar // default parameters
```
+You can specify an array of functions, which is useful if you separate your functions in to different files:
+
+```yml
+# serverless.yml
+...
+
+functions:
+ - ${file(./foo-functions.yml)}
+ - ${file(./bar-functions.yml)}
+```
+
+```yml
+# foo-functions.yml
+getFoo:
+ handler: handler.foo
+deleteFoo:
+ handler: handler.foo
+```
+
+## Packages
+
+OpenWhisk provides a concept called "packages" to manage related actions. Packages can contain multiple actions under a common identifier in a namespace. Configuration values needed by all actions in a package can be set as default properties on the package, rather than individually on each action.
+
+*Packages are identified using the following format:* `/namespaceName/packageName/actionName`.
+
+### Implicit Packages
+
+Functions can be assigned to packages by setting the function `name` with a package reference.
+
+```yaml
+functions:
+ foo:
+ handler: handler.foo
+ name: "myPackage/foo"
+ bar:
+ handler: handler.bar
+ name: "myPackage/bar"
+```
+
+In this example, two new actions (`foo` & `bar`) will be created using the `myPackage` package.
+
+Packages which do not exist will be automatically created during deployments. When using the `remove` command, any packages referenced in the `serverless.yml` will be deleted.
+
+### Explicit Packages
+
+Packages can also be defined explicitly to set shared configuration parameters. Default package parameters are merged into event parameters for each invocation.
+
+```yaml
+functions:
+ foo:
+ handler: handler.foo
+ name: "myPackage/foo"
+
+resources:
+ packages:
+ myPackage:
+ parameters:
+ hello: world
+```
+
+*Explicit packages support the following properties: `parameters`, `annotations` and `shared`.*
+
+## Binding Services (IBM Cloud Functions)
+
+***This feature requires the [IBM Cloud CLI](https://console.bluemix.net/docs/cli/reference/bluemix_cli/download_cli.html#download_install) and [IBM Cloud Functions plugin](https://console.bluemix.net/openwhisk/learn/cli) to be installed.***
+
+IBM Cloud Functions supports [automatic binding of service credentials](https://console.bluemix.net/docs/openwhisk/binding_services.html#binding_services) to actions using the CLI.
+
+Bound service credentials will be passed as the `__bx_creds` parameter in the invocation parameters.
+
+This feature is also available through the `serverless.yaml` file using the `bind` property for each function.
+
+```yaml
+functions:
+ my_function:
+ handler: file_name.handler
+ bind:
+ - service:
+ name: cloud-object-storage
+ instance: my-cos-storage
+```
+
+The `service` configuration supports the following properties.
+
+- `name`: identifier for the cloud service
+- `instance`: instance name for service (*optional*)
+- `key`: key name for instance and service (*optional*)
+
+*If the `instance` or `key` properties are missing, the first available instance and key found will be used.*
+
+Binding services removes the need to manually create default parameters for service keys from platform services.
+
+More details on binding service credentials to actions can be found in the [official documentation](https://console.bluemix.net/docs/openwhisk/binding_services.html#binding_services) and [this blog post](http://jamesthom.as/blog/2018/06/05/binding-iam-services-to-ibm-cloud-functions/).
+
+Packages defined in the `resources` section can bind services using the same configuration properties.
+
+```yaml
+resources:
+ packages:
+ myPackage:
+ bind:
+ - service:
+ name: cloud-object-storage
+ instance: my-cos-storage
+```
## Runtimes
The OpenWhisk provider plugin supports the following runtimes.
- Node.js
- Python
+- Java
- Php
- Swift
- Binary
diff --git a/docs/providers/openwhisk/guide/installation.md b/docs/providers/openwhisk/guide/installation.md
index 627f7f6d48e..c7f1cb0b532 100644
--- a/docs/providers/openwhisk/guide/installation.md
+++ b/docs/providers/openwhisk/guide/installation.md
@@ -44,19 +44,17 @@ To see which version of serverless you have installed run:
serverless --version
```
-
-
### Installing OpenWhisk Provider Plugin
-Now we need to install the provider plugin to allow the framework to deploy services to the platform. This plugin is also [published](http://npmjs.com/package/serverless-openwhisk) on [npm](https://npmjs.org) and can installed using the same `npm install` command.
+Now we need to install the provider plugin to allow the framework to deploy services to the platform. This plugin is also [published](http://npmjs.com/package/serverless-openwhisk) on [npm](https://npmjs.org) and can installed as a project dependency using the `npm install --save-dev` command.
```
-npm install -g serverless-openwhisk
+npm install serverless-openwhisk --save-dev
```
-*Due to an [outstanding issue](https://github.com/serverless/serverless/issues/2895) with provider plugins, the [OpenWhisk provider](https://github.com/serverless/serverless-openwhisk) must be installed as a global module.*
-
+Project templates already have this dependency listed in the `package.json` file allowing you to just `npm install` in the service directory.
+The `serverless-openwhisk` plugin must be saved as a `devDependency` in the project's `package.json` to ensure it is not bundled in the deployment package.
### Setting up OpenWhisk
diff --git a/docs/providers/openwhisk/guide/plugins.md b/docs/providers/openwhisk/guide/plugins.md
index f233ccbe977..c0d27405fc0 100644
--- a/docs/providers/openwhisk/guide/plugins.md
+++ b/docs/providers/openwhisk/guide/plugins.md
@@ -33,9 +33,25 @@ We need to tell Serverless that we want to use the plugin inside our service. We
plugins:
- custom-serverless-plugin
```
+The `plugins` section supports two formats:
-Plugins might want to add extra information which should be accessible to Serverless. The `custom` section in the `serverless.yml` file is the place where you can add necessary
-configurations for your plugins (the plugins author / documentation will tell you if you need to add anything there):
+Array object:
+```yml
+plugins:
+ - plugin1
+ - plugin2
+```
+
+Enhanced plugins object:
+```yml
+plugins:
+ localPath: './custom_serverless_plugins'
+ modules:
+ - plugin1
+ - plugin2
+```
+
+Plugins might want to add extra information which should be accessible to Serverless. The `custom` section in the `serverless.yml` file is the place where you can add necessary configurations for your plugins (the plugins author / documentation will tell you if you need to add anything there):
```yml
plugins:
@@ -47,9 +63,24 @@ custom:
## Service local plugin
-If you are working on a plugin or have a plugin that is just designed for one project you can add them to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`.
+If you are working on a plugin or have a plugin that is just designed for one project they can be loaded from the local folder. Local plugins can be added in the `plugins` array in `serverless.yml`.
+
+By default local plugins can be added to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`.
+```yml
+plugins:
+ - custom-serverless-plugin
+```
+
+Local plugins folder can be changed by enhancing `plugins` object:
+```yml
+plugins:
+ localPath: './custom_serverless_plugins'
+ modules:
+ - custom-serverless-plugin
+```
+The `custom-serverless-plugin` will be loaded from the `custom_serverless_plugins` directory at the root of your service. If the `localPath` is not provided or empty `.serverless_plugins` directory will be taken as the `localPath`.
-The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `.serverless_plugins` folder.
+The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `localPath` folder (`.serverless_plugins` by default).
### Load Order
diff --git a/docs/providers/openwhisk/guide/web-actions.md b/docs/providers/openwhisk/guide/web-actions.md
index 6df5c30dabf..79b640b8383 100644
--- a/docs/providers/openwhisk/guide/web-actions.md
+++ b/docs/providers/openwhisk/guide/web-actions.md
@@ -25,8 +25,7 @@ functions:
Functions with this annotation can be invoked through a URL template with the following parameters.
```
-https://{APIHOST}/api/v1/experimental/web/{USER_NAMESPACE}/{PACKAGE}/{ACTION_NAME}.{TYPE}
-
+https://{APIHOST}/api/v1/web/{USER_NAMESPACE}/{PACKAGE}/{ACTION_NAME}.{TYPE}
```
- *APIHOST* - platform endpoint e.g. *openwhisk.ng.bluemix.net.*
@@ -67,10 +66,10 @@ function main() {
Functions can access request parameters using the following environment variables.
-1. `**__ow_meta_verb:**` the HTTP method of the request.
-2. `**__ow_meta_headers:**` the request headers.
-3. `**__ow_meta_path:**` the unmatched path of the request.
-
-Full details on this new feature are available in this [blog post](https://medium.com/openwhisk/serverless-http-handlers-with-openwhisk-90a986cc7cdd#.2x09176m8).
+1. `__ow_method` - HTTP method of the request.
+2. `__ow_headers` - HTTP request headers.
+3. `__ow_path` - Unmatched URL path of the request.
+4. `__ow_body` - Body entity from request.
+5. `__ow_query` - Query parameters from the request.
-**\*IMPORTANT: [Web Actions](http://bit.ly/2xSRbOQ) is currently experimental and may be subject to breaking changes.***
+**Full details on this feature are available in this [here](https://github.com/apache/incubator-openwhisk/blob/master/docs/webactions.md).**
diff --git a/docs/providers/spotinst/README.md b/docs/providers/spotinst/README.md
index 5fbe8fba63a..945f713c527 100755
--- a/docs/providers/spotinst/README.md
+++ b/docs/providers/spotinst/README.md
@@ -12,7 +12,7 @@ layout: Doc
Welcome to the Serverless Spotinst documentation!
-If you have questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](https://forum.serverless.com/)
+If you have any questions, [search the forums](https://forum.serverless.com?utm_source=framework-docs) or [start your own thread](https://forum.serverless.com?utm_source=framework-docs)
diff --git a/docs/providers/spotinst/cli-reference/stage.md b/docs/providers/spotinst/cli-reference/stage.md
index e26da0f5532..479708f51e1 100644
--- a/docs/providers/spotinst/cli-reference/stage.md
+++ b/docs/providers/spotinst/cli-reference/stage.md
@@ -12,14 +12,30 @@ layout: Doc
# 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.
+Serverless allows you to specify different stages to deploy your project to. Changing the stage will change the environment your function is running on, which is helpful when you wish to keep production code partitioned from your development environment.
+
+Your function's stage is set to 'dev' by default. You can update the stage when deploying the function, either from the command line using the serverless framework, or by modifying the serverless.yml in your project. When utilizing this feature, remember to include a config file that holds the environment IDs associated with your stages. An example config.json would look something like this:
+```json
+{
+ "dev": "env-abcd1234",
+ "prod": "env-defg5678"
+}
+```
-## Through Serverless Framwork
+## Through Serverless Framework
To change the stage through the serverless framework you simply need to enter the command
```bash
serverless deploy --stage #{Your Stage Name}
```
+You will also need to update the environment parameter to point to the config.json:
+```yaml
+ spotinst:
+ environment: ${file(./config.json):${opt:stage, self:provider.stage, 'dev'}}
+```
+Note that while I am using 'dev' as the default stage, you may change this parameter to a custom default stage.
+
+
## Through the .yml File
@@ -32,4 +48,5 @@ provider:
spotinst:
environment: #{Your Environment ID}
```
+Be sure to also modify your environment ID when you change the stage if you are not working with a config file.
diff --git a/docs/providers/spotinst/examples/java8/README.md b/docs/providers/spotinst/examples/java8/README.md
index 34e9f2b30d1..8de2ab42bef 100644
--- a/docs/providers/spotinst/examples/java8/README.md
+++ b/docs/providers/spotinst/examples/java8/README.md
@@ -32,7 +32,7 @@ In your terminal window you should see the response
{"hello":"null"}
```
-Congrats you have just deployed and ran your Hello World function!
+Congrats you have deployed and ran your Hello World function!
## Short Hand Guide
diff --git a/docs/providers/spotinst/examples/node/README.md b/docs/providers/spotinst/examples/node/README.md
index a8345f43017..cd8f956d786 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 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.
+`serverless create --template spotinst-nodejs --path serviceName` `serviceName` is going to be a new directory where 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
@@ -32,7 +32,7 @@ In your terminal window you should see the response
{"hello":"from NodeJS8.3 function"}
```
-Congrats you have just deployed and ran your Hello World function!
+Congrats you have deployed and ran your Hello World function!
## Short Hand Guide
@@ -42,4 +42,4 @@ Congrats you have just deployed and ran your Hello World function!
`-t` is short hand for `--template`
-`-p` is short hang for `--path`
\ No newline at end of file
+`-p` is short hang for `--path`
diff --git a/docs/providers/spotinst/examples/python/README.md b/docs/providers/spotinst/examples/python/README.md
index c4f58a6284c..a6ae892364d 100644
--- a/docs/providers/spotinst/examples/python/README.md
+++ b/docs/providers/spotinst/examples/python/README.md
@@ -34,7 +34,7 @@ In your terminal window you should see the response
'{"hello":"from Python2.7 function"}'
```
-Congrats you have just deployed and ran your Hello World function!
+Congrats you have deployed and ran your Hello World function!
## Short Hand Guide
diff --git a/docs/providers/spotinst/examples/ruby/README.md b/docs/providers/spotinst/examples/ruby/README.md
index e0d7d2e2dd3..a7325a394c6 100644
--- a/docs/providers/spotinst/examples/ruby/README.md
+++ b/docs/providers/spotinst/examples/ruby/README.md
@@ -11,21 +11,21 @@ layout: Doc
# Hello World Ruby Example
-Make sure `serverless` is installed.
+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 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
-```bash
+```bash
serverless deploy
```
## 3. Invoke deployed function
```bash
serverless invoke --function hello
-```
+```
In your terminal window you should see the response
@@ -33,14 +33,14 @@ In your terminal window you should see the response
'{"hello":"from Ruby2.4.1 function"}'
```
-Congrats you have just deployed and ran your Hello World function!
+Congrats you have deployed and ran your Hello World function!
## Short Hand Guide
-`sls` is short hand for serverless cli commands
+`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
+`-p` is short hang for `--path`
diff --git a/docs/providers/spotinst/guide/IAM-roles.md b/docs/providers/spotinst/guide/IAM-roles.md
new file mode 100644
index 00000000000..ba2ec0516a1
--- /dev/null
+++ b/docs/providers/spotinst/guide/IAM-roles.md
@@ -0,0 +1,44 @@
+
+
+
+### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/iam-roles)
+
+
+# Spotinst Functions - IAM roles
+Functions sometimes rely on outside services from Amazon such as S3, and accessing these resources often requires authorization using IAM. Spotinst Functions can be configured with the relevant permissions with the inclusion of IAM role information in the serverless.yml file. See [Amazon's documentation][amazon-docs-url] for more information on IAM roles.
+
+## Requirements
+- You will need to create an IAM role on your AWS account and attach policies with the relevant permissions.
+- A spotinst role will be generated and linked with your AWS role
+- Only one Spotinst role per function.
+- Multiple functions can be associated with the same Spotinst role.
+
+## YML
+```yaml
+functions:
+ example:
+ runtime: nodejs8.3
+ handler: handler.main
+ memory: 128
+ timeout: 30
+ access: private
+ iamRoleConfig:
+ roleId: ${role-id}
+```
+
+## Parameters
+- roleId: the role created on the console
+ - ex : sfr-5ea76784
+
+
+
+For more information on how to set up IAM roles, check out our documentation [here][spotinst-help-center]
+
+[amazon-docs-url]: https://aws.amazon.com/iam/?sc_channel=PS&sc_campaign=acquisition_US&sc_publisher=google&sc_medium=iam_b&sc_content=amazon_iam_e&sc_detail=amazon%20iam&sc_category=iam&sc_segment=208382128687&sc_matchtype=e&sc_country=US&s_kwcid=AL!4422!3!208382128687!e!!g!!amazon%20iam&ef_id=WoypCQAABVVgCzd0:20180220230233:s
+[spotinst-help-center]: https://help.spotinst.com/hc/en-us/articles/360000317585?flash_digest=59d5566c556b5d4def591c69a62a56b6c1e16c61
diff --git a/docs/providers/spotinst/guide/active-versions.md b/docs/providers/spotinst/guide/active-versions.md
new file mode 100644
index 00000000000..d0a7168b8b0
--- /dev/null
+++ b/docs/providers/spotinst/guide/active-versions.md
@@ -0,0 +1,82 @@
+
+
+
+### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/active-versions)
+
+
+# Spotinst Functions - Active Versions
+
+Every time you update your function, a new version is being created by default. Version numbers have a unique ID that starts at 0 and incrementes by one each update. Each function version is immutable and cannot be changed.
+
+## Latest Version
+The 'Latest' version refers to the most recent version created by the last update. Unless otherwise specified, all incoming traffic is routed to the latest version.
+
+*Please note: the 'Latest' tag will point to a different version number each and every time you update your function.*
+
+Default configuration for activeVersions when a new function is created:
+```yaml
+activeVersions:
+ - version: $LATEST
+ percentage: 100.0
+```
+
+## Active Version
+The 'Active' version can point to more than one version of your function, including 'Latest'. This allows you to distribute your incoming traffic between multiple versions and dictate what percentage is sent to each version.
+
+For example, say you wanted to test a new version of your function to determine if it was production-ready. You could specify that 10% of the traffic be routed to that new version, and route the remaining 90% to the stable version. You can gradually route more traffic to the new version as you become more confident in its performance.
+
+### Examples
+```yaml
+activeVersions:
+ - version: $LATEST
+ percentage: 100.0
+```
+
+100% of traffic will go to the most recently published update.
+
+```yaml
+activeVersions:
+ - version: $LATEST
+ percentage: 80.0
+ - version: 2
+ percentage: 20.0
+```
+80% of traffic goes to the most recently published update, and 20% goes to version 2.
+
+```yaml
+activeVersions:
+ - version: 5
+ percentage: 50.0
+ - version: 3
+ percentage: 25.0
+ - version: 1
+ percentage: 25.0
+```
+Traffic is split between versions 1. 3, and 5. Changes made to your latest version will not affect production traffic.
+
+### Configure Active Version
+You can configure active versions in the serverless.yml file, but you can also use the Spotinst Console to configure the versions without deploying a new update. In the event you would like to change your 'Active' version configuration without updating your function, you can use the 'Configure Active Version' action from the console and the API
+- Console:
+ 1. Navigate to your function
+ 2. Click 'Actions'
+ 3. Select 'Configure Active Version'
+
+- API: (update function)
+```yaml
+activeVersions:
+ - version: $LATEST
+ percentage: 70.0
+ - version: 2
+ percentage: 30.0
+```
+
+### Requirements
+- The sum of all percentages must be 100%
+- You can set up to two decimal digits in the percentage
+- Changes made to the ratio using the Spotinst Console will be overwritten by the contents of activeVersions in your serverless.yml file.
diff --git a/docs/providers/spotinst/guide/cors.md b/docs/providers/spotinst/guide/cors.md
new file mode 100644
index 00000000000..95e23a9b54d
--- /dev/null
+++ b/docs/providers/spotinst/guide/cors.md
@@ -0,0 +1,42 @@
+
+
+
+### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/cors)
+
+
+# Spotinst Functions - CORS
+Cross-Origin Resource Sharing is a mechanism that allows restricted resources on a web page to be requested from a domain outside of the original. CORS defines a way in which a web service and server can interact to determine whether or not it is safe to allow a cross-origin request. Enabling CORS for your function allows you to specify safe domains, and enables out-of-the-box support for preflight HTTP requests (via the OPTIONS method) that will return the needed ‘access-control-*’ headers specified below. The actual HTTP request will return the ‘access-control-allow-origin’ method.
+You can enable CORS for cross-domain HTTP requests with Spotinst Functions. Add the required fields to you serverless.yml file.
+
+### Example CORS object:
+```yml
+ cors:
+ - enabled: true
+ origin: "http://foo.example"
+ headers: "Content-Type,X-PINGOTHER"
+ methods: "PUT,POST"
+```
+
+### Parameters:
+ - enabled: Boolean
+ - Specify if CORS is enabled for the function.
+ - default: false
+ - origin: String
+ - Specifies a domain/origin that may access the resource. A wildcard '*' may be used to allow any origin to access the resource.
+ - default: '*'
+ - methods: String
+ - Comma-separated list of HTTP methods that are allowed to access the resource. This is used in response to a preflight request.
+ - default: 'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'
+ - headers: String
+ - Comma-separated list of allowed headers.
+ - default: 'Content-Type,Authorization'
+
+
+
+
diff --git a/docs/providers/spotinst/guide/create-token.md b/docs/providers/spotinst/guide/create-token.md
index 9fbaf1d9b87..46e8bb87cee 100644
--- a/docs/providers/spotinst/guide/create-token.md
+++ b/docs/providers/spotinst/guide/create-token.md
@@ -7,7 +7,7 @@ layout: Doc
-->
-### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials)
+### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/create-token)
# Spotinst Functions - Create Token
diff --git a/docs/providers/spotinst/guide/document-store-sdk.md b/docs/providers/spotinst/guide/document-store-sdk.md
deleted file mode 100644
index 0ffa6b13d8a..00000000000
--- a/docs/providers/spotinst/guide/document-store-sdk.md
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-### [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
deleted file mode 100644
index f5887f5939b..00000000000
--- a/docs/providers/spotinst/guide/document-store.md
+++ /dev/null
@@ -1,175 +0,0 @@
-
-
-
-### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials)
-
-
-# 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)
-
-To access the document store you will need to make an API request inside a funciton formatted bellow.
-
-## Add New Value
-
-This is how to insert a new key/value pair into your document store in a specific environment
-
-**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:**
-
-```bash
-{
- "userDocument" : {
- "key": “${Your Key}”,
- "value": “${Your Value}”
- }
-}
-```
-
-
-## Update Value
-
-This is how to update a current key/value pair in your document store in a specific environment
-
-**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:**
-
-```bash
-{
- "userDocument" : {
- "value": “${Your Value}”
- }
-}
-```
-
-
-## Get Values
-
-There are two ways to get the documents from your store, either by specifing a key which will return both the key and the value or you can just leave this out and you will get all the keys in the environment
-
-### 1. Get Sinlge Key Pair
-
-**HTTPS Request:**
-
-```bash
-GET environment/${environmentId}/userDocument/${Key}?accountId=${accountId}
-```
-
-**Endpoint:**
-
-```bash
-api.spotinst.io/functions/
-```
-
-**Header:**
-
-```bash
-{
- "Content-Type": "application/json",
- "Authorization": "Bearer ${Spotinst API Token}"
-}
-```
-
-### 2. Get All Keys
-
-**HTTPS Request:**
-
-```bash
-GET environment/${environmentId}/userDocument?accountId=${accountId}
-```
-
-**Endpoint:**
-
-```bash
-api.spotinst.io/functions/
-```
-
-**Header:**
-
-```bash
-{
- "Content-Type": "application/json",
- "Authorization": "Bearer ${Spotinst API Token}"
-}
-```
-
-
-## Delete Value
-
-This is how to delete a specific key value pair from your document store
-
-**HTTPS Request:**
-
-```bash
-DELETE environment/${environmentId}/userDocument/${Key}?accountId=${accountId}
-```
-
-**Endpoint:**
-
-```bash
-https://api.spotinst.io/functions/
-```
-
-**Header:**
-
-```bash
-{
- "Content-Type": "application/json",
- "Authorization": "Bearer ${Spotinst API Token}"
-}
-```
-
-
-## GitHub
-
-Check out some examples to help you get started!
-
-[Get All Values Function](https://github.com/spotinst/spotinst-functions-examples/tree/master/node-docstore-getAll)
-
-[Insert New Value Function](https://github.com/spotinst/spotinst-functions-examples/tree/master/node-docstore-newValue)
\ No newline at end of file
diff --git a/docs/providers/spotinst/guide/endpoint-api.md b/docs/providers/spotinst/guide/endpoint-api.md
deleted file mode 100644
index 33790666268..00000000000
--- a/docs/providers/spotinst/guide/endpoint-api.md
+++ /dev/null
@@ -1,202 +0,0 @@
-
-
-
-### [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
deleted file mode 100644
index 19d3c5568ac..00000000000
--- a/docs/providers/spotinst/guide/endpoint-setup.md
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
-### [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/endpoints.md b/docs/providers/spotinst/guide/endpoints.md
new file mode 100644
index 00000000000..004a8077e1c
--- /dev/null
+++ b/docs/providers/spotinst/guide/endpoints.md
@@ -0,0 +1,32 @@
+
+
+
+### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials)
+
+
+# Spotinst Functions - Endpoints
+
+You are able to set your custom endpoint path in the serverless.yml file if you do not want to use the console or API. You will have to set up your environment Alias in the console but here you can set the path and method for your individual functions to be mapped to.
+
+Here is a sample function from a yml file. As you can see at the bottom of the file we have listed an endpoint with a path and method. Both of these will need to be set in order to be deployed properly
+
+```yml
+ hello:
+ runtime: nodejs8.3
+ handler: handler.main
+ memory: 128
+ timeout: 30
+ access: public
+ endpoint:
+ path: /home
+ method: get
+
+```
+
+For more information on how to set up endpoint alias and patterns check out our documentation [here](https://help.spotinst.com/hc/en-us/articles/115005893709)
\ No newline at end of file
diff --git a/docs/providers/spotinst/guide/intro.md b/docs/providers/spotinst/guide/intro.md
index ea915470093..f6b2a1ac5ea 100644
--- a/docs/providers/spotinst/guide/intro.md
+++ b/docs/providers/spotinst/guide/intro.md
@@ -92,11 +92,10 @@ functions:
timeout: 30
# access: private
# cron: # Setup scheduled trigger with cron expression
-# active: true
-# value: '* * * * *'
-# environmentVariables: {
-# Key: "Value",
-# }
+# active: true
+# value: '* * * * *'
+# environmentVariables:
+# key: value
```
When you deploy with the Framework by running `serverless deploy`, everything in `serverless.yml` is deployed at once.
diff --git a/docs/providers/spotinst/guide/quick-start.md b/docs/providers/spotinst/guide/quick-start.md
index 077ab90ad14..2292eb30369 100644
--- a/docs/providers/spotinst/guide/quick-start.md
+++ b/docs/providers/spotinst/guide/quick-start.md
@@ -6,6 +6,10 @@ description: Getting started with the Serverless Framework on AWS Lambda
layout: Doc
-->
+
+### [Read this on the main serverless docs site](https://serverless.com/framework/docs/providers/spotinst/guide/quick-start/)
+
+
# Spotinst - Quick Start
Here is a quick guide on how to create a new serverless project using the Spotinst NodeJS template. For more detailed instruction please check out the other reference material provided.
diff --git a/docs/providers/spotinst/guide/serverless.yml.md b/docs/providers/spotinst/guide/serverless.yml.md
index a53db4f0af0..8fe0c6e1212 100644
--- a/docs/providers/spotinst/guide/serverless.yml.md
+++ b/docs/providers/spotinst/guide/serverless.yml.md
@@ -5,23 +5,34 @@ menuOrder: 5
description: Serverless.yml reference
layout: Doc
-->
+
+
+### [Read this on the main serverless docs site](https://serverless.com/framework/docs/providers/spotinst/guide/serverless.yml/)
+
+
# Serverless.yml Reference
This is an outline of a `serverless.yml` file with descriptions of the properties for reference
```yml
-# serverless.yml
-
-# The service can be whatever you choose. You can have multiple functions
-# under one service
+# 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: your-service
-
-# The provider is Spotinst and the Environment ID can be found on the
-# Spotinst Console under Functions
+service: four
provider:
name: spotinst
+ #stage: #Optional setting. By default it is set to 'dev'
spotinst:
environment: #{Your Environment ID}
@@ -38,19 +49,27 @@ provider:
functions:
function-name:
- runtime: nodejs4.8
+ runtime: nodejs8.3
handler: handler.main
memory: 128
timeout: 30
-# access: public
-# cron:
-# active: false
-# value: '*/1 * * * *'
-# environmentVariables: {
-# Key: "Value",
-# }
-
+ access: private
+# activeVersions:
+# - version: $LATEST
+# percentage: 100.0
+# cors:
+# enabled: # false by default
+# origin: # '*' by default
+# headers: # 'Content-Type,Authorization' by default
+# methods: # 'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT' by default
+# 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/docs/providers/spotinst/guide/variables.md b/docs/providers/spotinst/guide/variables.md
index a120acc0901..eaf7722ca29 100644
--- a/docs/providers/spotinst/guide/variables.md
+++ b/docs/providers/spotinst/guide/variables.md
@@ -7,7 +7,7 @@ layout: Doc
-->
-### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/credentials)
+### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/spotinst/guide/variables)
# Spotinst Functions - Variables
@@ -26,9 +26,8 @@ Also you are able to enter in environment variables in the serverless.yml file.
functions:
test:
handler: handler.main
- environmentVariables: {
- Key: "Value"
- }
+ environmentVariables:
+ key: value
```
To access your variables in your code you just need to put `process.env['{Your Key}']` as needed in the handler file.
@@ -39,8 +38,8 @@ URL parameters can be use when a POST request is made to the endpoint of your fu
### 1. Node JS
-To access URL parameters in your NodeJS code you just need to put `event.query.['{Your Parameter Name}']` as needed
+To access URL parameters in your NodeJS code you just need to put `event.query['{Your Parameter Name}']` as needed
### 2. Python
-To access URL parameters in your NodeJS code you just need to put `os.environ['{Your Parameter Name}']` as needed
+To access URL parameters in your Python code you just need to put `os.environ['{Your Parameter Name}']` as needed
diff --git a/docs/providers/webtasks/README.md b/docs/providers/webtasks/README.md
index c6cd4c733b0..930332cbf89 100755
--- a/docs/providers/webtasks/README.md
+++ b/docs/providers/webtasks/README.md
@@ -12,7 +12,9 @@ layout: Doc
Welcome to the Serverless Webtasks documentation!
-If you have questions specific to the Webtasks provider, please join our [Slack](http://chat.webtask.io). For general Serverless Framework questions, join the [chat in gitter](https://gitter.im/serverless/serverless) or [post over on the forums](http://forum.serverless.com/)
+If you have questions specific to the Webtasks provider, please join our [Slack](http://chat.webtask.io).
+
+For general Serverless Framework questions, [search the forums](https://forum.serverless.com?utm_source=framework-docs) or [start your own thread](https://forum.serverless.com?utm_source=framework-docs)
#### Quick Start
diff --git a/lib/Serverless.js b/lib/Serverless.js
index 9b874221bd0..db9595571c1 100644
--- a/lib/Serverless.js
+++ b/lib/Serverless.js
@@ -4,6 +4,7 @@ const path = require('path');
const BbPromise = require('bluebird');
const os = require('os');
const updateNotifier = require('update-notifier');
+const platform = require('@serverless/platform-sdk');
const pkg = require('../package.json');
const CLI = require('./classes/CLI');
const Config = require('./classes/Config');
@@ -14,6 +15,7 @@ const Service = require('./classes/Service');
const Variables = require('./classes/Variables');
const ServerlessError = require('./classes/Error').ServerlessError;
const Version = require('./../package.json').version;
+const configUtils = require('./utils/config');
class Serverless {
constructor(config) {
@@ -75,6 +77,25 @@ class Serverless {
}
run() {
+ const config = configUtils.getConfig();
+ const currentId = config.userId;
+ const globalConfig = configUtils.getGlobalConfig();
+
+ let isTokenExpired = false;
+ if (globalConfig
+ && globalConfig.users
+ && globalConfig.users[currentId]
+ && globalConfig.users[currentId].auth
+ && globalConfig.users[currentId].auth.id_token
+ && !globalConfig.users[currentId].dashboard) {
+ isTokenExpired = true;
+ }
+
+ if (isTokenExpired && !this.processedInput.commands[0] === 'login') {
+ this.cli
+ .log('WARNING: Your login token has expired. Please run "serverless login" to login.');
+ }
+
this.utils.logStat(this).catch(() => BbPromise.resolve());
if (this.cli.displayHelp(this.processedInput)) {
@@ -87,14 +108,54 @@ class Serverless {
// populate variables after --help, otherwise help may fail to print
// (https://github.com/serverless/serverless/issues/2041)
return this.variables.populateService(this.pluginManager.cliOptions).then(() => {
+ if ((!this.processedInput.commands.includes('deploy') &&
+ !this.processedInput.commands.includes('remove')) || !this.config.servicePath) {
+ return BbPromise.resolve();
+ }
+
+ let username = null;
+ let idToken = null;
+ if (globalConfig
+ && globalConfig.users
+ && globalConfig.users[currentId]
+ && globalConfig.users[currentId].dashboard
+ && globalConfig.users[currentId].dashboard.username
+ && globalConfig.users[currentId].dashboard.idToken) {
+ username = globalConfig.users[currentId].dashboard.username;
+ idToken = globalConfig.users[currentId].dashboard.idToken;
+ }
+ if (!username || !idToken) {
+ return BbPromise.resolve();
+ }
+
+ if (!this.service.tenant && !this.service.app) {
+ this.cli.log('WARNING: Missing "tenant" and "app" properties in serverless.yml. Without these properties, you can not publish the service to the Serverless Platform.'); // eslint-disable-line
+ return BbPromise.resolve();
+ } else if (this.service.tenant && !this.service.app) {
+ const errorMessage = ['Missing "app" property in serverless.yml'].join('');
+ throw new this.classes.Error(errorMessage);
+ } else if (!this.service.tenant && this.service.app) {
+ const errorMessage = ['Missing "tenant" property in serverless.yml'].join('');
+ throw new this.classes.Error(errorMessage);
+ }
+
+ return platform.listTenants({ idToken, username }).then((tenants) => {
+ const tenantsList = tenants.map(tenant => tenant.tenantName);
+ if (!tenantsList.includes(this.service.tenant)) {
+ const errorMessage = [`tenant "${this.service
+ .tenant}" does not exist.`].join('');
+ throw new this.classes.Error(errorMessage);
+ }
+ });
+ }).then(() => {
+ // merge arrays after variables have been populated
+ // (https://github.com/serverless/serverless/issues/3511)
+ this.service.mergeArrays();
+
// populate function names after variables are loaded in case functions were externalized
// (https://github.com/serverless/serverless/issues/2997)
this.service.setFunctionNames(this.processedInput.options);
- // merge custom resources after variables have been populated
- // (https://github.com/serverless/serverless/issues/3511)
- this.service.mergeResourceArrays();
-
// validate the service configuration, now that variables are loaded
this.service.validate();
diff --git a/lib/classes/Error.js b/lib/classes/Error.js
index 47e21897c94..9329cb9dc32 100644
--- a/lib/classes/Error.js
+++ b/lib/classes/Error.js
@@ -19,7 +19,7 @@ const writeMessage = (messageType, message) => {
consoleLog(' ');
if (message) {
- consoleLog(chalk.white(` ${message}`));
+ consoleLog(` ${message}`);
}
consoleLog(' ');
@@ -61,17 +61,15 @@ module.exports.logError = (e) => {
consoleLog(' ');
}
- const platform = chalk.white(process.platform);
- const nodeVersion = chalk.white(process.version.replace(/^[v|V]/, ''));
- const slsVersion = chalk.white(version);
+ const platform = process.platform;
+ const nodeVersion = process.version.replace(/^[v|V]/, '');
+ const slsVersion = version;
consoleLog(chalk.yellow(' Get Support --------------------------------------------'));
- consoleLog(`${chalk.yellow(' Docs: ')}${chalk.white('docs.serverless.com')}`);
- consoleLog(`${chalk.yellow(' Bugs: ')}${chalk
- .white('github.com/serverless/serverless/issues')}`);
- consoleLog(`${chalk.yellow(' Forums: ')}${chalk.white('forum.serverless.com')}`);
- consoleLog(`${chalk.yellow(' Chat: ')}${chalk
- .white('gitter.im/serverless/serverless')}`);
+ consoleLog(`${chalk.yellow(' Docs: ')}${'docs.serverless.com'}`);
+ consoleLog(`${chalk.yellow(' Bugs: ')}${
+ 'github.com/serverless/serverless/issues'}`);
+ consoleLog(`${chalk.yellow(' Issues: ')}${'forum.serverless.com'}`);
consoleLog(' ');
consoleLog(chalk.yellow(' Your Environment Information -----------------------------'));
diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js
index 1325ec9a91d..2e10cf0b080 100644
--- a/lib/classes/PluginManager.js
+++ b/lib/classes/PluginManager.js
@@ -54,9 +54,9 @@ class PluginManager {
let pluginProvider = null;
// check if plugin is provider agnostic
if (pluginInstance.provider) {
- if (typeof pluginInstance.provider === 'string') {
+ if (_.isString(pluginInstance.provider)) {
pluginProvider = pluginInstance.provider;
- } else if (typeof pluginInstance.provider === 'object') {
+ } else if (_.isObject(pluginInstance.provider)) {
pluginProvider = pluginInstance.provider.constructor.getProviderName();
}
}
@@ -130,16 +130,34 @@ class PluginManager {
}
loadServicePlugins(servicePlugs) {
- const servicePlugins = Array.isArray(servicePlugs) ? servicePlugs : [];
-
// eslint-disable-next-line no-underscore-dangle
module.paths = Module._nodeModulePaths(process.cwd());
+ const pluginsObject = this.parsePluginsObject(servicePlugs);
// we want to load plugins installed locally in the service
- if (this.serverless && this.serverless.config && this.serverless.config.servicePath) {
- module.paths.unshift(path.join(this.serverless.config.servicePath, '.serverless_plugins'));
+ if (pluginsObject.localPath) {
+ module.paths.unshift(pluginsObject.localPath);
+ }
+ this.loadPlugins(pluginsObject.modules);
+ }
+
+ parsePluginsObject(servicePlugs) {
+ let localPath = (this.serverless && this.serverless.config &&
+ this.serverless.config.servicePath) &&
+ path.join(this.serverless.config.servicePath, '.serverless_plugins');
+ let modules = [];
+
+ if (_.isArray(servicePlugs)) {
+ modules = servicePlugs;
+ } else if (servicePlugs) {
+ localPath = servicePlugs.localPath &&
+ _.isString(servicePlugs.localPath) ? servicePlugs.localPath : localPath;
+ if (_.isArray(servicePlugs.modules)) {
+ modules = servicePlugs.modules;
+ }
}
- this.loadPlugins(servicePlugins);
+
+ return { modules, localPath };
}
createCommandAlias(alias, command) {
@@ -430,7 +448,7 @@ class PluginManager {
if (_.isPlainObject(value.customValidation) &&
value.customValidation.regularExpression instanceof RegExp &&
- typeof value.customValidation.errorMessage === 'string' &&
+ _.isString(value.customValidation.errorMessage) &&
!value.customValidation.regularExpression.test(this.cliOptions[key])) {
throw new this.serverless.classes.Error(value.customValidation.errorMessage);
}
diff --git a/lib/classes/PluginManager.test.js b/lib/classes/PluginManager.test.js
index 437a515ffd0..7a19f227d28 100644
--- a/lib/classes/PluginManager.test.js
+++ b/lib/classes/PluginManager.test.js
@@ -779,6 +779,64 @@ describe('PluginManager', () => {
});
});
+ describe('#parsePluginsObject()', () => {
+ const parsePluginsObjectAndVerifyResult = (servicePlugins, expectedResult) => {
+ const result = pluginManager.parsePluginsObject(servicePlugins);
+ expect(result).to.deep.equal(expectedResult);
+ };
+
+ it('should parse array object', () => {
+ const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2'];
+
+ parsePluginsObjectAndVerifyResult(servicePlugins, {
+ modules: servicePlugins,
+ localPath: path.join(serverless.config.servicePath, '.serverless_plugins'),
+ });
+ });
+
+ it('should parse plugins object', () => {
+ const servicePlugins = {
+ modules: ['ServicePluginMock1', 'ServicePluginMock2'],
+ localPath: './myplugins',
+ };
+
+ parsePluginsObjectAndVerifyResult(servicePlugins, {
+ modules: servicePlugins.modules,
+ localPath: servicePlugins.localPath,
+ });
+ });
+
+ it('should parse plugins object if format is not correct', () => {
+ const servicePlugins = {};
+
+ parsePluginsObjectAndVerifyResult(servicePlugins, {
+ modules: [],
+ localPath: path.join(serverless.config.servicePath, '.serverless_plugins'),
+ });
+ });
+
+ it('should parse plugins object if modules property is not an array', () => {
+ const servicePlugins = { modules: {} };
+
+ parsePluginsObjectAndVerifyResult(servicePlugins, {
+ modules: [],
+ localPath: path.join(serverless.config.servicePath, '.serverless_plugins'),
+ });
+ });
+
+ it('should parse plugins object if localPath is not correct', () => {
+ const servicePlugins = {
+ modules: ['ServicePluginMock1', 'ServicePluginMock2'],
+ localPath: {},
+ };
+
+ parsePluginsObjectAndVerifyResult(servicePlugins, {
+ modules: servicePlugins.modules,
+ localPath: path.join(serverless.config.servicePath, '.serverless_plugins'),
+ });
+ });
+ });
+
describe('command aliases', () => {
describe('#getAliasCommandTarget', () => {
it('should return an alias target', () => {
@@ -1511,6 +1569,66 @@ describe('PluginManager', () => {
});
});
+ describe('Plugin / Load local plugins', () => {
+ const cwd = process.cwd();
+ let serviceDir;
+ let tmpDir;
+ beforeEach(function () { // eslint-disable-line prefer-arrow-callback
+ tmpDir = testUtils.getTmpDirPath();
+ serviceDir = path.join(tmpDir, 'service');
+ fse.mkdirsSync(serviceDir);
+ process.chdir(serviceDir);
+ pluginManager.serverless.config.servicePath = serviceDir;
+ });
+
+ it('should load plugins from .serverless_plugins', () => {
+ const localPluginDir = path.join(serviceDir, '.serverless_plugins', 'local-plugin');
+ testUtils.installPlugin(localPluginDir, SynchronousPluginMock);
+
+ pluginManager.loadServicePlugins(['local-plugin']);
+ expect(pluginManager.plugins).to.satisfy(plugins =>
+ plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock'));
+ });
+
+ it('should load plugins from custom folder', () => {
+ const localPluginDir = path.join(serviceDir, 'serverless-plugins-custom', 'local-plugin');
+ testUtils.installPlugin(localPluginDir, SynchronousPluginMock);
+
+ pluginManager.loadServicePlugins({
+ localPath: path.join(serviceDir, 'serverless-plugins-custom'),
+ modules: ['local-plugin'],
+ });
+ // Had to use contructor.name because the class will be loaded via
+ // require and the reference will not match with SynchronousPluginMock
+ expect(pluginManager.plugins).to.satisfy(plugins =>
+ plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock'));
+ });
+
+ it('should load plugins from custom folder outside of serviceDir', () => {
+ serviceDir = path.join(tmpDir, 'serverless-plugins-custom');
+ const localPluginDir = path.join(serviceDir, 'local-plugin');
+ testUtils.installPlugin(localPluginDir, SynchronousPluginMock);
+
+ pluginManager.loadServicePlugins({
+ localPath: serviceDir,
+ modules: ['local-plugin'],
+ });
+ // Had to use contructor.name because the class will be loaded via
+ // require and the reference will not match with SynchronousPluginMock
+ expect(pluginManager.plugins).to.satisfy(plugins => plugins.some(plugin =>
+ plugin.constructor.name === 'SynchronousPluginMock'));
+ });
+
+ afterEach(function () { // eslint-disable-line prefer-arrow-callback
+ process.chdir(cwd);
+ try {
+ fse.removeSync(tmpDir);
+ } catch (e) {
+ // Couldn't delete temporary file
+ }
+ });
+ });
+
describe('Plugin / CLI integration', function () {
this.timeout(0);
@@ -1559,6 +1677,11 @@ describe('PluginManager', () => {
afterEach(function () { // eslint-disable-line prefer-arrow-callback
process.chdir(cwd);
+ try {
+ fse.removeSync(serviceDir);
+ } catch (e) {
+ // Couldn't delete temporary file
+ }
});
});
});
diff --git a/lib/classes/PromiseTracker.js b/lib/classes/PromiseTracker.js
new file mode 100644
index 00000000000..ea710720bbc
--- /dev/null
+++ b/lib/classes/PromiseTracker.js
@@ -0,0 +1,58 @@
+'use strict';
+
+const logWarning = require('./Error').logWarning;
+
+class PromiseTracker {
+ constructor() {
+ this.reset();
+ }
+ reset() {
+ this.promiseList = [];
+ this.promiseMap = {};
+ this.startTime = Date.now();
+ }
+ start() {
+ this.reset();
+ this.interval = setInterval(this.report.bind(this), 2500);
+ }
+ report() {
+ const delta = Date.now() - this.startTime;
+ logWarning('################################################################################');
+ logWarning(`# ${delta}: ${this.getSettled().length} of ${
+ this.getAll().length} promises have settled`);
+ const pending = this.getPending();
+ logWarning(`# ${delta}: ${pending.length} unsettled promises:`);
+ pending.forEach((promise) => {
+ logWarning(`# ${delta}: ${promise.waitList}`);
+ });
+ logWarning('################################################################################');
+ }
+ stop() {
+ clearInterval(this.interval);
+ this.reset();
+ }
+ add(variable, prms, specifier) {
+ const promise = prms;
+ promise.waitList = `${variable} waited on by: ${specifier}`;
+ promise.state = 'pending';
+ promise.then( // creates a promise with the following effects but that we otherwise ignore
+ () => { promise.state = 'resolved'; },
+ () => { promise.state = 'rejected'; });
+ this.promiseList.push(promise);
+ this.promiseMap[variable] = promise;
+ return promise;
+ }
+ contains(variable) {
+ return variable in this.promiseMap;
+ }
+ get(variable, specifier) {
+ const promise = this.promiseMap[variable];
+ promise.waitList += ` ${specifier}`;
+ return promise;
+ }
+ getPending() { return this.promiseList.filter(p => (p.state === 'pending')); }
+ getSettled() { return this.promiseList.filter(p => (p.state !== 'pending')); }
+ getAll() { return this.promiseList; }
+}
+
+module.exports = PromiseTracker;
diff --git a/lib/classes/PromiseTracker.test.js b/lib/classes/PromiseTracker.test.js
new file mode 100644
index 00000000000..0202770d833
--- /dev/null
+++ b/lib/classes/PromiseTracker.test.js
@@ -0,0 +1,64 @@
+'use strict';
+
+/* eslint-disable no-unused-expressions */
+
+const BbPromise = require('bluebird');
+const chai = require('chai');
+
+const PromiseTracker = require('../../lib/classes/PromiseTracker');
+
+chai.use(require('chai-as-promised'));
+
+const expect = chai.expect;
+
+/**
+ * Mostly this class is tested by its use in peer ~/lib/classes/Variables.js
+ *
+ * Mostly, I'm creating coverage but if errors are discovered, coverage for the specific cases
+ * can be created here.
+ */
+describe('PromiseTracker', () => {
+ let promiseTracker;
+ beforeEach(() => {
+ promiseTracker = new PromiseTracker();
+ });
+ it('logs a warning without throwing', () => {
+ promiseTracker.add('foo', BbPromise.resolve(), '${foo:}');
+ promiseTracker.add('foo', BbPromise.delay(10), '${foo:}');
+ promiseTracker.report(); // shouldn't throw
+ });
+ it('reports no pending promises when none have been added', () => {
+ const promises = promiseTracker.getPending();
+ expect(promises).to.be.an.instanceof(Array);
+ expect(promises.length).to.equal(0);
+ });
+ it('reports one pending promise when one has been added', () => {
+ let resolve;
+ const promise = new BbPromise((rslv) => { resolve = rslv; });
+ promiseTracker.add('foo', promise, '${foo:}');
+ return BbPromise.delay(1).then(() => {
+ const promises = promiseTracker.getPending();
+ expect(promises).to.be.an.instanceof(Array);
+ expect(promises.length).to.equal(1);
+ expect(promises[0]).to.equal(promise);
+ }).then(() => { resolve(); });
+ });
+ it('reports no settled promises when none have been added', () => {
+ const promises = promiseTracker.getSettled();
+ expect(promises).to.be.an.instanceof(Array);
+ expect(promises.length).to.equal(0);
+ });
+ it('reports one settled promise when one has been added', () => {
+ const promise = BbPromise.resolve();
+ promiseTracker.add('foo', promise, '${foo:}');
+ promise.state = 'resolved';
+ const promises = promiseTracker.getSettled();
+ expect(promises).to.be.an.instanceof(Array);
+ expect(promises.length).to.equal(1);
+ expect(promises[0]).to.equal(promise);
+ });
+ it('reports no promises when none have been added', () => {
+ const promises = promiseTracker.getAll();
+ expect(promises).to.be.an('array').that.is.empty;
+ });
+});
diff --git a/lib/classes/Service.js b/lib/classes/Service.js
index 68991debd7e..f4bb771d340 100644
--- a/lib/classes/Service.js
+++ b/lib/classes/Service.js
@@ -8,6 +8,9 @@ const semver = require('semver');
class Service {
constructor(serverless, data) {
+ // #######################################################################
+ // ## KEEP SYNCHRONIZED WITH EQUIVALENT IN ~/lib/plugins/print/print.js ##
+ // #######################################################################
this.serverless = serverless;
// Default properties
@@ -20,6 +23,7 @@ class Service {
};
this.custom = {};
this.plugins = [];
+ this.pluginsData = {};
this.functions = {};
this.resources = {};
this.package = {};
@@ -107,6 +111,14 @@ class Service {
throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`);
}
+ // #######################################################################
+ // ## KEEP SYNCHRONIZED WITH EQUIVALENT IN ~/lib/plugins/print/print.js ##
+ // #######################################################################
+ // #####################################################################
+ // ## KEEP SYNCHRONIZED WITH EQUIVALENT IN ~/lib/classes/Variables.js ##
+ // ## there, see `getValueFromSelf` ##
+ // ## here, see below ##
+ // #####################################################################
if (!_.isObject(serverlessFile.provider)) {
const providerName = serverlessFile.provider;
serverlessFile.provider = {
@@ -122,6 +134,8 @@ class Service {
that.service = serverlessFile.service;
}
+ that.app = serverlessFile.app;
+ that.tenant = serverlessFile.tenant;
that.custom = serverlessFile.custom;
that.plugins = serverlessFile.plugins;
that.resources = serverlessFile.resources;
@@ -163,19 +177,21 @@ class Service {
});
}
- mergeResourceArrays() {
- if (Array.isArray(this.resources)) {
- this.resources = this.resources.reduce((memo, value) => {
- if (value) {
- if (typeof value === 'object') {
- return _.merge(memo, value);
+ mergeArrays() {
+ ['resources', 'functions'].forEach(key => {
+ if (Array.isArray(this[key])) {
+ this[key] = this[key].reduce((memo, value) => {
+ if (value) {
+ if (typeof value === 'object') {
+ return _.merge(memo, value);
+ }
+ throw new Error(`Non-object value specified in ${key} array: ${value}`);
}
- throw new Error(`Non-object value specified in resources array: ${value}`);
- }
- return memo;
- }, {});
- }
+ return memo;
+ }, {});
+ }
+ });
}
validate() {
@@ -228,6 +244,11 @@ class Service {
getAllEventsInFunction(functionName) {
return this.getFunction(functionName).events;
}
+
+ publish(dataParam) {
+ const data = dataParam || {};
+ this.pluginsData = _.merge(this.pluginsData, data);
+ }
}
module.exports = Service;
diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js
index 96bf7529fad..446208a2f5b 100644
--- a/lib/classes/Service.test.js
+++ b/lib/classes/Service.test.js
@@ -669,7 +669,7 @@ describe('Service', () => {
});
});
- describe('#mergeResourceArrays', () => {
+ describe('#mergeArrays', () => {
it('should merge resources given as an array', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
@@ -691,7 +691,7 @@ describe('Service', () => {
},
];
- serviceInstance.mergeResourceArrays();
+ serviceInstance.mergeArrays();
expect(serviceInstance.resources).to.be.an('object');
expect(serviceInstance.resources.Resources).to.be.an('object');
@@ -708,7 +708,7 @@ describe('Service', () => {
Resources: 'foo',
};
- serviceInstance.mergeResourceArrays();
+ serviceInstance.mergeArrays();
expect(serviceInstance.resources).to.deep.eql({
Resources: 'foo',
@@ -728,7 +728,7 @@ describe('Service', () => {
},
];
- serviceInstance.mergeResourceArrays();
+ serviceInstance.mergeArrays();
expect(serviceInstance.resources).to.deep.eql({
aws: {
resourcesProp: 'value',
@@ -744,7 +744,7 @@ describe('Service', () => {
42,
];
- expect(() => serviceInstance.mergeResourceArrays()).to.throw(Error);
+ expect(() => serviceInstance.mergeArrays()).to.throw(Error);
});
it('should throw when given a string', () => {
@@ -755,7 +755,24 @@ describe('Service', () => {
'string',
];
- expect(() => serviceInstance.mergeResourceArrays()).to.throw(Error);
+ expect(() => serviceInstance.mergeArrays()).to.throw(Error);
+ });
+
+ it('should merge functions given as an array', () => {
+ const serverless = new Serverless();
+ const serviceInstance = new Service(serverless);
+
+ serviceInstance.functions = [{
+ a: {},
+ }, {
+ b: {},
+ }];
+
+ serviceInstance.mergeArrays();
+
+ expect(serviceInstance.functions).to.be.an('object');
+ expect(serviceInstance.functions.a).to.be.an('object');
+ expect(serviceInstance.functions.b).to.be.an('object');
});
});
diff --git a/lib/classes/Utils.js b/lib/classes/Utils.js
index 921a5ee596e..c0b7bb4545c 100644
--- a/lib/classes/Utils.js
+++ b/lib/classes/Utils.js
@@ -16,6 +16,7 @@ const isDockerContainer = require('is-docker');
const version = require('../../package.json').version;
const segment = require('../utils/segment');
const configUtils = require('../utils/config');
+const awsArnRegExs = require('../plugins/aws/utils/arnRegularExpressions');
class Utils {
constructor(serverless) {
@@ -38,15 +39,15 @@ class Utils {
return fse.mkdirsSync(path.dirname(filePath));
}
- writeFileSync(filePath, contents) {
- return writeFileSync(filePath, contents);
+ writeFileSync(filePath, contents, cycles) {
+ return writeFileSync(filePath, contents, cycles);
}
- writeFile(filePath, contents) {
+ writeFile(filePath, contents, cycles) {
const that = this;
return new BbPromise((resolve, reject) => {
try {
- that.writeFileSync(filePath, contents);
+ that.writeFileSync(filePath, contents, cycles);
} catch (e) {
reject(e);
}
@@ -204,11 +205,11 @@ class Utils {
}
// For HTTP events, see what authorizer types are enabled
- if (event.http && event.http.authorizer) {
- if ((typeof event.http.authorizer === 'string'
- && event.http.authorizer.toUpperCase() === 'AWS_IAM')
- || (event.http.authorizer.type
- && event.http.authorizer.type.toUpperCase() === 'AWS_IAM')) {
+ if (_.has(event, 'http.authorizer')) {
+ if ((_.isString(event.http.authorizer)
+ && _.toUpper(event.http.authorizer) === 'AWS_IAM')
+ || (event.http.authorizer.type
+ && _.toUpper(event.http.authorizer.type) === 'AWS_IAM')) {
hasIAMAuthorizer = true;
}
// There are three ways a user can specify a Custom authorizer:
@@ -217,18 +218,19 @@ class Utils {
// 2) By listing the name of a function in the same service for the name property
// in the authorizer object.
// 3) By listing a function's ARN in the arn property of the authorizer object.
- if ((typeof event.http.authorizer === 'string'
- && event.http.authorizer.toUpperCase() !== 'AWS_IAM'
- && !event.http.authorizer.includes('arn:aws:cognito-idp'))
- || event.http.authorizer.name
- || (event.http.authorizer.arn
- && event.http.authorizer.arn.includes('arn:aws:lambda'))) {
+
+ if ((_.isString(event.http.authorizer)
+ && _.toUpper(event.http.authorizer) !== 'AWS_IAM'
+ && !awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer))
+ || event.http.authorizer.name
+ || (event.http.authorizer.arn
+ && awsArnRegExs.lambdaArnExpr.test(event.http.authorizer.arn))) {
hasCustomAuthorizer = true;
}
- if ((typeof event.http.authorizer === 'string'
- && event.http.authorizer.includes('arn:aws:cognito-idp'))
- || (event.http.authorizer.arn
- && event.http.authorizer.arn.includes('arn:aws:cognito-idp'))) {
+ if ((_.isString(event.http.authorizer)
+ && awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer))
+ || (event.http.authorizer.arn
+ && awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer.arn))) {
hasCognitoAuthorizer = true;
}
}
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index 738a0347cc8..7cebde7aa7c 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -1,34 +1,117 @@
'use strict';
const _ = require('lodash');
-const path = require('path');
-const replaceall = require('replaceall');
-const logWarning = require('./Error').logWarning;
const BbPromise = require('bluebird');
const os = require('os');
+const path = require('path');
+const replaceall = require('replaceall');
+
const fse = require('../utils/fs/fse');
+const logWarning = require('./Error').logWarning;
+const PromiseTracker = require('./PromiseTracker');
+/**
+ * Maintainer's notes:
+ *
+ * This is a tricky class to modify and maintain. A few rules on how it works...
+ *
+ * 0. The population of a service consists of pre-population, followed by population. Pre-
+ * population consists of populating only those settings which are required for variable
+ * resolution. Current examples include region and stage as they must be resolved to a
+ * concrete value before they can be used in the provider's `request` method (used by
+ * `getValueFromCf`, `getValueFromS3`, and `getValueFromSsm`) to resolve the associated values.
+ * Original issue: #4725
+ * 1. All variable populations occur in generations. Each of these generations resolves each
+ * present variable in the given object or property (i.e. terminal string properties and/or
+ * property parts) once. This is to say that recursive resolutions should not be made. This is
+ * because cyclic references are allowed [i.e. ${self:} and the like]) and doing so leads to
+ * dependency and dead-locking issues. This leads to a problem with deep value population (i.e.
+ * populating ${self:foo.bar} when ${self:foo} has a value of {opt:bar}). To pause that, one must
+ * pause population, noting the continued depth to traverse. This motivated "deep" variables.
+ * Original issue #4687
+ * 2. The resolution of variables can get very confusing if the same object is used multiple times.
+ * An example of this is the print command which *implicitly/invisibly* populates the
+ * serverless.yml and then *explicitly/visibily* renders the same file again, without the
+ * adornments that the framework's components make to the originally loaded service. As a result,
+ * it is important to reset this object for each use.
+ * 3. Note that despite some AWS code herein that this class is used in all plugins. Obviously
+ * users avoid the AWS-specific variable types when targetting other clouds.
+ */
class Variables {
-
constructor(serverless) {
this.serverless = serverless;
this.service = this.serverless.service;
- this.cache = {};
+ this.tracker = new PromiseTracker();
- this.overwriteSyntax = RegExp(/,/g);
+ this.deep = [];
+ this.deepRefSyntax = RegExp(/(\${)?deep:\d+(\.[^}]+)*()}?/);
+ this.overwriteSyntax = RegExp(/\s*(?:,\s*)+/g);
this.fileRefSyntax = RegExp(/^file\((~?[a-zA-Z0-9._\-/]+?)\)/g);
this.envRefSyntax = RegExp(/^env:/g);
this.optRefSyntax = RegExp(/^opt:/g);
this.selfRefSyntax = RegExp(/^self:/g);
- this.cfRefSyntax = RegExp(/^cf:/g);
- this.s3RefSyntax = RegExp(/^s3:(.+?)\/(.+)$/);
- this.stringRefSyntax = RegExp(/('.*')|(".*")/g);
- this.ssmRefSyntax = RegExp(/^ssm:([a-zA-Z0-9_.\-/]+)[~]?(true|false)?/);
+ this.stringRefSyntax = RegExp(/(?:('|").*?\1)/g);
+ this.cfRefSyntax = RegExp(/^(?:\${)?cf:/g);
+ this.s3RefSyntax = RegExp(/^(?:\${)?s3:(.+?)\/(.+)$/);
+ this.ssmRefSyntax = RegExp(/^(?:\${)?ssm:([a-zA-Z0-9_.\-/]+)[~]?(true|false)?/);
}
loadVariableSyntax() {
this.variableSyntax = RegExp(this.service.provider.variableSyntax, 'g');
}
+
+ initialCall(func) {
+ this.deep = [];
+ this.tracker.start();
+ return func().finally(() => {
+ this.tracker.stop();
+ this.deep = [];
+ });
+ }
+
+ // #############
+ // ## SERVICE ##
+ // #############
+ disableDepedentServices(func) {
+ const dependentServices = [
+ { name: 'CloudFormation', method: 'getValueFromCf', original: this.getValueFromCf },
+ { name: 'S3', method: 'getValueFromS3', original: this.getValueFromS3 },
+ { name: 'SSM', method: 'getValueFromSsm', original: this.getValueFromSsm },
+ ];
+ const dependencyMessage = (configValue, serviceName) =>
+ `Variable dependency failure: variable '${configValue}' references service ${
+ serviceName} but using that service requires a concrete value to be called.`;
+ // replace and then restore the methods for obtaining values from dependent services. the
+ // replacement naturally rejects dependencies on these services that occur during prepopulation.
+ // prepopulation is, of course, the process of obtaining the required configuration for using
+ // these services.
+ dependentServices.forEach((dependentService) => { // knock out
+ this[dependentService.method] = (variableString) => BbPromise.reject(
+ dependencyMessage(variableString, dependentService.name));
+ });
+ return func()
+ .finally(() => {
+ dependentServices.forEach((dependentService) => { // restore
+ this[dependentService.method] = dependentService.original;
+ });
+ });
+ }
+ prepopulateService() {
+ const provider = this.serverless.getProvider('aws');
+ if (provider) {
+ const requiredConfigs = [
+ _.assign({ name: 'region' }, provider.getRegionSourceValue()),
+ _.assign({ name: 'stage' }, provider.getStageSourceValue()),
+ ];
+ return this.disableDepedentServices(() => {
+ const prepopulations = requiredConfigs.map(config =>
+ this.populateValue(config.value, true) // populate
+ .then(populated => _.assign(config, { populated })));
+ return this.assignProperties(provider, prepopulations);
+ });
+ }
+ return BbPromise.resolve();
+ }
/**
* Populate all variables in the service, conviently remove and restore the service attributes
* that confuse the population methods.
@@ -36,20 +119,124 @@ class Variables {
* @returns {Promise.|*} A promise resolving to the populated service.
*/
populateService(processedOptions) {
+ // #######################################################################
+ // ## KEEP SYNCHRONIZED WITH EQUIVALENT IN ~/lib/plugins/print/print.js ##
+ // #######################################################################
this.options = processedOptions || {};
this.loadVariableSyntax();
// store
const variableSyntaxProperty = this.service.provider.variableSyntax;
// remove
- this.service.provider.variableSyntax = true; // matches itself
- this.serverless.service.serverless = null;
- return this.populateObject(this.service).then(() => {
- // restore
- this.service.provider.variableSyntax = variableSyntaxProperty;
- this.serverless.service.serverless = this.serverless;
- return BbPromise.resolve(this.service);
- });
+ this.service.provider.variableSyntax = undefined; // otherwise matches itself
+ this.service.serverless = undefined;
+ return this.initialCall(() =>
+ this.prepopulateService()
+ .then(() => this.populateObjectImpl(this.service)
+ .finally(() => {
+ // restore
+ this.service.serverless = this.serverless;
+ this.service.provider.variableSyntax = variableSyntaxProperty;
+ }))
+ .then(() => this.service));
+ }
+ // ############
+ // ## OBJECT ##
+ // ############
+ /**
+ * The declaration of a terminal property. This declaration includes the path and value of the
+ * property.
+ * Example Input:
+ * {
+ * foo: {
+ * bar: 'baz'
+ * }
+ * }
+ * Example Result:
+ * [
+ * {
+ * path: ['foo', 'bar']
+ * value: 'baz
+ * }
+ * ]
+ * @typedef {Object} TerminalProperty
+ * @property {String[]} path The path to the terminal property
+ * @property {Date|RegEx|String} The value of the terminal property
+ */
+ /**
+ * Generate an array of objects noting the terminal properties of the given root object and their
+ * paths
+ * @param root The object to generate a terminal property path/value set for
+ * @param current The current part of the given root that terminal properties are being sought
+ * within
+ * @param [context] An array containing the path to the current object root (intended for internal
+ * use)
+ * @param [results] An array of current results (intended for internal use)
+ * @returns {TerminalProperty[]} The terminal properties of the given root object, with the path
+ * and value of each
+ */
+ getProperties(root, atRoot, current, cntxt, rslts) {
+ let context = cntxt;
+ if (!context) {
+ context = [];
+ }
+ let results = rslts;
+ if (!results) {
+ results = [];
+ }
+ const addContext = (value, key) =>
+ this.getProperties(root, false, value, context.concat(key), results);
+ if (
+ _.isArray(current)
+ ) {
+ _.map(current, addContext);
+ } else if (
+ _.isObject(current) &&
+ !_.isDate(current) &&
+ !_.isRegExp(current) &&
+ !_.isFunction(current)
+ ) {
+ if (atRoot || current !== root) {
+ _.mapValues(current, addContext);
+ }
+ } else {
+ results.push({ path: context, value: current });
+ }
+ return results;
+ }
+
+ /**
+ * @typedef {TerminalProperty} TerminalPropertyPopulated
+ * @property {Object} populated The populated value of the value at the path
+ */
+ /**
+ * Populate the given terminal properties, returning promises to do so
+ * @param properties The terminal properties to populate
+ * @returns {Promise[]} The promises that will resolve to the
+ * populated values of the given terminal properties
+ */
+ populateVariables(properties) {
+ const variables = properties.filter(property =>
+ _.isString(property.value) &&
+ property.value.match(this.variableSyntax));
+ return _.map(variables,
+ variable => this.populateValue(variable.value, false)
+ .then(populated => _.assign({}, variable, { populated })));
+ }
+ /**
+ * Assign the populated values back to the target object
+ * @param target The object to which the given populated terminal properties should be applied
+ * @param populations The fully populated terminal properties
+ * @returns {Promise} resolving with the number of changes that were applied to the given
+ * target
+ */
+ assignProperties(target, populations) { // eslint-disable-line class-methods-use-this
+ return BbPromise.all(populations)
+ .then((results) => results.forEach((result) => {
+ if (result.value !== result.populated) {
+ _.set(target, result.path, result.populated);
+ }
+ }));
}
/**
* Populate the variables in the given object.
@@ -57,78 +244,126 @@ class Variables {
* @returns {Promise.|*} A promise resolving to the in-place populated object.
*/
populateObject(objectToPopulate) {
- // Map terminal values of given root (i.e. for every leaf value...)
- const forEachLeaf = (root, context, callback) => {
- const addContext = (value, key) => forEachLeaf(value, context.concat(key), callback);
- if (
- _.isArray(root)
- ) {
- return _.map(root, addContext);
- } else if (
- _.isObject(root) &&
- !_.isDate(root) &&
- !_.isRegExp(root) &&
- !_.isFunction(root)
- ) {
- return _.extend({}, root, _.mapValues(root, addContext));
- }
- return callback(root, context);
- };
- // For every leaf value...
- const pendingLeaves = [];
- forEachLeaf(
- objectToPopulate,
- [],
- (leafValue, leafPath) => {
- if (typeof leafValue === 'string') {
- pendingLeaves.push(this
- .populateProperty(leafValue, true)
- .then(leafValuePopulated => _.set(objectToPopulate, leafPath, leafValuePopulated))
- );
- }
- }
- );
- return BbPromise.all(pendingLeaves).then(() => objectToPopulate);
+ return this.initialCall(() => this.populateObjectImpl(objectToPopulate));
}
+ populateObjectImpl(objectToPopulate) {
+ const leaves = this.getProperties(objectToPopulate, true, objectToPopulate);
+ const populations = this.populateVariables(leaves);
+ if (populations.length === 0) {
+ return BbPromise.resolve(objectToPopulate);
+ }
+ return this.assignProperties(objectToPopulate, populations)
+ .then(() => this.populateObjectImpl(objectToPopulate));
+ }
+ // ##############
+ // ## PROPERTY ##
+ // ##############
/**
- * Populate variables, in-place if specified, in the given property value.
- * @param propertyToPopulate The property to populate (only strings with variables are altered).
- * @param populateInPlace Whether to deeply clone the given property prior to population.
- * @returns {Promise.|*} A promise resolving to the populated result.
+ * Standard logic for cleaning a variable
+ * Example: cleanVariable('${opt:foo}') => 'opt:foo'
+ * @param match The variable match instance variable part
+ * @returns {string} The cleaned variable match
+ */
+ cleanVariable(match) {
+ return match.replace(
+ this.variableSyntax,
+ (context, contents) => contents.trim()
+ ).replace(/\s/g, '');
+ }
+ /**
+ * @typedef {Object} MatchResult
+ * @property {String} match The original property value that matched the variable syntax
+ * @property {String} variable The cleaned variable string that specifies the origin for the
+ * property value
+ */
+ /**
+ * Get matches against the configured variable syntax
+ * @param property The property value to attempt extracting matches from
+ * @returns {Object|String|MatchResult[]} The given property or the identified matches
*/
- populateProperty(propertyToPopulate, populateInPlace) {
- let property = propertyToPopulate;
- if (!populateInPlace) {
- property = _.cloneDeep(propertyToPopulate);
+ getMatches(property) {
+ if (typeof property !== 'string') {
+ return property;
}
- if (
- typeof property !== 'string' ||
- !property.match(this.variableSyntax)
- ) {
+ const matches = property.match(this.variableSyntax);
+ if (!matches || !matches.length) {
+ return property;
+ }
+ return _.map(matches, match => ({
+ match,
+ variable: this.cleanVariable(match),
+ }));
+ }
+ /**
+ * Populate the given matches, returning an array of Promises which will resolve to the populated
+ * values of the given matches
+ * @param {MatchResult[]} matches The matches to populate
+ * @returns {Promise[]} Promises for the eventual populated values of the given matches
+ */
+ populateMatches(matches, property) {
+ return _.map(matches, (match) => this.splitAndGet(match.variable, property));
+ }
+ /**
+ * Render the given matches and their associated results to the given value
+ * @param value The value into which to render the given results
+ * @param matches The matches on the given value where the results are to be rendered
+ * @param results The results that are to be rendered to the given value
+ * @returns {*} The populated value with the given results rendered according to the given matches
+ */
+ renderMatches(value, matches, results) {
+ let result = value;
+ for (let i = 0; i < matches.length; i += 1) {
+ this.warnIfNotFound(matches[i].variable, results[i]);
+ result = this.populateVariable(result, matches[i].match, results[i]);
+ }
+ return result;
+ }
+ /**
+ * Populate the given value, recursively if root is true
+ * @param valueToPopulate The value to populate variables within
+ * @param root Whether the caller is the root populator and thereby whether to recursively
+ * populate
+ * @returns {PromiseLike} A promise that resolves to the populated value, recursively if root
+ * is true
+ */
+ populateValue(valueToPopulate, root) {
+ const property = valueToPopulate;
+ const matches = this.getMatches(property);
+ if (!_.isArray(matches)) {
return BbPromise.resolve(property);
}
- const pendingMatches = [];
- property.match(this.variableSyntax).forEach((matchedString) => {
- const variableString = matchedString
- .replace(this.variableSyntax, (match, varName) => varName.trim())
- .replace(/\s/g, '');
-
- let pendingMatch;
- if (variableString.match(this.overwriteSyntax)) {
- pendingMatch = this.overwrite(variableString);
- } else {
- pendingMatch = this.getValueFromSource(variableString);
- }
- pendingMatches.push(pendingMatch.then(matchedValue => {
- this.warnIfNotFound(variableString, matchedValue);
- return this.populateVariable(property, matchedString, matchedValue)
- .then((populatedProperty) => {
- property = populatedProperty;
- });
- }));
- });
- return BbPromise.all(pendingMatches)
- .then(() => this.populateProperty(property, true));
+ const populations = this.populateMatches(matches, valueToPopulate);
+ return BbPromise.all(populations)
+ .then(results => this.renderMatches(property, matches, results))
+ .then((result) => {
+ if (root && matches.length) {
+ return this.populateValue(result, root);
+ }
+ return result;
+ });
+ }
+ /**
+ * Populate variables in the given property.
+ * @param propertyToPopulate The property to populate (replace variables with their values).
+ * @returns {Promise.|*} A promise resolving to the populated result.
+ */
+ populateProperty(propertyToPopulate) {
+ return this.initialCall(() => this.populateValue(propertyToPopulate, true));
+ }
+
+ /**
+ * Split the cleaned variable string containing one or more comma delimited variables and get a
+ * final value for the entirety of the string
+ * @param varible The variable string to split and get a final value for
+ * @param property The original property string the given variable was extracted from
+ * @returns {Promise} A promise resolving to the final value of the given variable
+ */
+ splitAndGet(variable, property) {
+ const parts = this.splitByComma(variable);
+ if (parts.length > 1) {
+ return this.overwrite(parts, property);
+ }
+ return this.getValueFromSource(parts[0], property);
}
/**
* Populate a given property, given the matched string to replace and the value to replace the
@@ -155,20 +390,63 @@ class Variables {
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
- return BbPromise.resolve(property);
+ return property;
+ }
+ // ###############
+ // ## VARIABLES ##
+ // ###############
+ /**
+ * Split a given string by whitespace padded commas excluding those within single or double quoted
+ * strings.
+ * @param string The string to split by comma.
+ */
+ splitByComma(string) {
+ const input = string.trim();
+ const stringMatches = [];
+ let match = this.stringRefSyntax.exec(input);
+ while (match) {
+ stringMatches.push({
+ start: match.index,
+ end: this.stringRefSyntax.lastIndex,
+ });
+ match = this.stringRefSyntax.exec(input);
+ }
+ const commaReplacements = [];
+ const contained = commaMatch => // curry the current commaMatch
+ stringMatch => // check whether stringMatch containing the commaMatch
+ stringMatch.start < commaMatch.index &&
+ this.overwriteSyntax.lastIndex < stringMatch.end;
+ match = this.overwriteSyntax.exec(input);
+ while (match) {
+ const matchContained = contained(match);
+ const containedBy = stringMatches.find(matchContained);
+ if (!containedBy) { // if uncontained, this comma respresents a splitting location
+ commaReplacements.push({
+ start: match.index,
+ end: this.overwriteSyntax.lastIndex,
+ });
+ }
+ match = this.overwriteSyntax.exec(input);
+ }
+ let prior = 0;
+ const results = [];
+ commaReplacements.forEach((replacement) => {
+ results.push(input.slice(prior, replacement.start));
+ prior = replacement.end;
+ });
+ results.push(input.slice(prior));
+ return results;
}
/**
- * Overwrite the given variable string, resolve each variable and resolve to the first valid
- * value.
+ * Resolve the given variable string that expresses a series of fallback values in case the
+ * initial values are not valid, resolving each variable and resolving to the first valid value.
* @param variableStringsString The overwrite string of variables to populate and choose from.
* @returns {Promise.|*} A promise resolving to the first validly populating variable
* in the given variable strings string.
*/
- overwrite(variableStringsString) {
- const variableStrings = variableStringsString.split(',');
+ overwrite(variableStrings, propertyString) {
const variableValues = variableStrings.map(variableString =>
- this.getValueFromSource(variableString)
- );
+ this.getValueFromSource(variableString, propertyString));
const validValue = value => (
value !== null &&
typeof value !== 'undefined' &&
@@ -176,53 +454,50 @@ class Variables {
);
return BbPromise.all(variableValues)
.then(values => // find and resolve first valid value, undefined if none
- BbPromise.resolve(values.find(validValue))
- );
+ BbPromise.resolve(values.find(validValue)));
}
/**
* Given any variable string, return the value it should be populated with.
* @param variableString The variable string to retrieve a value for.
* @returns {Promise.|*} A promise resolving to the given variables value.
*/
- getValueFromSource(variableString) {
- if (!(variableString in this.cache)) {
- let value;
+ getValueFromSource(variableString, propertyString) {
+ let ret;
+ if (this.tracker.contains(variableString)) {
+ ret = this.tracker.get(variableString, propertyString);
+ } else {
if (variableString.match(this.envRefSyntax)) {
- value = this.getValueFromEnv(variableString);
+ ret = this.getValueFromEnv(variableString);
} else if (variableString.match(this.optRefSyntax)) {
- value = this.getValueFromOptions(variableString);
+ ret = this.getValueFromOptions(variableString);
} else if (variableString.match(this.selfRefSyntax)) {
- value = this.getValueFromSelf(variableString);
+ ret = this.getValueFromSelf(variableString);
} else if (variableString.match(this.fileRefSyntax)) {
- value = this.getValueFromFile(variableString);
+ ret = this.getValueFromFile(variableString);
} else if (variableString.match(this.cfRefSyntax)) {
- value = this.getValueFromCf(variableString);
+ ret = this.getValueFromCf(variableString);
} else if (variableString.match(this.s3RefSyntax)) {
- value = this.getValueFromS3(variableString);
+ ret = this.getValueFromS3(variableString);
} else if (variableString.match(this.stringRefSyntax)) {
- value = this.getValueFromString(variableString);
+ ret = this.getValueFromString(variableString);
} else if (variableString.match(this.ssmRefSyntax)) {
- value = this.getValueFromSsm(variableString);
+ ret = this.getValueFromSsm(variableString);
+ } else if (variableString.match(this.deepRefSyntax)) {
+ ret = this.getValueFromDeep(variableString);
} else {
const errorMessage = [
`Invalid variable reference syntax for variable ${variableString}.`,
' You can only reference env vars, options, & files.',
' You can check our docs for more info.',
].join('');
- throw new this.serverless.classes.Error(errorMessage);
+ ret = BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}
- this.cache[variableString] = BbPromise.resolve(value)
- .then(variableValue => {
- if (_.isObject(variableValue) && variableValue !== this.service) {
- return this.populateObject(variableValue);
- }
- return variableValue;
- });
+ ret = this.tracker.add(variableString, ret, propertyString);
}
- return this.cache[variableString];
+ return ret;
}
- getValueFromEnv(variableString) {
+ getValueFromEnv(variableString) { // eslint-disable-line class-methods-use-this
const requestedEnvVar = variableString.split(':')[1];
let valueToPopulate;
if (requestedEnvVar !== '' || '' in process.env) {
@@ -233,7 +508,7 @@ class Variables {
return BbPromise.resolve(valueToPopulate);
}
- getValueFromString(variableString) {
+ getValueFromString(variableString) { // eslint-disable-line class-methods-use-this
const valueToPopulate = variableString.replace(/^['"]|['"]$/g, '');
return BbPromise.resolve(valueToPopulate);
}
@@ -250,9 +525,24 @@ class Variables {
}
getValueFromSelf(variableString) {
+ const selfServiceRex = /self:service\./;
+ let variable = variableString;
+ // ###################################################################
+ // ## KEEP SYNCHRONIZED WITH EQUIVALENT IN ~/lib/classes/Service.js ##
+ // ## there, see `loadServiceFileParam` ##
+ // ###################################################################
+ // The loaded service is altered during load in ~/lib/classes/Service (see loadServiceFileParam)
+ // Account for these so that user's reference to their file populate properly
+ if (variable === 'self:service.name') {
+ variable = 'self:service';
+ } else if (variable.match(selfServiceRex)) {
+ variable = variable.replace(selfServiceRex, 'self:serviceObject.');
+ } else if (variable === 'self:provider') {
+ variable = 'self:provider.name';
+ }
const valueToPopulate = this.service;
- const deepProperties = variableString.split(':')[1].split('.');
- return this.getDeepValue(deepProperties, valueToPopulate);
+ const deepProperties = variable.split(':')[1].split('.').filter(property => property);
+ return this.getDeeperValue(deepProperties, valueToPopulate);
}
getValueFromFile(variableString) {
@@ -262,13 +552,13 @@ class Variables {
.replace('~', os.homedir());
let referencedFileFullPath = (path.isAbsolute(referencedFileRelativePath) ?
- referencedFileRelativePath :
- path.join(this.serverless.config.servicePath, referencedFileRelativePath));
+ referencedFileRelativePath :
+ path.join(this.serverless.config.servicePath, referencedFileRelativePath));
// Get real path to handle potential symlinks (but don't fatal error)
referencedFileFullPath = fse.existsSync(referencedFileFullPath) ?
- fse.realpathSync(referencedFileFullPath) :
- referencedFileFullPath;
+ fse.realpathSync(referencedFileFullPath) :
+ referencedFileFullPath;
let fileExtension = referencedFileRelativePath.split('.');
fileExtension = fileExtension[fileExtension.length - 1];
@@ -281,7 +571,8 @@ class Variables {
// Process JS files
if (fileExtension === 'js') {
- const jsFile = require(referencedFileFullPath); // eslint-disable-line global-require
+ // eslint-disable-next-line global-require, import/no-dynamic-require
+ const jsFile = require(referencedFileFullPath);
const variableArray = variableString.split(':');
let returnValueFunction;
if (variableArray[1]) {
@@ -293,29 +584,28 @@ class Variables {
}
if (typeof returnValueFunction !== 'function') {
- throw new this.serverless.classes
- .Error([
- 'Invalid variable syntax when referencing',
- ` file "${referencedFileRelativePath}".`,
- ' Check if your javascript is exporting a function that returns a value.',
- ].join(''));
+ const errorMessage = [
+ 'Invalid variable syntax when referencing',
+ ` file "${referencedFileRelativePath}".`,
+ ' Check if your javascript is exporting a function that returns a value.',
+ ].join('');
+ return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}
- valueToPopulate = returnValueFunction.call(jsFile);
+ valueToPopulate = returnValueFunction.call(jsFile, this.serverless);
- return BbPromise.resolve(valueToPopulate).then(valueToPopulateResolved => {
+ return BbPromise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
let deepProperties = variableString.replace(matchedFileRefString, '');
deepProperties = deepProperties.slice(1).split('.');
deepProperties.splice(0, 1);
- return this.getDeepValue(deepProperties, valueToPopulateResolved)
- .then(deepValueToPopulateResolved => {
+ return this.getDeeperValue(deepProperties, valueToPopulateResolved)
+ .then((deepValueToPopulateResolved) => {
if (typeof deepValueToPopulateResolved === 'undefined') {
const errorMessage = [
'Invalid variable syntax when referencing',
` file "${referencedFileRelativePath}".`,
' Check if your javascript is returning the correct data.',
].join('');
- throw new this.serverless.classes
- .Error(errorMessage);
+ return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}
return BbPromise.resolve(deepValueToPopulateResolved);
});
@@ -334,11 +624,10 @@ class Variables {
` file "${referencedFileRelativePath}" sub properties`,
' Please use ":" to reference sub properties.',
].join('');
- throw new this.serverless.classes
- .Error(errorMessage);
+ return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}
deepProperties = deepProperties.slice(1).split('.');
- return this.getDeepValue(deepProperties, valueToPopulate);
+ return this.getDeeperValue(deepProperties, valueToPopulate);
}
}
return BbPromise.resolve(valueToPopulate);
@@ -352,9 +641,8 @@ class Variables {
.request('CloudFormation',
'describeStacks',
{ StackName: stackName },
- { useCache: true } // Use request cache
- )
- .then(result => {
+ { useCache: true })// Use request cache
+ .then((result) => {
const outputs = result.Stacks[0].Outputs;
const output = outputs.find(x => x.OutputKey === outputLogicalId);
@@ -364,11 +652,9 @@ class Variables {
` Stack name: "${stackName}"`,
` Requested variable: "${outputLogicalId}".`,
].join('');
- throw new this.serverless.classes
- .Error(errorMessage);
+ return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}
-
- return output.OutputValue;
+ return BbPromise.resolve(output.OutputValue);
});
}
@@ -376,62 +662,113 @@ class Variables {
const groups = variableString.match(this.s3RefSyntax);
const bucket = groups[1];
const key = groups[2];
- return this.serverless.getProvider('aws')
- .request('S3',
+ return this.serverless.getProvider('aws').request(
+ 'S3',
'getObject',
{
Bucket: bucket,
Key: key,
},
- { useCache: true } // Use request cache
- )
- .then(
- response => response.Body.toString(),
- err => {
+ { useCache: true }) // Use request cache
+ .then(response => BbPromise.resolve(response.Body.toString()))
+ .catch((err) => {
const errorMessage = `Error getting value for ${variableString}. ${err.message}`;
- throw new this.serverless.classes.Error(errorMessage);
- }
- );
+ return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
+ });
}
getValueFromSsm(variableString) {
const groups = variableString.match(this.ssmRefSyntax);
const param = groups[1];
const decrypt = (groups[2] === 'true');
- return this.serverless.getProvider('aws')
- .request('SSM',
+ return this.serverless.getProvider('aws').request(
+ 'SSM',
'getParameter',
{
Name: param,
WithDecryption: decrypt,
},
- { useCache: true } // Use request cache
- )
- .then(
- response => BbPromise.resolve(response.Parameter.Value),
- err => {
+ { useCache: true }) // Use request cache
+ .then(response => BbPromise.resolve(response.Parameter.Value))
+ .catch((err) => {
const expectedErrorMessage = `Parameter ${param} not found.`;
if (err.message !== expectedErrorMessage) {
- throw new this.serverless.classes.Error(err.message);
+ return BbPromise.reject(new this.serverless.classes.Error(err.message));
}
return BbPromise.resolve(undefined);
- }
- );
+ });
}
- getDeepValue(deepProperties, valueToPopulate) {
- return BbPromise.reduce(deepProperties, (computedValueToPopulateParam, subProperty) => {
- let computedValueToPopulate = computedValueToPopulateParam;
- if (typeof computedValueToPopulate === 'undefined') {
- computedValueToPopulate = {};
- } else if (subProperty !== '' || '' in computedValueToPopulate) {
- computedValueToPopulate = computedValueToPopulate[subProperty];
- }
- if (typeof computedValueToPopulate === 'string' &&
- computedValueToPopulate.match(this.variableSyntax)) {
- return this.populateProperty(computedValueToPopulate, true);
+ getDeepIndex(variableString) {
+ const deepIndexReplace = RegExp(/^deep:|(\.[^}]+)*$/g);
+ return variableString.replace(deepIndexReplace, '');
+ }
+ getVariableFromDeep(variableString) {
+ const index = this.getDeepIndex(variableString);
+ return this.deep[index];
+ }
+ getValueFromDeep(variableString) {
+ const deepPrefixReplace = RegExp(/(?:^deep:)\d+\.?/g);
+ const variable = this.getVariableFromDeep(variableString);
+ const deepRef = variableString.replace(deepPrefixReplace, '');
+ let ret = this.populateValue(variable);
+ if (deepRef.length) { // if there is a deep reference remaining
+ ret = ret.then((result) => {
+ if (_.isString(result) && result.match(this.variableSyntax)) {
+ const deepVariable = this.makeDeepVariable(result);
+ return BbPromise.resolve(this.appendDeepVariable(deepVariable, deepRef));
+ }
+ return this.getDeeperValue(deepRef.split('.'), result);
+ });
+ }
+ return ret;
+ }
+
+ makeDeepVariable(variable) {
+ let index = this.deep.findIndex((item) => variable === item);
+ if (index < 0) {
+ index = this.deep.push(variable) - 1;
+ }
+ const variableContainer = variable.match(this.variableSyntax)[0];
+ const variableString = this.cleanVariable(variableContainer);
+ return variableContainer
+ .replace(/\s/g, '')
+ .replace(variableString, `deep:${index}`);
+ }
+ appendDeepVariable(variable, subProperty) {
+ return `${variable.slice(0, variable.length - 1)}.${subProperty}}`;
+ }
+
+ /**
+ * Get a value that is within the given valueToPopulate. The deepProperties specify what value
+ * to retrieve from the given valueToPopulate. The trouble is that anywhere along this chain a
+ * variable can be discovered. If this occurs, to avoid cyclic dependencies, the resolution of
+ * the deep value from the given valueToPopulate must be halted. The discovered variable is thus
+ * set aside into a "deep variable" (see makeDeepVariable). The indexing into the given
+ * valueToPopulate is then resolved with a replacement ${deep:${index}.${remaining.properties}}
+ * variable (e.g. ${deep:1.foo}). This pauses the population for continuation during the next
+ * generation of evaluation (see getValueFromDeep)
+ * @param deepProperties The "path" of properties to follow in obtaining the deeper value
+ * @param valueToPopulate The value from which to obtain the deeper value
+ * @returns {Promise} A promise resolving to the deeper value or to a `deep` variable that
+ * will later resolve to the deeper value
+ */
+ getDeeperValue(deepProperties, valueToPopulate) {
+ return BbPromise.reduce(deepProperties, (reducedValueParam, subProperty) => {
+ let reducedValue = reducedValueParam;
+ if (_.isString(reducedValue) && reducedValue.match(this.deepRefSyntax)) { // build mode
+ reducedValue = this.appendDeepVariable(reducedValue, subProperty);
+ } else { // get mode
+ if (typeof reducedValue === 'undefined') {
+ reducedValue = {};
+ } else if (subProperty !== '' || '' in reducedValue) {
+ reducedValue = reducedValue[subProperty];
+ }
+ if (typeof reducedValue === 'string' && reducedValue.match(this.variableSyntax)) {
+ reducedValue = this.makeDeepVariable(reducedValue);
+ }
}
- return BbPromise.resolve(computedValueToPopulate);
+ return BbPromise.resolve(reducedValue);
}, valueToPopulate);
}
@@ -453,10 +790,10 @@ class Variables {
} else if (variableString.match(this.ssmRefSyntax)) {
varType = 'SSM parameter';
}
- logWarning(
- `A valid ${varType} to satisfy the declaration '${variableString}' could not be found.`
- );
+ logWarning(`A valid ${varType} to satisfy the declaration '${
+ variableString}' could not be found.`);
}
+ return valueToPopulate;
}
}
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index e59b552aada..1c8ec69eb24 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -2,36 +2,43 @@
/* eslint-disable no-unused-expressions */
+const BbPromise = require('bluebird');
+const chai = require('chai');
const jc = require('json-cycle');
+const os = require('os');
const path = require('path');
const proxyquire = require('proxyquire');
+const sinon = require('sinon');
const YAML = require('js-yaml');
-const chai = require('chai');
-const Variables = require('../../lib/classes/Variables');
-const Utils = require('../../lib/classes/Utils');
+
+const AwsProvider = require('../plugins/aws/provider/awsProvider');
const fse = require('../utils/fs/fse');
const Serverless = require('../../lib/Serverless');
-const sinon = require('sinon');
-const testUtils = require('../../tests/utils');
const slsError = require('./Error');
-const AwsProvider = require('../plugins/aws/provider/awsProvider');
-const BbPromise = require('bluebird');
-const os = require('os');
+const testUtils = require('../../tests/utils');
+const Utils = require('../../lib/classes/Utils');
+const Variables = require('../../lib/classes/Variables');
+
+BbPromise.longStackTraces(true);
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));
+chai.should();
+
const expect = chai.expect;
+
describe('Variables', () => {
+ let serverless;
+ beforeEach(() => {
+ serverless = new Serverless();
+ });
describe('#constructor()', () => {
- const serverless = new Serverless();
-
it('should attach serverless instance', () => {
const variablesInstance = new Variables(serverless);
- expect(typeof variablesInstance.serverless.version).to.be.equal('string');
+ expect(variablesInstance.serverless).to.equal(serverless);
});
-
it('should not set variableSyntax in constructor', () => {
const variablesInstance = new Variables(serverless);
expect(variablesInstance.variableSyntax).to.be.undefined;
@@ -40,110 +47,244 @@ describe('Variables', () => {
describe('#loadVariableSyntax()', () => {
it('should set variableSyntax', () => {
- const serverless = new Serverless();
-
+ // eslint-disable-next-line no-template-curly-in-string
serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}';
-
serverless.variables.loadVariableSyntax();
expect(serverless.variables.variableSyntax).to.be.a('RegExp');
});
});
describe('#populateService()', () => {
- it('should call populateProperty method', () => {
- const serverless = new Serverless();
-
- const populatePropertyStub = sinon
- .stub(serverless.variables, 'populateObject').resolves();
-
- return expect(serverless.variables.populateService()).to.be.fulfilled
- .then(() => {
- expect(populatePropertyStub.called).to.be.true;
- })
- .finally(() => serverless.variables.populateObject.restore());
+ it('should remove problematic attributes bofore calling populateObjectImpl with the service',
+ () => {
+ const prepopulateServiceStub = sinon.stub(serverless.variables, 'prepopulateService')
+ .returns(BbPromise.resolve());
+ const populateObjectStub = sinon.stub(serverless.variables, 'populateObjectImpl', (val) => {
+ expect(val).to.equal(serverless.service);
+ expect(val.provider.variableSyntax).to.be.undefined;
+ expect(val.serverless).to.be.undefined;
+ return BbPromise.resolve();
+ });
+ return serverless.variables.populateService().should.be.fulfilled
+ .then().finally(() => {
+ prepopulateServiceStub.restore();
+ populateObjectStub.restore();
+ });
+ });
+ it('should clear caches and remaining state *before* [pre]populating service',
+ () => {
+ const prepopulateServiceStub = sinon.stub(serverless.variables, 'prepopulateService',
+ (val) => {
+ expect(serverless.variables.deep).to.eql([]);
+ expect(serverless.variables.tracker.getAll()).to.eql([]);
+ return BbPromise.resolve(val);
+ });
+ const populateObjectStub = sinon.stub(serverless.variables, 'populateObjectImpl',
+ (val) => {
+ expect(serverless.variables.deep).to.eql([]);
+ expect(serverless.variables.tracker.getAll()).to.eql([]);
+ return BbPromise.resolve(val);
+ });
+ serverless.variables.deep.push('${foo:}');
+ const prms = BbPromise.resolve('foo');
+ serverless.variables.tracker.add('foo:', prms, '${foo:}');
+ prms.state = 'resolved';
+ return serverless.variables.populateService().should.be.fulfilled
+ .then().finally(() => {
+ prepopulateServiceStub.restore();
+ populateObjectStub.restore();
+ });
+ });
+ it('should clear caches and remaining *after* [pre]populating service',
+ () => {
+ const prepopulateServiceStub = sinon.stub(serverless.variables, 'prepopulateService',
+ (val) => {
+ serverless.variables.deep.push('${foo:}');
+ const promise = BbPromise.resolve(val);
+ serverless.variables.tracker.add('foo:', promise, '${foo:}');
+ promise.state = 'resolved';
+ return BbPromise.resolve();
+ });
+ const populateObjectStub = sinon.stub(serverless.variables, 'populateObjectImpl',
+ (val) => {
+ serverless.variables.deep.push('${bar:}');
+ const promise = BbPromise.resolve(val);
+ serverless.variables.tracker.add('bar:', promise, '${bar:}');
+ promise.state = 'resolved';
+ return BbPromise.resolve();
+ });
+ return serverless.variables.populateService().should.be.fulfilled
+ .then(() => {
+ expect(serverless.variables.deep).to.eql([]);
+ expect(serverless.variables.tracker.getAll()).to.eql([]);
+ })
+ .finally(() => {
+ prepopulateServiceStub.restore();
+ populateObjectStub.restore();
+ });
+ });
+ });
+ describe('#prepopulateService', () => {
+ // TL;DR: call populateService to test prepopulateService (note addition of 'pre')
+ //
+ // The prepopulateService method basically assumes invocation of of populateService (i.e. that
+ // variable syntax is loaded, and that the service object is cleaned up. Just use
+ // populateService to do that work.
+ let awsProvider;
+ let populateObjectImplStub;
+ let requestStub; // just in case... don't want to actually call...
+ beforeEach(() => {
+ awsProvider = new AwsProvider(serverless, {});
+ populateObjectImplStub = sinon.stub(serverless.variables, 'populateObjectImpl');
+ populateObjectImplStub.withArgs(serverless.variables.service).returns(BbPromise.resolve());
+ requestStub = sinon.stub(awsProvider, 'request', () =>
+ BbPromise.reject(new Error('unexpected')));
});
-
- it('should use variableSyntax', () => {
- const serverless = new Serverless();
-
- const variableSyntax = '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}';
- const fooValue = '${clientId()}';
- const barValue = 'test';
-
- serverless.service.provider.variableSyntax = variableSyntax;
-
- serverless.service.custom = {
- var: barValue,
- };
-
- serverless.service.resources = {
- foo: fooValue,
- bar: '${{self:custom.var}}',
- };
-
- return serverless.variables.populateService().then(() => {
- expect(serverless.service.provider.variableSyntax).to.equal(variableSyntax);
- expect(serverless.service.resources.foo).to.equal(fooValue);
- expect(serverless.service.resources.bar).to.equal(barValue);
+ afterEach(() => {
+ populateObjectImplStub.restore();
+ requestStub.restore();
+ });
+ const prepopulatedProperties = [
+ { name: 'region', getter: (provider) => provider.getRegion() },
+ { name: 'stage', getter: (provider) => provider.getStage() },
+ ];
+ describe('basic population tests', () => {
+ prepopulatedProperties.forEach((property) => {
+ it(`should populate variables in ${property.name} values`, () => {
+ awsProvider.options[property.name] = '${self:foobar, "default"}';
+ return serverless.variables.populateService().should.be.fulfilled
+ .then(() => expect(property.getter(awsProvider)).to.be.eql('default'));
+ });
+ });
+ });
+ //
+ describe('dependent service rejections', () => {
+ const dependentConfigs = [
+ { value: '${cf:stack.value}', name: 'CloudFormation' },
+ { value: '${s3:bucket/key}', name: 'S3' },
+ { value: '${ssm:/path/param}', name: 'SSM' },
+ ];
+ prepopulatedProperties.forEach(property => {
+ dependentConfigs.forEach(config => {
+ it(`should reject ${config.name} variables in ${property.name} values`, () => {
+ awsProvider.options[property.name] = config.value;
+ return serverless.variables.populateService()
+ .should.be.rejectedWith('Variable dependency failure');
+ });
+ it(`should reject recursively dependent ${config.name} service dependencies`, () => {
+ serverless.variables.service.custom = {
+ settings: config.value,
+ };
+ awsProvider.options.region = '${self:custom.settings.region}';
+ return serverless.variables.populateService()
+ .should.be.rejectedWith('Variable dependency failure');
+ });
+ });
+ });
+ });
+ describe('dependent service non-interference', () => {
+ const stateCombinations = [
+ { region: 'foo', state: 'bar' },
+ { region: 'foo', state: '${self:bar, "bar"}' },
+ { region: '${self:foo, "foo"}', state: 'bar' },
+ { region: '${self:foo, "foo"}', state: '${self:bar, "bar"}' },
+ ];
+ stateCombinations.forEach((combination) => {
+ it('must leave the dependent services in their original state', () => {
+ const dependentMethods = [
+ { name: 'getValueFromCf', original: serverless.variables.getValueFromCf },
+ { name: 'getValueFromS3', original: serverless.variables.getValueFromS3 },
+ { name: 'getValueFromSsm', original: serverless.variables.getValueFromSsm },
+ ];
+ awsProvider.options.region = combination.region;
+ awsProvider.options.state = combination.state;
+ return serverless.variables.populateService().should.be.fulfilled
+ .then(() => {
+ dependentMethods.forEach((method) => {
+ expect(serverless.variables[method.name]).to.equal(method.original);
+ });
+ });
+ });
});
});
});
- describe('#populateObject()', () => {
- it('should call populateProperty method', () => {
- const serverless = new Serverless();
- const object = {
- stage: '${opt:stage}',
+ describe('#getProperties', () => {
+ it('extracts all terminal properties of an object', () => {
+ const date = new Date();
+ const regex = /^.*$/g;
+ const func = () => {};
+ const obj = {
+ foo: {
+ bar: 'baz',
+ biz: 'buz',
+ },
+ b: [
+ { c: 'd' },
+ { e: 'f' },
+ ],
+ g: date,
+ h: regex,
+ i: func,
};
-
- const populatePropertyStub = sinon
- .stub(serverless.variables, 'populateProperty').resolves('prod');
-
- return serverless.variables.populateObject(object).then(() => {
- expect(populatePropertyStub.called).to.be.true;
- })
- .finally(() => serverless.variables.populateProperty.restore());
+ const expected = [
+ { path: ['foo', 'bar'], value: 'baz' },
+ { path: ['foo', 'biz'], value: 'buz' },
+ { path: ['b', 0, 'c'], value: 'd' },
+ { path: ['b', 1, 'e'], value: 'f' },
+ { path: ['g'], value: date },
+ { path: ['h'], value: regex },
+ { path: ['i'], value: func },
+ ];
+ const result = serverless.variables.getProperties(obj, true, obj);
+ expect(result).to.eql(expected);
});
+ it('ignores self references', () => {
+ const obj = {};
+ obj.self = obj;
+ const expected = [];
+ const result = serverless.variables.getProperties(obj, true, obj);
+ expect(result).to.eql(expected);
+ });
+ });
+ describe('#populateObject()', () => {
+ beforeEach(() => {
+ serverless.variables.loadVariableSyntax();
+ });
it('should populate object and return it', () => {
- const serverless = new Serverless();
const object = {
- stage: '${opt:stage}',
+ stage: '${opt:stage}', // eslint-disable-line no-template-curly-in-string
};
const expectedPopulatedObject = {
stage: 'prod',
};
- sinon.stub(serverless.variables, 'populateProperty').resolves('prod');
+ sinon.stub(serverless.variables, 'populateValue').resolves('prod');
- return serverless.variables.populateObject(object).then(populatedObject => {
+ return serverless.variables.populateObject(object).then((populatedObject) => {
expect(populatedObject).to.deep.equal(expectedPopulatedObject);
})
- .finally(() => serverless.variables.populateProperty.restore());
+ .finally(() => serverless.variables.populateValue.restore());
});
it('should persist keys with dot notation', () => {
- const serverless = new Serverless();
const object = {
- stage: '${opt:stage}',
+ stage: '${opt:stage}', // eslint-disable-line no-template-curly-in-string
};
object['some.nested.key'] = 'hello';
const expectedPopulatedObject = {
stage: 'prod',
};
expectedPopulatedObject['some.nested.key'] = 'hello';
-
- const populatePropertyStub = sinon.stub(serverless.variables, 'populateProperty');
- populatePropertyStub.onCall(0).resolves('prod');
- populatePropertyStub.onCall(1).resolves('hello');
-
- return serverless.variables.populateObject(object).then(populatedObject => {
- expect(populatedObject).to.deep.equal(expectedPopulatedObject);
- })
- .finally(() => serverless.variables.populateProperty.restore());
+ const populateValueStub = sinon.stub(serverless.variables, 'populateValue',
+ // eslint-disable-next-line no-template-curly-in-string
+ val => (val === '${opt:stage}' ? BbPromise.resolve('prod') : BbPromise.resolve(val)));
+ return serverless.variables.populateObject(object)
+ .should.become(expectedPopulatedObject)
+ .then().finally(() => populateValueStub.restore());
});
describe('significant variable usage corner cases', () => {
- let serverless;
let service;
const makeDefault = () => ({
service: 'my-service',
@@ -152,8 +293,8 @@ describe('Variables', () => {
},
});
beforeEach(() => {
- serverless = new Serverless();
service = makeDefault();
+ // eslint-disable-next-line no-template-curly-in-string
service.provider.variableSyntax = '\\${([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}'; // default
serverless.variables.service = service;
serverless.variables.loadVariableSyntax();
@@ -161,7 +302,7 @@ describe('Variables', () => {
});
it('should properly replace self-references', () => {
service.custom = {
- me: '${self:}',
+ me: '${self:}', // eslint-disable-line no-template-curly-in-string
};
const expected = makeDefault();
expected.custom = {
@@ -174,7 +315,7 @@ describe('Variables', () => {
it('should properly populate embedded variables', () => {
service.custom = {
val0: 'my value 0',
- val1: '0',
+ val1: '0', // eslint-disable-next-line no-template-curly-in-string
val2: '${self:custom.val${self:custom.val1}}',
};
const expected = {
@@ -188,7 +329,7 @@ describe('Variables', () => {
});
it('should properly populate an overwrite with a default value that is a string', () => {
service.custom = {
- val0: 'my value',
+ val0: 'my value', // eslint-disable-next-line no-template-curly-in-string
val1: '${self:custom.NOT_A_VAL1, self:custom.NOT_A_VAL2, "string"}',
};
const expected = {
@@ -201,7 +342,7 @@ describe('Variables', () => {
});
it('should properly populate overwrites where the first value is valid', () => {
service.custom = {
- val0: 'my value',
+ val0: 'my value', // eslint-disable-next-line no-template-curly-in-string
val1: '${self:custom.val0, self:custom.NOT_A_VAL1, self:custom.NOT_A_VAL2}',
};
const expected = {
@@ -214,7 +355,7 @@ describe('Variables', () => {
});
it('should properly populate overwrites where the middle value is valid', () => {
service.custom = {
- val0: 'my value',
+ val0: 'my value', // eslint-disable-next-line no-template-curly-in-string
val1: '${self:custom.NOT_A_VAL1, self:custom.val0, self:custom.NOT_A_VAL2}',
};
const expected = {
@@ -227,7 +368,7 @@ describe('Variables', () => {
});
it('should properly populate overwrites where the last value is valid', () => {
service.custom = {
- val0: 'my value',
+ val0: 'my value', // eslint-disable-next-line no-template-curly-in-string
val1: '${self:custom.NOT_A_VAL1, self:custom.NOT_A_VAL2, self:custom.val0}',
};
const expected = {
@@ -241,7 +382,7 @@ describe('Variables', () => {
it('should properly populate overwrites with nested variables in the first value', () => {
service.custom = {
val0: 'my value',
- val1: 0,
+ val1: 0, // eslint-disable-next-line no-template-curly-in-string
val2: '${self:custom.val${self:custom.val1}, self:custom.NO_1, self:custom.NO_2}',
};
const expected = {
@@ -256,7 +397,7 @@ describe('Variables', () => {
it('should properly populate overwrites with nested variables in the middle value', () => {
service.custom = {
val0: 'my value',
- val1: 0,
+ val1: 0, // eslint-disable-next-line no-template-curly-in-string
val2: '${self:custom.NO_1, self:custom.val${self:custom.val1}, self:custom.NO_2}',
};
const expected = {
@@ -271,7 +412,7 @@ describe('Variables', () => {
it('should properly populate overwrites with nested variables in the last value', () => {
service.custom = {
val0: 'my value',
- val1: 0,
+ val1: 0, // eslint-disable-next-line no-template-curly-in-string
val2: '${self:custom.NO_1, self:custom.NO_2, self:custom.val${self:custom.val1}}',
};
const expected = {
@@ -286,8 +427,8 @@ describe('Variables', () => {
it('should properly replace duplicate variable declarations', () => {
service.custom = {
val0: 'my value',
- val1: '${self:custom.val0}',
- val2: '${self:custom.val0}',
+ val1: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
+ val2: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
};
const expected = {
val0: 'my value',
@@ -300,10 +441,10 @@ describe('Variables', () => {
});
it('should recursively populate, regardless of order and duplication', () => {
service.custom = {
- val1: '${self:custom.depVal}',
- depVal: '${self:custom.val0}',
+ val1: '${self:custom.depVal}', // eslint-disable-line no-template-curly-in-string
+ depVal: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
val0: 'my value',
- val2: '${self:custom.depVal}',
+ val2: '${self:custom.depVal}', // eslint-disable-line no-template-curly-in-string
};
const expected = {
val1: 'my value',
@@ -315,11 +456,210 @@ describe('Variables', () => {
expect(result).to.eql(expected);
})).to.be.fulfilled;
});
- const pathAsyncLoadJs = 'async.load.js';
- const makeAsyncLoadJs = () => {
- const SUtils = new Utils();
- const tmpDirPath = testUtils.getTmpDirPath();
- const fileContent = `'use strict';
+ // see https://github.com/serverless/serverless/pull/4713#issuecomment-366975172
+ it('should handle deep references into deep variables', () => {
+ service.provider.stage = 'dev';
+ service.custom = {
+ stage: '${env:stage, self:provider.stage}',
+ secrets: '${self:custom.${self:custom.stage}}',
+ dev: {
+ SECRET: 'secret',
+ },
+ environment: {
+ SECRET: '${self:custom.secrets.SECRET}',
+ },
+ };
+ const expected = {
+ stage: 'dev',
+ secrets: {
+ SECRET: 'secret',
+ },
+ dev: {
+ SECRET: 'secret',
+ },
+ environment: {
+ SECRET: 'secret',
+ },
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should handle deep variables that reference overrides', () => {
+ service.custom = {
+ val1: '${self:not.a.value, "bar"}',
+ val2: '${self:custom.val1}',
+ };
+ const expected = {
+ val1: 'bar',
+ val2: 'bar',
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should handle deep references into deep variables', () => {
+ service.custom = {
+ val0: {
+ foo: 'bar',
+ },
+ val1: '${self:custom.val0}',
+ val2: '${self:custom.val1.foo}',
+ };
+ const expected = {
+ val0: {
+ foo: 'bar',
+ },
+ val1: {
+ foo: 'bar',
+ },
+ val2: 'bar',
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should handle deep variables that reference overrides', () => {
+ service.custom = {
+ val1: '${self:not.a.value, "bar"}',
+ val2: 'foo${self:custom.val1}',
+ };
+ const expected = {
+ val1: 'bar',
+ val2: 'foobar',
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should handle referenced deep variables that reference overrides', () => {
+ service.custom = {
+ val1: '${self:not.a.value, "bar"}',
+ val2: '${self:custom.val1}',
+ val3: '${self:custom.val2}',
+ };
+ const expected = {
+ val1: 'bar',
+ val2: 'bar',
+ val3: 'bar',
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should handle partial referenced deep variables that reference overrides', () => {
+ service.custom = {
+ val1: '${self:not.a.value, "bar"}',
+ val2: '${self:custom.val1}',
+ val3: 'foo${self:custom.val2}',
+ };
+ const expected = {
+ val1: 'bar',
+ val2: 'bar',
+ val3: 'foobar',
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should handle referenced contained deep variables that reference overrides', () => {
+ service.custom = {
+ val1: '${self:not.a.value, "bar"}',
+ val2: 'foo${self:custom.val1}',
+ val3: '${self:custom.val2}',
+ };
+ const expected = {
+ val1: 'bar',
+ val2: 'foobar',
+ val3: 'foobar',
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should handle multiple referenced contained deep variables referencing overrides', () => {
+ service.custom = {
+ val0: '${self:not.a.value, "foo"}',
+ val1: '${self:not.a.value, "bar"}',
+ val2: '${self:custom.val0}:${self:custom.val1}',
+ val3: '${self:custom.val2}',
+ };
+ const expected = {
+ val0: 'foo',
+ val1: 'bar',
+ val2: 'foo:bar',
+ val3: 'foo:bar',
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should handle deep variables regardless of custom variableSyntax', () => {
+ service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._\\\'",\\-\\/\\(\\)]+?)}}';
+ serverless.variables.loadVariableSyntax();
+ delete service.provider.variableSyntax;
+ service.custom = {
+ my0thStage: 'DEV',
+ my1stStage: '${{self:custom.my0thStage}}',
+ my2ndStage: '${{self:custom.my1stStage}}',
+ };
+ const expected = {
+ my0thStage: 'DEV',
+ my1stStage: 'DEV',
+ my2ndStage: 'DEV',
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should handle deep variables regardless of recursion into custom variableSyntax', () => {
+ service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._\\\'",\\-\\/\\(\\)]+?)}}';
+ serverless.variables.loadVariableSyntax();
+ delete service.provider.variableSyntax;
+ service.custom = {
+ my0thIndex: '0th',
+ my1stIndex: '1st',
+ my0thStage: 'DEV',
+ my1stStage: '${{self:custom.my${{self:custom.my0thIndex}}Stage}}',
+ my2ndStage: '${{self:custom.my${{self:custom.my1stIndex}}Stage}}',
+ };
+ const expected = {
+ my0thIndex: '0th',
+ my1stIndex: '1st',
+ my0thStage: 'DEV',
+ my1stStage: 'DEV',
+ my2ndStage: 'DEV',
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should handle deep variables in complex recursions of custom variableSyntax', () => {
+ service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._\\\'",\\-\\/\\(\\)]+?)}}';
+ serverless.variables.loadVariableSyntax();
+ delete service.provider.variableSyntax;
+ service.custom = {
+ i0: '0',
+ s0: 'DEV',
+ s1: '${{self:custom.s0}}! ${{self:custom.s0}}',
+ s2: 'I am a ${{self:custom.s0}}! A ${{self:custom.s${{self:custom.i0}}}}!',
+ s3: '${{self:custom.s0}}!, I am a ${{self:custom.s1}}!, ${{self:custom.s2}}',
+ };
+ const expected = {
+ i0: '0',
+ s0: 'DEV',
+ s1: 'DEV! DEV',
+ s2: 'I am a DEV! A DEV!',
+ s3: 'DEV!, I am a DEV! DEV!, I am a DEV! A DEV!',
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ describe('file reading cases', () => {
+ let tmpDirPath;
+ beforeEach(() => {
+ tmpDirPath = testUtils.getTmpDirPath();
+ fse.mkdirsSync(tmpDirPath);
+ serverless.config.update({ servicePath: tmpDirPath });
+ });
+ afterEach(() => {
+ fse.removeSync(tmpDirPath);
+ });
+ const makeTempFile = (fileName, fileContent) => {
+ fse.writeFileSync(path.join(tmpDirPath, fileName), fileContent);
+ };
+ const asyncFileName = 'async.load.js';
+ const asyncContent = `'use strict';
let i = 0
const str = () => new Promise((resolve) => {
setTimeout(() => {
@@ -341,506 +681,453 @@ module.exports = {
obj,
};
`;
- SUtils.writeFileSync(path.join(tmpDirPath, pathAsyncLoadJs), fileContent);
- serverless.config.update({ servicePath: tmpDirPath });
- };
- it('should populate any given variable only once', () => {
- makeAsyncLoadJs();
- service.custom = {
- val1: '${self:custom.val0}',
- val2: '${self:custom.val1}',
- val0: `\${file(${pathAsyncLoadJs}):str}`,
- };
- const expected = {
- val1: 'my-async-value-1',
- val2: 'my-async-value-1',
- val0: 'my-async-value-1',
- };
- return expect(serverless.variables.populateObject(service.custom).then((result) => {
- expect(result).to.eql(expected);
- })).to.be.fulfilled;
- });
- it('should populate any given variable only once regardless of ordering or reference count',
- () => {
- makeAsyncLoadJs();
+ it('should populate any given variable only once', () => {
+ makeTempFile(asyncFileName, asyncContent);
service.custom = {
- val9: '${self:custom.val7}',
- val7: '${self:custom.val5}',
- val5: '${self:custom.val3}',
- val3: '${self:custom.val1}',
- val1: '${self:custom.val0}',
- val2: '${self:custom.val1}',
- val4: '${self:custom.val3}',
- val6: '${self:custom.val5}',
- val8: '${self:custom.val7}',
- val0: `\${file(${pathAsyncLoadJs}):str}`,
+ val1: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
+ val2: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string
+ val0: `\${file(${asyncFileName}):str}`,
};
const expected = {
- val9: 'my-async-value-1',
- val7: 'my-async-value-1',
- val5: 'my-async-value-1',
- val3: 'my-async-value-1',
val1: 'my-async-value-1',
val2: 'my-async-value-1',
- val4: 'my-async-value-1',
- val6: 'my-async-value-1',
- val8: 'my-async-value-1',
val0: 'my-async-value-1',
};
- return expect(serverless.variables.populateObject(service.custom).then((result) => {
- expect(result).to.eql(expected);
- })).to.be.fulfilled;
- }
- );
- it('should populate async objects with contained variables',
- () => {
- makeAsyncLoadJs();
- serverless.variables.options = {
- stage: 'dev',
- };
- service.custom = {
- obj: `\${file(${pathAsyncLoadJs}):obj}`,
- };
- const expected = {
- obj: {
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should populate any given variable only once regardless of ordering or reference count',
+ () => {
+ makeTempFile(asyncFileName, asyncContent);
+ service.custom = {
+ val9: '${self:custom.val7}', // eslint-disable-line no-template-curly-in-string
+ val7: '${self:custom.val5}', // eslint-disable-line no-template-curly-in-string
+ val5: '${self:custom.val3}', // eslint-disable-line no-template-curly-in-string
+ val3: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string
+ val1: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
+ val2: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string
+ val4: '${self:custom.val3}', // eslint-disable-line no-template-curly-in-string
+ val6: '${self:custom.val5}', // eslint-disable-line no-template-curly-in-string
+ val8: '${self:custom.val7}', // eslint-disable-line no-template-curly-in-string
+ val0: `\${file(${asyncFileName}):str}`,
+ };
+ const expected = {
+ val9: 'my-async-value-1',
+ val7: 'my-async-value-1',
+ val5: 'my-async-value-1',
+ val3: 'my-async-value-1',
+ val1: 'my-async-value-1',
+ val2: 'my-async-value-1',
+ val4: 'my-async-value-1',
+ val6: 'my-async-value-1',
+ val8: 'my-async-value-1',
val0: 'my-async-value-1',
- val1: 'dev',
- },
- };
- return expect(serverless.variables.populateObject(service.custom).then((result) => {
- expect(result).to.eql(expected);
- })).to.be.fulfilled;
- }
- );
- const pathEmptyJs = 'empty.js';
- const makeEmptyJs = () => {
- const SUtils = new Utils();
- const tmpDirPath = testUtils.getTmpDirPath();
- const fileContent = `'use strict';
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ it('should populate async objects with contained variables',
+ () => {
+ makeTempFile(asyncFileName, asyncContent);
+ serverless.variables.options = {
+ stage: 'dev',
+ };
+ service.custom = {
+ obj: `\${file(${asyncFileName}):obj}`,
+ };
+ const expected = {
+ obj: {
+ val0: 'my-async-value-1',
+ val1: 'dev',
+ },
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ const selfFileName = 'self.yml';
+ const selfContent = `foo: baz
+bar: \${self:custom.self.foo}
+`;
+ it('should populate a "cyclic" reference across an unresolved dependency (issue #4687)',
+ () => {
+ makeTempFile(selfFileName, selfContent);
+ service.custom = {
+ self: `\${file(${selfFileName})}`,
+ };
+ const expected = {
+ self: {
+ foo: 'baz',
+ bar: 'baz',
+ },
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.become(expected);
+ });
+ const emptyFileName = 'empty.js';
+ const emptyContent = `'use strict';
module.exports = {
func: () => ({ value: 'a value' }),
}
`;
- SUtils.writeFileSync(path.join(tmpDirPath, pathEmptyJs), fileContent);
- serverless.config.update({ servicePath: tmpDirPath });
- };
- it('should reject population of an attribute not exported from a file',
- () => {
- makeEmptyJs();
- service.custom = {
- val: `\${file(${pathEmptyJs}):func.notAValue}`,
- };
- return expect(serverless.variables.populateObject(service.custom))
- .to.eventually.be.rejected;
- }
- );
+ it('should reject population of an attribute not exported from a file',
+ () => {
+ makeTempFile(emptyFileName, emptyContent);
+ service.custom = {
+ val: `\${file(${emptyFileName}):func.notAValue}`,
+ };
+ return serverless.variables.populateObject(service.custom)
+ .should.be.rejectedWith(serverless.classes.Error,
+ 'Invalid variable syntax when referencing file');
+ });
+ });
});
});
describe('#populateProperty()', () => {
- let serverless;
- let overwriteStub;
- let populateObjectStub;
- let getValueFromSourceStub;
- let populateVariableStub;
-
beforeEach(() => {
- serverless = new Serverless();
- overwriteStub = sinon.stub(serverless.variables, 'overwrite');
- populateObjectStub = sinon.stub(serverless.variables, 'populateObject');
- getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
- populateVariableStub = sinon.stub(serverless.variables, 'populateVariable');
- });
-
- afterEach(() => {
- serverless.variables.overwrite.restore();
- serverless.variables.populateObject.restore();
- serverless.variables.getValueFromSource.restore();
- serverless.variables.populateVariable.restore();
+ serverless.variables.loadVariableSyntax();
});
it('should call overwrite if overwrite syntax provided', () => {
+ // eslint-disable-next-line no-template-curly-in-string
const property = 'my stage is ${opt:stage, self:provider.stage}';
-
- serverless.variables.loadVariableSyntax();
-
- overwriteStub.resolves('dev');
- populateVariableStub.resolves('my stage is dev');
-
- return serverless.variables.populateProperty(property).then(newProperty => {
- expect(overwriteStub.called).to.equal(true);
- expect(populateVariableStub.called).to.equal(true);
- expect(newProperty).to.equal('my stage is dev');
-
- return BbPromise.resolve();
- });
+ serverless.variables.options = { stage: 'dev' };
+ serverless.service.provider.stage = 'prod';
+ return serverless.variables.populateProperty(property)
+ .should.eventually.eql('my stage is dev');
});
it('should allow a single-quoted string if overwrite syntax provided', () => {
+ // eslint-disable-next-line no-template-curly-in-string
const property = "my stage is ${opt:stage, 'prod'}";
-
- serverless.variables.loadVariableSyntax();
-
- overwriteStub.resolves('\'prod\'');
- populateVariableStub.resolves('my stage is prod');
-
- return expect(serverless.variables.populateProperty(property)).to.be.fulfilled
- .then(newProperty => expect(newProperty).to.equal('my stage is prod'));
+ serverless.variables.options = {};
+ return serverless.variables.populateProperty(property)
+ .should.eventually.eql('my stage is prod');
});
it('should allow a double-quoted string if overwrite syntax provided', () => {
+ // eslint-disable-next-line no-template-curly-in-string
const property = 'my stage is ${opt:stage, "prod"}';
-
- serverless.variables.loadVariableSyntax();
-
- overwriteStub.resolves('\'prod\'');
- populateVariableStub.resolves('my stage is prod');
-
- return expect(serverless.variables.populateProperty(property)).to.be.fulfilled
- .then(newProperty => expect(newProperty).to.equal('my stage is prod'));
+ serverless.variables.options = {};
+ return serverless.variables.populateProperty(property)
+ .should.eventually.eql('my stage is prod');
});
it('should call getValueFromSource if no overwrite syntax provided', () => {
+ // eslint-disable-next-line no-template-curly-in-string
const property = 'my stage is ${opt:stage}';
-
- serverless.variables.loadVariableSyntax();
-
- getValueFromSourceStub.resolves('prod');
- populateVariableStub.resolves('my stage is prod');
-
- return serverless.variables.populateProperty(property).then(newProperty => {
- expect(getValueFromSourceStub.called).to.be.true;
- expect(populateVariableStub.called).to.be.true;
- expect(newProperty).to.equal('my stage is prod');
-
- return BbPromise.resolve();
- });
- });
-
- it('should NOT call populateObject if variable value is a circular object', () => {
- serverless.variables.options = {
- stage: 'prod',
- };
- const property = '${opt:stage}';
- const variableValue = {
- stage: '${opt:stage}',
- };
- const variableValuePopulated = {
- stage: 'prod',
- };
-
- serverless.variables.cache['opt:stage'] = variableValuePopulated;
-
- serverless.variables.loadVariableSyntax();
-
- populateObjectStub.resolves(variableValuePopulated);
- getValueFromSourceStub.resolves(variableValue);
- populateVariableStub.resolves(variableValuePopulated);
-
- return serverless.variables.populateProperty(property).then(newProperty => {
- expect(populateObjectStub.called).to.equal(false);
- expect(getValueFromSourceStub.called).to.equal(true);
- expect(populateVariableStub.called).to.equal(true);
- expect(newProperty).to.deep.equal(variableValuePopulated);
-
- return BbPromise.resolve();
- });
+ serverless.variables.options = { stage: 'prod' };
+ return serverless.variables.populateProperty(property)
+ .should.eventually.eql('my stage is prod');
});
it('should warn if an SSM parameter does not exist', () => {
- const awsProvider = new AwsProvider(serverless, { stage: 'prod', region: 'us-west-2' });
- const param = '/some/path/to/invalidparam';
- const property = `\${ssm:${param}}`;
- const error = new Error(`Parameter ${param} not found.`);
-
- serverless.variables.options = {
+ const options = {
stage: 'prod',
region: 'us-east-1',
};
- serverless.variables.loadVariableSyntax();
-
- serverless.variables.getValueFromSource.restore();
- serverless.variables.populateVariable.restore();
+ serverless.variables.options = options;
+ const awsProvider = new AwsProvider(serverless, options);
+ const param = '/some/path/to/invalidparam';
+ const property = `\${ssm:${param}}`;
+ const error = new Error(`Parameter ${param} not found.`, 123);
const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
const warnIfNotFoundSpy = sinon.spy(serverless.variables, 'warnIfNotFound');
-
- return expect(serverless.variables.populateProperty(property)
- .then(newProperty => {
+ return serverless.variables.populateProperty(property)
+ .should.become(undefined)
+ .then(() => {
expect(requestStub.callCount).to.equal(1);
expect(warnIfNotFoundSpy.callCount).to.equal(1);
- expect(newProperty).to.be.undefined;
})
.finally(() => {
- getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
- populateVariableStub = sinon.stub(serverless.variables, 'populateVariable');
- })).to.be.fulfilled;
+ requestStub.restore();
+ warnIfNotFoundSpy.restore();
+ });
});
it('should throw an Error if the SSM request fails', () => {
- const awsProvider = new AwsProvider(serverless, { stage: 'prod', region: 'us-west-2' });
- const param = '/some/path/to/invalidparam';
- const property = `\${ssm:${param}}`;
- const error = new Error('Some random failure.');
-
- serverless.variables.options = {
+ const options = {
stage: 'prod',
region: 'us-east-1',
};
- serverless.variables.loadVariableSyntax();
-
- serverless.variables.getValueFromSource.restore();
+ serverless.variables.options = options;
+ const awsProvider = new AwsProvider(serverless, options);
+ const param = '/some/path/to/invalidparam';
+ const property = `\${ssm:${param}}`;
+ const error = new serverless.classes.Error('Some random failure.', 123);
const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
-
- return expect(serverless.variables.populateProperty(property)
- .finally(() => {
- getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
- expect(requestStub.callCount).to.equal(1);
- })).to.be.rejectedWith(serverless.classes.Error);
+ return serverless.variables.populateProperty(property)
+ .should.be.rejectedWith(serverless.classes.Error)
+ .then(() => expect(requestStub.callCount).to.equal(1))
+ .finally(() => requestStub.restore());
});
it('should run recursively if nested variables provided', () => {
- const property = 'my stage is ${env:${opt.name}}';
-
- serverless.variables.loadVariableSyntax();
-
- getValueFromSourceStub.onCall(0).resolves('stage');
- getValueFromSourceStub.onCall(1).resolves('dev');
- populateVariableStub.onCall(0).resolves('my stage is ${env:stage}');
- populateVariableStub.onCall(1).resolves('my stage is dev');
-
- return serverless.variables.populateProperty(property).then(newProperty => {
- expect(getValueFromSourceStub.callCount).to.equal(2);
- expect(populateVariableStub.callCount).to.equal(2);
- expect(newProperty).to.equal('my stage is dev');
- });
+ // eslint-disable-next-line no-template-curly-in-string
+ const property = 'my stage is ${env:${opt:name}}';
+ process.env.TEST_VAR = 'dev';
+ serverless.variables.options = { name: 'TEST_VAR' };
+ return serverless.variables.populateProperty(property)
+ .should.eventually.eql('my stage is dev')
+ .then().finally(() => { delete process.env.TEST_VAR; });
+ });
+ it('should run recursively through many nested variables', () => {
+ // eslint-disable-next-line no-template-curly-in-string
+ const property = 'my stage is ${env:${opt:name}}';
+ process.env.TEST_VAR = 'dev';
+ serverless.variables.options = {
+ name: 'T${opt:lvl0}',
+ lvl0: 'E${opt:lvl1}',
+ lvl1: 'S${opt:lvl2}',
+ lvl2: 'T${opt:lvl3}',
+ lvl3: '_${opt:lvl4}',
+ lvl4: 'V${opt:lvl5}',
+ lvl5: 'A${opt:lvl6}',
+ lvl6: 'R',
+ };
+ return serverless.variables.populateProperty(property)
+ .should.eventually.eql('my stage is dev')
+ .then().finally(() => { delete process.env.TEST_VAR; });
});
});
describe('#populateVariable()', () => {
it('should populate string variables as sub string', () => {
- const serverless = new Serverless();
const valueToPopulate = 'dev';
- const matchedString = '${opt:stage}';
+ const matchedString = '${opt:stage}'; // eslint-disable-line no-template-curly-in-string
+ // eslint-disable-next-line no-template-curly-in-string
const property = 'my stage is ${opt:stage}';
-
- return serverless.variables.populateVariable(property, matchedString, valueToPopulate)
- .then(newProperty => {
- expect(newProperty).to.equal('my stage is dev');
- });
+ serverless.variables.populateVariable(property, matchedString, valueToPopulate)
+ .should.eql('my stage is dev');
});
it('should populate number variables as sub string', () => {
- const serverless = new Serverless();
const valueToPopulate = 5;
- const matchedString = '${opt:number}';
+ const matchedString = '${opt:number}'; // eslint-disable-line no-template-curly-in-string
+ // eslint-disable-next-line no-template-curly-in-string
const property = 'your account number is ${opt:number}';
-
- return serverless.variables.populateVariable(property, matchedString, valueToPopulate)
- .then(newProperty => {
- expect(newProperty).to.equal('your account number is 5');
- });
+ serverless.variables.populateVariable(property, matchedString, valueToPopulate)
+ .should.eql('your account number is 5');
});
it('should populate non string variables', () => {
- const serverless = new Serverless();
const valueToPopulate = 5;
- const matchedString = '${opt:number}';
- const property = '${opt:number}';
-
+ const matchedString = '${opt:number}'; // eslint-disable-line no-template-curly-in-string
+ const property = '${opt:number}'; // eslint-disable-line no-template-curly-in-string
return serverless.variables.populateVariable(property, matchedString, valueToPopulate)
- .then(newProperty => {
- expect(newProperty).to.equal(5);
- });
+ .should.equal(5);
});
it('should throw error if populating non string or non number variable as sub string', () => {
- const serverless = new Serverless();
const valueToPopulate = {};
- const matchedString = '${opt:object}';
+ const matchedString = '${opt:object}'; // eslint-disable-line no-template-curly-in-string
+ // eslint-disable-next-line no-template-curly-in-string
const property = 'your account number is ${opt:object}';
- expect(() => serverless.variables
- .populateVariable(property, matchedString, valueToPopulate))
- .to.throw(Error);
+ return expect(() =>
+ serverless.variables.populateVariable(property, matchedString, valueToPopulate))
+ .to.throw(serverless.classes.Error);
+ });
+ });
+
+ describe('#splitByComma', () => {
+ it('should return a given empty string', () => {
+ const input = '';
+ const expected = [input];
+ expect(serverless.variables.splitByComma(input)).to.eql(expected);
+ });
+ it('should return a undelimited string', () => {
+ const input = 'foo:bar';
+ const expected = [input];
+ expect(serverless.variables.splitByComma(input)).to.eql(expected);
+ });
+ it('should split basic comma delimited strings', () => {
+ const input = 'my,values,to,split';
+ const expected = ['my', 'values', 'to', 'split'];
+ expect(serverless.variables.splitByComma(input)).to.eql(expected);
+ });
+ it('should remove leading and following white space', () => {
+ const input = ' \t\nfoobar\n\t ';
+ const expected = ['foobar'];
+ expect(serverless.variables.splitByComma(input)).to.eql(expected);
+ });
+ it('should remove white space surrounding commas', () => {
+ const input = 'a,b ,c , d, e , f\t,g\n,h,\ti,\nj,\t\n , \n\tk';
+ const expected = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'];
+ expect(serverless.variables.splitByComma(input)).to.eql(expected);
+ });
+ it('should ignore quoted commas', () => {
+ const input = '",", \',\', ",\', \',\'", "\',\', \',\'", \',", ","\', \'",", ","\'';
+ const expected = [
+ '","',
+ '\',\'',
+ '",\', \',\'"',
+ '"\',\', \',\'"',
+ '\',", ","\'',
+ '\'",", ","\'',
+ ];
+ expect(serverless.variables.splitByComma(input)).to.eql(expected);
+ });
+ it('should deal with a combination of these cases', () => {
+ const input = ' \t\n\'a\'\t\n , \n\t"foo,bar", opt:foo, ",", \',\', "\',\', \',\'", foo\n\t ';
+ const expected = ['\'a\'', '"foo,bar"', 'opt:foo', '","', '\',\'', '"\',\', \',\'"', 'foo'];
+ expect(serverless.variables.splitByComma(input)).to.eql(expected);
});
});
describe('#overwrite()', () => {
it('should overwrite undefined and null values', () => {
- const serverless = new Serverless();
- const getValueFromSourceStub = sinon
- .stub(serverless.variables, 'getValueFromSource');
-
+ const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
getValueFromSourceStub.onCall(0).resolves(undefined);
getValueFromSourceStub.onCall(1).resolves(null);
getValueFromSourceStub.onCall(2).resolves('variableValue');
-
- return serverless.variables.overwrite('opt:stage,env:stage,self:provider.stage')
- .then(valueToPopulate => {
+ return serverless.variables.overwrite(['opt:stage', 'env:stage', 'self:provider.stage'])
+ .should.be.fulfilled
+ .then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromSourceStub).to.have.been.calledThrice;
})
- .finally(() => serverless.variables.getValueFromSource.restore());
+ .finally(() => getValueFromSourceStub.restore());
});
it('should overwrite empty object values', () => {
- const serverless = new Serverless();
- const getValueFromSourceStub = sinon
- .stub(serverless.variables, 'getValueFromSource');
-
+ const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
getValueFromSourceStub.onCall(0).resolves({});
getValueFromSourceStub.onCall(1).resolves('variableValue');
-
- return serverless.variables.overwrite('opt:stage,env:stage').then(valueToPopulate => {
- expect(valueToPopulate).to.equal('variableValue');
- expect(getValueFromSourceStub).to.have.been.calledTwice;
- })
- .finally(() => serverless.variables.getValueFromSource.restore());
+ return serverless.variables.overwrite(['opt:stage', 'env:stage']).should.be.fulfilled
+ .then((valueToPopulate) => {
+ expect(valueToPopulate).to.equal('variableValue');
+ expect(getValueFromSourceStub).to.have.been.calledTwice;
+ })
+ .finally(() => getValueFromSourceStub.restore());
});
it('should not overwrite 0 values', () => {
- const serverless = new Serverless();
- const getValueFromSourceStub = sinon
- .stub(serverless.variables, 'getValueFromSource');
-
+ const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
getValueFromSourceStub.onCall(0).resolves(0);
getValueFromSourceStub.onCall(1).resolves('variableValue');
- getValueFromSourceStub.onCall(2).resolves('variableValue2');
- return serverless.variables.overwrite('opt:stage,env:stage,self:provider.stage')
- .then(valueToPopulate => {
- expect(valueToPopulate).to.equal(0);
- })
- .finally(() => serverless.variables.getValueFromSource.restore());
+ return serverless.variables.overwrite(['opt:stage', 'env:stage']).should.become(0)
+ .then().finally(() => getValueFromSourceStub.restore());
});
it('should not overwrite false values', () => {
- const serverless = new Serverless();
- const getValueFromSourceStub = sinon
- .stub(serverless.variables, 'getValueFromSource');
-
+ const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
getValueFromSourceStub.onCall(0).resolves(false);
getValueFromSourceStub.onCall(1).resolves('variableValue');
- getValueFromSourceStub.onCall(2).resolves('variableValue2');
-
- return serverless.variables.overwrite('opt:stage,env:stage,self:provider.stage')
- .then(valueToPopulate => {
- expect(valueToPopulate).to.be.false;
- })
- .finally(() => serverless.variables.getValueFromSource.restore());
+ return serverless.variables.overwrite(['opt:stage', 'env:stage']).should.become(false)
+ .then().finally(() => getValueFromSourceStub.restore());
});
it('should skip getting values once a value has been found', () => {
- const serverless = new Serverless();
- const getValueFromSourceStub = sinon
- .stub(serverless.variables, 'getValueFromSource');
-
+ const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
getValueFromSourceStub.onCall(0).resolves(undefined);
getValueFromSourceStub.onCall(1).resolves('variableValue');
getValueFromSourceStub.onCall(2).resolves('variableValue2');
-
- return serverless.variables.overwrite('opt:stage,env:stage,self:provider.stage')
- .then(valueToPopulate => {
- expect(valueToPopulate).to.equal('variableValue');
- })
- .finally(() => serverless.variables.getValueFromSource.restore());
+ return serverless.variables.overwrite(['opt:stage', 'env:stage', 'self:provider.stage'])
+ .should.be.fulfilled
+ .then(valueToPopulate => expect(valueToPopulate).to.equal('variableValue'))
+ .finally(() => getValueFromSourceStub.restore());
+ });
+ it('should properly handle string values containing commas', () => {
+ const str = '"foo,bar"';
+ const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource')
+ .resolves(undefined);
+ return serverless.variables.overwrite(['opt:stage', str])
+ .should.be.fulfilled
+ .then(() => expect(getValueFromSourceStub.getCall(1).args[0]).to.eql(str))
+ .finally(() => getValueFromSourceStub.restore());
});
});
describe('#getValueFromSource()', () => {
it('should call getValueFromEnv if referencing env var', () => {
- const serverless = new Serverless();
- const getValueFromEnvStub = sinon
- .stub(serverless.variables, 'getValueFromEnv').resolves('variableValue');
- return serverless.variables.getValueFromSource('env:TEST_VAR')
- .then(valueToPopulate => {
+ const getValueFromEnvStub = sinon.stub(serverless.variables, 'getValueFromEnv')
+ .resolves('variableValue');
+ return serverless.variables.getValueFromSource('env:TEST_VAR').should.be.fulfilled
+ .then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromEnvStub).to.have.been.called;
- expect(getValueFromEnvStub.calledWith('env:TEST_VAR')).to.equal(true);
+ expect(getValueFromEnvStub).to.have.been.calledWith('env:TEST_VAR');
})
- .finally(() => serverless.variables.getValueFromEnv.restore());
+ .finally(() => getValueFromEnvStub.restore());
});
it('should call getValueFromOptions if referencing an option', () => {
- const serverless = new Serverless();
const getValueFromOptionsStub = sinon
.stub(serverless.variables, 'getValueFromOptions')
.resolves('variableValue');
-
- return serverless.variables.getValueFromSource('opt:stage')
- .then(valueToPopulate => {
+ return serverless.variables.getValueFromSource('opt:stage').should.be.fulfilled
+ .then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromOptionsStub).to.have.been.called;
- expect(getValueFromOptionsStub.calledWith('opt:stage')).to.equal(true);
+ expect(getValueFromOptionsStub).to.have.been.calledWith('opt:stage');
})
- .finally(() => serverless.variables.getValueFromOptions.restore());
+ .finally(() => getValueFromOptionsStub.restore());
});
it('should call getValueFromSelf if referencing from self', () => {
- const serverless = new Serverless();
- const getValueFromSelfStub = sinon
- .stub(serverless.variables, 'getValueFromSelf').resolves('variableValue');
-
- return serverless.variables.getValueFromSource('self:provider')
- .then(valueToPopulate => {
+ const getValueFromSelfStub = sinon.stub(serverless.variables, 'getValueFromSelf')
+ .resolves('variableValue');
+ return serverless.variables.getValueFromSource('self:provider').should.be.fulfilled
+ .then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromSelfStub).to.have.been.called;
- expect(getValueFromSelfStub.calledWith('self:provider')).to.equal(true);
+ expect(getValueFromSelfStub).to.have.been.calledWith('self:provider');
})
- .finally(() => serverless.variables.getValueFromSelf.restore());
+ .finally(() => getValueFromSelfStub.restore());
});
it('should call getValueFromFile if referencing from another file', () => {
- const serverless = new Serverless();
- const getValueFromFileStub = sinon
- .stub(serverless.variables, 'getValueFromFile').resolves('variableValue');
-
- return serverless.variables.getValueFromSource('file(./config.yml)')
- .then(valueToPopulate => {
+ const getValueFromFileStub = sinon.stub(serverless.variables, 'getValueFromFile')
+ .resolves('variableValue');
+ return serverless.variables.getValueFromSource('file(./config.yml)').should.be.fulfilled
+ .then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromFileStub).to.have.been.called;
expect(getValueFromFileStub).to.have.been.calledWith('file(./config.yml)');
})
- .finally(() => serverless.variables.getValueFromFile.restore());
+ .finally(() => getValueFromFileStub.restore());
});
it('should call getValueFromCf if referencing CloudFormation Outputs', () => {
- const serverless = new Serverless();
- const getValueFromCfStub = sinon
- .stub(serverless.variables, 'getValueFromCf').resolves('variableValue');
- return serverless.variables.getValueFromSource('cf:test-stack.testOutput')
- .then(valueToPopulate => {
+ const getValueFromCfStub = sinon.stub(serverless.variables, 'getValueFromCf')
+ .resolves('variableValue');
+ return serverless.variables.getValueFromSource('cf:test-stack.testOutput').should.be.fulfilled
+ .then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromCfStub).to.have.been.called;
expect(getValueFromCfStub).to.have.been.calledWith('cf:test-stack.testOutput');
})
- .finally(() => serverless.variables.getValueFromCf.restore());
+ .finally(() => getValueFromCfStub.restore());
});
it('should call getValueFromS3 if referencing variable in S3', () => {
- const serverless = new Serverless();
- const getValueFromS3Stub = sinon
- .stub(serverless.variables, 'getValueFromS3').resolves('variableValue');
+ const getValueFromS3Stub = sinon.stub(serverless.variables, 'getValueFromS3')
+ .resolves('variableValue');
return serverless.variables.getValueFromSource('s3:test-bucket/path/to/key')
- .then(valueToPopulate => {
- expect(valueToPopulate).to.equal('variableValue');
- expect(getValueFromS3Stub).to.have.been.called;
- expect(getValueFromS3Stub).to.have.been.calledWith('s3:test-bucket/path/to/key');
- })
- .finally(() => serverless.variables.getValueFromS3.restore());
+ .should.be.fulfilled
+ .then((valueToPopulate) => {
+ expect(valueToPopulate).to.equal('variableValue');
+ expect(getValueFromS3Stub).to.have.been.called;
+ expect(getValueFromS3Stub).to.have.been.calledWith('s3:test-bucket/path/to/key');
+ })
+ .finally(() => getValueFromS3Stub.restore());
});
it('should call getValueFromSsm if referencing variable in SSM', () => {
- const serverless = new Serverless();
- const getValueFromSsmStub = sinon
- .stub(serverless.variables, 'getValueFromSsm').resolves('variableValue');
+ const getValueFromSsmStub = sinon.stub(serverless.variables, 'getValueFromSsm')
+ .resolves('variableValue');
return serverless.variables.getValueFromSource('ssm:/test/path/to/param')
- .then(valueToPopulate => {
- expect(valueToPopulate).to.equal('variableValue');
- expect(getValueFromSsmStub).to.have.been.called;
- expect(getValueFromSsmStub).to.have.been.calledWith('ssm:/test/path/to/param');
- })
- .finally(() => serverless.variables.getValueFromSsm.restore());
+ .should.be.fulfilled
+ .then((valueToPopulate) => {
+ expect(valueToPopulate).to.equal('variableValue');
+ expect(getValueFromSsmStub).to.have.been.called;
+ expect(getValueFromSsmStub).to.have.been.calledWith('ssm:/test/path/to/param');
+ })
+ .finally(() => getValueFromSsmStub.restore());
});
-
+ it('should reject invalid sources', () =>
+ serverless.variables.getValueFromSource('weird:source')
+ .should.be.rejectedWith(serverless.classes.Error));
describe('caching', () => {
const sources = [
{ function: 'getValueFromEnv', variableString: 'env:NODE_ENV' },
@@ -853,182 +1140,106 @@ module.exports = {
];
sources.forEach((source) => {
it(`should only call ${source.function} once, returning the cached value otherwise`, () => {
- const serverless = new Serverless();
- const getValueFunctionStub = sinon
- .stub(serverless.variables, source.function).resolves('variableValue');
- const firstCall = serverless.variables.getValueFromSource(source.variableString);
- const secondCall = BbPromise.delay(100)
- .then(() => serverless.variables.getValueFromSource(source.variableString));
- return BbPromise.all([firstCall, secondCall])
- .then(valueToPopulate => {
- expect(valueToPopulate).to.deep.equal(['variableValue', 'variableValue']);
+ const value = 'variableValue';
+ const getValueFunctionStub = sinon.stub(serverless.variables, source.function)
+ .resolves(value);
+ return BbPromise.all([
+ serverless.variables.getValueFromSource(source.variableString).should.become(value),
+ BbPromise.delay(100).then(() =>
+ serverless.variables.getValueFromSource(source.variableString).should.become(value)),
+ ]).then(() => {
expect(getValueFunctionStub).to.have.been.calledOnce;
expect(getValueFunctionStub).to.have.been.calledWith(source.variableString);
- })
- .finally(() => serverless.variables[source.function].restore());
+ }).finally(() =>
+ getValueFunctionStub.restore());
});
});
});
-
- it('should call populateObject if variable value is an object', () => {
- const serverless = new Serverless();
- serverless.variables.options = {
- stage: 'prod',
- };
- const property = 'self:stage';
- const variableValue = {
- stage: '${opt:stage}',
- };
- const variableValuePopulated = {
- stage: 'prod',
- };
-
- serverless.variables.loadVariableSyntax();
-
- const populateObjectStub = sinon
- .stub(serverless.variables, 'populateObject')
- .resolves(variableValuePopulated);
- const getValueFromSelfStub = sinon
- .stub(serverless.variables, 'getValueFromSelf')
- .resolves(variableValue);
-
- return serverless.variables.getValueFromSource(property)
- .then(newProperty => {
- expect(populateObjectStub.called).to.equal(true);
- expect(getValueFromSelfStub.called).to.equal(true);
- expect(newProperty).to.deep.equal(variableValuePopulated);
-
- return BbPromise.resolve();
- })
- .finally(() => {
- serverless.variables.populateObject.restore();
- serverless.variables.getValueFromSelf.restore();
- });
- });
-
- it('should NOT call populateObject if variable value is already cached', () => {
- const serverless = new Serverless();
- serverless.variables.options = {
- stage: 'prod',
- };
- const property = 'opt:stage';
- const variableValue = {
- stage: '${opt:stage}',
- };
- const variableValuePopulated = {
- stage: 'prod',
- };
-
- serverless.variables.cache['opt:stage'] = BbPromise.resolve(variableValuePopulated);
-
- serverless.variables.loadVariableSyntax();
-
- const populateObjectStub = sinon
- .stub(serverless.variables, 'populateObject')
- .resolves(variableValuePopulated);
- const getValueFromOptionsStub = sinon
- .stub(serverless.variables, 'getValueFromOptions')
- .resolves(variableValue);
-
- return serverless.variables.getValueFromSource(property)
- .then(newProperty => {
- expect(populateObjectStub.called).to.equal(false);
- expect(getValueFromOptionsStub.called).to.equal(false);
- expect(newProperty).to.deep.equal(variableValuePopulated);
-
- return BbPromise.resolve();
- })
- .finally(() => {
- serverless.variables.populateObject.restore();
- serverless.variables.getValueFromOptions.restore();
- });
- });
-
- it('should throw error if referencing an invalid source', () => {
- const serverless = new Serverless();
- expect(() => serverless.variables.getValueFromSource('weird:source'))
- .to.throw(Error);
- });
});
describe('#getValueFromEnv()', () => {
it('should get variable from environment variables', () => {
- const serverless = new Serverless();
process.env.TEST_VAR = 'someValue';
- return serverless.variables.getValueFromEnv('env:TEST_VAR').then(valueToPopulate => {
- expect(valueToPopulate).to.be.equal('someValue');
- })
- .finally(() => {
- delete process.env.TEST_VAR;
- });
+ return serverless.variables.getValueFromEnv('env:TEST_VAR')
+ .finally(() => { delete process.env.TEST_VAR; })
+ .should.become('someValue');
});
it('should allow top-level references to the environment variables hive', () => {
- const serverless = new Serverless();
process.env.TEST_VAR = 'someValue';
- return serverless.variables.getValueFromEnv('env:').then(valueToPopulate => {
+ return serverless.variables.getValueFromEnv('env:').then((valueToPopulate) => {
expect(valueToPopulate.TEST_VAR).to.be.equal('someValue');
})
- .finally(() => {
- delete process.env.TEST_VAR;
- });
+ .finally(() => { delete process.env.TEST_VAR; });
});
});
describe('#getValueFromOptions()', () => {
it('should get variable from options', () => {
- const serverless = new Serverless();
- serverless.variables.options = {
- stage: 'prod',
- };
- return serverless.variables.getValueFromOptions('opt:stage').then(valueToPopulate => {
- expect(valueToPopulate).to.be.equal('prod');
- });
+ serverless.variables.options = { stage: 'prod' };
+ return serverless.variables.getValueFromOptions('opt:stage').should.become('prod');
});
it('should allow top-level references to the options hive', () => {
- const serverless = new Serverless();
- serverless.variables.options = {
- stage: 'prod',
- };
- return serverless.variables.getValueFromOptions('opt:').then(valueToPopulate => {
- expect(valueToPopulate.stage).to.be.equal('prod');
- });
+ serverless.variables.options = { stage: 'prod' };
+ return serverless.variables.getValueFromOptions('opt:')
+ .should.become(serverless.variables.options);
});
});
describe('#getValueFromSelf()', () => {
+ beforeEach(() => {
+ serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}';
+ serverless.variables.loadVariableSyntax();
+ delete serverless.service.provider.variableSyntax;
+ });
it('should get variable from self serverless.yml file', () => {
- const serverless = new Serverless();
serverless.variables.service = {
service: 'testService',
provider: serverless.service.provider,
};
- serverless.variables.loadVariableSyntax();
- return serverless.variables.getValueFromSelf('self:service').then(valueToPopulate => {
- expect(valueToPopulate).to.be.equal('testService');
- });
+ return serverless.variables.getValueFromSelf('self:service').should.become('testService');
+ });
+ it('should redirect ${self:service.name} to ${self:service}', () => {
+ serverless.variables.service = {
+ service: 'testService',
+ provider: serverless.service.provider,
+ };
+ return serverless.variables.getValueFromSelf('self:service.name')
+ .should.become('testService');
+ });
+ it('should redirect ${self:provider} to ${self:provider.name}', () => {
+ serverless.variables.service = {
+ service: 'testService',
+ provider: { name: 'aws' },
+ };
+ return serverless.variables.getValueFromSelf('self:provider').should.become('aws');
+ });
+ it('should redirect ${self:service.awsKmsKeyArn} to ${self:serviceObject.awsKmsKeyArn}', () => {
+ const keyArn = 'arn:aws:kms:us-east-1:xxxxxxxxxxxx:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
+ serverless.variables.service = {
+ service: 'testService',
+ serviceObject: {
+ name: 'testService',
+ awsKmsKeyArn: keyArn,
+ },
+ };
+ return serverless.variables.getValueFromSelf('self:service.awsKmsKeyArn')
+ .should.become(keyArn);
});
-
it('should handle self-references to the root of the serverless.yml file', () => {
- const serverless = new Serverless();
serverless.variables.service = {
service: 'testService',
provider: 'testProvider',
defaults: serverless.service.defaults,
};
-
- serverless.variables.loadVariableSyntax();
-
- return serverless.variables.getValueFromSelf('self:').then(valueToPopulate => {
- expect(valueToPopulate.provider).to.be.equal('testProvider');
- });
+ return serverless.variables.getValueFromSelf('self:')
+ .should.eventually.equal(serverless.variables.service);
});
});
describe('#getValueFromFile()', () => {
it('should work for absolute paths with ~ ', () => {
- const serverless = new Serverless();
const expectedFileName = `${os.homedir()}/somedir/config.yml`;
const configYml = {
test: 1,
@@ -1038,17 +1249,11 @@ module.exports = {
prob: 'prob',
},
};
- const fileExistsStub = sinon
- .stub(serverless.utils, 'fileExistsSync').returns(true);
-
- const realpathSync = sinon
- .stub(fse, 'realpathSync').returns(expectedFileName);
-
- const readFileSyncStub = sinon
- .stub(serverless.utils, 'readFileSync').returns(configYml);
-
- return serverless.variables.getValueFromFile('file(~/somedir/config.yml)')
- .then(valueToPopulate => {
+ const fileExistsStub = sinon.stub(serverless.utils, 'fileExistsSync').returns(true);
+ const realpathSync = sinon.stub(fse, 'realpathSync').returns(expectedFileName);
+ const readFileSyncStub = sinon.stub(serverless.utils, 'readFileSync').returns(configYml);
+ return serverless.variables.getValueFromFile('file(~/somedir/config.yml)').should.be.fulfilled
+ .then((valueToPopulate) => {
expect(realpathSync).to.not.have.been.called;
expect(fileExistsStub).to.have.been.calledWithMatch(expectedFileName);
expect(readFileSyncStub).to.have.been.calledWithMatch(expectedFileName);
@@ -1062,7 +1267,6 @@ module.exports = {
});
it('should populate an entire variable file', () => {
- const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const configYml = {
@@ -1073,28 +1277,19 @@ module.exports = {
prob: 'prob',
},
};
-
- SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'),
- YAML.dump(configYml));
-
+ SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'), YAML.dump(configYml));
serverless.config.update({ servicePath: tmpDirPath });
-
- return serverless.variables.getValueFromFile('file(./config.yml)').then(valueToPopulate => {
- expect(valueToPopulate).to.deep.equal(configYml);
- });
+ return serverless.variables.getValueFromFile('file(./config.yml)')
+ .should.eventually.eql(configYml);
});
it('should get undefined if non existing file and the second argument is true', () => {
- const serverless = new Serverless();
const tmpDirPath = testUtils.getTmpDirPath();
-
serverless.config.update({ servicePath: tmpDirPath });
-
const realpathSync = sinon.spy(fse, 'realpathSync');
const existsSync = sinon.spy(fse, 'existsSync');
-
- return serverless.variables.getValueFromFile('file(./non-existing.yml)')
- .then(valueToPopulate => {
+ return serverless.variables.getValueFromFile('file(./non-existing.yml)').should.be.fulfilled
+ .then((valueToPopulate) => {
expect(realpathSync).to.not.have.been.called;
expect(existsSync).to.have.been.calledOnce;
expect(valueToPopulate).to.be.undefined;
@@ -1106,166 +1301,113 @@ module.exports = {
});
it('should populate non json/yml files', () => {
- const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
-
- SUtils.writeFileSync(path.join(tmpDirPath, 'someFile'),
- 'hello world');
-
+ SUtils.writeFileSync(path.join(tmpDirPath, 'someFile'), 'hello world');
serverless.config.update({ servicePath: tmpDirPath });
-
- return serverless.variables.getValueFromFile('file(./someFile)').then(valueToPopulate => {
- expect(valueToPopulate).to.equal('hello world');
- });
+ return serverless.variables.getValueFromFile('file(./someFile)')
+ .should.become('hello world');
});
it('should populate symlinks', () => {
- const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const realFilePath = path.join(tmpDirPath, 'someFile');
const symlinkPath = path.join(tmpDirPath, 'refSomeFile');
SUtils.writeFileSync(realFilePath, 'hello world');
fse.ensureSymlinkSync(realFilePath, symlinkPath);
-
serverless.config.update({ servicePath: tmpDirPath });
-
- return expect(serverless.variables.getValueFromFile('file(./refSomeFile)')).to.be.fulfilled
- .then(valueToPopulate => {
- expect(valueToPopulate).to.equal('hello world');
- })
- .finally(() => {
- fse.removeSync(realFilePath);
- fse.removeSync(symlinkPath);
- });
+ return serverless.variables.getValueFromFile('file(./refSomeFile)')
+ .should.become('hello world')
+ .then().finally(() => {
+ fse.removeSync(realFilePath);
+ fse.removeSync(symlinkPath);
+ });
});
it('should trim trailing whitespace and new line character', () => {
- const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
-
- SUtils.writeFileSync(path.join(tmpDirPath, 'someFile'),
- 'hello world \n');
-
+ SUtils.writeFileSync(path.join(tmpDirPath, 'someFile'), 'hello world \n');
serverless.config.update({ servicePath: tmpDirPath });
-
- return serverless.variables.getValueFromFile('file(./someFile)').then(valueToPopulate => {
- expect(valueToPopulate).to.equal('hello world');
- });
+ return serverless.variables.getValueFromFile('file(./someFile)')
+ .should.become('hello world');
});
it('should populate from another file when variable is of any type', () => {
- const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const configYml = {
- test: 1,
- test2: 'test2',
- testObj: {
+ test0: 0,
+ test1: 'test1',
+ test2: {
sub: 2,
prob: 'prob',
},
};
-
- SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'),
- YAML.dump(configYml));
-
+ SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'), YAML.dump(configYml));
serverless.config.update({ servicePath: tmpDirPath });
-
- return serverless.variables.getValueFromFile('file(./config.yml):testObj.sub')
- .then(valueToPopulate => {
- expect(valueToPopulate).to.equal(2);
- });
+ return serverless.variables.getValueFromFile('file(./config.yml):test2.sub')
+ .should.become(configYml.test2.sub);
});
it('should populate from a javascript file', () => {
- const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = 'module.exports.hello=function(){return "hello world";};';
-
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
-
serverless.config.update({ servicePath: tmpDirPath });
-
return serverless.variables.getValueFromFile('file(./hello.js):hello')
- .then(valueToPopulate => {
- expect(valueToPopulate).to.equal('hello world');
- });
+ .should.become('hello world');
});
it('should populate an entire variable exported by a javascript file', () => {
- const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = 'module.exports=function(){return { hello: "hello world" };};';
-
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
-
serverless.config.update({ servicePath: tmpDirPath });
-
return serverless.variables.getValueFromFile('file(./hello.js)')
- .then(valueToPopulate => {
- expect(valueToPopulate.hello).to.equal('hello world');
- });
+ .should.become({ hello: 'hello world' });
});
it('should throw if property exported by a javascript file is not a function', () => {
- const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = 'module.exports={ hello: "hello world" };';
-
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
-
serverless.config.update({ servicePath: tmpDirPath });
-
- expect(() => serverless.variables
- .getValueFromFile('file(./hello.js)')).to.throw(Error);
+ return serverless.variables.getValueFromFile('file(./hello.js)')
+ .should.be.rejectedWith(serverless.classes.Error);
});
it('should populate deep object from a javascript file', () => {
- const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = `module.exports.hello=function(){
return {one:{two:{three: 'hello world'}}}
};`;
-
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
-
serverless.config.update({ servicePath: tmpDirPath });
serverless.variables.loadVariableSyntax();
-
return serverless.variables.getValueFromFile('file(./hello.js):hello.one.two.three')
- .then(valueToPopulate => {
- expect(valueToPopulate).to.equal('hello world');
- });
+ .should.become('hello world');
});
it('should preserve the exported function context when executing', () => {
- const serverless = new Serverless();
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = `
module.exports.one = {two: {three: 'hello world'}}
module.exports.hello=function(){ return this; };`;
-
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
-
serverless.config.update({ servicePath: tmpDirPath });
serverless.variables.loadVariableSyntax();
-
return serverless.variables.getValueFromFile('file(./hello.js):hello.one.two.three')
- .then(valueToPopulate => {
- expect(valueToPopulate).to.equal('hello world');
- });
+ .should.become('hello world');
});
- it('should throw error if not using ":" syntax', () => {
- const serverless = new Serverless();
+ it('should file variable not using ":" syntax', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const configYml = {
@@ -1276,20 +1418,15 @@ module.exports = {
prob: 'prob',
},
};
-
- SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'),
- YAML.dump(configYml));
-
+ SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'), YAML.dump(configYml));
serverless.config.update({ servicePath: tmpDirPath });
-
- expect(() => serverless.variables
- .getValueFromFile('file(./config.yml).testObj.sub')).to.throw(Error);
+ return serverless.variables.getValueFromFile('file(./config.yml).testObj.sub')
+ .should.be.rejectedWith(serverless.classes.Error);
});
});
describe('#getValueFromCf()', () => {
it('should get variable from CloudFormation', () => {
- const serverless = new Serverless();
const options = {
stage: 'prod',
region: 'us-west-2',
@@ -1305,27 +1442,22 @@ module.exports = {
}],
}],
};
-
- const cfStub = sinon.stub(serverless.getProvider('aws'), 'request')
- .resolves(awsResponseMock);
+ const cfStub = sinon.stub(serverless.getProvider('aws'), 'request',
+ () => BbPromise.resolve(awsResponseMock));
return serverless.variables.getValueFromCf('cf:some-stack.MockExport')
- .then(valueToPopulate => {
- expect(valueToPopulate).to.be.equal('MockValue');
+ .should.become('MockValue')
+ .then(() => {
expect(cfStub).to.have.been.calledOnce;
expect(cfStub).to.have.been.calledWithExactly(
'CloudFormation',
'describeStacks',
- {
- StackName: 'some-stack',
- },
- { useCache: true }
- );
+ { StackName: 'some-stack' },
+ { useCache: true });
})
- .finally(() => serverless.getProvider('aws').request.restore());
+ .finally(() => cfStub.restore());
});
- it('should throw an error when variable from CloudFormation does not exist', () => {
- const serverless = new Serverless();
+ it('should reject CloudFormation variables that do not exist', () => {
const options = {
stage: 'prod',
region: 'us-west-2',
@@ -1341,35 +1473,26 @@ module.exports = {
}],
}],
};
-
- const cfStub = sinon.stub(serverless.getProvider('aws'), 'request')
- .resolves(awsResponseMock);
-
+ const cfStub = sinon.stub(serverless.getProvider('aws'), 'request',
+ () => BbPromise.resolve(awsResponseMock));
return serverless.variables.getValueFromCf('cf:some-stack.DoestNotExist')
- .then()
- .catch(error => {
+ .should.be.rejectedWith(serverless.classes.Error,
+ /to request a non exported variable from CloudFormation/)
+ .then(() => {
expect(cfStub).to.have.been.calledOnce;
expect(cfStub).to.have.been.calledWithExactly(
'CloudFormation',
'describeStacks',
- {
- StackName: 'some-stack',
- },
- { useCache: true }
- );
- expect(error).to.be.an.instanceof(Error);
- expect(error.message).to.match(/to request a non exported variable from CloudFormation/);
+ { StackName: 'some-stack' },
+ { useCache: true });
})
- .finally(() => serverless.getProvider('aws').request.restore());
+ .finally(() => cfStub.restore());
});
});
describe('#getValueFromS3()', () => {
- let serverless;
let awsProvider;
-
beforeEach(() => {
- serverless = new Serverless();
const options = {
stage: 'prod',
region: 'us-west-2',
@@ -1378,45 +1501,48 @@ module.exports = {
serverless.setProvider('aws', awsProvider);
serverless.variables.options = options;
});
-
it('should get variable from S3', () => {
const awsResponseMock = {
Body: 'MockValue',
};
- const s3Stub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
-
- return serverless.variables.getValueFromS3('s3:some.bucket/path/to/key').then(value => {
- expect(value).to.be.equal('MockValue');
- expect(s3Stub).to.have.been.calledOnce;
- expect(s3Stub).to.have.been.calledWithExactly(
- 'S3',
- 'getObject',
- {
- Bucket: 'some.bucket',
- Key: 'path/to/key',
- },
- { useCache: true }
- );
- })
- .finally(() => serverless.getProvider('aws').request.restore());
+ const s3Stub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
+ return serverless.variables.getValueFromS3('s3:some.bucket/path/to/key')
+ .should.become('MockValue')
+ .then(() => {
+ expect(s3Stub).to.have.been.calledOnce;
+ expect(s3Stub).to.have.been.calledWithExactly(
+ 'S3',
+ 'getObject',
+ {
+ Bucket: 'some.bucket',
+ Key: 'path/to/key',
+ },
+ { useCache: true });
+ })
+ .finally(() => s3Stub.restore());
});
it('should throw error if error getting value from S3', () => {
const error = new Error('The specified bucket is not valid');
- sinon.stub(awsProvider, 'request').rejects(error);
-
+ const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
return expect(serverless.variables.getValueFromS3('s3:some.bucket/path/to/key'))
- .to.be.rejectedWith('Error getting value for s3:some.bucket/path/to/key. ' +
- 'The specified bucket is not valid');
+ .to.be.rejectedWith(
+ serverless.classes.Error,
+ 'Error getting value for s3:some.bucket/path/to/key. The specified bucket is not valid')
+ .then().finally(() => requestStub.restore());
});
});
describe('#getValueFromSsm()', () => {
- let serverless;
+ const param = 'Param-01_valid.chars';
+ const value = 'MockValue';
+ const awsResponseMock = {
+ Parameter: {
+ Value: value,
+ },
+ };
let awsProvider;
-
beforeEach(() => {
- serverless = new Serverless();
const options = {
stage: 'prod',
region: 'us-west-2',
@@ -1425,154 +1551,112 @@ module.exports = {
serverless.setProvider('aws', awsProvider);
serverless.variables.options = options;
});
-
it('should get variable from Ssm using regular-style param', () => {
- const param = 'Param-01_valid.chars';
- const value = 'MockValue';
- const awsResponseMock = {
- Parameter: {
- Value: value,
- },
- };
- const ssmStub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
-
- return serverless.variables.getValueFromSsm(`ssm:${param}`).then(resolved => {
- expect(resolved).to.be.equal(value);
- expect(ssmStub).to.have.been.calledOnce;
- expect(ssmStub).to.have.been.calledWithExactly(
- 'SSM',
- 'getParameter',
- {
- Name: param,
- WithDecryption: false,
- },
- { useCache: true }
- );
- });
+ const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
+ return serverless.variables.getValueFromSsm(`ssm:${param}`)
+ .should.become(value)
+ .then(() => {
+ expect(ssmStub).to.have.been.calledOnce;
+ expect(ssmStub).to.have.been.calledWithExactly(
+ 'SSM',
+ 'getParameter',
+ {
+ Name: param,
+ WithDecryption: false,
+ },
+ { useCache: true });
+ })
+ .finally(() => ssmStub.restore());
});
-
it('should get variable from Ssm using path-style param', () => {
- const param = '/path/to/Param-01_valid.chars';
- const value = 'MockValue';
- const awsResponseMock = {
- Parameter: {
- Value: value,
- },
- };
- const ssmStub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
-
- return serverless.variables.getValueFromSsm(`ssm:${param}`).then(resolved => {
- expect(resolved).to.be.equal(value);
- expect(ssmStub).to.have.been.calledOnce;
- expect(ssmStub).to.have.been.calledWithExactly(
- 'SSM',
- 'getParameter',
- {
- Name: param,
- WithDecryption: false,
- },
- { useCache: true }
- );
- });
+ const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
+ return serverless.variables.getValueFromSsm(`ssm:${param}`)
+ .should.become(value)
+ .then(() => {
+ expect(ssmStub).to.have.been.calledOnce;
+ expect(ssmStub).to.have.been.calledWithExactly(
+ 'SSM',
+ 'getParameter',
+ {
+ Name: param,
+ WithDecryption: false,
+ },
+ { useCache: true });
+ })
+ .finally(() => ssmStub.restore());
});
-
it('should get encrypted variable from Ssm using extended syntax', () => {
- const param = '/path/to/Param-01_valid.chars';
- const value = 'MockValue';
- const awsResponseMock = {
- Parameter: {
- Value: value,
- },
- };
- const ssmStub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
-
- return serverless.variables.getValueFromSsm(`ssm:${param}~true`).then(resolved => {
- expect(resolved).to.be.equal(value);
- expect(ssmStub).to.have.been.calledOnce;
- expect(ssmStub).to.have.been.calledWithExactly(
- 'SSM',
- 'getParameter',
- {
- Name: param,
- WithDecryption: true,
- },
- { useCache: true }
- );
- });
+ const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
+ return serverless.variables.getValueFromSsm(`ssm:${param}~true`)
+ .should.become(value)
+ .then(() => {
+ expect(ssmStub).to.have.been.calledOnce;
+ expect(ssmStub).to.have.been.calledWithExactly(
+ 'SSM',
+ 'getParameter',
+ {
+ Name: param,
+ WithDecryption: true,
+ },
+ { useCache: true });
+ })
+ .finally(() => ssmStub.restore());
});
-
it('should get unencrypted variable from Ssm using extended syntax', () => {
- const param = '/path/to/Param-01_valid.chars';
- const value = 'MockValue';
- const awsResponseMock = {
- Parameter: {
- Value: value,
- },
- };
- const ssmStub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
-
- return serverless.variables.getValueFromSsm(`ssm:${param}~false`).then(resolved => {
- expect(resolved).to.be.equal(value);
- expect(ssmStub).to.have.been.calledOnce;
- expect(ssmStub).to.have.been.calledWithExactly(
- 'SSM',
- 'getParameter',
- {
- Name: param,
- WithDecryption: false,
- },
- { useCache: true }
- );
- });
+ const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
+ return serverless.variables.getValueFromSsm(`ssm:${param}~false`)
+ .should.become(value)
+ .then(() => {
+ expect(ssmStub).to.have.been.calledOnce;
+ expect(ssmStub).to.have.been.calledWithExactly(
+ 'SSM',
+ 'getParameter',
+ {
+ Name: param,
+ WithDecryption: false,
+ },
+ { useCache: true });
+ })
+ .finally(() => ssmStub.restore());
});
-
it('should ignore bad values for extended syntax', () => {
- const param = '/path/to/Param-01_valid.chars';
- const value = 'MockValue';
- const awsResponseMock = {
- Parameter: {
- Value: value,
- },
- };
- const ssmStub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
-
- return serverless.variables.getValueFromSsm(`ssm:${param}~badvalue`).then(resolved => {
- expect(resolved).to.be.equal(value);
- expect(ssmStub).to.have.been.calledOnce;
- expect(ssmStub).to.have.been.calledWithExactly(
- 'SSM',
- 'getParameter',
- {
- Name: param,
- WithDecryption: false,
- },
- { useCache: true }
- );
- });
+ const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
+ return serverless.variables.getValueFromSsm(`ssm:${param}~badvalue`)
+ .should.become(value)
+ .then(() => {
+ expect(ssmStub).to.have.been.calledOnce;
+ expect(ssmStub).to.have.been.calledWithExactly(
+ 'SSM',
+ 'getParameter',
+ {
+ Name: param,
+ WithDecryption: false,
+ },
+ { useCache: true });
+ })
+ .finally(() => ssmStub.restore());
});
it('should return undefined if SSM parameter does not exist', () => {
- const param = 'ssm:/some/path/to/invalidparam';
const error = new Error(`Parameter ${param} not found.`);
- sinon.stub(awsProvider, 'request').rejects(error);
-
- return expect(() => serverless.variables.getValueFromSsm(param).to.be(undefined));
+ const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
+ return serverless.variables.getValueFromSsm(`ssm:${param}`)
+ .should.become(undefined)
+ .then().finally(() => requestStub.restore());
});
- it('should throw exception if SSM request returns unexpected error', () => {
- const param = 'ssm:/some/path/to/invalidparam';
+ it('should reject if SSM request returns unexpected error', () => {
const error = new Error(
'User: is not authorized to perform: ssm:GetParameter on resource: ');
- sinon.stub(awsProvider, 'request').rejects(error);
-
- return expect(() => serverless.variables.getValueFromSsm(param).to.throw(error));
+ const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
+ return serverless.variables.getValueFromSsm(`ssm:${param}`)
+ .should.be.rejected
+ .then().finally(() => requestStub.restore());
});
});
- describe('#getDeepValue()', () => {
+ describe('#getDeeperValue()', () => {
it('should get deep values', () => {
- const serverless = new Serverless();
-
const valueToPopulateMock = {
service: 'testService',
custom: {
@@ -1581,116 +1665,98 @@ module.exports = {
},
},
};
-
serverless.variables.loadVariableSyntax();
-
- return serverless.variables.getDeepValue(['custom', 'subProperty', 'deep'],
- valueToPopulateMock).then(valueToPopulate => {
- expect(valueToPopulate).to.be.equal('deepValue');
- });
+ return serverless.variables.getDeeperValue(['custom', 'subProperty', 'deep'],
+ valueToPopulateMock).should.become('deepValue');
});
-
it('should not throw error if referencing invalid properties', () => {
- const serverless = new Serverless();
-
const valueToPopulateMock = {
service: 'testService',
custom: {
subProperty: 'hello',
},
};
-
serverless.variables.loadVariableSyntax();
-
- return serverless.variables.getDeepValue(['custom', 'subProperty', 'deep', 'deeper'],
- valueToPopulateMock).then(valueToPopulate => {
- expect(valueToPopulate).to.deep.equal({});
- });
+ return serverless.variables.getDeeperValue(['custom', 'subProperty', 'deep', 'deeper'],
+ valueToPopulateMock).should.eventually.deep.equal({});
});
-
- it('should get deep values with variable references', () => {
- const serverless = new Serverless();
-
+ it('should return a simple deep variable when final deep value is variable', () => {
serverless.variables.service = {
service: 'testService',
custom: {
- anotherVar: '${self:custom.var}',
subProperty: {
+ // eslint-disable-next-line no-template-curly-in-string
deep: '${self:custom.anotherVar.veryDeep}',
},
- var: {
- veryDeep: 'someValue',
- },
},
provider: serverless.service.provider,
};
-
serverless.variables.loadVariableSyntax();
-
- return serverless.variables.getDeepValue(['custom', 'subProperty', 'deep'],
- serverless.variables.service).then(valueToPopulate => {
- expect(valueToPopulate).to.be.equal('someValue');
- });
+ return serverless.variables.getDeeperValue(
+ ['custom', 'subProperty', 'deep'],
+ serverless.variables.service
+ ).should.become('${deep:0}');
+ });
+ it('should return a deep continuation when middle deep value is variable', () => {
+ serverless.variables.service = {
+ service: 'testService',
+ custom: {
+ anotherVar: '${self:custom.var}', // eslint-disable-line no-template-curly-in-string
+ },
+ provider: serverless.service.provider,
+ };
+ serverless.variables.loadVariableSyntax();
+ return serverless.variables.getDeeperValue(
+ ['custom', 'anotherVar', 'veryDeep'],
+ serverless.variables.service)
+ .should.become('${deep:0.veryDeep}');
});
});
-
describe('#warnIfNotFound()', () => {
let logWarningSpy;
let consoleLogStub;
let varProxy;
-
beforeEach(() => {
logWarningSpy = sinon.spy(slsError, 'logWarning');
consoleLogStub = sinon.stub(console, 'log').returns();
- const ProxyQuiredVariables = proxyquire('./Variables.js', {
- './Error': logWarningSpy,
- });
- varProxy = new ProxyQuiredVariables(new Serverless());
+ const ProxyQuiredVariables = proxyquire('./Variables.js', { './Error': logWarningSpy });
+ varProxy = new ProxyQuiredVariables(serverless);
});
-
afterEach(() => {
logWarningSpy.restore();
consoleLogStub.restore();
});
-
it('should do nothing if variable has valid value.', () => {
varProxy.warnIfNotFound('self:service', 'a-valid-value');
expect(logWarningSpy).to.not.have.been.calledOnce;
});
-
it('should log if variable has null value.', () => {
varProxy.warnIfNotFound('self:service', null);
expect(logWarningSpy).to.have.been.calledOnce;
});
-
it('should log if variable has undefined value.', () => {
varProxy.warnIfNotFound('self:service', undefined);
expect(logWarningSpy).to.have.been.calledOnce;
});
-
it('should log if variable has empty object value.', () => {
varProxy.warnIfNotFound('self:service', {});
expect(logWarningSpy).to.have.been.calledOnce;
});
-
it('should detect the "environment variable" variable type', () => {
varProxy.warnIfNotFound('env:service', null);
expect(logWarningSpy).to.have.been.calledOnce;
expect(logWarningSpy.args[0][0]).to.contain('environment variable');
});
-
it('should detect the "option" variable type', () => {
varProxy.warnIfNotFound('opt:service', null);
expect(logWarningSpy).to.have.been.calledOnce;
expect(logWarningSpy.args[0][0]).to.contain('option');
});
-
it('should detect the "service attribute" variable type', () => {
varProxy.warnIfNotFound('self:service', null);
expect(logWarningSpy).to.have.been.calledOnce;
expect(logWarningSpy.args[0][0]).to.contain('service attribute');
});
-
it('should detect the "file" variable type', () => {
varProxy.warnIfNotFound('file(service)', null);
expect(logWarningSpy).to.have.been.calledOnce;
diff --git a/lib/plugins/Plugins.json b/lib/plugins/Plugins.json
index 161303d0de9..15ef563af29 100644
--- a/lib/plugins/Plugins.json
+++ b/lib/plugins/Plugins.json
@@ -46,6 +46,7 @@
"./aws/package/compile/events/cloudWatchEvent/index.js",
"./aws/package/compile/events/cloudWatchLog/index.js",
"./aws/package/compile/events/cognitoUserPool/index.js",
+ "./aws/package/compile/events/sqs/index.js",
"./aws/deployFunction/index.js",
"./aws/deployList/index.js",
"./aws/invokeLocal/index.js",
diff --git a/lib/plugins/aws/deploy/lib/cleanupS3Bucket.js b/lib/plugins/aws/deploy/lib/cleanupS3Bucket.js
index 47bc280c92a..e5440360946 100644
--- a/lib/plugins/aws/deploy/lib/cleanupS3Bucket.js
+++ b/lib/plugins/aws/deploy/lib/cleanupS3Bucket.js
@@ -33,7 +33,7 @@ module.exports = {
removeObjects(objectsToRemove) {
if (objectsToRemove && objectsToRemove.length) {
- this.serverless.cli.log('Removing old service versions...');
+ this.serverless.cli.log('Removing old service artifacts from S3...');
return this.provider.request('S3',
'deleteObjects',
diff --git a/lib/plugins/aws/deploy/lib/createStack.js b/lib/plugins/aws/deploy/lib/createStack.js
index de10370e8b8..f37c2379b3b 100644
--- a/lib/plugins/aws/deploy/lib/createStack.js
+++ b/lib/plugins/aws/deploy/lib/createStack.js
@@ -32,6 +32,10 @@ module.exports = {
params.RoleARN = this.serverless.service.provider.cfnRole;
}
+ if (this.serverless.service.provider.notificationArns) {
+ params.NotificationARNs = this.serverless.service.provider.notificationArns;
+ }
+
return this.provider.request(
'CloudFormation',
'createStack',
diff --git a/lib/plugins/aws/deploy/lib/createStack.test.js b/lib/plugins/aws/deploy/lib/createStack.test.js
index 28d5820e99b..eff833e4587 100644
--- a/lib/plugins/aws/deploy/lib/createStack.test.js
+++ b/lib/plugins/aws/deploy/lib/createStack.test.js
@@ -6,7 +6,6 @@ const path = require('path');
const AwsProvider = require('../../provider/awsProvider');
const AwsDeploy = require('../index');
const Serverless = require('../../../../Serverless');
-const BbPromise = require('bluebird');
const testUtils = require('../../../../../tests/utils');
describe('createStack', () => {
@@ -73,6 +72,22 @@ describe('createStack', () => {
.to.equal('arn:aws:iam::123456789012:role/myrole');
});
});
+
+ it('should use use notificationArns if it is specified', () => {
+ const mytopicArn = 'arn:aws:sns::123456789012:mytopic';
+ awsDeploy.serverless.service.provider.notificationArns = [mytopicArn];
+
+ const createStackStub = sinon
+ .stub(awsDeploy.provider, 'request').resolves();
+ sinon.stub(awsDeploy, 'monitorStack').resolves();
+
+ return awsDeploy.create().then(() => {
+ expect(createStackStub.args[0][2].NotificationARNs)
+ .to.deep.equal([mytopicArn]);
+ awsDeploy.provider.request.restore();
+ awsDeploy.monitorStack.restore();
+ });
+ });
});
describe('#createStack()', () => {
@@ -98,8 +113,7 @@ describe('createStack', () => {
it('should set the createLater flag and resolve if deployment bucket is provided', () => {
awsDeploy.serverless.service.provider.deploymentBucket = 'serverless';
- sandbox.stub(awsDeploy.provider, 'request')
- .returns(BbPromise.reject({ message: 'does not exist' }));
+ sandbox.stub(awsDeploy.provider, 'request').rejects(new Error('does not exist'));
return awsDeploy.createStack().then(() => {
expect(awsDeploy.createLater).to.equal(true);
diff --git a/lib/plugins/aws/deploy/lib/extendedValidate.test.js b/lib/plugins/aws/deploy/lib/extendedValidate.test.js
index 7b1d3d9adcf..7cbabdc5fb9 100644
--- a/lib/plugins/aws/deploy/lib/extendedValidate.test.js
+++ b/lib/plugins/aws/deploy/lib/extendedValidate.test.js
@@ -1,6 +1,6 @@
'use strict';
-const expect = require('chai').expect;
+const chai = require('chai');
const sinon = require('sinon');
const path = require('path');
const AwsProvider = require('../../provider/awsProvider');
@@ -8,6 +8,10 @@ const AwsDeploy = require('../index');
const Serverless = require('../../../../Serverless');
const testUtils = require('../../../../../tests/utils');
+chai.use(require('sinon-chai'));
+
+const expect = chai.expect;
+
describe('extendedValidate', () => {
let awsDeploy;
const tmpDirPath = testUtils.getTmpDirPath();
@@ -60,8 +64,8 @@ describe('extendedValidate', () => {
});
afterEach(() => {
- awsDeploy.serverless.utils.fileExistsSync.restore();
- awsDeploy.serverless.utils.readFileSync.restore();
+ fileExistsSyncStub.restore();
+ readFileSyncStub.restore();
});
it('should throw error if state file does not exist', () => {
diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.js
index 951d11ace3e..ed9d2e10092 100644
--- a/lib/plugins/aws/deploy/lib/uploadArtifacts.js
+++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.js
@@ -2,6 +2,7 @@
/* eslint-disable no-use-before-define */
+const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
@@ -9,6 +10,8 @@ const BbPromise = require('bluebird');
const filesize = require('filesize');
const normalizeFiles = require('../../lib/normalizeFiles');
+const NUM_CONCURRENT_UPLOADS = 3;
+
module.exports = {
uploadArtifacts() {
return BbPromise.bind(this)
@@ -76,36 +79,36 @@ module.exports = {
},
uploadFunctions() {
- let shouldUploadService = false;
this.serverless.cli.log('Uploading artifacts...');
+
const functionNames = this.serverless.service.getAllFunctions();
- return BbPromise.map(functionNames, (name) => {
- const functionArtifactFileName = this.provider.naming.getFunctionArtifactName(name);
- const functionObject = this.serverless.service.getFunction(name);
- functionObject.package = functionObject.package || {};
- let artifactFilePath = functionObject.package.artifact ||
- this.serverless.service.package.artifact;
- if (!artifactFilePath ||
- (this.serverless.service.artifact && !functionObject.package.artifact)) {
- if (this.serverless.service.package.individually || functionObject.package.individually) {
- const artifactFileName = functionArtifactFileName;
- artifactFilePath = path.join(this.packagePath, artifactFileName);
- return this.uploadZipFile(artifactFilePath);
+ const artifactFilePaths = _.uniq(
+ _.map(functionNames, (name) => {
+ const functionArtifactFileName = this.provider.naming.getFunctionArtifactName(name);
+ const functionObject = this.serverless.service.getFunction(name);
+ functionObject.package = functionObject.package || {};
+ const artifactFilePath = functionObject.package.artifact ||
+ this.serverless.service.package.artifact;
+
+ if (!artifactFilePath ||
+ (this.serverless.service.artifact && !functionObject.package.artifact)) {
+ if (this.serverless.service.package.individually || functionObject.package.individually) {
+ const artifactFileName = functionArtifactFileName;
+ return path.join(this.packagePath, artifactFileName);
+ }
+ return path.join(this.packagePath, this.provider.naming.getServiceArtifactName());
}
- shouldUploadService = true;
- return BbPromise.resolve();
- }
+
+ return artifactFilePath;
+ })
+ );
+
+ return BbPromise.map(artifactFilePaths, (artifactFilePath) => {
+ const stats = fs.statSync(artifactFilePath);
+ this.serverless.cli.log(`Uploading service .zip file to S3 (${filesize(stats.size)})...`);
return this.uploadZipFile(artifactFilePath);
- }, { concurrency: 3 }).then(() => {
- if (shouldUploadService) {
- const artifactFileName = this.provider.naming.getServiceArtifactName();
- const artifactFilePath = path.join(this.packagePath, artifactFileName);
- const stats = fs.statSync(artifactFilePath);
- this.serverless.cli.log(`Uploading service .zip file to S3 (${filesize(stats.size)})...`);
- return this.uploadZipFile(artifactFilePath);
- }
- return BbPromise.resolve();
- });
+ }, { concurrency: NUM_CONCURRENT_UPLOADS }
+ );
},
};
diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js
index df96e23ff68..a54249fe6fe 100644
--- a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js
+++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js
@@ -90,8 +90,8 @@ describe('uploadArtifacts', () => {
});
afterEach(() => {
- normalizeFiles.normalizeCloudFormationTemplate.restore();
- awsDeploy.provider.request.restore();
+ normalizeCloudFormationTemplateStub.restore();
+ uploadStub.restore();
});
it('should upload the CloudFormation file to the S3 bucket', () => {
@@ -159,8 +159,8 @@ describe('uploadArtifacts', () => {
});
afterEach(() => {
- fs.readFileSync.restore();
- awsDeploy.provider.request.restore();
+ readFileSyncStub.restore();
+ uploadStub.restore();
});
it('should throw for null artifact paths', () => {
@@ -225,19 +225,46 @@ describe('uploadArtifacts', () => {
});
describe('#uploadFunctions()', () => {
+ let uploadZipFileStub;
+
+ beforeEach(() => {
+ sinon.stub(fs, 'statSync').returns({ size: 1024 });
+ uploadZipFileStub = sinon.stub(awsDeploy, 'uploadZipFile').resolves();
+ });
+
+ afterEach(() => {
+ fs.statSync.restore();
+ uploadZipFileStub.restore();
+ });
+
it('should upload the service artifact file to the S3 bucket', () => {
awsDeploy.serverless.config.servicePath = 'some/path';
awsDeploy.serverless.service.service = 'new-service';
- sinon.stub(fs, 'statSync').returns({ size: 0 });
+ return awsDeploy.uploadFunctions().then(() => {
+ expect(uploadZipFileStub.calledOnce).to.be.equal(true);
+ const expectedPath = path.join('foo', '.serverless', 'new-service.zip');
+ expect(uploadZipFileStub.args[0][0]).to.be.equal(expectedPath);
+ });
+ });
- const uploadZipFileStub = sinon
- .stub(awsDeploy, 'uploadZipFile').resolves();
+ it('should upload a single .zip file to the S3 bucket when not packaging individually', () => {
+ awsDeploy.serverless.service.functions = {
+ first: {
+ package: {
+ artifact: 'artifact.zip',
+ },
+ },
+ second: {
+ package: {
+ artifact: 'artifact.zip',
+ },
+ },
+ };
return awsDeploy.uploadFunctions().then(() => {
expect(uploadZipFileStub.calledOnce).to.be.equal(true);
- fs.statSync.restore();
- awsDeploy.uploadZipFile.restore();
+ expect(uploadZipFileStub.args[0][0]).to.be.equal('artifact.zip');
});
});
@@ -256,16 +283,12 @@ describe('uploadArtifacts', () => {
},
};
- const uploadZipFileStub = sinon
- .stub(awsDeploy, 'uploadZipFile').resolves();
-
return awsDeploy.uploadFunctions().then(() => {
expect(uploadZipFileStub.calledTwice).to.be.equal(true);
expect(uploadZipFileStub.args[0][0])
.to.be.equal(awsDeploy.serverless.service.functions.first.package.artifact);
expect(uploadZipFileStub.args[1][0])
.to.be.equal(awsDeploy.serverless.service.functions.second.package.artifact);
- awsDeploy.uploadZipFile.restore();
});
});
@@ -284,18 +307,12 @@ describe('uploadArtifacts', () => {
},
};
- const uploadZipFileStub = sinon
- .stub(awsDeploy, 'uploadZipFile').resolves();
- sinon.stub(fs, 'statSync').returns({ size: 1024 });
-
return awsDeploy.uploadFunctions().then(() => {
expect(uploadZipFileStub.calledTwice).to.be.equal(true);
expect(uploadZipFileStub.args[0][0])
.to.be.equal(awsDeploy.serverless.service.functions.first.package.artifact);
expect(uploadZipFileStub.args[1][0])
.to.be.equal(awsDeploy.serverless.service.package.artifact);
- awsDeploy.uploadZipFile.restore();
- fs.statSync.restore();
});
});
@@ -303,16 +320,11 @@ describe('uploadArtifacts', () => {
awsDeploy.serverless.config.servicePath = 'some/path';
awsDeploy.serverless.service.service = 'new-service';
- sinon.stub(fs, 'statSync').returns({ size: 1024 });
- sinon.stub(awsDeploy, 'uploadZipFile').resolves();
sinon.spy(awsDeploy.serverless.cli, 'log');
return awsDeploy.uploadFunctions().then(() => {
const expected = 'Uploading service .zip file to S3 (1 KB)...';
expect(awsDeploy.serverless.cli.log.calledWithExactly(expected)).to.be.equal(true);
-
- fs.statSync.restore();
- awsDeploy.uploadZipFile.restore();
});
});
});
diff --git a/lib/plugins/aws/deploy/lib/validateTemplate.js b/lib/plugins/aws/deploy/lib/validateTemplate.js
index 85265f685e8..8c1626447d2 100644
--- a/lib/plugins/aws/deploy/lib/validateTemplate.js
+++ b/lib/plugins/aws/deploy/lib/validateTemplate.js
@@ -1,14 +1,15 @@
'use strict';
+const getS3EndpointForRegion = require('../../utils/getS3EndpointForRegion');
module.exports = {
validateTemplate() {
const bucketName = this.bucketName;
const artifactDirectoryName = this.serverless.service.package.artifactDirectoryName;
const compiledTemplateFileName = 'compiled-cloudformation-template.json';
-
+ const s3Endpoint = getS3EndpointForRegion(this.provider.getRegion());
this.serverless.cli.log('Validating template...');
const params = {
- TemplateURL: `https://s3.amazonaws.com/${bucketName}/${artifactDirectoryName}/${compiledTemplateFileName}`,
+ TemplateURL: `https://${s3Endpoint}/${bucketName}/${artifactDirectoryName}/${compiledTemplateFileName}`,
};
return this.provider.request(
diff --git a/lib/plugins/aws/deployFunction/index.js b/lib/plugins/aws/deployFunction/index.js
index a5b4d34f940..f0ce80ccba7 100644
--- a/lib/plugins/aws/deployFunction/index.js
+++ b/lib/plugins/aws/deployFunction/index.js
@@ -57,19 +57,19 @@ class AwsDeployFunction {
'getFunction',
params
)
- .then((result) => {
- this.serverless.service.provider.remoteFunctionData = result;
- return result;
- })
- .catch(() => {
- const errorMessage = [
- `The function "${this.options.function}" you want to update is not yet deployed.`,
- ' Please run "serverless deploy" to deploy your service.',
- ' After that you can redeploy your services functions with the',
- ' "serverless deploy function" command.',
- ].join('');
- throw new this.serverless.classes.Error(errorMessage);
- });
+ .then((result) => {
+ this.serverless.service.provider.remoteFunctionData = result;
+ return result;
+ })
+ .catch(() => {
+ const errorMessage = [
+ `The function "${this.options.function}" you want to update is not yet deployed.`,
+ ' Please run "serverless deploy" to deploy your service.',
+ ' After that you can redeploy your services functions with the',
+ ' "serverless deploy function" command.',
+ ].join('');
+ throw new this.serverless.classes.Error(errorMessage);
+ });
}
normalizeArnRole(role) {
@@ -84,8 +84,8 @@ class AwsDeployFunction {
const roleProperties = roleResource.Properties;
const compiledFullRoleName = `${roleProperties.Path || '/'}${roleProperties.RoleName}`;
- return this.provider.getAccountId().then((accountId) =>
- `arn:aws:iam::${accountId}:role${compiledFullRoleName}`
+ return this.provider.getAccountInfo().then((result) =>
+ `arn:${result.partition}:iam::${result.accountId}:role${compiledFullRoleName}`
);
}
@@ -164,6 +164,10 @@ class AwsDeployFunction {
const errorMessage = 'Invalid characters in environment variable';
throw new this.serverless.classes.Error(errorMessage);
}
+ if (!_.isString(params.Environment.Variables[key])) {
+ const errorMessage = `Environment variable ${key} must contain strings`;
+ throw new this.serverless.classes.Error(errorMessage);
+ }
});
}
}
diff --git a/lib/plugins/aws/deployFunction/index.test.js b/lib/plugins/aws/deployFunction/index.test.js
index ecbf65ddd0f..526607c92d1 100644
--- a/lib/plugins/aws/deployFunction/index.test.js
+++ b/lib/plugins/aws/deployFunction/index.test.js
@@ -1,5 +1,5 @@
'use strict';
-
+/* eslint-disable no-unused-expressions */
const chai = require('chai');
const sinon = require('sinon');
const path = require('path');
@@ -120,13 +120,13 @@ describe('AwsDeployFunction', () => {
});
describe('#normalizeArnRole', () => {
- let getAccountIdStub;
+ let getAccountInfoStub;
let getRoleStub;
beforeEach(() => {
- getAccountIdStub = sinon
- .stub(awsDeployFunction.provider, 'getAccountId')
- .resolves('123456789012');
+ getAccountInfoStub = sinon
+ .stub(awsDeployFunction.provider, 'getAccountInfo')
+ .resolves({ accountId: '123456789012', partition: 'aws' });
getRoleStub = sinon
.stub(awsDeployFunction.provider, 'request')
.resolves({ Arn: 'arn:aws:iam::123456789012:role/role_2' });
@@ -144,7 +144,7 @@ describe('AwsDeployFunction', () => {
});
afterEach(() => {
- awsDeployFunction.provider.getAccountId.restore();
+ awsDeployFunction.provider.getAccountInfo.restore();
awsDeployFunction.provider.request.restore();
serverless.service.resources = undefined;
});
@@ -153,7 +153,7 @@ describe('AwsDeployFunction', () => {
const arn = 'arn:aws:iam::123456789012:role/role';
return awsDeployFunction.normalizeArnRole(arn).then((result) => {
- expect(getAccountIdStub.calledOnce).to.be.equal(false);
+ expect(getAccountInfoStub).to.not.have.been.called;
expect(result).to.be.equal(arn);
});
});
@@ -162,7 +162,7 @@ describe('AwsDeployFunction', () => {
const roleName = 'MyCustomRole';
return awsDeployFunction.normalizeArnRole(roleName).then((result) => {
- expect(getAccountIdStub.calledOnce).to.be.equal(true);
+ expect(getAccountInfoStub).to.have.been.called;
expect(result).to.be.equal('arn:aws:iam::123456789012:role/role_123');
});
});
@@ -177,7 +177,7 @@ describe('AwsDeployFunction', () => {
return awsDeployFunction.normalizeArnRole(roleObj).then((result) => {
expect(getRoleStub.calledOnce).to.be.equal(true);
- expect(getAccountIdStub.calledOnce).to.be.equal(false);
+ expect(getAccountInfoStub).to.not.have.been.called;
expect(result).to.be.equal('arn:aws:iam::123456789012:role/role_2');
});
});
@@ -336,7 +336,7 @@ describe('AwsDeployFunction', () => {
awsDeployFunction.options = options;
return expect(awsDeployFunction.updateFunctionConfiguration()).to.be.fulfilled
- .then(() => expect(updateFunctionConfigurationStub).to.not.be.called);
+ .then(() => expect(updateFunctionConfigurationStub).to.not.be.called);
});
it('should fail when using invalid characters in environment variable', () => {
@@ -353,6 +353,20 @@ describe('AwsDeployFunction', () => {
expect(() => awsDeployFunction.updateFunctionConfiguration()).to.throw(Error);
});
+ it('should fail when using non-string values as environment variables', () => {
+ options.functionObj = {
+ name: 'first',
+ description: 'change',
+ environment: {
+ COUNTER: 6,
+ },
+ };
+
+ awsDeployFunction.options = options;
+
+ expect(() => awsDeployFunction.updateFunctionConfiguration()).to.throw(Error);
+ });
+
it('should inherit provider-level config', () => {
options.functionObj = {
name: 'first',
diff --git a/lib/plugins/aws/invoke/index.js b/lib/plugins/aws/invoke/index.js
index 58cd837d902..2354a1d282d 100644
--- a/lib/plugins/aws/invoke/index.js
+++ b/lib/plugins/aws/invoke/index.js
@@ -83,12 +83,12 @@ class AwsInvoke {
}
log(invocationReply) {
- const color = !invocationReply.FunctionError ? 'white' : 'red';
+ const color = !invocationReply.FunctionError ? (x => x) : chalk.red;
if (invocationReply.Payload) {
const response = JSON.parse(invocationReply.Payload);
- this.consoleLog(chalk[color](JSON.stringify(response, null, 4)));
+ this.consoleLog(color(JSON.stringify(response, null, 4)));
}
if (invocationReply.LogResult) {
diff --git a/lib/plugins/aws/invoke/index.test.js b/lib/plugins/aws/invoke/index.test.js
index 8f3a7bcda88..91591169587 100644
--- a/lib/plugins/aws/invoke/index.test.js
+++ b/lib/plugins/aws/invoke/index.test.js
@@ -6,7 +6,6 @@ const path = require('path');
const AwsInvoke = require('./index');
const AwsProvider = require('../provider/awsProvider');
const Serverless = require('../../../Serverless');
-const chalk = require('chalk');
const testUtils = require('../../../../tests/utils');
describe('AwsInvoke', () => {
@@ -271,7 +270,7 @@ describe('AwsInvoke', () => {
};
return awsInvoke.log(invocationReplyMock).then(() => {
- const expectedPayloadMessage = `${chalk.white('{\n "testProp": "testValue"\n}')}`;
+ const expectedPayloadMessage = '{\n "testProp": "testValue"\n}';
expect(consoleLogStub.calledWith(expectedPayloadMessage)).to.equal(true);
});
diff --git a/lib/plugins/aws/invokeLocal/fixture/handlerWithLoadingError.js b/lib/plugins/aws/invokeLocal/fixture/handlerWithLoadingError.js
new file mode 100644
index 00000000000..a945eb0fdf1
--- /dev/null
+++ b/lib/plugins/aws/invokeLocal/fixture/handlerWithLoadingError.js
@@ -0,0 +1,2 @@
+'use strict';
+throw new Error('loading exception');
diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js
index ecc355cf91d..ae181fa018f 100644
--- a/lib/plugins/aws/invokeLocal/index.js
+++ b/lib/plugins/aws/invokeLocal/index.js
@@ -130,8 +130,9 @@ class AwsInvokeLocal {
}
if (runtime === 'python2.7' || runtime === 'python3.6') {
- const handlerPath = handler.split('.')[0];
- const handlerName = handler.split('.')[1];
+ const handlerComponents = handler.split(/\./);
+ const handlerPath = handlerComponents.slice(0, -1).join('.');
+ const handlerName = handlerComponents.pop();
return this.invokeLocalPython(
process.platform === 'win32' ? 'python.exe' : runtime,
handlerPath,
@@ -248,24 +249,24 @@ class AwsInvokeLocal {
invokeLocalNodeJs(handlerPath, handlerName, event, customContext) {
let lambda;
-
+ let pathToHandler;
try {
/*
* we need require() here to load the handler from the file system
* which the user has to supply by passing the function name
*/
-
+ pathToHandler = path.join(
+ this.serverless.config.servicePath,
+ this.options.extraServicePath || '',
+ handlerPath
+ );
const handlersContainer = require( // eslint-disable-line global-require
- path.join(
- this.serverless.config.servicePath,
- this.options.extraServicePath || '',
- handlerPath
- )
+ pathToHandler
);
lambda = handlersContainer[handlerName];
} catch (error) {
this.serverless.cli.consoleLog(error);
- process.exit(0);
+ throw new Error(`Exception encountered when loading ${pathToHandler}`);
}
const callback = (err, result) => {
diff --git a/lib/plugins/aws/invokeLocal/index.test.js b/lib/plugins/aws/invokeLocal/index.test.js
index c2e83743507..87be0e5841b 100644
--- a/lib/plugins/aws/invokeLocal/index.test.js
+++ b/lib/plugins/aws/invokeLocal/index.test.js
@@ -503,6 +503,13 @@ describe('AwsInvokeLocal', () => {
expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"errorMessage": "failed"');
});
+
+ it('should throw when module loading error', () => {
+ awsInvokeLocal.serverless.config.servicePath = __dirname;
+
+ expect(() => awsInvokeLocal.invokeLocalNodeJs('fixture/handlerWithLoadingError',
+ 'anyMethod')).to.throw(/Exception encountered when loading/);
+ });
});
// Ignored because it fails in CI
diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js
index f2c81c776e4..8e25ec3c588 100644
--- a/lib/plugins/aws/lib/naming.js
+++ b/lib/plugins/aws/lib/naming.js
@@ -46,6 +46,10 @@ module.exports = {
// Stack
getStackName() {
+ if (this.provider.serverless.service.provider.stackName &&
+ _.isString(this.provider.serverless.service.provider.stackName)) {
+ return `${this.provider.serverless.service.provider.stackName}`;
+ }
return `${this.provider.serverless.service.service}-${this.provider.getStage()}`;
},
@@ -145,6 +149,10 @@ module.exports = {
// API Gateway
getApiGatewayName() {
+ if (this.provider.serverless.service.provider.apiName &&
+ _.isString(this.provider.serverless.service.provider.apiName)) {
+ return `${this.provider.serverless.service.provider.apiName}`;
+ }
return `${this.provider.getStage()}-${this.provider.serverless.service.service}`;
},
generateApiGatewayDeploymentLogicalId() {
@@ -257,6 +265,15 @@ module.exports = {
return `CognitoUserPool${this.normalizeNameToAlphaNumericOnly(poolId)}`;
},
+ // SQS
+ getQueueLogicalId(functionName, queueName) {
+ return `${
+ this.getNormalizedFunctionName(functionName)
+ }EventSourceMappingSQS${
+ this.normalizeNameToAlphaNumericOnly(queueName)
+ }`;
+ },
+
// Permissions
getLambdaS3PermissionLogicalId(functionName, bucketName) {
return `${this.getNormalizedFunctionName(functionName)}LambdaPermission${this
@@ -282,8 +299,9 @@ module.exports = {
getLambdaApiGatewayPermissionLogicalId(functionName) {
return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionApiGateway`;
},
- getLambdaAlexaSkillPermissionLogicalId(functionName) {
- return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlexaSkill`;
+ getLambdaAlexaSkillPermissionLogicalId(functionName, alexaSkillIndex) {
+ return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlexaSkill${
+ alexaSkillIndex || '0'}`;
},
getLambdaAlexaSmartHomePermissionLogicalId(functionName, alexaSmartHomeIndex) {
return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionAlexaSmartHome${
diff --git a/lib/plugins/aws/lib/naming.test.js b/lib/plugins/aws/lib/naming.test.js
index 7becb143864..5c76568b3bd 100644
--- a/lib/plugins/aws/lib/naming.test.js
+++ b/lib/plugins/aws/lib/naming.test.js
@@ -94,11 +94,18 @@ describe('#naming()', () => {
});
describe('#getStackName()', () => {
- it('should use the service name and stage from the service and config', () => {
+ it('should use the service name & stage if custom stack name not provided', () => {
serverless.service.service = 'myService';
expect(sdk.naming.getStackName()).to.equal(`${serverless.service.service}-${
sdk.naming.provider.getStage()}`);
});
+
+ it('should use the custom stack name if provided', () => {
+ serverless.service.provider.stackName = 'app-dev-testApp';
+ serverless.service.service = 'myService';
+ serverless.service.provider.stage = sdk.naming.provider.getStage();
+ expect(sdk.naming.getStackName()).to.equal('app-dev-testApp');
+ });
});
describe('#getRolePath()', () => {
@@ -214,11 +221,18 @@ describe('#naming()', () => {
});
describe('#getApiGatewayName()', () => {
- it('should return the composition of stage and service name', () => {
+ it('should return the composition of stage & service name if custom name not provided', () => {
serverless.service.service = 'myService';
expect(sdk.naming.getApiGatewayName())
.to.equal(`${sdk.naming.provider.getStage()}-${serverless.service.service}`);
});
+
+ it('should return the custom api name if provided', () => {
+ serverless.service.provider.apiName = 'app-dev-testApi';
+ serverless.service.service = 'myService';
+ serverless.service.provider.stage = sdk.naming.provider.getStage();
+ expect(sdk.naming.getApiGatewayName()).to.equal('app-dev-testApi');
+ });
});
describe('#generateApiGatewayDeploymentLogicalId()', () => {
@@ -464,9 +478,15 @@ describe('#naming()', () => {
describe('#getLambdaAlexaSkillPermissionLogicalId()', () => {
it('should normalize the function name and append the standard suffix',
+ () => {
+ expect(sdk.naming.getLambdaAlexaSkillPermissionLogicalId('functionName', 2))
+ .to.equal('FunctionNameLambdaPermissionAlexaSkill2');
+ });
+
+ it('should normalize the function name and append a default suffix if not defined',
() => {
expect(sdk.naming.getLambdaAlexaSkillPermissionLogicalId('functionName'))
- .to.equal('FunctionNameLambdaPermissionAlexaSkill');
+ .to.equal('FunctionNameLambdaPermissionAlexaSkill0');
});
});
@@ -502,4 +522,11 @@ describe('#naming()', () => {
)).to.equal('FunctionNameLambdaPermissionCognitoUserPoolPool1TriggerSourceCustomMessage');
});
});
+
+ describe('#getQueueLogicalId()', () => {
+ it('should normalize the function name and add the standard suffix', () => {
+ expect(sdk.naming.getQueueLogicalId('functionName', 'MyQueue'))
+ .to.equal('FunctionNameEventSourceMappingSQSMyQueue');
+ });
+ });
});
diff --git a/lib/plugins/aws/lib/updateStack.js b/lib/plugins/aws/lib/updateStack.js
index 5ac87bf5993..5d53811c49b 100644
--- a/lib/plugins/aws/lib/updateStack.js
+++ b/lib/plugins/aws/lib/updateStack.js
@@ -2,6 +2,7 @@
const _ = require('lodash');
const BbPromise = require('bluebird');
+const getS3EndpointForRegion = require('../utils/getS3EndpointForRegion');
const NO_UPDATE_MESSAGE = 'No updates are to be performed.';
@@ -13,7 +14,8 @@ module.exports = {
const stackName = this.provider.naming.getStackName();
let stackTags = { STAGE: this.provider.getStage() };
const compiledTemplateFileName = 'compiled-cloudformation-template.json';
- const templateUrl = `https://s3.amazonaws.com/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`;
+ const s3Endpoint = getS3EndpointForRegion(this.provider.getRegion());
+ const templateUrl = `https://${s3Endpoint}/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`;
// Merge additional stack tags
if (typeof this.serverless.service.provider.stackTags === 'object') {
@@ -36,6 +38,10 @@ module.exports = {
params.RoleARN = this.serverless.service.provider.cfnRole;
}
+ if (this.serverless.service.provider.notificationArns) {
+ params.NotificationARNs = this.serverless.service.provider.notificationArns;
+ }
+
return this.provider.request('CloudFormation',
'createStack',
params)
@@ -44,7 +50,8 @@ module.exports = {
update() {
const compiledTemplateFileName = 'compiled-cloudformation-template.json';
- const templateUrl = `https://s3.amazonaws.com/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`;
+ const s3Endpoint = getS3EndpointForRegion(this.provider.getRegion());
+ const templateUrl = `https://${s3Endpoint}/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`;
this.serverless.cli.log('Updating Stack...');
const stackName = this.provider.naming.getStackName();
@@ -70,9 +77,13 @@ module.exports = {
params.RoleARN = this.serverless.service.provider.cfnRole;
}
+ if (this.serverless.service.provider.notificationArns) {
+ params.NotificationARNs = this.serverless.service.provider.notificationArns;
+ }
+
// Policy must have at least one statement, otherwise no updates would be possible at all
if (this.serverless.service.provider.stackPolicy &&
- this.serverless.service.provider.stackPolicy.length) {
+ !_.isEmpty(this.serverless.service.provider.stackPolicy)) {
params.StackPolicyBody = JSON.stringify({
Statement: this.serverless.service.provider.stackPolicy,
});
diff --git a/lib/plugins/aws/lib/updateStack.test.js b/lib/plugins/aws/lib/updateStack.test.js
index ad172de4e12..bf692981308 100644
--- a/lib/plugins/aws/lib/updateStack.test.js
+++ b/lib/plugins/aws/lib/updateStack.test.js
@@ -92,6 +92,21 @@ describe('updateStack', () => {
awsDeploy.monitorStack.restore();
});
});
+
+ it('should use use notificationArns if it is specified', () => {
+ const mytopicArn = 'arn:aws:sns::123456789012:mytopic';
+ awsDeploy.serverless.service.provider.notificationArns = [mytopicArn];
+
+ const createStackStub = sinon.stub(awsDeploy.provider, 'request').resolves();
+ sinon.stub(awsDeploy, 'monitorStack').resolves();
+
+ return awsDeploy.createFallback().then(() => {
+ expect(createStackStub.args[0][2].NotificationARNs)
+ .to.deep.equal([mytopicArn]);
+ awsDeploy.provider.request.restore();
+ awsDeploy.monitorStack.restore();
+ });
+ });
});
describe('#update()', () => {
@@ -168,6 +183,16 @@ describe('updateStack', () => {
.to.equal('arn:aws:iam::123456789012:role/myrole');
});
});
+
+ it('should use use notificationArns if it is specified', () => {
+ const mytopicArn = 'arn:aws:sns::123456789012:mytopic';
+ awsDeploy.serverless.service.provider.notificationArns = [mytopicArn];
+
+ return awsDeploy.update().then(() => {
+ expect(updateStackStub.args[0][2].NotificationARNs)
+ .to.deep.equal([mytopicArn]);
+ });
+ });
});
describe('#updateStack()', () => {
diff --git a/lib/plugins/aws/logs/index.js b/lib/plugins/aws/logs/index.js
index 2d4a42c05e6..0eea806b015 100644
--- a/lib/plugins/aws/logs/index.js
+++ b/lib/plugins/aws/logs/index.js
@@ -87,6 +87,11 @@ class AwsLogs {
} else {
params.startTime = moment.utc(this.options.startTime).valueOf();
}
+ } else {
+ params.startTime = moment().subtract(10, 'm').valueOf();
+ if (this.options.tail) {
+ params.startTime = moment().subtract(10, 's').valueOf();
+ }
}
return this.provider
diff --git a/lib/plugins/aws/logs/index.test.js b/lib/plugins/aws/logs/index.test.js
index 0883a012b3d..343459124bf 100644
--- a/lib/plugins/aws/logs/index.test.js
+++ b/lib/plugins/aws/logs/index.test.js
@@ -157,10 +157,11 @@ describe('AwsLogs', () => {
describe('#showLogs()', () => {
let clock;
+ const fakeTime = new Date(Date.UTC(2016, 9, 1)).getTime();
beforeEach(() => {
- // new Date() => return the fake Date 'Sat Sep 01 2016 00:00:00'
- clock = sinon.useFakeTimers(new Date(Date.UTC(2016, 9, 1)).getTime());
+ // set the fake Date 'Sat Sep 01 2016 00:00:00'
+ clock = sinon.useFakeTimers(fakeTime);
});
afterEach(() => {
@@ -209,7 +210,7 @@ describe('AwsLogs', () => {
interleaved: true,
logStreamNames: logStreamNamesMock,
filterPattern: 'error',
- startTime: 1475269200000,
+ startTime: fakeTime - (3 * 60 * 60 * 1000), // -3h
}
)).to.be.equal(true);
awsLogs.provider.request.restore();
@@ -264,5 +265,98 @@ describe('AwsLogs', () => {
awsLogs.provider.request.restore();
});
});
+
+ it('should call filterLogEvents API with latest 10 minutes if startTime not given', () => {
+ const replyMock = {
+ events: [
+ {
+ logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba',
+ timestamp: 1469687512311,
+ message: 'test',
+ },
+ {
+ logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba',
+ timestamp: 1469687512311,
+ message: 'test',
+ },
+ ],
+ };
+ const logStreamNamesMock = [
+ '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba',
+ '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba',
+ ];
+ const filterLogEventsStub = sinon.stub(awsLogs.provider, 'request').resolves(replyMock);
+ awsLogs.serverless.service.service = 'new-service';
+ awsLogs.options = {
+ stage: 'dev',
+ region: 'us-east-1',
+ function: 'first',
+ logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'),
+ };
+
+ return awsLogs.showLogs(logStreamNamesMock)
+ .then(() => {
+ expect(filterLogEventsStub.calledOnce).to.be.equal(true);
+ expect(filterLogEventsStub.calledWithExactly(
+ 'CloudWatchLogs',
+ 'filterLogEvents',
+ {
+ logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'),
+ interleaved: true,
+ logStreamNames: logStreamNamesMock,
+ startTime: fakeTime - (10 * 60 * 1000), // fakeTime - 10 minutes
+ }
+ )).to.be.equal(true);
+
+ awsLogs.provider.request.restore();
+ });
+ });
+
+ it('should call filterLogEvents API which starts 10 seconds in the past if tail given', () => {
+ const replyMock = {
+ events: [
+ {
+ logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba',
+ timestamp: 1469687512311,
+ message: 'test',
+ },
+ {
+ logStreamName: '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba',
+ timestamp: 1469687512311,
+ message: 'test',
+ },
+ ],
+ };
+ const logStreamNamesMock = [
+ '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba',
+ '2016/07/28/[$LATEST]83f5206ab2a8488290349b9c1fbfe2ba',
+ ];
+ const filterLogEventsStub = sinon.stub(awsLogs.provider, 'request').resolves(replyMock);
+ awsLogs.serverless.service.service = 'new-service';
+ awsLogs.options = {
+ stage: 'dev',
+ region: 'us-east-1',
+ function: 'first',
+ logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'),
+ tail: true,
+ };
+
+ return awsLogs.showLogs(logStreamNamesMock)
+ .then(() => {
+ expect(filterLogEventsStub.calledOnce).to.be.equal(true);
+ expect(filterLogEventsStub.calledWithExactly(
+ 'CloudWatchLogs',
+ 'filterLogEvents',
+ {
+ logGroupName: awsLogs.provider.naming.getLogGroupName('new-service-dev-first'),
+ interleaved: true,
+ logStreamNames: logStreamNamesMock,
+ startTime: fakeTime - (10 * 1000), // fakeTime - 10 minutes
+ }
+ )).to.be.equal(true);
+
+ awsLogs.provider.request.restore();
+ });
+ });
});
});
diff --git a/lib/plugins/aws/package/compile/events/alexaSkill/index.js b/lib/plugins/aws/package/compile/events/alexaSkill/index.js
index ecc32cb4451..89c863bb4b2 100644
--- a/lib/plugins/aws/package/compile/events/alexaSkill/index.js
+++ b/lib/plugins/aws/package/compile/events/alexaSkill/index.js
@@ -15,10 +15,45 @@ class AwsCompileAlexaSkillEvents {
compileAlexaSkillEvents() {
this.serverless.service.getAllFunctions().forEach((functionName) => {
const functionObj = this.serverless.service.getFunction(functionName);
+ let alexaSkillNumberInFunction = 0;
if (functionObj.events) {
functionObj.events.forEach(event => {
- if (event === 'alexaSkill') {
+ if (event === 'alexaSkill' || event.alexaSkill) {
+ let enabled = true;
+ let appId;
+ if (event === 'alexaSkill') {
+ const warningMessage = [
+ 'Warning! You are using an old syntax for alexaSkill which doesn\'t',
+ ' restrict the invocation solely to your skill.',
+ ' Please refer to the documentation for additional information.',
+ ].join('');
+ this.serverless.cli.log(warningMessage);
+ } else if (_.isString(event.alexaSkill)) {
+ appId = event.alexaSkill;
+ } else if (_.isPlainObject(event.alexaSkill)) {
+ if (!_.isString(event.alexaSkill.appId)) {
+ const errorMessage = [
+ `Missing "appId" property for alexaSkill event in function ${functionName}`,
+ ' The correct syntax is: appId: amzn1.ask.skill.xx-xx-xx-xx-xx',
+ ' OR an object with "appId" property.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+ appId = event.alexaSkill.appId;
+ // Parameter `enabled` is optional, hence the explicit non-equal check for false.
+ enabled = event.alexaSkill.enabled !== false;
+ } else {
+ const errorMessage = [
+ `Alexa Skill event of function "${functionName}" is not an object or string.`,
+ ' The correct syntax is: alexaSkill.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+ alexaSkillNumberInFunction++;
+
const lambdaLogicalId = this.provider.naming
.getLambdaLogicalId(functionName);
@@ -31,13 +66,18 @@ class AwsCompileAlexaSkillEvents {
'Arn',
],
},
- Action: 'lambda:InvokeFunction',
+ Action: enabled ? 'lambda:InvokeFunction' : 'lambda:DisableInvokeFunction',
Principal: 'alexa-appkit.amazon.com',
},
};
+ if (appId) {
+ permissionTemplate.Properties.EventSourceToken = appId.replace(/\\n|\\r/g, '');
+ }
+
const lambdaPermissionLogicalId = this.provider.naming
- .getLambdaAlexaSkillPermissionLogicalId(functionName);
+ .getLambdaAlexaSkillPermissionLogicalId(functionName,
+ alexaSkillNumberInFunction);
const permissionCloudForamtionResource = {
[lambdaPermissionLogicalId]: permissionTemplate,
@@ -45,13 +85,6 @@ class AwsCompileAlexaSkillEvents {
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
permissionCloudForamtionResource);
- } else if (event.alexaSkill) {
- const errorMessage = [
- `Alexa Skill event of function "${functionName}" is not an object or string.`,
- ' The correct syntax is: alexaSkill.',
- ' Please check the docs for more info.',
- ].join('');
- throw new this.serverless.classes.Error(errorMessage);
}
});
}
diff --git a/lib/plugins/aws/package/compile/events/alexaSkill/index.test.js b/lib/plugins/aws/package/compile/events/alexaSkill/index.test.js
index 338a9fd0838..cb9090ae6cc 100644
--- a/lib/plugins/aws/package/compile/events/alexaSkill/index.test.js
+++ b/lib/plugins/aws/package/compile/events/alexaSkill/index.test.js
@@ -1,5 +1,7 @@
'use strict';
+/* eslint-disable no-unused-expressions */
+
const expect = require('chai').expect;
const AwsProvider = require('../../../../provider/awsProvider');
const AwsCompileAlexaSkillEvents = require('./index');
@@ -8,11 +10,19 @@ const Serverless = require('../../../../../../Serverless');
describe('AwsCompileAlexaSkillEvents', () => {
let serverless;
let awsCompileAlexaSkillEvents;
+ let consolePrinted;
beforeEach(() => {
serverless = new Serverless();
serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} };
serverless.setProvider('aws', new AwsProvider(serverless));
+ consolePrinted = '';
+ serverless.cli = {
+ // serverless.cli isn't available in tests, so we will mimic it.
+ log: txt => {
+ consolePrinted += `${txt}\r\n`;
+ },
+ };
awsCompileAlexaSkillEvents = new AwsCompileAlexaSkillEvents(serverless);
});
@@ -25,7 +35,42 @@ describe('AwsCompileAlexaSkillEvents', () => {
});
describe('#compileAlexaSkillEvents()', () => {
- it('should throw an error if alexaSkill event is not an string', () => {
+ it('should show a warning if alexaSkill appId is not specified', () => {
+ awsCompileAlexaSkillEvents.serverless.service.functions = {
+ first: {
+ events: [
+ 'alexaSkill',
+ ],
+ },
+ };
+
+ awsCompileAlexaSkillEvents.compileAlexaSkillEvents();
+
+ expect(consolePrinted).to.contain.string('old syntax for alexaSkill');
+
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Type
+ ).to.equal('AWS::Lambda::Permission');
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Properties.FunctionName
+ ).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] });
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Properties.Action
+ ).to.equal('lambda:InvokeFunction');
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Properties.Principal
+ ).to.equal('alexa-appkit.amazon.com');
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Properties.EventSourceToken
+ ).to.be.undefined;
+ });
+
+ it('should throw an error if alexaSkill event is not a string or an object', () => {
awsCompileAlexaSkillEvents.serverless.service.functions = {
first: {
events: [
@@ -39,11 +84,36 @@ describe('AwsCompileAlexaSkillEvents', () => {
expect(() => awsCompileAlexaSkillEvents.compileAlexaSkillEvents()).to.throw(Error);
});
- it('should create corresponding resources when a alexaSkill event is provided', () => {
+ it('should throw an error if alexaSkill event appId is not a string', () => {
awsCompileAlexaSkillEvents.serverless.service.functions = {
first: {
events: [
- 'alexaSkill',
+ {
+ alexaSkill: {
+ appId: 42,
+ },
+ },
+ ],
+ },
+ };
+
+ expect(() => awsCompileAlexaSkillEvents.compileAlexaSkillEvents()).to.throw(Error);
+ });
+
+ it('should create corresponding resources when multiple alexaSkill events are provided', () => {
+ const skillId1 = 'amzn1.ask.skill.xx-xx-xx-xx';
+ const skillId2 = 'amzn1.ask.skill.yy-yy-yy-yy';
+ awsCompileAlexaSkillEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ alexaSkill: skillId1,
+ },
+ {
+ alexaSkill: {
+ appId: skillId2,
+ },
+ },
],
},
};
@@ -52,20 +122,84 @@ describe('AwsCompileAlexaSkillEvents', () => {
expect(awsCompileAlexaSkillEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources
- .FirstLambdaPermissionAlexaSkill.Type
+ .FirstLambdaPermissionAlexaSkill1.Type
).to.equal('AWS::Lambda::Permission');
expect(awsCompileAlexaSkillEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources
- .FirstLambdaPermissionAlexaSkill.Properties.FunctionName
+ .FirstLambdaPermissionAlexaSkill1.Properties.FunctionName
).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] });
expect(awsCompileAlexaSkillEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources
- .FirstLambdaPermissionAlexaSkill.Properties.Action
+ .FirstLambdaPermissionAlexaSkill1.Properties.Action
).to.equal('lambda:InvokeFunction');
expect(awsCompileAlexaSkillEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources
- .FirstLambdaPermissionAlexaSkill.Properties.Principal
+ .FirstLambdaPermissionAlexaSkill1.Properties.Principal
).to.equal('alexa-appkit.amazon.com');
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Properties.EventSourceToken
+ ).to.equal(skillId1);
+
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill2.Type
+ ).to.equal('AWS::Lambda::Permission');
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill2.Properties.FunctionName
+ ).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] });
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill2.Properties.Action
+ ).to.equal('lambda:InvokeFunction');
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill2.Properties.Principal
+ ).to.equal('alexa-appkit.amazon.com');
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill2.Properties.EventSourceToken
+ ).to.equal(skillId2);
+ });
+
+ it('should create corresponding resources when a disabled alexaSkill event is provided', () => {
+ const skillId1 = 'amzn1.ask.skill.xx-xx-xx-xx';
+ awsCompileAlexaSkillEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ alexaSkill: {
+ appId: skillId1,
+ enabled: false,
+ },
+ },
+ ],
+ },
+ };
+
+ awsCompileAlexaSkillEvents.compileAlexaSkillEvents();
+
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Type
+ ).to.equal('AWS::Lambda::Permission');
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Properties.FunctionName
+ ).to.deep.equal({ 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] });
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Properties.Action
+ ).to.equal('lambda:DisableInvokeFunction');
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Properties.Principal
+ ).to.equal('alexa-appkit.amazon.com');
+ expect(awsCompileAlexaSkillEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionAlexaSkill1.Properties.EventSourceToken
+ ).to.equal(skillId1);
});
it('should not create corresponding resources when alexaSkill event is not given', () => {
@@ -82,5 +216,22 @@ describe('AwsCompileAlexaSkillEvents', () => {
.compiledCloudFormationTemplate.Resources
).to.deep.equal({});
});
+
+ it('should not not throw error when other events are present', () => {
+ awsCompileAlexaSkillEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'get',
+ path: '/',
+ },
+ },
+ ],
+ },
+ };
+
+ expect(() => awsCompileAlexaSkillEvents.compileAlexaSkillEvents()).to.not.throw();
+ });
});
});
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 5d15ad887ef..118b60325d5 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js
@@ -2,6 +2,7 @@
const BbPromise = require('bluebird');
const _ = require('lodash');
+const awsArnRegExs = require('../../../../../utils/arnRegularExpressions');
module.exports = {
compileAuthorizers() {
@@ -23,20 +24,25 @@ module.exports = {
const authorizerLogicalId = this.provider.naming.getAuthorizerLogicalId(authorizer.name);
- if (typeof authorizer.arn === 'string' && authorizer.arn.match(/^arn:aws:cognito-idp/)) {
+ if (typeof authorizer.arn === 'string'
+ && awsArnRegExs.cognitoIdpArnExpr.test(authorizer.arn)) {
authorizerProperties.Type = 'COGNITO_USER_POOLS';
authorizerProperties.ProviderARNs = [authorizer.arn];
} else {
authorizerProperties.AuthorizerUri =
- { 'Fn::Join': ['',
- [
- 'arn:aws:apigateway:',
- { Ref: 'AWS::Region' },
- ':lambda:path/2015-03-31/functions/',
- authorizer.arn,
- '/invocations',
+ {
+ 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':apigateway:',
+ { Ref: 'AWS::Region' },
+ ':lambda:path/2015-03-31/functions/',
+ authorizer.arn,
+ '/invocations',
+ ],
],
- ] };
+ };
authorizerProperties.Type = authorizer.type ? authorizer.type.toUpperCase() : 'TOKEN';
}
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js
index ee84ad8a24b..5ec1dc89366 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js
@@ -39,15 +39,18 @@ describe('#compileAuthorizers()', () => {
expect(resource.Type).to.equal('AWS::ApiGateway::Authorizer');
expect(resource.Properties.AuthorizerResultTtlInSeconds).to.equal(300);
- expect(resource.Properties.AuthorizerUri).to.deep.equal({ 'Fn::Join': ['',
- [
- 'arn:aws:apigateway:',
- { Ref: 'AWS::Region' },
- ':lambda:path/2015-03-31/functions/',
- { 'Fn::GetAtt': ['SomeLambdaFunction', 'Arn'] },
- '/invocations',
+ expect(resource.Properties.AuthorizerUri).to.deep.equal({
+ 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':apigateway:',
+ { Ref: 'AWS::Region' },
+ ':lambda:path/2015-03-31/functions/',
+ { 'Fn::GetAtt': ['SomeLambdaFunction', 'Arn'] },
+ '/invocations',
+ ],
],
- ],
});
expect(resource.Properties.IdentitySource).to.equal('method.request.header.Authorization');
expect(resource.Properties.IdentityValidationExpression).to.equal(undefined);
@@ -77,15 +80,18 @@ describe('#compileAuthorizers()', () => {
.compiledCloudFormationTemplate.Resources.AuthorizerApiGatewayAuthorizer;
expect(resource.Type).to.equal('AWS::ApiGateway::Authorizer');
- expect(resource.Properties.AuthorizerUri).to.deep.equal({ 'Fn::Join': ['',
- [
- 'arn:aws:apigateway:',
- { Ref: 'AWS::Region' },
- ':lambda:path/2015-03-31/functions/',
- 'foo',
- '/invocations',
+ expect(resource.Properties.AuthorizerUri).to.deep.equal({
+ 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':apigateway:',
+ { Ref: 'AWS::Region' },
+ ':lambda:path/2015-03-31/functions/',
+ 'foo',
+ '/invocations',
+ ],
],
- ],
});
expect(resource.Properties.AuthorizerResultTtlInSeconds).to.equal(500);
expect(resource.Properties.IdentitySource).to.equal('method.request.header.Custom');
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 167d113fd0a..99797e4cfa8 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js
@@ -25,6 +25,16 @@ module.exports = {
'Access-Control-Allow-Credentials': `'${config.allowCredentials}'`,
};
+ // Enable CORS Max Age usage if set
+ if (_.has(config, 'maxAge')) {
+ if (_.isInteger(config.maxAge) && config.maxAge > 0) {
+ preflightHeaders['Access-Control-Max-Age'] = `'${config.maxAge}'`;
+ } else {
+ const errorMessage = 'maxAge should be an integer over 0';
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+ }
+
if (_.includes(config.methods, 'ANY')) {
preflightHeaders['Access-Control-Allow-Methods'] =
preflightHeaders['Access-Control-Allow-Methods']
@@ -44,6 +54,7 @@ module.exports = {
RequestTemplates: {
'application/json': '{statusCode:200}',
},
+ ContentHandling: 'CONVERT_TO_TEXT',
IntegrationResponses: this.generateCorsIntegrationResponses(preflightHeaders),
},
ResourceId: resourceRef,
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 db2d06c876b..64f65624b4c 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
@@ -67,18 +67,21 @@ describe('#compileCors()', () => {
headers: ['*'],
methods: ['OPTIONS', 'PUT'],
allowCredentials: false,
+ maxAge: 86400,
},
'users/create': {
origins: ['*', 'http://example.com'],
headers: ['*'],
methods: ['OPTIONS', 'POST'],
allowCredentials: true,
+ maxAge: 86400,
},
'users/delete': {
origins: ['*'],
headers: ['CustomHeaderA', 'CustomHeaderB'],
methods: ['OPTIONS', 'DELETE'],
allowCredentials: false,
+ maxAge: 86400,
},
'users/any': {
origins: ['http://example.com'],
@@ -117,6 +120,13 @@ describe('#compileCors()', () => {
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
).to.equal('\'true\'');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreateOptions
+ .Properties.Integration.IntegrationResponses[0]
+ .ResponseParameters['method.response.header.Access-Control-Max-Age']
+ ).to.equal('\'86400\'');
+
// users/update
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
@@ -139,6 +149,13 @@ describe('#compileCors()', () => {
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
).to.equal('\'false\'');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersUpdateOptions
+ .Properties.Integration.IntegrationResponses[0]
+ .ResponseParameters['method.response.header.Access-Control-Max-Age']
+ ).to.equal('\'86400\'');
+
// users/delete
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
@@ -168,6 +185,13 @@ describe('#compileCors()', () => {
.ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
).to.equal('\'false\'');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersDeleteOptions
+ .Properties.Integration.IntegrationResponses[0]
+ .ResponseParameters['method.response.header.Access-Control-Max-Age']
+ ).to.equal('\'86400\'');
+
// users/any
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
@@ -198,4 +222,34 @@ describe('#compileCors()', () => {
).to.equal('\'false\'');
});
});
+
+ it('should throw error if maxAge is not an integer greater than 0', () => {
+ awsCompileApigEvents.validated.corsPreflight = {
+ 'users/update': {
+ origin: 'http://example.com',
+ headers: ['*'],
+ methods: ['OPTIONS', 'PUT'],
+ allowCredentials: false,
+ maxAge: -1,
+ },
+ };
+
+ expect(() => awsCompileApigEvents.compileCors())
+ .to.throw(Error, 'maxAge should be an integer over 0');
+ });
+
+ it('should throw error if maxAge is not an integer', () => {
+ awsCompileApigEvents.validated.corsPreflight = {
+ 'users/update': {
+ origin: 'http://example.com',
+ headers: ['*'],
+ methods: ['OPTIONS', 'PUT'],
+ allowCredentials: false,
+ maxAge: 'five',
+ },
+ };
+
+ expect(() => awsCompileApigEvents.compileCors())
+ .to.throw(Error, 'maxAge should be an integer over 0');
+ });
});
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 dd1d804a9a0..9eb923ba596 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js
@@ -28,11 +28,9 @@ module.exports = {
[
'https://',
this.provider.getApiGatewayRestApiId(),
- `.execute-api.${
- this.provider.getRegion()
- }.amazonaws.com/${
- this.provider.getStage()
- }`,
+ `.execute-api.${this.provider.getRegion()}.`,
+ { Ref: 'AWS::URLSuffix' },
+ `/${this.provider.getStage()}`,
],
],
},
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js
index 3013f1e1144..98be9ee278e 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js
@@ -63,7 +63,9 @@ describe('#compileDeployment()', () => {
[
'https://',
{ Ref: awsCompileApigEvents.apiGatewayRestApiLogicalId },
- '.execute-api.us-east-1.amazonaws.com/dev',
+ '.execute-api.us-east-1.',
+ { Ref: 'AWS::URLSuffix' },
+ '/dev',
],
],
},
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js
index 62e0acfa61d..c9314397b7a 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js
@@ -1,6 +1,7 @@
'use strict';
const _ = require('lodash');
+const awsArnRegExs = require('../../../../../../utils/arnRegularExpressions');
module.exports = {
getMethodAuthorization(http) {
@@ -13,12 +14,22 @@ module.exports = {
}
if (http.authorizer) {
+ if (http.authorizer.type && http.authorizer.authorizerId) {
+ return {
+ Properties: {
+ AuthorizationType: http.authorizer.type,
+ AuthorizerId: http.authorizer.authorizerId,
+ },
+ };
+ }
+
const authorizerLogicalId = this.provider.naming
.getAuthorizerLogicalId(http.authorizer.name);
let authorizationType;
const authorizerArn = http.authorizer.arn;
- if (typeof authorizerArn === 'string' && authorizerArn.match(/^arn:aws:cognito-idp/)) {
+ if (typeof authorizerArn === 'string'
+ && awsArnRegExs.cognitoIdpArnExpr.test(authorizerArn)) {
authorizationType = 'COGNITO_USER_POOLS';
} else {
authorizationType = 'CUSTOM';
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 2f47e5c50e4..5b985331a9c 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
@@ -399,6 +399,32 @@ describe('#compileMethods()', () => {
});
});
+ it('should set custom authorizer config with authorizeId', () => {
+ awsCompileApigEvents.validated.events = [
+ {
+ functionName: 'First',
+ http: {
+ path: 'users/create',
+ method: 'post',
+ authorizer: {
+ type: 'COGNITO_USER_POOLS',
+ authorizerId: 'gy7lyj',
+ },
+ },
+ },
+ ];
+ return awsCompileApigEvents.compileMethods().then(() => {
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType
+ ).to.equal('COGNITO_USER_POOLS');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizerId
+ ).to.equal('gy7lyj');
+ });
+ });
+
it('should set authorizer config if given as ARN string', () => {
awsCompileApigEvents.validated.events = [
{
@@ -407,6 +433,7 @@ describe('#compileMethods()', () => {
authorizer: {
name: 'Authorizer',
},
+ integration: 'AWS',
path: 'users/create',
method: 'post',
},
@@ -456,7 +483,7 @@ describe('#compileMethods()', () => {
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersCreatePost.Properties
.Integration.RequestTemplates['application/json']
- ).to.not.match(/undefined/);
+ ).to.not.match(/undefined/);
});
});
@@ -479,8 +506,8 @@ describe('#compileMethods()', () => {
return awsCompileApigEvents.compileMethods().then(() => {
const jsonRequestTemplatesString = awsCompileApigEvents.serverless.service.provider
- .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties
- .Integration.RequestTemplates['application/json'];
+ .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties
+ .Integration.RequestTemplates['application/json'];
const cognitoPoolClaimsRegex = /"cognitoPoolClaims"\s*:\s*(\{[^}]*\})/;
const cognitoPoolClaimsString = jsonRequestTemplatesString.match(cognitoPoolClaimsRegex)[1];
const cognitoPoolClaims = JSON.parse(cognitoPoolClaimsString);
@@ -507,8 +534,8 @@ describe('#compileMethods()', () => {
return awsCompileApigEvents.compileMethods().then(() => {
const jsonRequestTemplatesString = awsCompileApigEvents.serverless.service.provider
- .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties
- .Integration.RequestTemplates['application/json'];
+ .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties
+ .Integration.RequestTemplates['application/json'];
const cognitoPoolClaimsRegex = /"cognitoPoolClaims"\s*:\s*(\{[^}]*\})/;
const cognitoPoolClaimsString = jsonRequestTemplatesString.match(cognitoPoolClaimsRegex)[1];
const cognitoPoolClaims = JSON.parse(cognitoPoolClaimsString);
@@ -536,8 +563,8 @@ describe('#compileMethods()', () => {
return awsCompileApigEvents.compileMethods().then(() => {
const jsonRequestTemplatesString = awsCompileApigEvents.serverless.service.provider
- .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties
- .Integration.RequestTemplates['application/json'];
+ .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties
+ .Integration.RequestTemplates['application/json'];
const cognitoPoolClaimsRegex = /"cognitoPoolClaims"\s*:\s*(\{[^}]*\})/;
const cognitoPoolClaimsString = jsonRequestTemplatesString.match(cognitoPoolClaimsRegex)[1];
const cognitoPoolClaims = JSON.parse(cognitoPoolClaimsString);
@@ -568,7 +595,7 @@ describe('#compileMethods()', () => {
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersCreatePost.Properties
.Integration.RequestTemplates['application/json']
- ).to.not.match(/extraCognitoPoolClaims/);
+ ).to.not.match(/extraCognitoPoolClaims/);
});
});
@@ -665,7 +692,9 @@ describe('#compileMethods()', () => {
).to.deep.equal({
'Fn::Join': [
'', [
- 'arn:aws:apigateway:', { Ref: 'AWS::Region' },
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':apigateway:', { Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/', { 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] },
'/invocations',
],
@@ -677,7 +706,9 @@ describe('#compileMethods()', () => {
).to.deep.equal({
'Fn::Join': [
'', [
- 'arn:aws:apigateway:', { Ref: 'AWS::Region' },
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':apigateway:', { Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/', { 'Fn::GetAtt': ['SecondLambdaFunction', 'Arn'] },
'/invocations',
],
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 1a900bd9d9b..d9e76d3fded 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
@@ -26,6 +26,9 @@ const DEFAULT_COMMON_TEMPLATE = `
"sub": "$context.authorizer.claims.sub"
},
+ #set( $map = $context.authorizer )
+ "enhancedAuthContext": $loop,
+
#set( $map = $input.params().header )
"headers": $loop,
@@ -62,7 +65,9 @@ module.exports = {
Uri: {
'Fn::Join': ['',
[
- 'arn:aws:apigateway:',
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
{ 'Fn::GetAtt': [lambdaLogicalId, 'Arn'] },
@@ -180,7 +185,6 @@ module.exports = {
return !_.isEmpty(integrationRequestTemplates) ? integrationRequestTemplates : undefined;
},
-
getIntegrationRequestParameters(http) {
const parameters = {};
if (http.request && http.request.parameters) {
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 2c09c0c7ba4..61641b53505 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js
@@ -2,6 +2,7 @@
const _ = require('lodash');
const BbPromise = require('bluebird');
+const awsArnRegExs = require('../../../../../utils/arnRegularExpressions');
module.exports = {
@@ -18,29 +19,34 @@ module.exports = {
'Fn::GetAtt': [singlePermissionMapping.lambdaLogicalId, 'Arn'],
},
Action: 'lambda:InvokeFunction',
- Principal: 'apigateway.amazonaws.com',
- SourceArn: { 'Fn::Join': ['',
- [
- 'arn:aws:execute-api:',
- { Ref: 'AWS::Region' },
- ':',
- { Ref: 'AWS::AccountId' },
- ':',
- this.provider.getApiGatewayRestApiId(),
- '/*/*',
+ Principal: { 'Fn::Join': ['', ['apigateway.', { Ref: 'AWS::URLSuffix' }]] },
+ SourceArn: {
+ 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':execute-api:',
+ { Ref: 'AWS::Region' },
+ ':',
+ { Ref: 'AWS::AccountId' },
+ ':',
+ this.provider.getApiGatewayRestApiId(),
+ '/*/*',
+ ],
],
- ] },
+ },
},
},
});
if (singlePermissionMapping.event.http.authorizer &&
- singlePermissionMapping.event.http.authorizer.arn) {
+ singlePermissionMapping.event.http.authorizer.arn) {
const authorizer = singlePermissionMapping.event.http.authorizer;
const authorizerPermissionLogicalId = this.provider.naming
.getLambdaApiGatewayPermissionLogicalId(authorizer.name);
- if (typeof authorizer.arn === 'string' && authorizer.arn.match(/^arn:aws:cognito-idp/)) {
+ if (typeof authorizer.arn === 'string'
+ && awsArnRegExs.cognitoIdpArnExpr.test(authorizer.arn)) {
return;
}
@@ -50,7 +56,7 @@ module.exports = {
Properties: {
FunctionName: authorizer.arn,
Action: 'lambda:InvokeFunction',
- Principal: 'apigateway.amazonaws.com',
+ Principal: { 'Fn::Join': ['', ['apigateway.', { Ref: 'AWS::URLSuffix' }]] },
},
},
});
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js
index 3602cab8f5f..a0ff32a6749 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js
@@ -48,17 +48,76 @@ describe('#awsCompilePermissions()', () => {
.Resources.FirstLambdaPermissionApiGateway
.Properties.FunctionName['Fn::GetAtt'][0]).to.equal('FirstLambdaFunction');
- const deepObj = { 'Fn::Join': ['',
- [
- 'arn:aws:execute-api:',
- { Ref: 'AWS::Region' },
- ':',
- { Ref: 'AWS::AccountId' },
- ':',
- { Ref: 'ApiGatewayRestApi' },
- '/*/*',
+ const deepObj = {
+ 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':execute-api:',
+ { Ref: 'AWS::Region' },
+ ':',
+ { Ref: 'AWS::AccountId' },
+ ':',
+ { Ref: 'ApiGatewayRestApi' },
+ '/*/*',
+ ],
],
- ] };
+ };
+
+ expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstLambdaPermissionApiGateway
+ .Properties.SourceArn).to.deep.equal(deepObj);
+ });
+ });
+
+ it('should create limited permission resource scope to REST API with restApiId provided', () => {
+ awsCompileApigEvents.serverless.service.provider.apiGateway = {
+ restApiId: 'xxxxx',
+ };
+ awsCompileApigEvents.validated.events = [
+ {
+ functionName: 'First',
+ http: {
+ path: 'foo/bar',
+ method: 'post',
+ },
+ },
+ ];
+ awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
+ awsCompileApigEvents.permissionMapping = [
+ {
+ lambdaLogicalId: 'FirstLambdaFunction',
+ resourceName: 'FooBar',
+ event: {
+ http: {
+ path: 'foo/bar',
+ method: 'post',
+ },
+ functionName: 'First',
+ },
+ },
+ ];
+
+ return awsCompileApigEvents.compilePermissions().then(() => {
+ expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstLambdaPermissionApiGateway
+ .Properties.FunctionName['Fn::GetAtt'][0]).to.equal('FirstLambdaFunction');
+
+ const deepObj = {
+ 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':execute-api:',
+ { Ref: 'AWS::Region' },
+ ':',
+ { Ref: 'AWS::AccountId' },
+ ':',
+ 'xxxxx',
+ '/*/*',
+ ],
+ ],
+ };
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.FirstLambdaPermissionApiGateway
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 fc3dd0af46f..d0cfb52a85b 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js
@@ -15,7 +15,7 @@ module.exports = {
let endpointType = 'EDGE';
if (this.serverless.service.provider.endpointType) {
- const validEndpointTypes = ['REGIONAL', 'EDGE'];
+ const validEndpointTypes = ['REGIONAL', 'EDGE', 'PRIVATE'];
endpointType = this.serverless.service.provider.endpointType;
if (typeof endpointType !== 'string') {
@@ -23,8 +23,8 @@ module.exports = {
}
- if (!validEndpointTypes.includes(endpointType.toUpperCase())) {
- const message = 'endpointType must be one of "REGIONAL" or "EDGE". ' +
+ if (!_.includes(validEndpointTypes, endpointType.toUpperCase())) {
+ const message = 'endpointType must be one of "REGIONAL" or "EDGE" or "PRIVATE". ' +
`You provided ${endpointType}.`;
throw new this.serverless.classes.Error(message);
}
@@ -43,6 +43,17 @@ module.exports = {
},
});
+ if (!_.isEmpty(this.serverless.service.provider.resourcePolicy)) {
+ const policy = {
+ Version: '2012-10-17',
+ Statement: this.serverless.service.provider.resourcePolicy,
+ };
+ _.merge(this.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[this.apiGatewayRestApiLogicalId].Properties, {
+ Policy: policy,
+ });
+ }
+
return BbPromise.resolve();
},
};
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 eb3aa7e8a9b..4aa68dd8c98 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
@@ -16,8 +16,36 @@ describe('#compileRestApi()', () => {
Properties: {
Name: 'dev-new-service',
EndpointConfiguration: {
- Types: [
- 'EDGE',
+ Types: ['EDGE'],
+ },
+ },
+ },
+ },
+ };
+
+ const serviceResourcesAwsResourcesObjectWithResourcePolicyMock = {
+ Resources: {
+ ApiGatewayRestApi: {
+ Type: 'AWS::ApiGateway::RestApi',
+ Properties: {
+ Name: 'dev-new-service',
+ EndpointConfiguration: {
+ Types: ['EDGE'],
+ },
+ Policy: {
+ Version: '2012-10-17',
+ Statement: [
+ {
+ Effect: 'Allow',
+ Principal: '*',
+ Action: 'execute-api:Invoke',
+ Resource: ['execute-api:/*/*/*'],
+ Condition: {
+ IpAddress: {
+ 'aws:SourceIp': ['123.123.123.123'],
+ },
+ },
+ },
],
},
},
@@ -49,40 +77,66 @@ describe('#compileRestApi()', () => {
};
});
- it('should create a REST API resource', () => awsCompileApigEvents
- .compileRestApi().then(() => {
- expect(
- awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
- .Resources
- ).to.deep.equal(
- serviceResourcesAwsResourcesObjectMock.Resources
- );
- })
- );
+ it('should create a REST API resource', () =>
+ awsCompileApigEvents.compileRestApi().then(() => {
+ expect(awsCompileApigEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources).to.deep.equal(
+ serviceResourcesAwsResourcesObjectMock.Resources
+ );
+ }));
- 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('should create a REST API resource with resource policy', () => {
+ awsCompileApigEvents.serverless.service.provider.resourcePolicy = [
+ {
+ Effect: 'Allow',
+ Principal: '*',
+ Action: 'execute-api:Invoke',
+ Resource: ['execute-api:/*/*/*'],
+ Condition: {
+ IpAddress: {
+ 'aws:SourceIp': ['123.123.123.123'],
+ },
+ },
+ },
+ ];
+ return awsCompileApigEvents.compileRestApi().then(() => {
+ expect(awsCompileApigEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources).to.deep.equal(
+ serviceResourcesAwsResourcesObjectWithResourcePolicyMock.Resources
+ );
+ });
+ });
+
+ 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);
});
+ it('should compile if endpointType property is REGIONAL', () => {
+ awsCompileApigEvents.serverless.service.provider.endpointType = 'REGIONAL';
+ expect(() => awsCompileApigEvents.compileRestApi()).to.not.throw(Error);
+ });
+
+ it('should compile if endpointType property is PRIVATE', () => {
+ awsCompileApigEvents.serverless.service.provider.endpointType = 'PRIVATE';
+ expect(() => awsCompileApigEvents.compileRestApi()).to.not.throw(Error);
+ });
+
it('throw error if endpointType property is not EDGE or REGIONAL', () => {
awsCompileApigEvents.serverless.service.provider.endpointType = 'Testing';
- expect(() => awsCompileApigEvents.compileRestApi()).to.throw(Error);
+ expect(() => awsCompileApigEvents.compileRestApi()).to.throw('endpointType must be one of');
});
});
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.js
index 7bc199bb34e..4206370ddcf 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.js
@@ -5,7 +5,7 @@ const BbPromise = require('bluebird');
module.exports = {
compileUsagePlan() {
- if (this.serverless.service.provider.apiKeys) {
+ if (this.serverless.service.provider.usagePlan || this.serverless.service.provider.apiKeys) {
this.apiGatewayUsagePlanLogicalId = this.provider.naming.getUsagePlanLogicalId();
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[this.apiGatewayUsagePlanLogicalId]: {
@@ -14,9 +14,7 @@ module.exports = {
Properties: {
ApiStages: [
{
- ApiId: {
- Ref: this.apiGatewayRestApiLogicalId,
- },
+ ApiId: this.provider.getApiGatewayRestApiId(),
Stage: this.provider.getStage(),
},
],
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.test.js
index 370a8c59471..168d83f5424 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.test.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/usagePlan.test.js
@@ -17,21 +17,6 @@ describe('#compileUsagePlan()', () => {
serverless = new Serverless();
serverless.setProvider('aws', new AwsProvider(serverless, options));
serverless.service.service = 'first-service';
- serverless.service.provider = {
- name: 'aws',
- apiKeys: ['1234567890'],
- usagePlan: {
- quota: {
- limit: 500,
- offset: 10,
- period: 'MONTH',
- },
- throttle: {
- burstLimit: 200,
- rateLimit: 100,
- },
- },
- };
serverless.service.provider.compiledCloudFormationTemplate = {
Resources: {},
Outputs: {},
@@ -41,8 +26,67 @@ describe('#compileUsagePlan()', () => {
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
});
- it('should compile usage plan resource', () =>
- awsCompileApigEvents.compileUsagePlan().then(() => {
+ it('should compile default usage plan resource', () => {
+ serverless.service.provider.apiKeys = ['1234567890'];
+ return awsCompileApigEvents.compileUsagePlan().then(() => {
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[
+ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId()
+ ].Type
+ ).to.equal('AWS::ApiGateway::UsagePlan');
+
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[
+ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId()
+ ].DependsOn
+ ).to.equal('ApiGatewayDeploymentTest');
+
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[
+ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId()
+ ].Properties.ApiStages[0].ApiId.Ref
+ ).to.equal('ApiGatewayRestApi');
+
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[
+ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId()
+ ].Properties.ApiStages[0].Stage
+ ).to.equal('dev');
+
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[
+ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId()
+ ].Properties.Description
+ ).to.equal('Usage plan for first-service dev stage');
+
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[
+ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId()
+ ].Properties.UsagePlanName
+ ).to.equal('first-service-dev');
+ });
+ });
+
+ it('should compile custom usage plan resource', () => {
+ serverless.service.provider.usagePlan = {
+ quota: {
+ limit: 500,
+ offset: 10,
+ period: 'MONTH',
+ },
+ throttle: {
+ burstLimit: 200,
+ rateLimit: 100,
+ },
+ };
+
+ return awsCompileApigEvents.compileUsagePlan().then(() => {
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources[
@@ -105,6 +149,22 @@ describe('#compileUsagePlan()', () => {
awsCompileApigEvents.provider.naming.getUsagePlanLogicalId()
].Properties.UsagePlanName
).to.equal('first-service-dev');
- })
- );
+ });
+ });
+
+ it('should compile custom usage plan resource with restApiId provided', () => {
+ serverless.service.provider.apiKeys = ['1234567890'];
+ awsCompileApigEvents.serverless.service.provider.apiGateway = {
+ restApiId: 'xxxxx',
+ };
+
+ return awsCompileApigEvents.compileUsagePlan().then(() => {
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[
+ awsCompileApigEvents.provider.naming.getUsagePlanLogicalId()
+ ].Properties.ApiStages[0].ApiId
+ ).to.equal('xxxxx');
+ });
+ });
});
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 bbafbb75163..f70b893184c 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js
@@ -1,6 +1,7 @@
'use strict';
const _ = require('lodash');
+const awsArnRegExs = require('../../../../../utils/arnRegularExpressions');
const NOT_FOUND = -1;
const DEFAULT_STATUS_CODES = {
@@ -61,6 +62,11 @@ module.exports = {
cors.origin = http.cors.origin || '*';
cors.allowCredentials = cors.allowCredentials || http.cors.allowCredentials;
+ // when merging, last one defined wins
+ if (_.has(http.cors, 'maxAge')) {
+ cors.maxAge = http.cors.maxAge;
+ }
+
corsPreflight[http.path] = cors;
}
@@ -78,6 +84,11 @@ module.exports = {
http.request = this.getRequest(http);
http.request.passThrough = this.getRequestPassThrough(http);
http.response = this.getResponse(http);
+ if (http.integration === 'AWS' && _.isEmpty(http.response)) {
+ http.response = {
+ statusCodes: DEFAULT_STATUS_CODES,
+ };
+ }
} 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) {
@@ -204,6 +215,7 @@ module.exports = {
let resultTtlInSeconds;
let identityValidationExpression;
let claims;
+ let authorizerId;
if (typeof authorizer === 'string') {
if (authorizer.toUpperCase() === 'AWS_IAM') {
@@ -216,7 +228,10 @@ module.exports = {
name = this.provider.naming.extractAuthorizerNameFromArn(arn);
}
} else if (typeof authorizer === 'object') {
- if (authorizer.type && authorizer.type.toUpperCase() === 'AWS_IAM') {
+ if (authorizer.type && authorizer.authorizerId) {
+ type = authorizer.type;
+ authorizerId = authorizer.authorizerId;
+ } else if (authorizer.type && authorizer.type.toUpperCase() === 'AWS_IAM') {
type = 'AWS_IAM';
} else if (authorizer.arn) {
arn = authorizer.arn;
@@ -258,7 +273,9 @@ module.exports = {
const integration = this.getIntegration(http);
if (integration === 'AWS_PROXY'
- && typeof arn === 'string' && arn.match(/^arn:aws:cognito-idp/) && authorizer.claims) {
+ && typeof arn === 'string'
+ && awsArnRegExs.cognitoIdpArnExpr.test(arn)
+ && authorizer.claims) {
const errorMessage = [
'Cognito claims can only be filtered when using the lambda integration type',
];
@@ -269,6 +286,7 @@ module.exports = {
type,
name,
arn,
+ authorizerId,
resultTtlInSeconds,
identitySource,
identityValidationExpression,
@@ -327,10 +345,15 @@ module.exports = {
if (cors.methods.indexOf(http.method.toUpperCase()) === NOT_FOUND) {
cors.methods.push(http.method.toUpperCase());
}
+ if (_.has(cors, 'maxAge')) {
+ if (!_.isInteger(cors.maxAge) || cors.maxAge < 1) {
+ const errorMessage = 'maxAge should be an integer over 0';
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+ }
} else {
cors.methods.push(http.method.toUpperCase());
}
-
return cors;
},
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 a2132f048e0..6cbbfaac553 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
@@ -643,6 +643,7 @@ describe('#validate()', () => {
headers: ['X-Foo-Bar'],
origins: ['acme.com'],
methods: ['POST', 'OPTIONS'],
+ maxAge: 86400,
},
},
},
@@ -657,10 +658,11 @@ describe('#validate()', () => {
methods: ['POST', 'OPTIONS'],
origins: ['acme.com'],
allowCredentials: false,
+ maxAge: 86400,
});
});
- it('should merge all preflight origins, method, headers and allowCredentials for a path', () => {
+ it('should merge all preflight cors options for a path', () => {
awsCompileApigEvents.serverless.service.functions = {
first: {
events: [
@@ -673,6 +675,7 @@ describe('#validate()', () => {
'http://example.com',
],
allowCredentials: true,
+ maxAge: 10000,
},
},
}, {
@@ -683,6 +686,7 @@ describe('#validate()', () => {
origins: [
'http://example2.com',
],
+ maxAge: 86400,
},
},
}, {
@@ -717,12 +721,35 @@ describe('#validate()', () => {
.to.deep.equal(['http://example2.com', 'http://example.com']);
expect(validated.corsPreflight['users/{id}'].headers)
.to.deep.equal(['TestHeader2', 'TestHeader']);
+ expect(validated.corsPreflight.users.maxAge)
+ .to.equal(86400);
expect(validated.corsPreflight.users.allowCredentials)
.to.equal(true);
expect(validated.corsPreflight['users/{id}'].allowCredentials)
.to.equal(false);
});
+ it('should throw an error if the maxAge is not a positive integer', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'POST',
+ path: '/foo/bar',
+ cors: {
+ origin: '*',
+ maxAge: -1,
+ },
+ },
+ },
+ ],
+ },
+ };
+
+ expect(() => awsCompileApigEvents.validate()).to.throw(Error);
+ });
+
it('should add default statusCode to custom statusCodes', () => {
awsCompileApigEvents.serverless.service.functions = {
first: {
@@ -1728,4 +1755,54 @@ describe('#validate()', () => {
expect(validated.events).to.be.an('Array').with.length(1);
expect(validated.events[0].http.request.passThrough).to.equal(undefined);
});
+
+ it('should set default statusCodes to response for lambda by default', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'lambda',
+ integrationMethod: 'GET',
+ },
+ },
+ ],
+ },
+ };
+
+ const validated = awsCompileApigEvents.validate();
+ expect(validated.events).to.be.an('Array').with.length(1);
+ expect(validated.events[0].http.response.statusCodes).to.deep.equal({
+ 200: {
+ pattern: '',
+ },
+ 400: {
+ pattern: '[\\s\\S]*\\[400\\][\\s\\S]*',
+ },
+ 401: {
+ pattern: '[\\s\\S]*\\[401\\][\\s\\S]*',
+ },
+ 403: {
+ pattern: '[\\s\\S]*\\[403\\][\\s\\S]*',
+ },
+ 404: {
+ pattern: '[\\s\\S]*\\[404\\][\\s\\S]*',
+ },
+ 422: {
+ pattern: '[\\s\\S]*\\[422\\][\\s\\S]*',
+ },
+ 500: {
+ pattern:
+ '[\\s\\S]*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\])[\\s\\S]*',
+ },
+ 502: {
+ pattern: '[\\s\\S]*\\[502\\][\\s\\S]*',
+ },
+ 504: {
+ pattern: '([\\s\\S]*\\[504\\][\\s\\S]*)|(^[Task timed out].*)',
+ },
+ });
+ });
});
diff --git a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js
index d8ef7b2e998..6f7fd8acba2 100644
--- a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js
+++ b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js
@@ -26,6 +26,7 @@ class AwsCompileCloudWatchEventEvents {
let Input;
let InputPath;
let Description;
+ let Name;
if (typeof event.cloudwatchEvent === 'object') {
if (!event.cloudwatchEvent.event) {
@@ -45,6 +46,7 @@ class AwsCompileCloudWatchEventEvents {
Input = event.cloudwatchEvent.input;
InputPath = event.cloudwatchEvent.inputPath;
Description = event.cloudwatchEvent.description;
+ Name = event.cloudwatchEvent.name;
if (Input && InputPath) {
const errorMessage = [
@@ -87,6 +89,7 @@ class AwsCompileCloudWatchEventEvents {
"EventPattern": ${EventPattern.replace(/\\n|\\r/g, '')},
"State": "${State}",
${Description ? `"Description": "${Description}",` : ''}
+ ${Name ? `"Name": "${Name}",` : ''}
"Targets": [{
${Input ? `"Input": "${Input.replace(/\\n|\\r/g, '')}",` : ''}
${InputPath ? `"InputPath": "${InputPath.replace(/\r?\n/g, '')}",` : ''}
@@ -104,7 +107,7 @@ class AwsCompileCloudWatchEventEvents {
"FunctionName": { "Fn::GetAtt": ["${
lambdaLogicalId}", "Arn"] },
"Action": "lambda:InvokeFunction",
- "Principal": "events.amazonaws.com",
+ "Principal": { "Fn::Join": ["", ["events.", { "Ref": "AWS::URLSuffix" }]] },
"SourceArn": { "Fn::GetAtt": ["${cloudWatchLogicalId}", "Arn"] }
}
}
diff --git a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js
index 081bc08c7d3..0dd5123a2d7 100644
--- a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js
+++ b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.test.js
@@ -246,6 +246,34 @@ describe('awsCompileCloudWatchEventEvents', () => {
).to.equal('test description');
});
+ it('should respect name variable', () => {
+ awsCompileCloudWatchEventEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ cloudwatchEvent: {
+ event: {
+ source: ['aws.ec2'],
+ 'detail-type': ['EC2 Instance State-change Notification'],
+ detail: { state: ['pending'] },
+ },
+ enabled: false,
+ input: '{"key":"value"}',
+ name: 'test-event-name',
+ },
+ },
+ ],
+ },
+ };
+
+ awsCompileCloudWatchEventEvents.compileCloudWatchEventEvents();
+
+ expect(awsCompileCloudWatchEventEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventsRuleCloudWatchEvent1
+ .Properties.Name
+ ).to.equal('test-event-name');
+ });
+
it('should respect input variable as an object', () => {
awsCompileCloudWatchEventEvents.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 697d9736d09..f356d0bb779 100644
--- a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js
+++ b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js
@@ -76,7 +76,7 @@ class AwsCompileCloudWatchLogEvents {
.getCloudWatchLogLogicalId(functionName, cloudWatchLogNumberInFunction);
const lambdaPermissionLogicalId = this.provider.naming
.getLambdaCloudWatchLogPermissionLogicalId(functionName,
- cloudWatchLogNumberInFunction);
+ cloudWatchLogNumberInFunction);
// unescape quotes once when the first quote is detected escaped
const idxFirstSlash = FilterPattern.indexOf('\\');
@@ -98,33 +98,36 @@ class AwsCompileCloudWatchLogEvents {
`;
const permissionTemplate = `
- {
- "Type": "AWS::Lambda::Permission",
- "Properties": {
- "FunctionName": { "Fn::GetAtt": ["${
+ {
+ "Type": "AWS::Lambda::Permission",
+ "Properties": {
+ "FunctionName": { "Fn::GetAtt": ["${
lambdaLogicalId}", "Arn"] },
- "Action": "lambda:InvokeFunction",
- "Principal": {
- "Fn::Join": [ "", [
- "logs.",
- { "Ref": "AWS::Region" },
- ".amazonaws.com"
- ] ]
- },
- "SourceArn": {
- "Fn::Join": [ "", [
- "arn:aws:logs:",
- { "Ref": "AWS::Region" },
- ":",
- { "Ref": "AWS::AccountId" },
- ":log-group:",
- "${LogGroupName}",
- ":*"
- ] ]
- }
+ "Action": "lambda:InvokeFunction",
+ "Principal": {
+ "Fn::Join": [ "", [
+ "logs.",
+ { "Ref": "AWS::Region" },
+ ".",
+ { "Ref": "AWS::URLSuffix" }
+ ] ]
+ },
+ "SourceArn": {
+ "Fn::Join": [ "", [
+ "arn:",
+ { "Ref": "AWS::Partition" },
+ ":logs:",
+ { "Ref": "AWS::Region" },
+ ":",
+ { "Ref": "AWS::AccountId" },
+ ":log-group:",
+ "${LogGroupName}",
+ ":*"
+ ] ]
}
}
- `;
+ }
+ `;
const newCloudWatchLogRuleObject = {
[cloudWatchLogLogicalId]: JSON.parse(cloudWatchLogRuleTemplate),
diff --git a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js
index d94063701c0..a85c67a4c40 100644
--- a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js
+++ b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js
@@ -20,19 +20,20 @@ class AwsCompileCognitoUserPoolEvents {
this.hooks = {
'package:compileEvents': this.compileCognitoUserPoolEvents.bind(this),
+ 'after:package:finalize': this.mergeWithCustomResources.bind(this),
};
}
- compileCognitoUserPoolEvents() {
+ findUserPoolsAndFunctions() {
const userPools = [];
const cognitoUserPoolTriggerFunctions = [];
// Iterate through all functions declared in `serverless.yml`
- this.serverless.service.getAllFunctions().forEach((functionName) => {
+ _.forEach(this.serverless.service.getAllFunctions(), (functionName) => {
const functionObj = this.serverless.service.getFunction(functionName);
if (functionObj.events) {
- functionObj.events.forEach(event => {
+ _.forEach(functionObj.events, (event) => {
if (event.cognitoUserPool) {
// Check event definition for `cognitoUserPool` object
if (typeof event.cognitoUserPool === 'object') {
@@ -80,51 +81,61 @@ class AwsCompileCognitoUserPoolEvents {
}
});
- // Generate CloudFormation templates for Cognito User Pool changes
- _.forEach(userPools, (poolName) => {
- // Create a `LambdaConfig` object for the CloudFormation template
- const currentPoolTriggerFunctions = _.filter(cognitoUserPoolTriggerFunctions, {
- poolName,
- });
-
- const lambdaConfig = _.reduce(currentPoolTriggerFunctions, (result, value) => {
- const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(value.functionName);
+ return { cognitoUserPoolTriggerFunctions, userPools };
+ }
- // Return a new object to avoid lint errors
- return Object.assign({}, result, {
- [value.triggerSource]: {
- 'Fn::GetAtt': [
- lambdaLogicalId,
- 'Arn',
- ],
- },
- });
- }, {});
+ generateTemplateForPool(poolName, currentPoolTriggerFunctions) {
+ const lambdaConfig = _.reduce(currentPoolTriggerFunctions, (result, value) => {
+ const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(value.functionName);
+
+ // Return a new object to avoid lint errors
+ return Object.assign({}, result, {
+ [value.triggerSource]: {
+ 'Fn::GetAtt': [
+ lambdaLogicalId,
+ 'Arn',
+ ],
+ },
+ });
+ }, {});
- const userPoolLogicalId = this.provider.naming.getCognitoUserPoolLogicalId(poolName);
+ const userPoolLogicalId = this.provider.naming.getCognitoUserPoolLogicalId(poolName);
- const DependsOn = _.map(currentPoolTriggerFunctions, (value) => this
- .provider.naming.getLambdaLogicalId(value.functionName));
+ // Attach `DependsOn` for any relevant Lambdas
+ const DependsOn = _.map(currentPoolTriggerFunctions, (value) => this
+ .provider.naming.getLambdaLogicalId(value.functionName));
- const userPoolTemplate = {
+ return {
+ [userPoolLogicalId]: {
Type: 'AWS::Cognito::UserPool',
Properties: {
UserPoolName: poolName,
LambdaConfig: lambdaConfig,
},
DependsOn,
- };
+ },
+ };
+ }
- const userPoolCFResource = {
- [userPoolLogicalId]: userPoolTemplate,
- };
+ compileCognitoUserPoolEvents() {
+ const result = this.findUserPoolsAndFunctions();
+ const cognitoUserPoolTriggerFunctions = result.cognitoUserPoolTriggerFunctions;
+ const userPools = result.userPools;
+
+ // Generate CloudFormation templates for Cognito User Pool changes
+ _.forEach(userPools, (poolName) => {
+ const currentPoolTriggerFunctions = _.filter(cognitoUserPoolTriggerFunctions, { poolName });
+ const userPoolCFResource = this.generateTemplateForPool(
+ poolName,
+ currentPoolTriggerFunctions
+ );
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
userPoolCFResource);
});
// Generate CloudFormation templates for IAM permissions to allow Cognito to trigger Lambda
- cognitoUserPoolTriggerFunctions.forEach((cognitoUserPoolTriggerFunction) => {
+ _.forEach(cognitoUserPoolTriggerFunctions, (cognitoUserPoolTriggerFunction) => {
const userPoolLogicalId = this.provider.naming
.getCognitoUserPoolLogicalId(cognitoUserPoolTriggerFunction.poolName);
const lambdaLogicalId = this.provider.naming
@@ -140,7 +151,7 @@ class AwsCompileCognitoUserPoolEvents {
],
},
Action: 'lambda:InvokeFunction',
- Principal: 'cognito-idp.amazonaws.com',
+ Principal: { 'Fn::Join': ['', ['cognito-idp.', { Ref: 'AWS::URLSuffix' }]] },
SourceArn: {
'Fn::GetAtt': [
userPoolLogicalId,
@@ -151,7 +162,7 @@ class AwsCompileCognitoUserPoolEvents {
};
const lambdaPermissionLogicalId = this.provider.naming
.getLambdaCognitoUserPoolPermissionLogicalId(cognitoUserPoolTriggerFunction.functionName,
- cognitoUserPoolTriggerFunction.poolName, cognitoUserPoolTriggerFunction.triggerSource);
+ cognitoUserPoolTriggerFunction.poolName, cognitoUserPoolTriggerFunction.triggerSource);
const permissionCFResource = {
[lambdaPermissionLogicalId]: permissionTemplate,
};
@@ -159,6 +170,43 @@ class AwsCompileCognitoUserPoolEvents {
permissionCFResource);
});
}
+
+ mergeWithCustomResources() {
+ const result = this.findUserPoolsAndFunctions();
+ const cognitoUserPoolTriggerFunctions = result.cognitoUserPoolTriggerFunctions;
+ const userPools = result.userPools;
+
+ _.forEach(userPools, (poolName) => {
+ const currentPoolTriggerFunctions = _.filter(cognitoUserPoolTriggerFunctions, { poolName });
+ const userPoolLogicalId = this.provider.naming.getCognitoUserPoolLogicalId(poolName);
+
+ // If overrides exist in `Resources`, merge them in
+ if (_.has(this.serverless.service.resources, userPoolLogicalId)) {
+ const customUserPool = this.serverless.service.resources[userPoolLogicalId];
+ const generatedUserPool = this.generateTemplateForPool(
+ poolName,
+ currentPoolTriggerFunctions
+ )[userPoolLogicalId];
+
+ // Merge `DependsOn` clauses
+ const customUserPoolDependsOn = _.get(customUserPool, 'DependsOn', []);
+ const DependsOn = generatedUserPool.DependsOn.concat(customUserPoolDependsOn);
+
+ // Merge default and custom resources, and `DependsOn` clause
+ const mergedTemplate = Object.assign(
+ {},
+ _.merge(generatedUserPool, customUserPool),
+ { DependsOn }
+ );
+
+ // Merge resource back into `Resources`
+ _.merge(
+ this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
+ { [userPoolLogicalId]: mergedTemplate }
+ );
+ }
+ });
+ }
}
module.exports = AwsCompileCognitoUserPoolEvents;
diff --git a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js
index 6ec9a6c2805..a67e80bb712 100644
--- a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js
+++ b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js
@@ -1,5 +1,6 @@
'use strict';
+const _ = require('lodash');
const expect = require('chai').expect;
const AwsProvider = require('../../../../provider/awsProvider');
const AwsCompileCognitoUserPoolEvents = require('./index');
@@ -115,9 +116,15 @@ describe('AwsCompileCognitoUserPoolEvents', () => {
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.Type
).to.equal('AWS::Cognito::UserPool');
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.DependsOn
+ ).to.have.lengthOf(1);
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.Type
).to.equal('AWS::Cognito::UserPool');
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.DependsOn
+ ).to.have.lengthOf(1);
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources
.FirstLambdaPermissionCognitoUserPoolMyUserPool1TriggerSourcePreSignUp.Type
@@ -153,9 +160,15 @@ describe('AwsCompileCognitoUserPoolEvents', () => {
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.Type
).to.equal('AWS::Cognito::UserPool');
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.DependsOn
+ ).to.have.lengthOf(1);
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.Type
).to.equal('AWS::Cognito::UserPool');
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.DependsOn
+ ).to.have.lengthOf(1);
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources
.FirstLambdaPermissionCognitoUserPoolMyUserPool1TriggerSourcePreSignUp.Type
@@ -195,6 +208,9 @@ describe('AwsCompileCognitoUserPoolEvents', () => {
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.Type
).to.equal('AWS::Cognito::UserPool');
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1.DependsOn
+ ).to.have.lengthOf(1);
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool1
.Properties.LambdaConfig.PreSignUp['Fn::GetAtt'][0]
@@ -204,6 +220,9 @@ describe('AwsCompileCognitoUserPoolEvents', () => {
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.Type
).to.equal('AWS::Cognito::UserPool');
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2.DependsOn
+ ).to.have.lengthOf(1);
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources.CognitoUserPoolMyUserPool2
.Properties.LambdaConfig.PreSignUp['Fn::GetAtt'][0]
@@ -250,10 +269,14 @@ describe('AwsCompileCognitoUserPoolEvents', () => {
.compiledCloudFormationTemplate.Resources
.CognitoUserPoolMyUserPool.Type
).to.equal('AWS::Cognito::UserPool');
- expect(Object.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources
- .CognitoUserPoolMyUserPool.Properties.LambdaConfig).length
- ).to.equal(2);
+ .CognitoUserPoolMyUserPool.Properties.LambdaConfig)
+ ).to.have.lengthOf(2);
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.DependsOn
+ ).to.have.lengthOf(2);
expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources
.FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type
@@ -279,4 +302,145 @@ describe('AwsCompileCognitoUserPoolEvents', () => {
).to.deep.equal({});
});
});
+
+ describe('#mergeWithCustomResources()', () => {
+ it('does not merge if no custom resource is found in Resources', () => {
+ awsCompileCognitoUserPoolEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ cognitoUserPool: {
+ pool: 'MyUserPool',
+ trigger: 'PreSignUp',
+ },
+ },
+ ],
+ },
+ };
+ awsCompileCognitoUserPoolEvents.serverless.service.resources = {};
+
+ awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents();
+ awsCompileCognitoUserPoolEvents.mergeWithCustomResources();
+
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.Type
+ ).to.equal('AWS::Cognito::UserPool');
+ expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.Properties)
+ ).to.have.lengthOf(2);
+ expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.Properties.LambdaConfig)
+ ).to.have.lengthOf(1);
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type
+ ).to.equal('AWS::Lambda::Permission');
+ });
+
+ it('should merge custom resources found in Resources', () => {
+ awsCompileCognitoUserPoolEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ cognitoUserPool: {
+ pool: 'MyUserPool',
+ trigger: 'PreSignUp',
+ },
+ },
+ ],
+ },
+ };
+ awsCompileCognitoUserPoolEvents.serverless.service.resources = {
+ CognitoUserPoolMyUserPool: {
+ Type: 'AWS::Cognito::UserPool',
+ Properties: {
+ UserPoolName: 'ProdMyUserPool',
+ MfaConfiguration: 'OFF',
+ EmailVerificationSubject: 'Your verification code',
+ EmailVerificationMessage: 'Your verification code is {####}.',
+ SmsVerificationMessage: 'Your verification code is {####}.',
+ },
+ },
+ };
+
+ awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents();
+ awsCompileCognitoUserPoolEvents.mergeWithCustomResources();
+
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.Type
+ ).to.equal('AWS::Cognito::UserPool');
+ expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.Properties)
+ ).to.have.lengthOf(6);
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.DependsOn
+ ).to.have.lengthOf(1);
+ expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.Properties.LambdaConfig)
+ ).to.have.lengthOf(1);
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type
+ ).to.equal('AWS::Lambda::Permission');
+ });
+
+ it('should merge `DependsOn` clauses correctly if being overridden from Resources', () => {
+ awsCompileCognitoUserPoolEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ cognitoUserPool: {
+ pool: 'MyUserPool',
+ trigger: 'PreSignUp',
+ },
+ },
+ ],
+ },
+ };
+ awsCompileCognitoUserPoolEvents.serverless.service.resources = {
+ CognitoUserPoolMyUserPool: {
+ DependsOn: ['Something', 'SomethingElse', ['Nothing', 'NothingAtAll']],
+ Type: 'AWS::Cognito::UserPool',
+ Properties: {
+ UserPoolName: 'ProdMyUserPool',
+ MfaConfiguration: 'OFF',
+ EmailVerificationSubject: 'Your verification code',
+ EmailVerificationMessage: 'Your verification code is {####}.',
+ SmsVerificationMessage: 'Your verification code is {####}.',
+ },
+ },
+ };
+
+ awsCompileCognitoUserPoolEvents.compileCognitoUserPoolEvents();
+ awsCompileCognitoUserPoolEvents.mergeWithCustomResources();
+
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.Type
+ ).to.equal('AWS::Cognito::UserPool');
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.DependsOn
+ ).to.have.lengthOf(4);
+ expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.Properties)
+ ).to.have.lengthOf(6);
+ expect(_.keys(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .CognitoUserPoolMyUserPool.Properties.LambdaConfig)
+ ).to.have.lengthOf(1);
+ expect(awsCompileCognitoUserPoolEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .FirstLambdaPermissionCognitoUserPoolMyUserPoolTriggerSourcePreSignUp.Type
+ ).to.equal('AWS::Lambda::Permission');
+ });
+ });
});
diff --git a/lib/plugins/aws/package/compile/events/iot/index.js b/lib/plugins/aws/package/compile/events/iot/index.js
index 64633ebc7aa..d8d5ef892e0 100644
--- a/lib/plugins/aws/package/compile/events/iot/index.js
+++ b/lib/plugins/aws/package/compile/events/iot/index.js
@@ -80,10 +80,12 @@ class AwsCompileIoTEvents {
"Properties": {
"FunctionName": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] },
"Action": "lambda:InvokeFunction",
- "Principal": "iot.amazonaws.com",
+ "Principal": { "Fn::Join": ["", [ "iot.", { "Ref": "AWS::URLSuffix" } ]] },
"SourceArn": { "Fn::Join": ["",
[
- "arn:aws:iot:",
+ "arn:",
+ { "Ref": "AWS::Partition" },
+ ":iot:",
{ "Ref": "AWS::Region" },
":",
{ "Ref": "AWS::AccountId" },
diff --git a/lib/plugins/aws/package/compile/events/s3/index.js b/lib/plugins/aws/package/compile/events/s3/index.js
index 707d84aaeb9..4c2c9dc4a0b 100644
--- a/lib/plugins/aws/package/compile/events/s3/index.js
+++ b/lib/plugins/aws/package/compile/events/s3/index.js
@@ -147,7 +147,7 @@ class AwsCompileS3Events {
_.forEach(dependsOnToCreate, (item) => {
const lambdaPermissionLogicalId = this.provider.naming
.getLambdaS3PermissionLogicalId(item.functionName,
- item.bucketName);
+ item.bucketName);
bucketTemplate.DependsOn.push(lambdaPermissionLogicalId);
});
@@ -177,17 +177,21 @@ class AwsCompileS3Events {
],
},
Action: 'lambda:InvokeFunction',
- Principal: 's3.amazonaws.com',
- SourceArn: { 'Fn::Join': ['',
- [
- `arn:aws:s3:::${s3EnabledFunction.bucketName}`,
+ Principal: { 'Fn::Join': ['', ['s3.', { Ref: 'AWS::URLSuffix' }]] },
+ SourceArn: {
+ 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ `:s3:::${s3EnabledFunction.bucketName}`,
+ ],
],
- ] },
+ },
},
};
const lambdaPermissionLogicalId = this.provider.naming
.getLambdaS3PermissionLogicalId(s3EnabledFunction.functionName,
- s3EnabledFunction.bucketName);
+ s3EnabledFunction.bucketName);
const permissionCFResource = {
[lambdaPermissionLogicalId]: permissionTemplate,
};
diff --git a/lib/plugins/aws/package/compile/events/schedule/index.js b/lib/plugins/aws/package/compile/events/schedule/index.js
index b115262af2a..5ec91bd4646 100644
--- a/lib/plugins/aws/package/compile/events/schedule/index.js
+++ b/lib/plugins/aws/package/compile/events/schedule/index.js
@@ -114,7 +114,7 @@ class AwsCompileScheduledEvents {
"FunctionName": { "Fn::GetAtt": ["${
lambdaLogicalId}", "Arn"] },
"Action": "lambda:InvokeFunction",
- "Principal": "events.amazonaws.com",
+ "Principal": { "Fn::Join": ["", [ "events.", { "Ref": "AWS::URLSuffix" } ]] },
"SourceArn": { "Fn::GetAtt": ["${scheduleLogicalId}", "Arn"] }
}
}
diff --git a/lib/plugins/aws/package/compile/events/sns/index.js b/lib/plugins/aws/package/compile/events/sns/index.js
index 69325ec4064..cde37bd2f63 100644
--- a/lib/plugins/aws/package/compile/events/sns/index.js
+++ b/lib/plugins/aws/package/compile/events/sns/index.js
@@ -123,7 +123,9 @@ class AwsCompileSNSEvents {
topicArn = {
'Fn::Join': ['',
[
- 'arn:aws:sns:',
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':sns:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
@@ -166,7 +168,7 @@ class AwsCompileSNSEvents {
Properties: {
FunctionName: endpoint,
Action: 'lambda:InvokeFunction',
- Principal: 'sns.amazonaws.com',
+ Principal: { 'Fn::Join': ['', ['sns.', { Ref: 'AWS::URLSuffix' }]] },
SourceArn: topicArn,
},
},
diff --git a/lib/plugins/aws/package/compile/events/sqs/index.js b/lib/plugins/aws/package/compile/events/sqs/index.js
new file mode 100644
index 00000000000..3c728f06c3b
--- /dev/null
+++ b/lib/plugins/aws/package/compile/events/sqs/index.js
@@ -0,0 +1,172 @@
+'use strict';
+
+const _ = require('lodash');
+
+class AwsCompileSQSEvents {
+ constructor(serverless) {
+ this.serverless = serverless;
+ this.provider = this.serverless.getProvider('aws');
+
+ this.hooks = {
+ 'package:compileEvents': this.compileSQSEvents.bind(this),
+ };
+ }
+
+ compileSQSEvents() {
+ this.serverless.service.getAllFunctions().forEach((functionName) => {
+ const functionObj = this.serverless.service.getFunction(functionName);
+
+ if (functionObj.events) {
+ const sqsStatement = {
+ Effect: 'Allow',
+ Action: [
+ 'sqs:ReceiveMessage',
+ 'sqs:DeleteMessage',
+ 'sqs:GetQueueAttributes',
+ ],
+ Resource: [],
+ };
+
+ functionObj.events.forEach(event => {
+ if (event.sqs) {
+ let EventSourceArn;
+ let BatchSize = 10;
+ let Enabled = 'True';
+
+ // TODO validate arn syntax
+ if (typeof event.sqs === 'object') {
+ if (!event.sqs.arn) {
+ const errorMessage = [
+ `Missing "arn" property for sqs event in function "${functionName}"`,
+ ' The correct syntax is: sqs: ',
+ ' OR an object with an "arn" property.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes
+ .Error(errorMessage);
+ }
+ if (typeof event.sqs.arn !== 'string') {
+ // for dynamic arns (GetAtt/ImportValue)
+ if (Object.keys(event.sqs.arn).length !== 1
+ || !(_.has(event.sqs.arn, 'Fn::ImportValue')
+ || _.has(event.sqs.arn, 'Fn::GetAtt'))) {
+ const errorMessage = [
+ `Bad dynamic ARN property on sqs event in function "${functionName}"`,
+ ' If you use a dynamic "arn" (such as with Fn::GetAtt or Fn::ImportValue)',
+ ' there must only be one key (either Fn::GetAtt or Fn::ImportValue) in the arn',
+ ' object. Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes
+ .Error(errorMessage);
+ }
+ }
+ EventSourceArn = event.sqs.arn;
+ BatchSize = event.sqs.batchSize
+ || BatchSize;
+ if (typeof event.sqs.enabled !== 'undefined') {
+ Enabled = event.sqs.enabled ? 'True' : 'False';
+ }
+ } else if (typeof event.sqs === 'string') {
+ EventSourceArn = event.sqs;
+ } else {
+ const errorMessage = [
+ `SQS event of function "${functionName}" is not an object nor a string`,
+ ' The correct syntax is: sqs: ',
+ ' OR an object with an "arn" property.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes
+ .Error(errorMessage);
+ }
+
+ const queueName = (function () {
+ if (EventSourceArn['Fn::GetAtt']) {
+ return EventSourceArn['Fn::GetAtt'][0];
+ } else if (EventSourceArn['Fn::ImportValue']) {
+ return EventSourceArn['Fn::ImportValue'];
+ }
+ return EventSourceArn.split(':').pop();
+ }());
+
+ const lambdaLogicalId = this.provider.naming
+ .getLambdaLogicalId(functionName);
+ const queueLogicalId = this.provider.naming
+ .getQueueLogicalId(functionName, queueName);
+
+ const funcRole = functionObj.role || this.serverless.service.provider.role;
+ let dependsOn = '"IamRoleLambdaExecution"';
+ if (funcRole) {
+ if ( // check whether the custom role is an ARN
+ typeof funcRole === 'string' &&
+ funcRole.indexOf(':') !== -1
+ ) {
+ dependsOn = '[]';
+ } else if ( // otherwise, check if we have an in-service reference to a role ARN
+ typeof funcRole === 'object' &&
+ 'Fn::GetAtt' in funcRole &&
+ Array.isArray(funcRole['Fn::GetAtt']) &&
+ funcRole['Fn::GetAtt'].length === 2 &&
+ typeof funcRole['Fn::GetAtt'][0] === 'string' &&
+ typeof funcRole['Fn::GetAtt'][1] === 'string' &&
+ funcRole['Fn::GetAtt'][1] === 'Arn'
+ ) {
+ dependsOn = `"${funcRole['Fn::GetAtt'][0]}"`;
+ } else if ( // otherwise, check if we have an import
+ typeof funcRole === 'object' &&
+ 'Fn::ImportValue' in funcRole
+ ) {
+ dependsOn = '[]';
+ } else if (typeof funcRole === 'string') {
+ dependsOn = `"${funcRole}"`;
+ }
+ }
+ const sqsTemplate = `
+ {
+ "Type": "AWS::Lambda::EventSourceMapping",
+ "DependsOn": ${dependsOn},
+ "Properties": {
+ "BatchSize": ${BatchSize},
+ "EventSourceArn": ${JSON.stringify(EventSourceArn)},
+ "FunctionName": {
+ "Fn::GetAtt": [
+ "${lambdaLogicalId}",
+ "Arn"
+ ]
+ },
+ "Enabled": "${Enabled}"
+ }
+ }
+ `;
+
+ // add event source ARNs to PolicyDocument statements
+ sqsStatement.Resource.push(EventSourceArn);
+
+ const newSQSObject = {
+ [queueLogicalId]: JSON.parse(sqsTemplate),
+ };
+
+ _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
+ newSQSObject);
+ }
+ });
+
+ // update the PolicyDocument statements (if default policy is used)
+ if (this.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution) {
+ const statement = this.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources
+ .IamRoleLambdaExecution
+ .Properties
+ .Policies[0]
+ .PolicyDocument
+ .Statement;
+ if (sqsStatement.Resource.length) {
+ statement.push(sqsStatement);
+ }
+ }
+ }
+ });
+ }
+}
+
+module.exports = AwsCompileSQSEvents;
diff --git a/lib/plugins/aws/package/compile/events/sqs/index.test.js b/lib/plugins/aws/package/compile/events/sqs/index.test.js
new file mode 100644
index 00000000000..a251c9d8723
--- /dev/null
+++ b/lib/plugins/aws/package/compile/events/sqs/index.test.js
@@ -0,0 +1,547 @@
+'use strict';
+
+const expect = require('chai').expect;
+const AwsProvider = require('../../../../provider/awsProvider');
+const AwsCompileSQSEvents = require('./index');
+const Serverless = require('../../../../../../Serverless');
+
+describe('AwsCompileSQSEvents', () => {
+ let serverless;
+ let awsCompileSQSEvents;
+
+ beforeEach(() => {
+ serverless = new Serverless();
+ serverless.service.provider.compiledCloudFormationTemplate = {
+ Resources: {
+ IamRoleLambdaExecution: {
+ Properties: {
+ Policies: [
+ {
+ PolicyDocument: {
+ Statement: [],
+ },
+ },
+ ],
+ },
+ },
+ },
+ };
+ serverless.setProvider('aws', new AwsProvider(serverless));
+ awsCompileSQSEvents = new AwsCompileSQSEvents(serverless);
+ awsCompileSQSEvents.serverless.service.service = 'new-service';
+ });
+
+ describe('#constructor()', () => {
+ it('should set the provider variable to be an instance of AwsProvider', () =>
+ expect(awsCompileSQSEvents.provider).to.be.instanceof(AwsProvider));
+ });
+
+ describe('#compileSQSEvents()', () => {
+ it('should throw an error if sqs event type is not a string or an object', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: 42,
+ },
+ ],
+ },
+ };
+
+ expect(() => awsCompileSQSEvents.compileSQSEvents()).to.throw(Error);
+ });
+
+ it('should throw an error if the "arn" property is not given', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: {
+ arn: null,
+ },
+ },
+ ],
+ },
+ };
+
+ expect(() => awsCompileSQSEvents.compileSQSEvents()).to.throw(Error);
+ });
+
+ it('should not throw error or merge role statements if default policy is not present', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: 'arn:aws:sqs:region:account:queueName',
+ },
+ ],
+ },
+ };
+
+ // pretend that the default IamRoleLambdaExecution is not in place
+ awsCompileSQSEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamRoleLambdaExecution = null;
+
+ expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error);
+ expect(awsCompileSQSEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamRoleLambdaExecution
+ ).to.equal(null);
+ });
+
+ it('should not throw error if custom IAM role is set in function', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ role: 'arn:aws:iam::account:role/foo',
+ events: [
+ {
+ sqs: 'arn:aws:sqs:region:account:MyQueue',
+ },
+ ],
+ },
+ };
+
+ // pretend that the default IamRoleLambdaExecution is not in place
+ awsCompileSQSEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamRoleLambdaExecution = null;
+
+ expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.be.instanceof(Array);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn.length).to.equal(0);
+ expect(awsCompileSQSEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamRoleLambdaExecution
+ ).to.equal(null);
+ });
+
+ it('should not throw error if custom IAM role name reference is set in function', () => {
+ const roleLogicalId = 'RoleLogicalId';
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ role: roleLogicalId,
+ events: [
+ {
+ sqs: 'arn:aws:sqs:region:account:MyQueue',
+ },
+ ],
+ },
+ };
+
+ // pretend that the default IamRoleLambdaExecution is not in place
+ awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution = null;
+
+ expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.equal(roleLogicalId);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution).to.equal(null);
+ });
+
+ it('should not throw error if custom IAM role reference is set in function', () => {
+ const roleLogicalId = 'RoleLogicalId';
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ role: { 'Fn::GetAtt': [roleLogicalId, 'Arn'] },
+ events: [
+ {
+ sqs: 'arn:aws:sqs:region:account:MyQueue',
+ },
+ ],
+ },
+ };
+
+ // pretend that the default IamRoleLambdaExecution is not in place
+ awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution = null;
+
+ expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.equal(roleLogicalId);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution).to.equal(null);
+ });
+
+ it('should not throw error if custom IAM role is set in provider', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: 'arn:aws:sqs:region:account:MyQueue',
+ },
+ ],
+ },
+ };
+
+ // pretend that the default IamRoleLambdaExecution is not in place
+ awsCompileSQSEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamRoleLambdaExecution = null;
+
+ awsCompileSQSEvents.serverless.service.provider
+ .role = 'arn:aws:iam::account:role/foo';
+
+ expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.be.instanceof(Array);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn.length).to.equal(0);
+ expect(awsCompileSQSEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamRoleLambdaExecution
+ ).to.equal(null);
+ });
+
+ it('should not throw error if IAM role is imported', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ role: { 'Fn::ImportValue': 'ExportedRoleId' },
+ events: [
+ {
+ sqs: 'arn:aws:sqs:region:account:MyQueue',
+ },
+ ],
+ },
+ };
+
+ // pretend that the default IamRoleLambdaExecution is not in place
+ awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution = null;
+
+ expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn.length).to.equal(0);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution).to.equal(null);
+ });
+
+
+ it('should not throw error if custom IAM role reference is set in provider', () => {
+ const roleLogicalId = 'RoleLogicalId';
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: 'arn:aws:sqs:region:account:MyQueue',
+ },
+ ],
+ },
+ };
+
+ // pretend that the default IamRoleLambdaExecution is not in place
+ awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution = null;
+
+ awsCompileSQSEvents.serverless.service.provider
+ .role = { 'Fn::GetAtt': [roleLogicalId, 'Arn'] };
+
+ expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.equal(roleLogicalId);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution).to.equal(null);
+ });
+
+ it('should not throw error if custom IAM role name reference is set in provider', () => {
+ const roleLogicalId = 'RoleLogicalId';
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: 'arn:aws:sqs:region:account:MyQueue',
+ },
+ ],
+ },
+ };
+
+ // pretend that the default IamRoleLambdaExecution is not in place
+ awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution = null;
+
+ awsCompileSQSEvents.serverless.service.provider
+ .role = roleLogicalId;
+
+ expect(() => { awsCompileSQSEvents.compileSQSEvents(); }).to.not.throw(Error);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FirstEventSourceMappingSQSMyQueue.DependsOn).to.equal(roleLogicalId);
+ expect(awsCompileSQSEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.IamRoleLambdaExecution).to.equal(null);
+ });
+
+ describe('when a queue ARN is given', () => {
+ it('should create event source mappings when a queue ARN is given', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: {
+ arn: 'arn:aws:sqs:region:account:MyFirstQueue',
+ batchSize: 1,
+ enabled: false,
+ },
+ },
+ {
+ sqs: {
+ arn: 'arn:aws:sqs:region:account:MySecondQueue',
+ },
+ },
+ {
+ sqs: 'arn:aws:sqs:region:account:MyThirdQueue',
+ },
+ ],
+ },
+ };
+
+ awsCompileSQSEvents.compileSQSEvents();
+
+ // event 1
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyFirstQueue
+ .Type
+ ).to.equal('AWS::Lambda::EventSourceMapping');
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyFirstQueue
+ .DependsOn
+ ).to.equal('IamRoleLambdaExecution');
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyFirstQueue
+ .Properties.EventSourceArn
+ ).to.equal(
+ awsCompileSQSEvents.serverless.service.functions.first.events[0]
+ .sqs.arn
+ );
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyFirstQueue
+ .Properties.BatchSize
+ ).to.equal(
+ awsCompileSQSEvents.serverless.service.functions.first.events[0]
+ .sqs.batchSize
+ );
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyFirstQueue
+ .Properties.Enabled
+ ).to.equal('False');
+
+ // event 2
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMySecondQueue
+ .Type
+ ).to.equal('AWS::Lambda::EventSourceMapping');
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMySecondQueue
+ .DependsOn
+ ).to.equal('IamRoleLambdaExecution');
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMySecondQueue
+ .Properties.EventSourceArn
+ ).to.equal(
+ awsCompileSQSEvents.serverless.service.functions.first.events[1]
+ .sqs.arn
+ );
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMySecondQueue
+ .Properties.BatchSize
+ ).to.equal(10);
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMySecondQueue
+ .Properties.Enabled
+ ).to.equal('True');
+
+ // event 3
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyThirdQueue
+ .Type
+ ).to.equal('AWS::Lambda::EventSourceMapping');
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyThirdQueue
+ .DependsOn
+ ).to.equal('IamRoleLambdaExecution');
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyThirdQueue
+ .Properties.EventSourceArn
+ ).to.equal(
+ awsCompileSQSEvents.serverless.service.functions.first.events[2]
+ .sqs
+ );
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyThirdQueue
+ .Properties.BatchSize
+ ).to.equal(10);
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingSQSMyThirdQueue
+ .Properties.Enabled
+ ).to.equal('True');
+ });
+
+ it('should allow specifying SQS Queues as CFN reference types', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: {
+ arn: { 'Fn::GetAtt': ['SomeQueue', 'Arn'] },
+ },
+ },
+ {
+ sqs: {
+ arn: { 'Fn::ImportValue': 'ForeignQueue' },
+ },
+ },
+ ],
+ },
+ };
+
+ awsCompileSQSEvents.compileSQSEvents();
+
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstEventSourceMappingSQSSomeQueue.Properties.EventSourceArn
+ ).to.deep.equal(
+ { 'Fn::GetAtt': ['SomeQueue', 'Arn'] }
+ );
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution
+ .Properties.Policies[0].PolicyDocument.Statement[0]
+ ).to.deep.equal(
+ {
+ Action: [
+ 'sqs:ReceiveMessage',
+ 'sqs:DeleteMessage',
+ 'sqs:GetQueueAttributes',
+ ],
+ Effect: 'Allow',
+ Resource: [
+ {
+ 'Fn::GetAtt': [
+ 'SomeQueue',
+ 'Arn',
+ ],
+ },
+ {
+ 'Fn::ImportValue': 'ForeignQueue',
+ },
+ ],
+ }
+ );
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ .FirstEventSourceMappingSQSForeignQueue.Properties.EventSourceArn
+ ).to.deep.equal(
+ { 'Fn::ImportValue': 'ForeignQueue' }
+ );
+ });
+
+ it('fails if keys other than Fn::GetAtt/ImportValue are used for dynamic queue ARN', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: {
+ arn: {
+ 'Fn::GetAtt': ['SomeQueue', 'Arn'],
+ batchSize: 1,
+ },
+ },
+ },
+ ],
+ },
+ };
+
+ expect(() => awsCompileSQSEvents.compileSQSEvents()).to.throw(Error);
+ });
+
+ it('should add the necessary IAM role statements', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: 'arn:aws:sqs:region:account:MyFirstQueue',
+ },
+ {
+ sqs: 'arn:aws:sqs:region:account:MySecondQueue',
+ },
+ ],
+ },
+ };
+
+ const iamRoleStatements = [
+ {
+ Effect: 'Allow',
+ Action: [
+ 'sqs:ReceiveMessage',
+ 'sqs:DeleteMessage',
+ 'sqs:GetQueueAttributes',
+ ],
+ Resource: [
+ 'arn:aws:sqs:region:account:MyFirstQueue',
+ 'arn:aws:sqs:region:account:MySecondQueue',
+ ],
+ },
+ ];
+
+ awsCompileSQSEvents.compileSQSEvents();
+
+ expect(awsCompileSQSEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamRoleLambdaExecution.Properties.Policies[0]
+ .PolicyDocument.Statement
+ ).to.deep.equal(iamRoleStatements);
+ });
+ });
+
+ it('should not create event source mapping when sqs events are not given', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [],
+ },
+ };
+
+ awsCompileSQSEvents.compileSQSEvents();
+
+ // should be 1 because we've mocked the IamRoleLambdaExecution above
+ expect(
+ Object.keys(awsCompileSQSEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources).length
+ ).to.equal(1);
+ });
+
+ it('should not add the IAM role statements when sqs events are not given', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [],
+ },
+ };
+
+ awsCompileSQSEvents.compileSQSEvents();
+
+ expect(
+ awsCompileSQSEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamRoleLambdaExecution.Properties.Policies[0]
+ .PolicyDocument.Statement.length
+ ).to.equal(0);
+ });
+
+ it('should remove all non-alphanumerics from queue names for the resource logical ids', () => {
+ awsCompileSQSEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ sqs: 'arn:aws:sqs:region:account:some-queue-name',
+ },
+ ],
+ },
+ };
+
+ awsCompileSQSEvents.compileSQSEvents();
+
+ expect(awsCompileSQSEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ ).to.have.any.keys('FirstEventSourceMappingSQSSomequeuename');
+ });
+ });
+});
diff --git a/lib/plugins/aws/package/compile/functions/index.js b/lib/plugins/aws/package/compile/functions/index.js
index 971bf1e2bd6..90f68b80b95 100644
--- a/lib/plugins/aws/package/compile/functions/index.js
+++ b/lib/plugins/aws/package/compile/functions/index.js
@@ -123,13 +123,23 @@ class AwsCompileFunctions {
newFunction.Properties.Timeout = Timeout;
newFunction.Properties.Runtime = Runtime;
+ // publish these properties to the platform
+ this.serverless.service.functions[functionName].memory = MemorySize;
+ this.serverless.service.functions[functionName].timeout = Timeout;
+ this.serverless.service.functions[functionName].runtime = Runtime;
+
if (functionObject.description) {
newFunction.Properties.Description = functionObject.description;
}
- if (functionObject.tags && typeof functionObject.tags === 'object') {
+ if (functionObject.tags || this.serverless.service.provider.tags) {
+ const tags = Object.assign(
+ {},
+ this.serverless.service.provider.tags,
+ functionObject.tags
+ );
newFunction.Properties.Tags = [];
- _.forEach(functionObject.tags, (Value, Key) => {
+ _.forEach(tags, (Value, Key) => {
newFunction.Properties.Tags.push({ Key, Value });
});
}
@@ -249,6 +259,13 @@ class AwsCompileFunctions {
invalidEnvVar = `Invalid characters in environment variable ${key}`;
return false; // break loop with lodash
}
+ const value = newFunction.Properties.Environment.Variables[key];
+ const isCFRef = _.isObject(value) &&
+ !_.some(value, (v, k) => k !== 'Ref' && !_.startsWith(k, 'Fn::'));
+ if (!isCFRef && !_.isString(value)) {
+ invalidEnvVar = `Environment variable ${key} must contain string`;
+ return false;
+ }
}
);
@@ -279,6 +296,19 @@ class AwsCompileFunctions {
delete newFunction.Properties.VpcConfig;
}
+ if (functionObject.reservedConcurrency) {
+ if (_.isInteger(functionObject.reservedConcurrency)) {
+ newFunction.Properties.ReservedConcurrentExecutions = functionObject.reservedConcurrency;
+ } else {
+ const errorMessage = [
+ 'You should use integer as reservedConcurrency value on function: ',
+ `${newFunction.Properties.FunctionName}`,
+ ].join('');
+
+ return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
+ }
+ }
+
newFunction.DependsOn = [this.provider.naming.getLogGroupLogicalId(functionName)]
.concat(newFunction.DependsOn || []);
diff --git a/lib/plugins/aws/package/compile/functions/index.test.js b/lib/plugins/aws/package/compile/functions/index.test.js
index b5e18c68424..c743626b74a 100644
--- a/lib/plugins/aws/package/compile/functions/index.test.js
+++ b/lib/plugins/aws/package/compile/functions/index.test.js
@@ -25,6 +25,7 @@ describe('AwsCompileFunctions', () => {
};
serverless = new Serverless(options);
serverless.setProvider('aws', new AwsProvider(serverless, options));
+ serverless.cli = new serverless.classes.CLI();
awsCompileFunctions = new AwsCompileFunctions(serverless, options);
awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = {
Resources: {},
@@ -521,7 +522,104 @@ describe('AwsCompileFunctions', () => {
});
});
- it('should create a function resource with tags', () => {
+ it('should create a function resource with provider level tags', () => {
+ const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName;
+ const s3FileName = awsCompileFunctions.serverless.service.package.artifact
+ .split(path.sep).pop();
+ awsCompileFunctions.serverless.service.functions = {
+ func: {
+ handler: 'func.function.handler',
+ name: 'new-service-dev-func',
+ },
+ };
+
+ awsCompileFunctions.serverless.service.provider.tags = {
+ foo: 'bar',
+ baz: 'qux',
+ };
+
+ const compiledFunction = {
+ Type: 'AWS::Lambda::Function',
+ DependsOn: [
+ 'FuncLogGroup',
+ 'IamRoleLambdaExecution',
+ ],
+ Properties: {
+ Code: {
+ S3Key: `${s3Folder}/${s3FileName}`,
+ S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
+ },
+ FunctionName: 'new-service-dev-func',
+ Handler: 'func.function.handler',
+ MemorySize: 1024,
+ Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
+ Runtime: 'nodejs4.3',
+ Timeout: 6,
+ Tags: [
+ { Key: 'foo', Value: 'bar' },
+ { Key: 'baz', Value: 'qux' },
+ ],
+ },
+ };
+
+ return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled
+ .then(() => {
+ expect(
+ awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FuncLambdaFunction
+ ).to.deep.equal(compiledFunction);
+ });
+ });
+
+ it('should create a function resource with function level tags', () => {
+ const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName;
+ const s3FileName = awsCompileFunctions.serverless.service.package.artifact
+ .split(path.sep).pop();
+ awsCompileFunctions.serverless.service.functions = {
+ func: {
+ handler: 'func.function.handler',
+ name: 'new-service-dev-func',
+ tags: {
+ foo: 'bar',
+ baz: 'qux',
+ },
+ },
+ };
+
+ const compiledFunction = {
+ Type: 'AWS::Lambda::Function',
+ DependsOn: [
+ 'FuncLogGroup',
+ 'IamRoleLambdaExecution',
+ ],
+ Properties: {
+ Code: {
+ S3Key: `${s3Folder}/${s3FileName}`,
+ S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
+ },
+ FunctionName: 'new-service-dev-func',
+ Handler: 'func.function.handler',
+ MemorySize: 1024,
+ Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
+ Runtime: 'nodejs4.3',
+ Timeout: 6,
+ Tags: [
+ { Key: 'foo', Value: 'bar' },
+ { Key: 'baz', Value: 'qux' },
+ ],
+ },
+ };
+
+ return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled
+ .then(() => {
+ expect(
+ awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FuncLambdaFunction
+ ).to.deep.equal(compiledFunction);
+ });
+ });
+
+ it('should create a function resource with provider and function level tags', () => {
const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName;
const s3FileName = awsCompileFunctions.serverless.service.package.artifact
.split(path.sep).pop();
@@ -536,6 +634,11 @@ describe('AwsCompileFunctions', () => {
},
};
+ awsCompileFunctions.serverless.service.provider.tags = {
+ foo: 'quux',
+ corge: 'uier',
+ };
+
const compiledFunction = {
Type: 'AWS::Lambda::Function',
DependsOn: [
@@ -555,6 +658,7 @@ describe('AwsCompileFunctions', () => {
Timeout: 6,
Tags: [
{ Key: 'foo', Value: 'bar' },
+ { Key: 'corge', Value: 'uier' },
{ Key: 'baz', Value: 'qux' },
],
},
@@ -1352,6 +1456,53 @@ describe('AwsCompileFunctions', () => {
return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error);
});
+ it('should throw an error if environment variable is not a string', () => {
+ awsCompileFunctions.serverless.service.functions = {
+ func: {
+ handler: 'func.function.handler',
+ name: 'new-service-dev-func',
+ environment: {
+ counter: 18,
+ },
+ },
+ };
+
+ return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error);
+ });
+
+ it('should accept an environment variable with CF ref and functions', () => {
+ awsCompileFunctions.serverless.service.functions = {
+ func: {
+ handler: 'func.function.handler',
+ name: 'new-service-dev-func',
+ environment: {
+ counter: {
+ Ref: 'TestVariable',
+ },
+ list: {
+ 'Fn::Join:': [', ', ['a', 'b', 'c']],
+ },
+ },
+ },
+ };
+
+ return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled
+ .then(() => {
+ awsCompileFunctions.serverless.service.functions = {
+ func: {
+ handler: 'func.function.handler',
+ name: 'new-service-dev-func',
+ environment: {
+ counter: {
+ NotRef: 'TestVariable',
+ },
+ },
+ },
+ };
+ return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error);
+ });
+ });
+
it('should consider function based config when creating a function resource', () => {
const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName;
const s3FileName = awsCompileFunctions.serverless.service.package.artifact
@@ -1730,6 +1881,65 @@ describe('AwsCompileFunctions', () => {
);
});
});
+
+ it('should set function declared reserved concurrency limit', () => {
+ const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName;
+ const s3FileName = awsCompileFunctions.serverless.service.package.artifact
+ .split(path.sep).pop();
+ awsCompileFunctions.serverless.service.functions = {
+ func: {
+ handler: 'func.function.handler',
+ name: 'new-service-dev-func',
+ reservedConcurrency: 5,
+ },
+ };
+ const compiledFunction = {
+ Type: 'AWS::Lambda::Function',
+ DependsOn: [
+ 'FuncLogGroup',
+ 'IamRoleLambdaExecution',
+ ],
+ Properties: {
+ Code: {
+ S3Key: `${s3Folder}/${s3FileName}`,
+ S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
+ },
+ FunctionName: 'new-service-dev-func',
+ Handler: 'func.function.handler',
+ MemorySize: 1024,
+ ReservedConcurrentExecutions: 5,
+ Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
+ Runtime: 'nodejs4.3',
+ Timeout: 6,
+ },
+ };
+
+ return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled
+ .then(() => {
+ expect(
+ awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.FuncLambdaFunction
+ ).to.deep.equal(compiledFunction);
+ });
+ });
+
+ it('should throw an informative error message if non-integer reserved concurrency limit set ' +
+ 'on function', () => {
+ awsCompileFunctions.serverless.service.functions = {
+ func: {
+ handler: 'func.function.handler',
+ name: 'new-service-dev-func',
+ reservedConcurrency: '1',
+ },
+ };
+
+ const errorMessage = [
+ 'You should use integer as reservedConcurrency value on function: ',
+ 'new-service-dev-func',
+ ].join('');
+
+ return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(errorMessage);
+ });
});
describe('#compileRole()', () => {
diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.js b/lib/plugins/aws/package/lib/generateCoreTemplate.js
index fae3c200071..98341552c09 100644
--- a/lib/plugins/aws/package/lib/generateCoreTemplate.js
+++ b/lib/plugins/aws/package/lib/generateCoreTemplate.js
@@ -24,6 +24,7 @@ module.exports = {
);
const bucketName = this.serverless.service.provider.deploymentBucket;
+ const isS3TransferAccelerationSupported = this.provider.isS3TransferAccelerationSupported();
const isS3TransferAccelerationEnabled = this.provider.isS3TransferAccelerationEnabled();
const isS3TransferAccelerationDisabled = this.provider.isS3TransferAccelerationDisabled();
@@ -53,7 +54,7 @@ module.exports = {
});
}
- if (isS3TransferAccelerationEnabled) {
+ if (isS3TransferAccelerationEnabled && isS3TransferAccelerationSupported) {
// enable acceleration via CloudFormation
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ServerlessDeploymentBucket.Properties = {
@@ -64,7 +65,7 @@ module.exports = {
// keep track of acceleration status via CloudFormation Output
this.serverless.service.provider.compiledCloudFormationTemplate
.Outputs.ServerlessDeploymentBucketAccelerated = { Value: true };
- } else if (isS3TransferAccelerationDisabled) {
+ } else if (isS3TransferAccelerationDisabled && isS3TransferAccelerationSupported) {
// explicitly disable acceleration via CloudFormation
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ServerlessDeploymentBucket.Properties = {
diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.test.js b/lib/plugins/aws/package/lib/generateCoreTemplate.test.js
index e4cb5eba22f..72a2ae361ef 100644
--- a/lib/plugins/aws/package/lib/generateCoreTemplate.test.js
+++ b/lib/plugins/aws/package/lib/generateCoreTemplate.test.js
@@ -161,6 +161,21 @@ describe('#generateCoreTemplate()', () => {
});
});
+ it('should exclude AccelerateConfiguration for govcloud region', () => {
+ sinon.stub(awsPlugin.provider, 'request').resolves();
+ sinon.stub(serverless.utils, 'writeFileSync').resolves();
+ serverless.config.servicePath = './';
+ awsPlugin.provider.options.region = 'us-gov-west-1';
+
+ return awsPlugin.generateCoreTemplate()
+ .then(() => {
+ const template = serverless.service.provider.coreCloudFormationTemplate;
+ expect(template.Resources.ServerlessDeploymentBucket).to.be.deep.equal({
+ Type: 'AWS::S3::Bucket',
+ });
+ });
+ });
+
it('should explode if transfer acceleration is both enabled and disabled', () => {
sinon.stub(awsPlugin.provider, 'request').resolves();
sinon.stub(serverless.utils, 'writeFileSync').resolves();
diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.js b/lib/plugins/aws/package/lib/mergeIamTemplates.js
index a17e2dffd8a..0f28c782619 100644
--- a/lib/plugins/aws/package/lib/mergeIamTemplates.js
+++ b/lib/plugins/aws/package/lib/mergeIamTemplates.js
@@ -7,6 +7,7 @@ const path = require('path');
module.exports = {
mergeIamTemplates() {
this.validateStatements(this.serverless.service.provider.iamRoleStatements);
+ this.validateManagedPolicies(this.serverless.service.provider.iamManagedPolicies);
return this.merge();
},
@@ -32,7 +33,7 @@ module.exports = {
if (_.has(this.serverless.service.provider, 'logRetentionInDays')) {
if (_.isInteger(this.serverless.service.provider.logRetentionInDays) &&
- this.serverless.service.provider.logRetentionInDays > 0) {
+ this.serverless.service.provider.logRetentionInDays > 0) {
newLogGroup[logGroupLogicalId].Properties.RetentionInDays
= this.serverless.service.provider.logRetentionInDays;
} else {
@@ -91,8 +92,10 @@ module.exports = {
.PolicyDocument
.Statement[0]
.Resource
- .push({ 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}' +
- `:log-group:${this.provider.naming.getLogGroupName(functionObject.name)}:*` });
+ .push({
+ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
+ `:log-group:${this.provider.naming.getLogGroupName(functionObject.name)}:*`,
+ });
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources[this.provider.naming.getRoleLogicalId()]
@@ -101,8 +104,10 @@ module.exports = {
.PolicyDocument
.Statement[1]
.Resource
- .push({ 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}' +
- `:log-group:${this.provider.naming.getLogGroupName(functionObject.name)}:*:*` });
+ .push({
+ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
+ `:log-group:${this.provider.naming.getLogGroupName(functionObject.name)}:*:*`,
+ });
});
if (this.serverless.service.provider.iamRoleStatements) {
@@ -120,6 +125,14 @@ module.exports = {
.Statement.concat(this.serverless.service.provider.iamRoleStatements);
}
+ if (this.serverless.service.provider.iamManagedPolicies) {
+ // add iam managed policies
+ const iamManagedPolicies = this.serverless.service.provider.iamManagedPolicies;
+ if (iamManagedPolicies.length > 0) {
+ this.mergeManagedPolicies(iamManagedPolicies);
+ }
+ }
+
// check if one of the functions contains vpc configuration
const vpcConfigProvided = [];
this.serverless.service.getAllFunctions().forEach((functionName) => {
@@ -131,17 +144,30 @@ module.exports = {
if (_.includes(vpcConfigProvided, true) || this.serverless.service.provider.vpc) {
// add managed iam policy to allow ENI management
- this.serverless.service.provider.compiledCloudFormationTemplate
- .Resources[this.provider.naming.getRoleLogicalId()]
- .Properties
- .ManagedPolicyArns = [
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
- ];
+ this.mergeManagedPolicies([{
+ 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
+ ],
+ ],
+ }]);
}
return BbPromise.resolve();
},
+ mergeManagedPolicies(managedPolicies) {
+ const resource = this.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[this.provider.naming.getRoleLogicalId()]
+ .Properties;
+ if (!_.has(resource, 'ManagedPolicyArns') || _.isEmpty(resource.ManagedPolicyArns)) {
+ resource.ManagedPolicyArns = [];
+ }
+ resource.ManagedPolicyArns = resource.ManagedPolicyArns.concat(managedPolicies);
+ },
+
validateStatements(statements) {
// Verify that iamRoleStatements (if present) is an array of { Effect: ...,
// Action: ..., Resource: ... } objects.
@@ -154,7 +180,7 @@ module.exports = {
} else {
const descriptions = statements.map((statement, i) => {
const missing = ['Effect', 'Action', 'Resource'].filter(
- prop => statement[prop] === undefined);
+ prop => statement[prop] === undefined);
return missing.length === 0 ? null :
`statement ${i} is missing the following properties: ${missing.join(', ')}`;
});
@@ -173,4 +199,14 @@ module.exports = {
throw new this.serverless.classes.Error(errorMessage);
}
},
+
+ validateManagedPolicies(iamManagedPolicies) {
+ // Verify that iamManagedPolicies (if present) is an array
+ if (!iamManagedPolicies) {
+ return;
+ }
+ if (!_.isArray(iamManagedPolicies)) {
+ throw new this.serverless.classes.Error('iamManagedPolicies should be an array of arns');
+ }
+ },
};
diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
index 1407989251f..1a236a74311 100644
--- a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
+++ b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js
@@ -95,7 +95,7 @@ describe('#mergeIamTemplates()', () => {
],
Resource: [
{
- 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:'
+ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+ `log-group:/aws/lambda/${qualifiedFunction}:*`,
},
],
@@ -107,7 +107,7 @@ describe('#mergeIamTemplates()', () => {
],
Resource: [
{
- 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:'
+ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+ `log-group:/aws/lambda/${qualifiedFunction}:*:*`,
},
],
@@ -155,6 +155,56 @@ describe('#mergeIamTemplates()', () => {
});
});
+ it('should add managed policy arns', () => {
+ awsPackage.serverless.service.provider.iamManagedPolicies = [
+ 'some:aws:arn:xxx:*:*',
+ 'someOther:aws:arn:xxx:*:*',
+ { 'Fn::Join': [':', ['arn:aws:iam:', { Ref: 'AWSAccountId' }, 'some/path']] },
+ ];
+ return awsPackage.mergeIamTemplates()
+ .then(() => {
+ expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[awsPackage.provider.naming.getRoleLogicalId()]
+ .Properties
+ .ManagedPolicyArns
+ ).to.deep.equal(awsPackage.serverless.service.provider.iamManagedPolicies);
+ });
+ });
+
+ it('should merge managed policy arns when vpc config supplied', () => {
+ awsPackage.serverless.service.provider.vpc = {
+ securityGroupIds: ['xxx'],
+ subnetIds: ['xxx'],
+ };
+ const iamManagedPolicies = [
+ 'some:aws:arn:xxx:*:*',
+ 'someOther:aws:arn:xxx:*:*',
+ { 'Fn::Join': [':', ['arn:aws:iam:', { Ref: 'AWSAccountId' }, 'some/path']] },
+ ];
+ awsPackage.serverless.service.provider.iamManagedPolicies = iamManagedPolicies;
+ const expectedManagedPolicyArns = [
+ 'some:aws:arn:xxx:*:*',
+ 'someOther:aws:arn:xxx:*:*',
+ { 'Fn::Join': [':', ['arn:aws:iam:', { Ref: 'AWSAccountId' }, 'some/path']] },
+ { 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
+ ],
+ ],
+ },
+ ];
+ return awsPackage.mergeIamTemplates()
+ .then(() => {
+ expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources[awsPackage.provider.naming.getRoleLogicalId()]
+ .Properties
+ .ManagedPolicyArns
+ ).to.deep.equal(expectedManagedPolicyArns);
+ });
+ });
+
it('should throw error if custom IAM policy statements is not an array', () => {
awsPackage.serverless.service.provider.iamRoleStatements = {
policy: 'some_value',
@@ -179,7 +229,7 @@ describe('#mergeIamTemplates()', () => {
}];
expect(() => awsPackage.mergeIamTemplates()).to.throw(
- 'missing the following properties: Effect');
+ 'missing the following properties: Effect');
});
it('should throw error if a custom IAM policy statement does not have an Action field', () => {
@@ -189,7 +239,7 @@ describe('#mergeIamTemplates()', () => {
}];
expect(() => awsPackage.mergeIamTemplates()).to.throw(
- 'missing the following properties: Action');
+ 'missing the following properties: Action');
});
it('should throw error if a custom IAM policy statement does not have a Resource field', () => {
@@ -199,7 +249,7 @@ describe('#mergeIamTemplates()', () => {
}];
expect(() => awsPackage.mergeIamTemplates()).to.throw(
- 'missing the following properties: Resource');
+ 'missing the following properties: Resource');
});
it('should throw an error describing all problematics custom IAM policy statements', () => {
@@ -222,6 +272,12 @@ describe('#mergeIamTemplates()', () => {
.to.throw(/statement 0 is missing.*Resource; statement 2 is missing.*Effect, Action/);
});
+ it('should throw error if managed policies is not an array', () => {
+ awsPackage.serverless.service.provider.iamManagedPolicies = 'a string';
+ expect(() => awsPackage.mergeIamTemplates())
+ .to.throw('iamManagedPolicies should be an array of arns');
+ });
+
it('should add a CloudWatch LogGroup resource', () => {
const normalizedName = awsPackage.provider.naming.getLogGroupLogicalId(functionName);
return awsPackage.mergeIamTemplates().then(() => {
@@ -234,44 +290,44 @@ describe('#mergeIamTemplates()', () => {
LogGroupName: awsPackage.provider.naming.getLogGroupName(functionName),
},
}
- );
+ );
});
});
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,
- },
- }
- );
+ , () => {
+ 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');
- });
+ , () => {
+ 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';
@@ -300,7 +356,7 @@ describe('#mergeIamTemplates()', () => {
LogGroupName: awsPackage.provider.naming.getLogGroupName(f.func0.name),
},
}
- );
+ );
expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
.Resources[normalizedNames[1]]
).to.deep.equal(
@@ -310,7 +366,7 @@ describe('#mergeIamTemplates()', () => {
LogGroupName: awsPackage.provider.naming.getLogGroupName(f.func1.name),
},
}
- );
+ );
});
});
@@ -326,7 +382,7 @@ describe('#mergeIamTemplates()', () => {
.Resource
).to.deep.equal([
{
- 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:'
+ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+ `log-group:/aws/lambda/${qualifiedFunction}:*`,
},
]);
@@ -339,7 +395,7 @@ describe('#mergeIamTemplates()', () => {
.Resource
).to.deep.equal([
{
- 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:'
+ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+ `log-group:/aws/lambda/${qualifiedFunction}:*:*`,
},
]);
@@ -367,12 +423,16 @@ describe('#mergeIamTemplates()', () => {
.Resource
).to.deep.equal(
[
- { 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:'
- + 'log-group:/aws/lambda/func0:*' },
- { 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:'
- + 'log-group:/aws/lambda/func1:*' },
+ {
+ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+ + 'log-group:/aws/lambda/func0:*',
+ },
+ {
+ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+ + 'log-group:/aws/lambda/func1:*',
+ },
]
- );
+ );
expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
.Resources[awsPackage.provider.naming.getRoleLogicalId()]
.Properties
@@ -382,12 +442,16 @@ describe('#mergeIamTemplates()', () => {
.Resource
).to.deep.equal(
[
- { 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:'
- + 'log-group:/aws/lambda/func0:*:*' },
- { 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:'
- + 'log-group:/aws/lambda/func1:*:*' },
+ {
+ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+ + 'log-group:/aws/lambda/func0:*:*',
+ },
+ {
+ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:'
+ + 'log-group:/aws/lambda/func1:*:*',
+ },
]
- );
+ );
});
});
@@ -409,7 +473,7 @@ describe('#mergeIamTemplates()', () => {
.then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
.Resources[awsPackage.provider.naming.getRoleLogicalId()]
).to.exist
- );
+ );
});
it('should not add the default role if role is defined on a provider level', () => {
@@ -449,7 +513,7 @@ describe('#mergeIamTemplates()', () => {
.then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
.Resources[awsPackage.provider.naming.getRoleLogicalId()]
).to.not.exist
- );
+ );
});
describe('ManagedPolicyArns property', () => {
@@ -463,8 +527,8 @@ describe('#mergeIamTemplates()', () => {
return awsPackage.mergeIamTemplates()
.then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
- .Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns
- ).to.not.exist
+ .Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns
+ ).to.not.exist
);
});
@@ -478,9 +542,15 @@ describe('#mergeIamTemplates()', () => {
.then(() => {
expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
.Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns
- ).to.deep.equal([
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
- ]);
+ ).to.deep.equal([{
+ 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
+ ],
+ ],
+ }]);
});
});
@@ -504,9 +574,15 @@ describe('#mergeIamTemplates()', () => {
.then(() => {
expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
.Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns
- ).to.deep.equal([
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
- ]);
+ ).to.deep.equal([{
+ 'Fn::Join': ['',
+ [
+ 'arn:',
+ { Ref: 'AWS::Partition' },
+ ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
+ ],
+ ],
+ }]);
});
});
@@ -525,8 +601,8 @@ describe('#mergeIamTemplates()', () => {
return awsPackage.mergeIamTemplates()
.then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
- .Resources[awsPackage.provider.naming.getRoleLogicalId()]
- ).to.not.exist
+ .Resources[awsPackage.provider.naming.getRoleLogicalId()]
+ ).to.not.exist
);
});
});
diff --git a/lib/plugins/aws/package/lib/saveServiceState.js b/lib/plugins/aws/package/lib/saveServiceState.js
index 46756bd6271..7d6afe2fbb6 100644
--- a/lib/plugins/aws/package/lib/saveServiceState.js
+++ b/lib/plugins/aws/package/lib/saveServiceState.js
@@ -36,7 +36,7 @@ module.exports = {
},
};
- this.serverless.utils.writeFileSync(serviceStateFilePath, state);
+ this.serverless.utils.writeFileSync(serviceStateFilePath, state, true);
return BbPromise.resolve();
},
diff --git a/lib/plugins/aws/package/lib/saveServiceState.test.js b/lib/plugins/aws/package/lib/saveServiceState.test.js
index db9b02bd1f0..064fb861c0f 100644
--- a/lib/plugins/aws/package/lib/saveServiceState.test.js
+++ b/lib/plugins/aws/package/lib/saveServiceState.test.js
@@ -63,7 +63,7 @@ describe('#saveServiceState()', () => {
};
expect(getServiceStateFileNameStub.calledOnce).to.equal(true);
- expect(writeFileSyncStub.calledWithExactly(filePath, expectedStateFileContent))
+ expect(writeFileSyncStub.calledWithExactly(filePath, expectedStateFileContent, true))
.to.equal(true);
});
});
@@ -97,7 +97,7 @@ describe('#saveServiceState()', () => {
};
expect(getServiceStateFileNameStub.calledOnce).to.equal(true);
- expect(writeFileSyncStub.calledWithExactly(filePath, expectedStateFileContent))
+ expect(writeFileSyncStub.calledWithExactly(filePath, expectedStateFileContent, true))
.to.equal(true);
});
});
diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js
index 6eae516f637..50f7a33c01b 100644
--- a/lib/plugins/aws/provider/awsProvider.js
+++ b/lib/plugins/aws/provider/awsProvider.js
@@ -12,6 +12,7 @@ const https = require('https');
const fs = require('fs');
const objectHash = require('object-hash');
const PromiseQueue = require('promise-queue');
+const getS3EndpointForRegion = require('../utils/getS3EndpointForRegion');
const constants = {
providerName: 'aws',
@@ -80,8 +81,8 @@ const impl = {
const profileCredentials = new AWS.SharedIniFileCredentials(params);
if (!(profileCredentials.accessKeyId
- || profileCredentials.sessionToken
- || profileCredentials.roleArn)) {
+ || profileCredentials.sessionToken
+ || profileCredentials.roleArn)) {
throw new Error(`Profile ${profile} does not exist`);
}
@@ -126,9 +127,7 @@ class AwsProvider {
|| process.env.https_proxy;
if (proxy) {
- const proxyOptions = url.parse(proxy);
- proxyOptions.secureEndpoint = true;
- AWS.config.httpOptions.agent = new HttpsProxyAgent(proxyOptions);
+ AWS.config.httpOptions.agent = new HttpsProxyAgent(url.parse(proxy));
}
const ca = process.env.ca
@@ -200,20 +199,27 @@ class AwsProvider {
const requestOptions = _.isObject(options) ? options : {};
const shouldCache = _.get(requestOptions, 'useCache', false);
const paramsHash = objectHash.sha1(params);
+ const MAX_TRIES = 4;
const persistentRequest = (f) => new BbPromise((resolve, reject) => {
- const doCall = () => {
+ const doCall = (numTry) => {
f()
- .then(resolve)
- .catch((e) => {
- if (e.statusCode === 429) {
- that.serverless.cli.log("'Too many requests' received, sleeping 5 seconds");
- setTimeout(doCall, 5000);
+ // We're resembling if/else logic, therefore single `then` instead of `then`/`catch` pair
+ .then(resolve, e => {
+ if (numTry < MAX_TRIES &&
+ ((e.providerError && e.providerError.retryable) || e.statusCode === 429)) {
+ that.serverless.cli.log(
+ _.join([
+ `Recoverable error occurred (${e.message}), sleeping for 5 seconds.`,
+ `Try ${numTry + 1} of ${MAX_TRIES}`,
+ ], ' ')
+ );
+ setTimeout(doCall, 5000, numTry + 1);
} else {
reject(e);
}
});
};
- return doCall();
+ return doCall(0);
});
// Emit a warning for misuses of the old signature including stage and region
@@ -250,21 +256,29 @@ class AwsProvider {
const errorMessage = [
'AWS provider credentials not found.',
' Learn how to set up AWS provider credentials',
- ` in our docs here: ${chalk.green('http://bit.ly/aws-creds-setup')}.`,
+ ` in our docs here: <${chalk.green('http://bit.ly/aws-creds-setup')}>.`,
].join('');
message = errorMessage;
userStats.track('user_awsCredentialsNotFound');
+ // We do not want to trigger the retry mechanism for credential errors
+ return BbPromise.reject(Object.assign(
+ new this.serverless.classes.Error(message, err.statusCode),
+ { providerError: _.assign({}, err, { retryable: false }) }
+ ));
}
- return BbPromise.reject(new this.serverless.classes.Error(message, err.statusCode));
+ return BbPromise.reject(Object.assign(
+ new this.serverless.classes.Error(message, err.statusCode),
+ { providerError: err }
+ ));
});
})
- .then(data => {
- const result = BbPromise.resolve(data);
- if (shouldCache) {
- _.set(this.requestCache, `${service}.${method}.${paramsHash}`, result);
- }
- return result;
- }));
+ .then(data => {
+ const result = BbPromise.resolve(data);
+ if (shouldCache) {
+ _.set(this.requestCache, `${service}.${method}.${paramsHash}`, result);
+ }
+ return result;
+ }));
if (shouldCache) {
_.set(this.requestCache, `${service}.${method}.${paramsHash}`, request);
@@ -309,8 +323,17 @@ class AwsProvider {
canUseS3TransferAcceleration(service, method) {
// TODO enable more S3 APIs?
return service === 'S3'
- && ['upload', 'putObject'].indexOf(method) !== -1
- && this.isS3TransferAccelerationEnabled();
+ && ['upload', 'putObject'].indexOf(method) !== -1
+ && this.isS3TransferAccelerationEnabled();
+ }
+
+ // This function will be used to block the addition of transfer acceleration options
+ // to the cloudformation template for regions where acceleration is not supported (ie, govcloud)
+ isS3TransferAccelerationSupported() {
+ // Only enable s3 transfer acceleration for standard regions (non govcloud/china)
+ // since those regions do not yet support it
+ const endpoint = getS3EndpointForRegion(this.getRegion());
+ return endpoint === 's3.amazonaws.com';
}
isS3TransferAccelerationEnabled() {
@@ -330,13 +353,28 @@ class AwsProvider {
credentials.useAccelerateEndpoint = true; // eslint-disable-line no-param-reassign
}
+ getValues(source, paths) {
+ return paths.map(path => ({
+ path,
+ value: _.get(source, path.join('.')),
+ }));
+ }
+ firstValue(values) {
+ return values.reduce((result, current) => (result.value ? result : current), {});
+ }
+
+ getRegionSourceValue() {
+ const values = this.getValues(this, [
+ ['options', 'region'],
+ ['serverless', 'config', 'region'],
+ ['serverless', 'service', 'provider', 'region'],
+ ]);
+ return this.firstValue(values);
+ }
getRegion() {
const defaultRegion = 'us-east-1';
-
- return _.get(this, 'options.region')
- || _.get(this, 'serverless.config.region')
- || _.get(this, 'serverless.service.provider.region')
- || defaultRegion;
+ const regionSourceValue = this.getRegionSourceValue();
+ return regionSourceValue.value || defaultRegion;
}
getServerlessDeploymentBucketName() {
@@ -352,18 +390,38 @@ class AwsProvider {
).then((result) => result.StackResourceDetail.PhysicalResourceId);
}
+ getStageSourceValue() {
+ const values = this.getValues(this, [
+ ['options', 'stage'],
+ ['serverless', 'config', 'stage'],
+ ['serverless', 'service', 'provider', 'stage'],
+ ]);
+ return this.firstValue(values);
+ }
getStage() {
const defaultStage = 'dev';
-
- return _.get(this, 'options.stage')
- || _.get(this, 'serverless.config.stage')
- || _.get(this, 'serverless.service.provider.stage')
- || defaultStage;
+ const stageSourceValue = this.getStageSourceValue();
+ return stageSourceValue.value || defaultStage;
}
getAccountId() {
+ return this.getAccountInfo()
+ .then((result) => result.accountId);
+ }
+
+ getAccountInfo() {
return this.request('STS', 'getCallerIdentity', {})
- .then((result) => result.Account);
+ .then((result) => {
+ const arn = result.Arn;
+ const accountId = result.Account;
+ const partition = _.nth(_.split(arn, ':'), 1); // ex: arn:aws:iam:acctId:user/xyz
+ return {
+ accountId,
+ partition,
+ arn: result.Arn,
+ userId: result.UserId,
+ };
+ });
}
/**
@@ -383,7 +441,7 @@ class AwsProvider {
*/
getApiGatewayRestApiRootResourceId() {
if (this.serverless.service.provider.apiGateway
- && this.serverless.service.provider.apiGateway.restApiRootResourceId) {
+ && this.serverless.service.provider.apiGateway.restApiRootResourceId) {
return this.serverless.service.provider.apiGateway.restApiRootResourceId;
}
return { 'Fn::GetAtt': [this.naming.getRestApiLogicalId(), 'RootResourceId'] };
@@ -394,7 +452,7 @@ class AwsProvider {
*/
getApiGatewayPredefinedResources() {
if (!this.serverless.service.provider.apiGateway
- || !this.serverless.service.provider.apiGateway.restApiResources) {
+ || !this.serverless.service.provider.apiGateway.restApiResources) {
return [];
}
diff --git a/lib/plugins/aws/provider/awsProvider.test.js b/lib/plugins/aws/provider/awsProvider.test.js
index 297b298b670..b518cc28dc7 100644
--- a/lib/plugins/aws/provider/awsProvider.test.js
+++ b/lib/plugins/aws/provider/awsProvider.test.js
@@ -1,5 +1,7 @@
'use strict';
+/* eslint-disable no-unused-expressions */
+
const _ = require('lodash');
const BbPromise = require('bluebird');
const chai = require('chai');
@@ -72,118 +74,118 @@ describe('AwsProvider', () => {
// clear env
delete process.env.AWS_CLIENT_TIMEOUT;
});
- });
- describe('#constructor() certificate authority - environment variable', () => {
- afterEach('Environment Variable Cleanup', () => {
- // clear env
- delete process.env.ca;
- });
- it('should set AWS ca single', () => {
- process.env.ca = '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----';
- const newAwsProvider = new AwsProvider(serverless, options);
+ describe('certificate authority - environment variable', () => {
+ afterEach('Environment Variable Cleanup', () => {
+ // clear env
+ delete process.env.ca;
+ });
+ it('should set AWS ca single', () => {
+ process.env.ca = '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----';
+ const newAwsProvider = new AwsProvider(serverless, options);
- expect(typeof newAwsProvider.sdk.config.httpOptions.agent).to.not.equal('undefined');
- });
+ expect(typeof newAwsProvider.sdk.config.httpOptions.agent).to.not.equal('undefined');
+ });
- it('should set AWS ca multiple', () => {
- const certContents = '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----';
- process.env.ca = `${certContents},${certContents}`;
- const newAwsProvider = new AwsProvider(serverless, options);
+ it('should set AWS ca multiple', () => {
+ const certContents = '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----';
+ process.env.ca = `${certContents},${certContents}`;
+ const newAwsProvider = new AwsProvider(serverless, options);
- expect(typeof newAwsProvider.sdk.config.httpOptions.agent).to.not.equal('undefined');
+ expect(typeof newAwsProvider.sdk.config.httpOptions.agent).to.not.equal('undefined');
+ });
});
- });
- describe('#constructor() certificate authority - file', () => {
- const certContents = '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----';
- const tmpdir = os.tmpdir();
- let file1 = null;
- let file2 = null;
- beforeEach('Create CA Files and env vars', () => {
- file1 = path.join(tmpdir, 'ca1.txt');
- file2 = path.join(tmpdir, 'ca2.txt');
- fs.writeFileSync(file1, certContents);
- fs.writeFileSync(file2, certContents);
- });
-
- afterEach('CA File Cleanup', () => {
- // delete files
- fs.unlinkSync(file1);
- fs.unlinkSync(file2);
- // clear env
- delete process.env.ca;
- delete process.env.cafile;
- });
+ describe('certificate authority - file', () => {
+ const certContents = '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----';
+ const tmpdir = os.tmpdir();
+ let file1 = null;
+ let file2 = null;
+ beforeEach('Create CA Files and env vars', () => {
+ file1 = path.join(tmpdir, 'ca1.txt');
+ file2 = path.join(tmpdir, 'ca2.txt');
+ fs.writeFileSync(file1, certContents);
+ fs.writeFileSync(file2, certContents);
+ });
- it('should set AWS cafile single', () => {
- process.env.cafile = file1;
- const newAwsProvider = new AwsProvider(serverless, options);
+ afterEach('CA File Cleanup', () => {
+ // delete files
+ fs.unlinkSync(file1);
+ fs.unlinkSync(file2);
+ // clear env
+ delete process.env.ca;
+ delete process.env.cafile;
+ });
- expect(typeof newAwsProvider.sdk.config.httpOptions.agent).to.not.equal('undefined');
- });
+ it('should set AWS cafile single', () => {
+ process.env.cafile = file1;
+ const newAwsProvider = new AwsProvider(serverless, options);
- it('should set AWS cafile multiple', () => {
- process.env.cafile = `${file1},${file2}`;
- const newAwsProvider = new AwsProvider(serverless, options);
+ expect(typeof newAwsProvider.sdk.config.httpOptions.agent).to.not.equal('undefined');
+ });
- expect(typeof newAwsProvider.sdk.config.httpOptions.agent).to.not.equal('undefined');
- });
+ it('should set AWS cafile multiple', () => {
+ process.env.cafile = `${file1},${file2}`;
+ const newAwsProvider = new AwsProvider(serverless, options);
- it('should set AWS ca and cafile', () => {
- process.env.ca = certContents;
- process.env.cafile = file1;
- const newAwsProvider = new AwsProvider(serverless, options);
+ expect(typeof newAwsProvider.sdk.config.httpOptions.agent).to.not.equal('undefined');
+ });
- expect(typeof newAwsProvider.sdk.config.httpOptions.agent).to.not.equal('undefined');
+ it('should set AWS ca and cafile', () => {
+ process.env.ca = certContents;
+ process.env.cafile = file1;
+ const newAwsProvider = new AwsProvider(serverless, options);
+
+ expect(typeof newAwsProvider.sdk.config.httpOptions.agent).to.not.equal('undefined');
+ });
});
- });
- describe('when checking for the deploymentBucket config', () => {
- it('should do nothing if the deploymentBucket config is not used', () => {
- serverless.service.provider.deploymentBucket = undefined;
+ describe('deploymentBucket configuration', () => {
+ it('should do nothing if not defined', () => {
+ serverless.service.provider.deploymentBucket = undefined;
- const newAwsProvider = new AwsProvider(serverless, options);
+ const newAwsProvider = new AwsProvider(serverless, options);
- expect(newAwsProvider.serverless.service.provider.deploymentBucket).to.equal(undefined);
- });
+ expect(newAwsProvider.serverless.service.provider.deploymentBucket).to.equal(undefined);
+ });
- it('should do nothing if the deploymentBucket config is a string', () => {
- serverless.service.provider.deploymentBucket = 'my.deployment.bucket';
+ it('should do nothing if the value is a string', () => {
+ serverless.service.provider.deploymentBucket = 'my.deployment.bucket';
- const newAwsProvider = new AwsProvider(serverless, options);
+ const newAwsProvider = new AwsProvider(serverless, options);
- expect(newAwsProvider.serverless.service.provider.deploymentBucket)
- .to.equal('my.deployment.bucket');
- });
+ expect(newAwsProvider.serverless.service.provider.deploymentBucket)
+ .to.equal('my.deployment.bucket');
+ });
- it('should save the object and use the name for the deploymentBucket if provided', () => {
- const deploymentBucketObject = {
- name: 'my.deployment.bucket',
- serverSideEncryption: 'AES256',
- };
- serverless.service.provider.deploymentBucket = deploymentBucketObject;
+ it('should save a given object and use name from it', () => {
+ const deploymentBucketObject = {
+ name: 'my.deployment.bucket',
+ serverSideEncryption: 'AES256',
+ };
+ serverless.service.provider.deploymentBucket = deploymentBucketObject;
- const newAwsProvider = new AwsProvider(serverless, options);
+ const newAwsProvider = new AwsProvider(serverless, options);
- expect(newAwsProvider.serverless.service.provider.deploymentBucket)
- .to.equal('my.deployment.bucket');
- expect(newAwsProvider.serverless.service.provider.deploymentBucketObject)
- .to.deep.equal(deploymentBucketObject);
- });
+ expect(newAwsProvider.serverless.service.provider.deploymentBucket)
+ .to.equal('my.deployment.bucket');
+ expect(newAwsProvider.serverless.service.provider.deploymentBucketObject)
+ .to.deep.equal(deploymentBucketObject);
+ });
- it('should save the object and nullify the name if it is not provided', () => {
- const deploymentBucketObject = {
- serverSideEncryption: 'AES256',
- };
- serverless.service.provider.deploymentBucket = deploymentBucketObject;
+ it('should save a given object and nullify the name if one is not provided', () => {
+ const deploymentBucketObject = {
+ serverSideEncryption: 'AES256',
+ };
+ serverless.service.provider.deploymentBucket = deploymentBucketObject;
- const newAwsProvider = new AwsProvider(serverless, options);
+ const newAwsProvider = new AwsProvider(serverless, options);
- expect(newAwsProvider.serverless.service.provider.deploymentBucket)
- .to.equal(null);
- expect(newAwsProvider.serverless.service.provider.deploymentBucketObject)
- .to.deep.equal(deploymentBucketObject);
+ expect(newAwsProvider.serverless.service.provider.deploymentBucket)
+ .to.equal(null);
+ expect(newAwsProvider.serverless.service.provider.deploymentBucketObject)
+ .to.deep.equal(deploymentBucketObject);
+ });
});
});
@@ -230,27 +232,23 @@ describe('AwsProvider', () => {
});
it('should retry if error code is 429', (done) => {
- let first = true;
const error = {
statusCode: 429,
+ retryable: true,
message: 'Testing retry',
};
+ const sendFake = {
+ send: sinon.stub(),
+ };
+ sendFake.send.onFirstCall().yields(error);
+ sendFake.send.yields(undefined, {});
class FakeS3 {
constructor(credentials) {
this.credentials = credentials;
}
error() {
- return {
- send(cb) {
- if (first) {
- first = false;
- cb(error);
- } else {
- cb(undefined, {});
- }
- },
- };
+ return sendFake;
}
}
awsProvider.sdk = {
@@ -258,8 +256,40 @@ describe('AwsProvider', () => {
};
awsProvider.request('S3', 'error', {})
.then(data => {
- expect(data).to.exist; // eslint-disable-line no-unused-expressions
- expect(first).to.be.false; // eslint-disable-line no-unused-expressions
+ expect(data).to.exist;
+ expect(sendFake.send).to.have.been.calledTwice;
+ done();
+ })
+ .catch(done);
+ });
+
+ it('should retry if error code is 429 and retryable is set to false', (done) => {
+ const error = {
+ statusCode: 429,
+ retryable: false,
+ message: 'Testing retry',
+ };
+ const sendFake = {
+ send: sinon.stub(),
+ };
+ sendFake.send.onFirstCall().yields(error);
+ sendFake.send.yields(undefined, {});
+ class FakeS3 {
+ constructor(credentials) {
+ this.credentials = credentials;
+ }
+
+ error() {
+ return sendFake;
+ }
+ }
+ awsProvider.sdk = {
+ S3: FakeS3,
+ };
+ awsProvider.request('S3', 'error', {})
+ .then(data => {
+ expect(data).to.exist;
+ expect(sendFake.send).to.have.been.calledTwice;
done();
})
.catch(done);
@@ -321,6 +351,36 @@ describe('AwsProvider', () => {
.catch(done);
});
+ it('should not retry for missing credentials', (done) => {
+ const error = {
+ statusCode: 403,
+ message: 'Missing credentials in config',
+ };
+ const sendFake = {
+ send: sinon.stub().yields(error),
+ };
+ class FakeS3 {
+ constructor(credentials) {
+ this.credentials = credentials;
+ }
+
+ error() {
+ return sendFake;
+ }
+ }
+ awsProvider.sdk = {
+ S3: FakeS3,
+ };
+ awsProvider.request('S3', 'error', {})
+ .then(() => done('Should not succeed'))
+ .catch((err) => {
+ expect(sendFake.send).to.have.been.calledOnce;
+ expect(err.message).to.contain('in our docs here:');
+ done();
+ })
+ .catch(done);
+ });
+
it('should enable S3 acceleration if CLI option is provided', () => {
// mocking S3 for testing
class FakeS3 {
@@ -393,9 +453,9 @@ describe('AwsProvider', () => {
{},
{ useCache: true }
)
- .then(data => {
- expect(data.called).to.equal(true);
- });
+ .then(data => {
+ expect(data.called).to.equal(true);
+ });
});
it('should resolve to the same response with mutiple parallel requests', () => {
@@ -442,19 +502,19 @@ describe('AwsProvider', () => {
}
return BbPromise.all(requests)
- .then(results => {
- expect(_.size(results, numTests));
- _.forEach(results, result => {
- expect(result).to.deep.equal(expectedResult);
+ .then(results => {
+ expect(_.size(results, numTests));
+ _.forEach(results, result => {
+ expect(result).to.deep.equal(expectedResult);
+ });
+ return BbPromise.join(
+ expect(sendStub).to.have.been.calledOnce,
+ expect(requestSpy).to.have.callCount(numTests)
+ );
+ })
+ .finally(() => {
+ requestSpy.restore();
});
- return BbPromise.join(
- expect(sendStub).to.have.been.calledOnce,
- expect(requestSpy).to.have.callCount(numTests)
- );
- })
- .finally(() => {
- requestSpy.restore();
- });
});
});
});
@@ -693,6 +753,69 @@ describe('AwsProvider', () => {
});
});
+ describe('values', () => {
+ const obj = {
+ a: 'b',
+ c: {
+ d: 'e',
+ f: {
+ g: 'h',
+ },
+ },
+ };
+ const paths = [
+ ['a'],
+ ['c', 'd'],
+ ['c', 'f', 'g'],
+ ];
+ const getExpected = [
+ { path: paths[0], value: obj.a },
+ { path: paths[1], value: obj.c.d },
+ { path: paths[2], value: obj.c.f.g },
+ ];
+ describe('#getValues', () => {
+ it('should return an array of values given paths to them', () => {
+ expect(awsProvider.getValues(obj, paths)).to.eql(getExpected);
+ });
+ });
+ describe('#firstValue', () => {
+ it('should ignore entries without a \'value\' attribute', () => {
+ const input = _.cloneDeep(getExpected);
+ delete input[0].value;
+ delete input[2].value;
+ expect(awsProvider.firstValue(input)).to.eql(getExpected[1]);
+ });
+ it('should ignore entries with an undefined \'value\' attribute', () => {
+ const input = _.cloneDeep(getExpected);
+ input[0].value = undefined;
+ input[2].value = undefined;
+ expect(awsProvider.firstValue(input)).to.eql(getExpected[1]);
+ });
+ it('should return the first value', () => {
+ expect(awsProvider.firstValue(getExpected)).to.equal(getExpected[0]);
+ });
+ it('should return the middle value', () => {
+ const input = _.cloneDeep(getExpected);
+ delete input[0].value;
+ delete input[2].value;
+ expect(awsProvider.firstValue(input)).to.equal(input[1]);
+ });
+ it('should return the last value', () => {
+ const input = _.cloneDeep(getExpected);
+ delete input[0].value;
+ delete input[1].value;
+ expect(awsProvider.firstValue(input)).to.equal(input[2]);
+ });
+ it('should return the last object if none have valid values', () => {
+ const input = _.cloneDeep(getExpected);
+ delete input[0].value;
+ delete input[1].value;
+ delete input[2].value;
+ expect(awsProvider.firstValue(input)).to.equal(input[2]);
+ });
+ });
+ });
+
describe('#getRegion()', () => {
let newAwsProvider;
@@ -835,6 +958,30 @@ describe('AwsProvider', () => {
});
});
+ describe('#getAccountInfo()', () => {
+ it('should return the AWS account id and partition', () => {
+ const accountId = '12345678';
+ const partition = 'aws';
+
+ const stsGetCallerIdentityStub = sinon
+ .stub(awsProvider, 'request')
+ .resolves({
+ ResponseMetadata: { RequestId: '12345678-1234-1234-1234-123456789012' },
+ UserId: 'ABCDEFGHIJKLMNOPQRSTU:VWXYZ',
+ Account: accountId,
+ Arn: 'arn:aws:sts::123456789012:assumed-role/ROLE-NAME/VWXYZ',
+ });
+
+ return awsProvider.getAccountInfo()
+ .then((result) => {
+ expect(stsGetCallerIdentityStub.calledOnce).to.equal(true);
+ expect(result.accountId).to.equal(accountId);
+ expect(result.partition).to.equal(partition);
+ awsProvider.request.restore();
+ });
+ });
+ });
+
describe('#getAccountId()', () => {
it('should return the AWS account id', () => {
const accountId = '12345678';
diff --git a/lib/plugins/aws/utils/arnRegularExpressions.js b/lib/plugins/aws/utils/arnRegularExpressions.js
new file mode 100644
index 00000000000..3f4f9467f78
--- /dev/null
+++ b/lib/plugins/aws/utils/arnRegularExpressions.js
@@ -0,0 +1,6 @@
+'use strict';
+
+module.exports = {
+ cognitoIdpArnExpr: /^arn:[a-zA-Z-]*:cognito-idp/,
+ lambdaArnExpr: /arn:[a-zA-Z-]*:lambda/,
+};
diff --git a/lib/plugins/aws/utils/formatLambdaLogEvent.js b/lib/plugins/aws/utils/formatLambdaLogEvent.js
index 2c2286e9b98..62efe8f644c 100644
--- a/lib/plugins/aws/utils/formatLambdaLogEvent.js
+++ b/lib/plugins/aws/utils/formatLambdaLogEvent.js
@@ -34,7 +34,7 @@ module.exports = (msgParam) => {
} else if (!isNaN((new Date(splitted[1])).getTime())) {
date = splitted[1];
reqId = splitted[2];
- level = `${chalk.white(splitted[0])}\t`;
+ level = `${splitted[0]}\t`;
} else {
return msg;
}
diff --git a/lib/plugins/aws/utils/formatLambdaLogEvent.test.js b/lib/plugins/aws/utils/formatLambdaLogEvent.test.js
index b1ae167f2fa..a5d4639dfc4 100644
--- a/lib/plugins/aws/utils/formatLambdaLogEvent.test.js
+++ b/lib/plugins/aws/utils/formatLambdaLogEvent.test.js
@@ -37,7 +37,7 @@ describe('#formatLambdaLogEvent()', () => {
const momentDate = moment('2016-01-01T12:00:00Z').format('YYYY-MM-DD HH:mm:ss.SSS (Z)');
expectedLogMessage += `${chalk.green(momentDate)}\t`;
expectedLogMessage += `${chalk.yellow('99c30000-b01a-11e5-93f7-b8e85631a00e')}\t`;
- expectedLogMessage += `${chalk.white('[INFO]')}\t`;
+ expectedLogMessage += `${'[INFO]'}\t`;
expectedLogMessage += 'test';
expect(formatLambdaLogEvent(pythonLoggerLine)).to.equal(expectedLogMessage);
diff --git a/lib/plugins/aws/utils/getS3EndpointForRegion.js b/lib/plugins/aws/utils/getS3EndpointForRegion.js
new file mode 100644
index 00000000000..39392ff1099
--- /dev/null
+++ b/lib/plugins/aws/utils/getS3EndpointForRegion.js
@@ -0,0 +1,11 @@
+'use strict';
+
+module.exports = function getS3EndpointForRegion(region) {
+ const strRegion = region.toLowerCase();
+ // look for govcloud - currently s3-us-gov-west-1.amazonaws.com
+ if (strRegion.match(/us-gov/)) return `s3-${strRegion}.amazonaws.com`;
+ // look for china - currently s3.cn-north-1.amazonaws.com.cn
+ if (strRegion.match(/cn-/)) return `s3.${strRegion}.amazonaws.com.cn`;
+ // default s3 endpoint for other regions
+ return 's3.amazonaws.com';
+};
diff --git a/lib/plugins/aws/utils/getS3EndpointForRegion.test.js b/lib/plugins/aws/utils/getS3EndpointForRegion.test.js
new file mode 100644
index 00000000000..e543fb59110
--- /dev/null
+++ b/lib/plugins/aws/utils/getS3EndpointForRegion.test.js
@@ -0,0 +1,21 @@
+'use strict';
+const expect = require('chai').expect;
+const getS3EndpointForRegion = require('./getS3EndpointForRegion');
+
+describe('getS3EndpointForRegion', () => {
+ it('should return standard endpoint for us-east-1', () => {
+ const expected = 's3.amazonaws.com';
+ const actual = getS3EndpointForRegion('us-east-1');
+ expect(actual).to.equal(expected);
+ });
+ it('should return govcloud endpoint for us-gov-west-1', () => {
+ const expected = 's3-us-gov-west-1.amazonaws.com';
+ const actual = getS3EndpointForRegion('us-gov-west-1');
+ expect(actual).to.equal(expected);
+ });
+ it('should return china endpoint for cn-north-1', () => {
+ const expected = 's3.cn-north-1.amazonaws.com.cn';
+ const actual = getS3EndpointForRegion('cn-north-1');
+ expect(actual).to.equal(expected);
+ });
+});
diff --git a/lib/plugins/create/create.js b/lib/plugins/create/create.js
index 84368002a70..d1aeb1cbe2d 100644
--- a/lib/plugins/create/create.js
+++ b/lib/plugins/create/create.js
@@ -31,9 +31,12 @@ const validTemplates = [
'aws-go',
'aws-go-dep',
'azure-nodejs',
+ 'fn-nodejs',
+ 'fn-go',
'google-nodejs',
'kubeless-python',
'kubeless-nodejs',
+ 'openwhisk-java-maven',
'openwhisk-nodejs',
'openwhisk-php',
'openwhisk-python',
diff --git a/lib/plugins/create/create.test.js b/lib/plugins/create/create.test.js
index c0bf9d726da..a6e10f2a1ca 100644
--- a/lib/plugins/create/create.test.js
+++ b/lib/plugins/create/create.test.js
@@ -143,7 +143,6 @@ describe('Create', () => {
expect(dirContent).to.include('aws-csharp.csproj');
expect(dirContent).to.include('build.cmd');
expect(dirContent).to.include('build.sh');
- expect(dirContent).to.include('global.json');
});
});
@@ -159,7 +158,7 @@ describe('Create', () => {
expect(dirContent).to.include('build.sh');
expect(dirContent).to.include('build.cmd');
expect(dirContent).to.include('aws-fsharp.fsproj');
- expect(dirContent).to.include('global.json');
+ expect(dirContent).to.not.include('global.json');
});
});
@@ -197,7 +196,7 @@ describe('Create', () => {
expect(dirContent).to.include('serverless.yml');
expect(dirContent).to.include('pom.xml');
- expect(dirContent).to.include(path.join('src', 'main', 'resources', 'log4j.properties'));
+ expect(dirContent).to.include(path.join('src', 'main', 'resources', 'log4j2.xml'));
expect(dirContent).to.include(path.join('src', 'main', 'java', 'com', 'serverless',
'Handler.java'));
expect(dirContent).to.include(path.join('src', 'main', 'java', 'com', 'serverless',
@@ -264,9 +263,9 @@ describe('Create', () => {
expect(dirContent).to.include('gradlew.bat');
expect(dirContent).to.include('package.json');
expect(dirContent).to.include(path.join('gradle', 'wrapper',
- 'gradle-wrapper.jar'));
+ 'gradle-wrapper.jar'));
expect(dirContent).to.include(path.join('gradle', 'wrapper',
- 'gradle-wrapper.properties'));
+ 'gradle-wrapper.properties'));
expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless',
'Handler.kt'));
expect(dirContent).to.include(path.join('src', 'main', 'kotlin', 'com', 'serverless',
@@ -291,17 +290,17 @@ describe('Create', () => {
expect(dirContent).to.include('gradlew');
expect(dirContent).to.include('gradlew.bat');
expect(dirContent).to.include(path.join('gradle', 'wrapper',
- 'gradle-wrapper.jar'));
+ 'gradle-wrapper.jar'));
expect(dirContent).to.include(path.join('gradle', 'wrapper',
- 'gradle-wrapper.properties'));
+ 'gradle-wrapper.properties'));
expect(dirContent).to.include(path.join('src', 'main', 'resources',
- 'log4j.properties'));
+ 'log4j.properties'));
expect(dirContent).to.include(path.join('src', 'main', 'java',
- 'com', 'serverless', 'Handler.java'));
+ 'com', 'serverless', 'Handler.java'));
expect(dirContent).to.include(path.join('src', 'main', 'java',
- 'com', 'serverless', 'ApiGatewayResponse.java'));
+ 'com', 'serverless', 'ApiGatewayResponse.java'));
expect(dirContent).to.include(path.join('src', 'main', 'java',
- 'com', 'serverless', 'Response.java'));
+ 'com', 'serverless', 'Response.java'));
expect(dirContent).to.include(path.join('.gitignore'));
});
});
@@ -319,17 +318,17 @@ describe('Create', () => {
expect(dirContent).to.include('gradlew');
expect(dirContent).to.include('gradlew.bat');
expect(dirContent).to.include(path.join('gradle', 'wrapper',
- 'gradle-wrapper.jar'));
+ 'gradle-wrapper.jar'));
expect(dirContent).to.include(path.join('gradle', 'wrapper',
- 'gradle-wrapper.properties'));
+ 'gradle-wrapper.properties'));
expect(dirContent).to.include(path.join('src', 'main', 'resources',
- 'log4j.properties'));
+ 'log4j.properties'));
expect(dirContent).to.include(path.join('src', 'main', 'groovy',
- 'com', 'serverless', 'Handler.groovy'));
+ 'com', 'serverless', 'Handler.groovy'));
expect(dirContent).to.include(path.join('src', 'main', 'groovy',
- 'com', 'serverless', 'ApiGatewayResponse.groovy'));
+ 'com', 'serverless', 'ApiGatewayResponse.groovy'));
expect(dirContent).to.include(path.join('src', 'main', 'groovy',
- 'com', 'serverless', 'Response.groovy'));
+ 'com', 'serverless', 'Response.groovy'));
expect(dirContent).to.include('.gitignore');
});
});
@@ -354,6 +353,23 @@ describe('Create', () => {
});
});
+ it('should generate scaffolding for "openwhisk-java-maven" template', () => {
+ process.chdir(tmpDir);
+ create.options.template = 'openwhisk-java-maven';
+
+ return create.create().then(() => {
+ const dirContent = walkDirSync(tmpDir)
+ .map(elem => elem.replace(path.join(tmpDir, path.sep), ''));
+ expect(dirContent).to.include('pom.xml');
+ expect(dirContent).to.include(path.join('src', 'main', 'java',
+ 'com', 'example', 'FunctionApp.java'));
+ expect(dirContent).to.include(path.join('src', 'test', 'java',
+ 'com', 'example', 'FunctionAppTest.java'));
+ expect(dirContent).to.include('.gitignore');
+ expect(dirContent).to.include('serverless.yml');
+ });
+ });
+
it('should generate scaffolding for "openwhisk-nodejs" template', () => {
process.chdir(tmpDir);
create.options.template = 'openwhisk-nodejs';
@@ -509,7 +525,7 @@ describe('Create', () => {
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'));
+ 'com', 'serverless', 'Handler.java'));
expect(dirContent).to.include('.gitignore');
});
});
@@ -527,6 +543,38 @@ describe('Create', () => {
});
});
+ it('should generate scaffolding for "fn-nodejs" template', () => {
+ process.chdir(tmpDir);
+ create.options.template = 'fn-nodejs';
+
+ 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('.gitignore');
+ expect(dirContent).to.include(path.join('hello', 'func.js'));
+ expect(dirContent).to.include(path.join('hello', 'package.json'));
+ expect(dirContent).to.include(path.join('hello', 'test.json'));
+ });
+ });
+
+ it('should generate scaffolding for "fn-go" template', () => {
+ process.chdir(tmpDir);
+ create.options.template = 'fn-go';
+
+ 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('.gitignore');
+ expect(dirContent).to.include(path.join('hello', 'func.go'));
+ expect(dirContent).to.include(path.join('hello', 'Gopkg.toml'));
+ expect(dirContent).to.include(path.join('hello', 'test.json'));
+ });
+ });
+
it('should generate scaffolding for "plugin" template', () => {
process.chdir(tmpDir);
create.options.template = 'plugin';
@@ -608,7 +656,7 @@ describe('Create', () => {
});
it('should create a custom renamed service in the directory if using ' +
- 'the "path" and "name" option', () => {
+ 'the "path" and "name" option', () => {
process.chdir(tmpDir);
create.options.path = 'my-new-service';
@@ -640,7 +688,7 @@ describe('Create', () => {
create.options.template = 'aws-nodejs';
create.options.path = '';
create.serverless.utils.copyDirContentsSync(path.join(create.serverless.config.serverlessPath,
- 'plugins', 'create', 'templates', create.options.template), tmpDir);
+ 'plugins', 'create', 'templates', create.options.template), tmpDir);
const dirContent = fs.readdirSync(tmpDir);
@@ -749,13 +797,12 @@ describe('Create', () => {
return create.create().then(() => {
const dirContent = walkDirSync(tmpDir)
- .map(elem => elem.replace(path.join(tmpDir, path.sep), ''));
+ .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-csharp/.vs/aws-csharp/v15/.suo b/lib/plugins/create/templates/aws-csharp/.vs/aws-csharp/v15/.suo
deleted file mode 100644
index 9c228fb45c0..00000000000
Binary files a/lib/plugins/create/templates/aws-csharp/.vs/aws-csharp/v15/.suo and /dev/null differ
diff --git a/lib/plugins/create/templates/aws-csharp/aws-csharp.csproj b/lib/plugins/create/templates/aws-csharp/aws-csharp.csproj
index efa84bbf57f..9cedd48b6b0 100644
--- a/lib/plugins/create/templates/aws-csharp/aws-csharp.csproj
+++ b/lib/plugins/create/templates/aws-csharp/aws-csharp.csproj
@@ -1,7 +1,8 @@
- netcoreapp1.0
+ netcoreapp2.0
+ true
CsharpHandlers
aws-csharp
@@ -12,7 +13,7 @@
-
+
diff --git a/lib/plugins/create/templates/aws-csharp/build.cmd b/lib/plugins/create/templates/aws-csharp/build.cmd
index f33ae0254e9..9bd8efb21a6 100644
--- a/lib/plugins/create/templates/aws-csharp/build.cmd
+++ b/lib/plugins/create/templates/aws-csharp/build.cmd
@@ -1,2 +1,2 @@
dotnet restore
-dotnet lambda package --configuration release --framework netcoreapp1.0 --output-package bin/release/netcoreapp1.0/deploy-package.zip
\ No newline at end of file
+dotnet lambda package --configuration release --framework netcoreapp2.0 --output-package bin/release/netcoreapp2.0/deploy-package.zip
diff --git a/lib/plugins/create/templates/aws-csharp/build.sh b/lib/plugins/create/templates/aws-csharp/build.sh
index 892b0f28986..f05233ca576 100755
--- a/lib/plugins/create/templates/aws-csharp/build.sh
+++ b/lib/plugins/create/templates/aws-csharp/build.sh
@@ -1,10 +1,11 @@
#!/bin/bash
-#install zip
-apt-get -qq update
-apt-get -qq -y install zip
+#install zip on debian OS, since microsoft/dotnet container doesn't have zip by default
+if [ -f /etc/debian_version ]
+then
+ apt -qq update
+ apt -qq -y install zip
+fi
dotnet restore
-
-#create deployment package
-dotnet lambda package --configuration release --framework netcoreapp1.0 --output-package bin/release/netcoreapp1.0/deploy-package.zip
+dotnet lambda package --configuration release --framework netcoreapp2.0 --output-package bin/release/netcoreapp2.0/deploy-package.zip
diff --git a/lib/plugins/create/templates/aws-csharp/global.json b/lib/plugins/create/templates/aws-csharp/global.json
deleted file mode 100644
index 8af244a4642..00000000000
--- a/lib/plugins/create/templates/aws-csharp/global.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "sdk": {
- "version": "1.0.4"
- }
-}
diff --git a/lib/plugins/create/templates/aws-csharp/serverless.yml b/lib/plugins/create/templates/aws-csharp/serverless.yml
index fb55c7aacee..8f2c5f37033 100644
--- a/lib/plugins/create/templates/aws-csharp/serverless.yml
+++ b/lib/plugins/create/templates/aws-csharp/serverless.yml
@@ -19,7 +19,7 @@ service: aws-csharp # NOTE: update this with your service name
provider:
name: aws
- runtime: dotnetcore1.0
+ runtime: dotnetcore2.0
# you can overwrite defaults here
# stage: dev
@@ -47,7 +47,7 @@ provider:
# you can add packaging information here
package:
- artifact: bin/release/netcoreapp1.0/deploy-package.zip
+ artifact: bin/release/netcoreapp2.0/deploy-package.zip
# exclude:
# - exclude-me.js
# - exclude-me-dir/**
@@ -67,7 +67,7 @@ functions:
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
-# - alexaSkill
+# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
# - iot:
# sql: "SELECT * FROM 'some_topic'"
diff --git a/lib/plugins/create/templates/aws-fsharp/aws-fsharp.fsproj b/lib/plugins/create/templates/aws-fsharp/aws-fsharp.fsproj
index f3a1aafd44b..cb30c3303b2 100644
--- a/lib/plugins/create/templates/aws-fsharp/aws-fsharp.fsproj
+++ b/lib/plugins/create/templates/aws-fsharp/aws-fsharp.fsproj
@@ -1,7 +1,7 @@
-
+
- netcoreapp1.0
+ netcoreapp2.0
FsharpHandlers
aws-fsharp
@@ -14,11 +14,10 @@
-
-
+
diff --git a/lib/plugins/create/templates/aws-fsharp/build.cmd b/lib/plugins/create/templates/aws-fsharp/build.cmd
index 468f8503abc..9bd8efb21a6 100644
--- a/lib/plugins/create/templates/aws-fsharp/build.cmd
+++ b/lib/plugins/create/templates/aws-fsharp/build.cmd
@@ -1,2 +1,2 @@
dotnet restore
-dotnet lambda package --configuration release --framework netcoreapp1.0 --output-package bin/release/netcoreapp1.0/deploy-package.zip
+dotnet lambda package --configuration release --framework netcoreapp2.0 --output-package bin/release/netcoreapp2.0/deploy-package.zip
diff --git a/lib/plugins/create/templates/aws-fsharp/build.sh b/lib/plugins/create/templates/aws-fsharp/build.sh
index f57aebe45af..f05233ca576 100644
--- a/lib/plugins/create/templates/aws-fsharp/build.sh
+++ b/lib/plugins/create/templates/aws-fsharp/build.sh
@@ -1,15 +1,11 @@
#!/bin/bash
-isMacOs=`uname -a | grep Darwin`
-
-#install zip
-if [ -z "$isMacOs" ]
+#install zip on debian OS, since microsoft/dotnet container doesn't have zip by default
+if [ -f /etc/debian_version ]
then
- apt-get -qq update
- apt-get -qq -y install zip
+ apt -qq update
+ apt -qq -y install zip
fi
dotnet restore
-
-#create deployment package
-dotnet lambda package --configuration release --framework netcoreapp1.0 --output-package bin/release/netcoreapp1.0/deploy-package.zip
+dotnet lambda package --configuration release --framework netcoreapp2.0 --output-package bin/release/netcoreapp2.0/deploy-package.zip
diff --git a/lib/plugins/create/templates/aws-fsharp/global.json b/lib/plugins/create/templates/aws-fsharp/global.json
deleted file mode 100644
index 8af244a4642..00000000000
--- a/lib/plugins/create/templates/aws-fsharp/global.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "sdk": {
- "version": "1.0.4"
- }
-}
diff --git a/lib/plugins/create/templates/aws-fsharp/serverless.yml b/lib/plugins/create/templates/aws-fsharp/serverless.yml
index 21dd15dd37d..e12d4231b3f 100644
--- a/lib/plugins/create/templates/aws-fsharp/serverless.yml
+++ b/lib/plugins/create/templates/aws-fsharp/serverless.yml
@@ -19,7 +19,7 @@ service: aws-fsharp # NOTE: update this with your service name
provider:
name: aws
- runtime: dotnetcore1.0
+ runtime: dotnetcore2.0
# you can overwrite defaults here
# stage: dev
@@ -47,7 +47,7 @@ provider:
# you can add packaging information here
package:
- artifact: bin/release/netcoreapp1.0/deploy-package.zip
+ artifact: bin/release/netcoreapp2.0/deploy-package.zip
# exclude:
# - exclude-me.js
# - exclude-me-dir/**
@@ -67,7 +67,7 @@ functions:
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
-# - alexaSkill
+# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
# - iot:
# sql: "SELECT * FROM 'some_topic'"
diff --git a/lib/plugins/create/templates/aws-go-dep/Gopkg.lock b/lib/plugins/create/templates/aws-go-dep/Gopkg.lock
deleted file mode 100644
index f93855f76bc..00000000000
--- a/lib/plugins/create/templates/aws-go-dep/Gopkg.lock
+++ /dev/null
@@ -1,19 +0,0 @@
-# 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
index 8fce8929ead..a844086a94a 100644
--- a/lib/plugins/create/templates/aws-go-dep/Gopkg.toml
+++ b/lib/plugins/create/templates/aws-go-dep/Gopkg.toml
@@ -22,4 +22,4 @@
[[constraint]]
name = "github.com/aws/aws-lambda-go"
- version = "^1.0.1"
+ version = "1.x"
diff --git a/lib/plugins/create/templates/aws-go-dep/serverless.yml b/lib/plugins/create/templates/aws-go-dep/serverless.yml
index 67f92c77f2e..76819414366 100644
--- a/lib/plugins/create/templates/aws-go-dep/serverless.yml
+++ b/lib/plugins/create/templates/aws-go-dep/serverless.yml
@@ -69,7 +69,7 @@ functions:
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
-# - alexaSkill
+# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
# - iot:
# sql: "SELECT * FROM 'some_topic'"
diff --git a/lib/plugins/create/templates/aws-go/serverless.yml b/lib/plugins/create/templates/aws-go/serverless.yml
index c3a1e2c95d4..47fd7378729 100644
--- a/lib/plugins/create/templates/aws-go/serverless.yml
+++ b/lib/plugins/create/templates/aws-go/serverless.yml
@@ -69,7 +69,7 @@ functions:
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
-# - alexaSkill
+# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
# - iot:
# sql: "SELECT * FROM 'some_topic'"
diff --git a/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml b/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml
index a1adaaf4d92..e7956282923 100644
--- a/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml
+++ b/lib/plugins/create/templates/aws-groovy-gradle/serverless.yml
@@ -64,7 +64,7 @@ functions:
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
-# - alexaSkill
+# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
# - iot:
# sql: "SELECT * FROM 'some_topic'"
diff --git a/lib/plugins/create/templates/aws-java-gradle/serverless.yml b/lib/plugins/create/templates/aws-java-gradle/serverless.yml
index e8811732e9e..c1079296fda 100644
--- a/lib/plugins/create/templates/aws-java-gradle/serverless.yml
+++ b/lib/plugins/create/templates/aws-java-gradle/serverless.yml
@@ -64,7 +64,7 @@ functions:
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
-# - alexaSkill
+# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
# - iot:
# sql: "SELECT * FROM 'some_topic'"
diff --git a/lib/plugins/create/templates/aws-java-maven/pom.xml b/lib/plugins/create/templates/aws-java-maven/pom.xml
index 4bafb38b4da..6c3af73ed4a 100644
--- a/lib/plugins/create/templates/aws-java-maven/pom.xml
+++ b/lib/plugins/create/templates/aws-java-maven/pom.xml
@@ -6,7 +6,7 @@
jar
dev
hello
-
+
1.8
1.8
@@ -16,13 +16,18 @@
com.amazonaws
- aws-lambda-java-core
+ aws-lambda-java-log4j2
1.1.0
- com.amazonaws
- aws-lambda-java-log4j
- 1.0.0
+ org.apache.logging.log4j
+ log4j-core
+ 2.8.2
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.8.2
com.fasterxml.jackson.core
@@ -65,8 +70,22 @@
shade
+
+
+
+
+
+
+
+
+ com.github.edwgiz
+ maven-shade-plugin.log4j2-cachefile-transformer
+ 2.8.1
+
+
diff --git a/lib/plugins/create/templates/aws-java-maven/serverless.yml b/lib/plugins/create/templates/aws-java-maven/serverless.yml
index 106bab8aaca..7f94d0131c5 100644
--- a/lib/plugins/create/templates/aws-java-maven/serverless.yml
+++ b/lib/plugins/create/templates/aws-java-maven/serverless.yml
@@ -64,7 +64,7 @@ functions:
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
-# - alexaSkill
+# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
# - iot:
# sql: "SELECT * FROM 'some_topic'"
diff --git a/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/ApiGatewayResponse.java b/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/ApiGatewayResponse.java
index cb25c5deaa1..083529604b2 100644
--- a/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/ApiGatewayResponse.java
+++ b/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/ApiGatewayResponse.java
@@ -5,7 +5,8 @@
import java.util.Collections;
import java.util.Map;
-import org.apache.log4j.Logger;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -47,7 +48,7 @@ public static Builder builder() {
public static class Builder {
- private static final Logger LOG = Logger.getLogger(ApiGatewayResponse.Builder.class);
+ private static final Logger LOG = LogManager.getLogger(ApiGatewayResponse.Builder.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
diff --git a/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java b/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java
index 6d6c670e5bd..fa7403ca05a 100644
--- a/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java
+++ b/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java
@@ -3,21 +3,19 @@
import java.util.Collections;
import java.util.Map;
-import org.apache.log4j.BasicConfigurator;
-import org.apache.log4j.Logger;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
public class Handler implements RequestHandler