Repository: serverless/serverless-openwhisk Branch: master Commit: c83d8b71fb09 Files: 130 Total size: 474.5 KB Directory structure: gitextract_ir1tuu5t/ ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── compile/ │ ├── apigw/ │ │ ├── README.md │ │ ├── index.js │ │ └── tests/ │ │ └── index.js │ ├── cloudant/ │ │ ├── README.md │ │ ├── index.js │ │ └── tests/ │ │ └── index.js │ ├── functions/ │ │ ├── README.md │ │ ├── index.js │ │ ├── runtimes/ │ │ │ ├── base.js │ │ │ ├── binary.js │ │ │ ├── docker.js │ │ │ ├── index.js │ │ │ ├── java.js │ │ │ ├── node.js │ │ │ ├── php.js │ │ │ ├── python.js │ │ │ ├── ruby.js │ │ │ ├── sequence.js │ │ │ ├── swift.js │ │ │ └── tests/ │ │ │ ├── all.js │ │ │ ├── base.js │ │ │ ├── binary.js │ │ │ ├── docker.js │ │ │ ├── index.js │ │ │ ├── java.js │ │ │ ├── node.js │ │ │ ├── php.js │ │ │ ├── python.js │ │ │ ├── ruby.js │ │ │ ├── sequence.js │ │ │ └── swift.js │ │ └── tests/ │ │ └── index.js │ ├── message_hub/ │ │ ├── README.md │ │ ├── index.js │ │ └── tests/ │ │ └── index.js │ ├── packages/ │ │ ├── README.md │ │ ├── index.js │ │ └── tests/ │ │ └── index.js │ ├── rules/ │ │ ├── README.md │ │ ├── index.js │ │ └── tests/ │ │ └── index.js │ ├── schedule/ │ │ ├── README.md │ │ ├── index.js │ │ └── tests/ │ │ └── index.js │ ├── servicebindings/ │ │ ├── README.md │ │ ├── index.js │ │ └── tests/ │ │ └── index.js │ └── triggers/ │ ├── README.md │ ├── index.js │ └── tests/ │ └── index.js ├── configCredentials/ │ ├── index.js │ └── tests/ │ └── index.js ├── deploy/ │ ├── README.md │ ├── index.js │ ├── lib/ │ │ ├── deployApiGw.js │ │ ├── deployFeeds.js │ │ ├── deployFunctions.js │ │ ├── deployPackages.js │ │ ├── deployRules.js │ │ ├── deployServiceBindings.js │ │ ├── deployTriggers.js │ │ ├── initializeResources.js │ │ └── validate.js │ └── tests/ │ ├── all.js │ ├── deployApiGw.js │ ├── deployFeeds.js │ ├── deployFunctions.js │ ├── deployPackages.js │ ├── deployRules.js │ ├── deployServiceBindings.js │ ├── deployTriggers.js │ ├── index.js │ ├── initializeResources.js │ ├── resources/ │ │ ├── swagger.json │ │ ├── swagger_default_ns.json │ │ ├── swagger_ns_paths.json │ │ └── swagger_paths.json │ └── validate.js ├── deployFunction/ │ ├── index.js │ └── tests/ │ └── index.js ├── index.js ├── info/ │ ├── index.js │ └── tests/ │ └── index.js ├── invoke/ │ ├── README.md │ ├── index.js │ └── tests/ │ └── index.js ├── invokeLocal/ │ ├── index.js │ ├── invoke.py │ └── tests/ │ ├── fixture/ │ │ └── handlerWithError.js │ └── index.js ├── logs/ │ ├── index.js │ └── tests/ │ └── index.js ├── package.json ├── provider/ │ ├── cliTokenManager.js │ ├── credentials.js │ ├── openwhiskProvider.js │ └── tests/ │ ├── cliTokenManager.js │ ├── credentials.js │ ├── index.js │ └── openwhiskProvider.js ├── remove/ │ ├── README.md │ ├── index.js │ ├── lib/ │ │ ├── removeFeeds.js │ │ ├── removeFunctions.js │ │ ├── removePackages.js │ │ ├── removeRoutes.js │ │ ├── removeRules.js │ │ ├── removeTriggers.js │ │ ├── setupResources.js │ │ ├── util.js │ │ └── validate.js │ └── tests/ │ ├── all.js │ ├── index.js │ ├── removeFeeds.js │ ├── removeFunctions.js │ ├── removePackages.js │ ├── removeRoutes.js │ ├── removeRules.js │ ├── removeTriggers.js │ └── setupResources.js ├── tests/ │ └── all.js ├── tools/ │ └── travis/ │ ├── build.sh │ └── setup.sh └── utils/ └── index.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ coverage node_modules tmp tmpdirs-serverless ================================================ FILE: .eslintrc.js ================================================ module.exports = { "extends": "airbnb", "plugins": [], "rules": { "func-names": "off", // doesn't work in node v4 :( "strict": "off", "prefer-rest-params": "off", "react/require-extension" : "off", "import/no-extraneous-dependencies" : "off" }, "env": { "mocha": true } }; ================================================ FILE: .gitignore ================================================ # Logs *.log npm-debug.log # Runtime data pids *.pid *.seed dist # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules package-lock.json # IDE stuff **/.idea .vscode/ # OS stuff .DS_Store .tmp # Serverless stuff admin.env .env tmp .coveralls.yml tmpdirs-serverless ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "11" - "10" - "8" sudo: required services: - docker before_install: - cd $TRAVIS_BUILD_DIR install: - ./tools/travis/setup.sh - cd $TRAVIS_BUILD_DIR script: - ./tools/travis/build.sh - cd $TRAVIS_BUILD_DIR - npm run report ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2018 Serverless, Inc. http://www.serverless.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ **📦 Archived - This repository is archived and preserved for reference only. No updates, issues, or pull requests will be accepted. If you have questions, please reach out to our support team.** --- # Serverless Apache OpenWhisk Plugin [![Build Status](https://travis-ci.org/serverless/serverless-openwhisk.svg?branch=master)](https://travis-ci.org/serverless/serverless-openwhisk) [![codecov](https://codecov.io/gh/serverless/serverless-openwhisk/branch/master/graph/badge.svg)](https://codecov.io/gh/serverless/serverless-openwhisk) This plugin enables support for the [Apache OpenWhisk platform](https://openwhisk.apache.org/) within the Serverless Framework. ## Getting Started ### Register account with Apache OpenWhisk Before you can deploy your service to Apache OpenWhisk, you need to have an account registered with the platform. - *Want to run the platform locally?* Please read the project's [*Quick Start*](https://github.com/openwhisk/openwhisk#quick-start) guide for deploying it locally. - *Want to use a hosted provider?* Please [sign up](https://cloud.ibm.com/registration) for a free account with [IBM Cloud](https://cloud.ibm.com/) and then follow the instructions for getting access to [IBM Cloud Functions (Apache OpenWhisk)](https://cloud.ibm.com/openwhisk). ### Set up account credentials Account credentials for OpenWhisk can be provided through a configuration file or environment variables. This plugin requires the API endpoint, namespace and authentication credentials. **Do you want to use a configuration file for storing these values?** Please [follow the instructions](https://console.ng.bluemix.net/openwhisk/cli) for setting up the OpenWhisk command-line utility. This tool stores account credentials in the `.wskprops` file in the user's home directory. The plugin automatically extracts credentials from this file at runtime. No further configuration is needed. **Do you want to use environment variables for credentials?** Use the following environment variables to be pass in account credentials. These values override anything extracted from the configuration file. - *OW_APIHOST* - Platform endpoint, e.g. `openwhisk.ng.bluemix.net` - *OW_AUTH* - Authentication key, e.g. `xxxxxx:yyyyy` - *OW_NAMESPACE* - Namespace, defaults to user-provided credentials - *OW_APIGW_ACCESS_TOKEN* - API gateway access token (optional) - *OW_IAM_NAMESPACE_API_KEY* - IBM Cloud IAM API key (optional & overrides `auth`). ### Install Serverless Framework ```shell $ npm install --global serverless ``` **_This framework plugin requires Node.js runtime version 6.0 or above._** ### Create Service From Template Using the `create` command, you can create an example service from the [following template](https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates/openwhisk-nodejs). ```shell serverless create --template openwhisk-nodejs --path my_service cd my_service npm install ``` More service examples are available in the [`serverless-examples`](https://github.com/serverless/examples) repository. **Using a self-hosted version of the platform?** Ensure you set the `ignore_certs` option in the serverless.yaml prior to deployment. ``` provider: name: openwhisk ignore_certs: true ``` ### Deploy Service The sample service from the template can be deployed without modification. ```shell serverless deploy ``` If the deployment succeeds, the following messages will be printed to the console. ```sh $ serverless deploy Serverless: Packaging service... Serverless: Compiling Functions... Serverless: Compiling API Gateway definitions... Serverless: Compiling Rules... Serverless: Compiling Triggers & Feeds... Serverless: Deploying Functions... Serverless: Deployment successful! Service Information platform: openwhisk.ng.bluemix.net namespace: _ service: my_service actions: my_service-dev-hello triggers: **no triggers deployed*** rules: **no rules deployed** endpoints: **no routes deployed** web-actions: **no web actions deployed** ``` ### Test Service Use the `invoke` command to test your newly deployed service. ```shell $ serverless invoke --function hello { "payload": "Hello, World!" } $ serverless invoke --function hello --data '{"name": "OpenWhisk"}' { "payload": "Hello, OpenWhisk!" } ``` *Add the `-v` or `--verbose` flag to show more [invocation details](https://github.com/apache/incubator-openwhisk/blob/master/docs/annotations.md#annotations-specific-to-activations), e.g. activation id and duration details.* ```shell $ serverless invoke --function hello -v => action () activation () duration: 96ms (init: 83ms, wait: 35ms) { "payload": "Hello, OpenWhisk!" } ``` ## Writing Functions - Node.js Here's an `index.js` file containing an example handler function. ```javascript function main(params) { const name = params.name || 'World'; return {payload: 'Hello, ' + name + '!'}; }; exports.main = main; ``` Modules [should return the function handler](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#packaging-an-action-as-a-nodejs-module) as a custom property on the global `exports` object. In the `serverless.yaml` file, the `handler` property is used to denote the source file and module property containing the serverless function. ```yaml functions: my_function: handler: index.main ``` ### Request Properties OpenWhisk executes the handler function for each request. This function is called with a single argument, an object [containing the request properties](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#passing-parameters-to-an-action). ```javascript function main(params) { const parameter = params.parameter_name; ... }; ``` ### Function Return Values The handler must return an object from the function call. Returning `undefined` or `null` will result in an error. If the handler is carrying out an [asynchronous task](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#creating-asynchronous-actions), it can return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). ```javascript // synchronous return function main () { return { payload: "..." } } // asychronous return function main(args) { return new Promise(function(resolve, reject) { setTimeout(function() { resolve({ done: true }); }, 2000); }) } ``` If you want to return an error message, return an object with an `error` property with the message. Promise values that are rejected will be interpreted as runtime errors. ```javascript // synchronous return function main () { return { error: "..." } } // asychronous return function main(args) { return new Promise(function(resolve, reject) { setTimeout(function() { reject("error message"); }, 2000); }) } ``` ### Using NPM Modules NPM modules must be [installed locally](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#packaging-an-action-as-a-nodejs-module) in the `node_modules` directory before deployment. This directory will be packaged up in the deployment artefact. Any dependencies included in `node_modules` will be available through `require()` in the runtime environment. OpenWhisk provides a number of popular NPM modules in the runtime environment. Using these modules doesn't require them to be included in the deployment package. See [this list](https://github.com/openwhisk/openwhisk/blob/master/docs/reference.md#javascript-runtime-environments) for full details of which modules are available. ```javascript const leftPad = require("left-pad") function pad_lines(args) { const lines = args.lines || []; return { padded: lines.map(l => leftPad(l, 30, ".")) } }; exports.handler = pad_lines; ``` ## Writing Functions - PHP Here's an `index.php` file containing an example handler function. ```php $greeting]; } ``` In the `serverless.yaml` file, the `handler` property is used to denote the source file and function name of the serverless function. ```yaml functions: my_function: handler: index.main runtime: php ``` ### Request Properties OpenWhisk executes the handler function for each request. This function is called with a single argument, an associative array [containing the request properties](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#passing-parameters-to-an-action). ```php function main(array $args) : array { $name = $args["name"] ?? "stranger"; ... } ``` ### Function Return Values The handler must return an associative array from the function call. ```php func main(args: [String:Any]) -> [String:Any] { ... return ["foo" => $bar]; } ``` If you want to return an error message, return an object with an `error` property with the message. ## Writing Functions - Python Here's an `index.py` file containing an example handler function. ```python def endpoint(params): name = params.get("name", "stranger") greeting = "Hello " + name + "!" print(greeting) return {"greeting": greeting} ``` In the `serverless.yaml` file, the `handler` property is used to denote the source file and module property containing the serverless function. ```yaml functions: my_function: handler: index.endpoint runtime: python:3 ``` ### Request Properties OpenWhisk executes the handler function for each request. This function is called with a single argument, a dictionary [containing the request properties](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#passing-parameters-to-an-action). ```python def endpoint(params): name = params.get("name", "stranger") ... ``` ### Function Return Values The handler must return a dictionary from the function call. ```python def endpoint(params): ... return {"foo": "bar"} ``` If you want to return an error message, return an object with an `error` property with the message. ## Writing Functions - Ruby Here's an `hello.rb` file containing an example handler function. ```ruby def main(args) name = args["name"] || "stranger" greeting = "Hello #{name}!" puts greeting { "greeting" => greeting } end ``` In the `serverless.yaml` file, the `handler` property is used to denote the source file and function name of the serverless function. ```yaml functions: my_function: handler: hello.main runtime: ruby ``` ### Request Properties OpenWhisk executes the handler function for each request. This function is called with a single argument, which is a hash [containing the request properties](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#passing-parameters-to-an-action). ```ruby def main(args) name = args["name"] || "stranger" ... ``` ### Function Return Values The handler must return a hash from the function call. ```ruby def main(args) ... { "greeting" => greeting } end ``` If you want to return an error message, return an `error` property string in the return hash. ## Writing Functions - Swift Here's an `index.swift` file containing an example handler function. ```swift func main(args: [String:Any]) -> [String:Any] { if let name = args["name"] as? String { return [ "greeting" : "Hello \(name)!" ] } else { return [ "greeting" : "Hello stranger!" ] } } ``` In the `serverless.yaml` file, the `handler` property is used to denote the source file and module property containing the serverless function. ```yaml functions: my_function: handler: index.main runtime: swift ``` ### Request Properties OpenWhisk executes the handler function for each request. This function is called with a single argument, a dictionary [containing the request properties](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#passing-parameters-to-an-action). ```swift func main(args: [String:Any]) -> [String:Any] { let prop = args["prop"] as? String } ``` ### Function Return Values The handler must return a dictionary from the function call. ```swift func main(args: [String:Any]) -> [String:Any] { ... return ["foo": "bar"] } ``` If you want to return an error message, return an object with an `error` property with the message. ### Codable Support Swift 4 runtimes support [Codable types](https://developer.apple.com/documentation/swift/codable) to handle the converting between JSON input parameters and response types to native Swift types. ```swift struct Employee: Codable { let id: Int? let name: String? } // codable main function func main(input: Employee, respondWith: (Employee?, Error?) -> Void) -> Void { // For simplicity, just passing same Employee instance forward respondWith(input, nil) } ``` ### Pre-Compiled Swift Binaries OpenWhisk supports creating Swift actions from a pre-compiled binary. This reduces startup time for Swift actions by removing the need for a dynamic compilation step. In the `serverless.yaml` file, the `handler` property can refer to the zip file containing a binary file produced by the build. ```yaml functions: hello: handler: action.zip ``` Compiling a single Swift file to a binary can be handled using this Docker command with the OpenWhisk Swift runtime image. `main.swift` is the file containing the swift code and `action.zip` is the zip archive produced. ``` docker run -i openwhisk/action-swift-v4.2 -compile main < main.swift > action.zip ``` Swift packages containing multiple source files with a package descriptor (`Package.swift` ) can be built using the following command. ``` zip - -r * | docker run -i openwhisk/action-swift-v4.2 -compile main > action.zip ``` ## Writing Functions - Java Here's an `src/main/java/HelloWorld.java` file containing an example handler function. ```java import com.google.gson.JsonObject; public class HelloWorld { public static JsonObject main(JsonObject args) throws Exception { final String name = args.getAsJsonPrimitive("name").getAsString(); final JsonObject response = new JsonObject(); response.addProperty("greeting", "Hello " + name + "!"); return response; } } ``` Here is a simple `pom.xml` file that will allow you to use Maven to build it. You will notice that `gson` is excluded from the uberjar. That is because OpenWhisk already provides this dependency. ```xml 4.0.0 hello hello-world 1.0 com.google.code.gson gson 2.8.2 org.apache.maven.plugins maven-shade-plugin 3.1.0 package shade true com.google.code.gson:gson ``` In the `serverless.yaml` file (see below), the `handler` property is the uberjar produced by calling `mvn clean package`, a colon, and then the fully qualified class name of the class with the main function. If you do not provide a class name after the jar, it will look for a class in the default package called `Main`. ```yaml service: my-java-service provider: name: openwhisk runtime: java functions: hello: handler: target/hello-world-1.0.jar:HelloWorld plugins: - serverless-openwhisk ``` ### Request Properties OpenWhisk executes the handler function for each request. This function is called with a single argument, a `com.google.gson.JsonObject` [containing the request properties](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#passing-parameters-to-an-action). ```java import com.google.gson.JsonObject; public class MyActionClass { public static JsonObject main(JsonObject args) throws Exception { final String name = args.getAsJsonPrimitive("name").getAsString(); ... } } ``` ### Function Return Values The handler must return an `com.google.gson.JsonObject` from the function call. ```java import com.google.gson.JsonObject; public class MyActionClass { public static JsonObject main(JsonObject args) throws Exception { ... final JsonObject response = new JsonObject(); response.addProperty("greeting", "Hello " + name + "!"); return response; } } ``` If you want to return an error message, throw an exception. ## Writing Functions - Binary OpenWhisk supports executing a compiled binary for the function handler. Using a Python wrapper, the file will be invoked within the `openwhisk/dockerskeleton` Docker container. The binary must be compiled for the correct platform architecture and only link to shared libraries installed in the `openwhisk/dockerskeleton` runtime. In the `serverless.yaml` file, the `handler` property is used to denote the binary file to upload. ```yaml functions: my_function: handler: bin_file runtime: binary ``` ### Request Properties OpenWhisk executes the binary file for each request. Event parameters are streamed to `stdio` as a JSON object string. ### Function Return Values The handler must write a JSON object string with the response parameters to `stdout` before exiting. If you want to return an error message, return an object with an `error` property with the message. ## Custom Runtime Images OpenWhisk actions can use [custom Docker images as the runtime environment](https://medium.com/openwhisk/large-applications-on-openwhisk-bcf15bff94ec). This allows extra packages, libraries or tools to be pre-installed in the runtime environment. Using a custom runtime image, with extra libraries and dependencies built-in, is useful for overcoming the [maximum deployment size](https://github.com/apache/incubator-openwhisk/blob/master/docs/reference.md#system-limits) on actions. *Images must implement the [API used by the platform](http://jamesthom.as/blog/2017/01/16/openwhisk-docker-actions/) to interact with runtime environments. Images must also be available on Docker Hub. OpenWhisk does not support private Docker registries.* OpenWhisk publishes the [existing runtime images on Docker Hub](https://hub.docker.com/r/openwhisk/). Using these images in the `FROM` directive in the `Dockerfile` is an easy way to [create new images](https://docs.docker.com/engine/reference/commandline/build/) compatible with the platform. In the `serverless.yaml` file, the `image` property is used to denote the custom runtime image. ```yaml functions: my_function: handler: source.js runtime: nodejs image: dockerhub_user/image_name ``` *Node.js, Swift, Python and Binary runtimes support using a custom image property.* ## Writing Functions - Docker OpenWhisk supports creating actions from public images on Docker Hub without handler files. These images are expected to support the platform API used to instantiate and invoke serverless functions. All necessary files for execution must be provided within the image. Local source files will not be uploaded to the runtime environment. In the `serverless.yaml` file, the `handler` property is used to denote the image label. ```yaml functions: my_function: handler: repo/image_name runtime: docker ``` ## Working With 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`. ***Rules and triggers can not be created within packages.*** ### Implicit Packages Actions 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: name: optionalCustomName parameters: hello: world ``` *Explicit packages support the following properties: `name`, `parameters`, `annotations`, `services` and `shared`.* ### Binding Packages OpenWhisk also supports "binding" external packages into your workspace. Bound packages can have default parameters set for shared actions. For example, binding the `/whisk.system/cloudant` package into a new package allows you to set default values for the `username`, `password` and `dbname` properties. Actions from this package can then be invoked with having to pass these parameters in. Define packages explicitly with a `binding` parameter to use this behaviour. ```yaml resources: packages: mySamples: binding: /whisk.system/cloudant parameters: username: bernie password: sanders dbname: vermont ``` For more details on package binding, please see the documentation [here](https://github.com/apache/incubator-openwhisk/blob/master/docs/packages.md#creating-and-using-package-bindings). ## 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 ``` ## Runtime Configuration Properties The following OpenWhisk configuration properties are supported for functions defined in the `serverless.yaml` file. ```yaml functions: my_function: handler: file_name.handler_func name: "custom_function_name" runtime: 'runtime_label' // defaults to nodejs:default namespace: "..." // defaults to user-provided credentials memory: 256 // 128 to 512 (MB). timeout: 60 // 0.1 to 600 (seconds) concurrency: 1 // 1 to 500, default is 1 parameters: foo: bar // default parameters annotations: foo: bar // action annotations bind: - service: name: cloud-object-storage instance: my-cos-storage ``` ## Writing Sequences OpenWhisk supports a special type of serverless function called [sequences](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md#creating-action-sequences). These functions are defined from a list of other serverless functions. Upon invocation, the platform executes each function in series. Request parameters are passed into the first function in the list. Each subsequent function call is passed the output from the previous step as input parameters. The last function's return value is returned as the response result. Here's an example of the configuration to define a sequence function, composed of three other functions. ```yaml functions: my_function: sequence: - parse_input - do_some_algorithm - construct_output ``` *Sequence functions do not have a handler file defined. If you want to refer to functions not defined in the serverless project, use the fully qualified identifier e.g. /namespace/package/action_name* ## Connecting HTTP Endpoints Functions can be bound to public URL endpoints using the [API Gateway service](https://github.com/openwhisk/openwhisk/blob/master/docs/apigateway.md). HTTP requests to configured endpoints will invoke functions on-demand. Requests parameters are passed as function arguments. Function return values are serialised as the JSON response body. HTTP endpoints for functions can be configured through the `serverless.yaml` file. ```yaml functions: my_function: handler: index.main events: - http: GET /api/greeting ``` HTTP event configuration also supports using explicit parameters. - `method` - HTTP method (mandatory). - `path` - URI path for API gateway (mandatory). - `resp` - controls [web action content type](https://github.com/apache/incubator-openwhisk/blob/master/docs/webactions.md#additional-features), values include: `json`, `html`, `http`, `svg`or `text` (optional, defaults to `json`). ```yaml functions: my_function: handler: index.main events: - http: method: GET path: /api/greeting resp: http ``` API Gateway hosts serving the API endpoints will be shown during deployment. ```shell $ serverless deploy ... endpoints: GET https://xxx-gws.api-gw.mybluemix.net/service_name/api/greeting --> service_name-dev-my_function ``` Calling the configured API endpoints will execute the deployed functions. ````shell $ http get https://xxx-gws.api-gw.mybluemix.net/api/greeting?user="James Thomas" HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Date: Mon, 19 Dec 2016 15:47:53 GMT { "message": "Hello James Thomas!" } ```` 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. ### URL Path Parameters The API Gateway service [supports path parameters]() 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.* ### CORS Support API Gateway endpoints automatically include CORS headers for all endpoints under the service base path. This property can be disabled by manually configuring the `resources.apigw.cors` property. ```yaml resources: apigw: cors: false ``` ### Application Authentication API endpoints can be protected by API keys with a secret or API keys alone. Setting the HTTP headers used to pass keys and secrets automatically enables API Gateway authentication. This parameter configures the HTTP header containing the API key. Without the additional secret header, authentication uses an API key alone. ```yaml resources: apigw: auth: key: API-Key-Header ``` Adding the secret header parameter enables authentication using keys with secrets. ```yaml resources: apigw: auth: key: API-Key-Header secret: API-Key-Secret-Header ``` *See the API Gateway [configuration panel](https://cloud.ibm.com/openwhisk/apimanagement) to manage API keys and secrets after authentication is enabled.* ### Application Authentication with OAuth API endpoints can also be protected by an external OAuth providers. OAuth tokens must be included as the Authorization header of each API request. Token will be validated with the specified token provider. If the token is invalid, requests are rejected with response code 401. The following OAuth providers are supported: *[IBM Cloud App ID](https://cloud.ibm.com/catalog/services/app-id), Google, Facebook and Github.* ```yaml resources: apigw: oauth: provider: app-id || google || facebook || github ``` If the `app-id` provider is selected, the tenant identifier must be provided as an additional configuration token. This can be retrieved from the `tenantId` property of provisioned service credentials for the instance ```yaml resources: apigw: oauth: provider: app-id tenant: uuid ``` *Application Authentication with keys (and secrets) and OAuth support are mutually exclusive configuration options.* ### Rate Limiting API Gateways endpoints support rate limiting to reject excess traffic. When rate limiting is enabled, API calls falling outside of the limit will be rejected and response code 429 will be returned. **Rate limiting is on a per-key basis and application authentication (without oauth) must be enabled.** The leaky bucket algorithm is used to prevent sudden bursts of invocations of APIs. If the limit is set as 10 calls per minute, users will be restricted to 1 call every 6 seconds (60/10 = 6). ```yaml resources: apigw: rate_limit: rate: 100 unit: minute || second || hour || day ``` - `rate`: number of API calls per unit of time. - `unit`: unit of time (*minute, second, hour, day*) used to threshold API calls with rate. ### Base Path All API Gateway endpoints defined as HTTP events in the `serverless.yml` are deployed under the default base path (`/`). This basepath can be configured explicitly using the following parameter. ```yaml resources: apigw: basepath: /api ``` ### API Name The service name is used as the API identifier in the API Gateway swagger files. This can be configured explicitly using the following parameter. ```yaml resources: apigw: name: my-api-name ``` ## Exporting Web Actions Functions can be turned into "*web actions*" which return HTTP content without use of an API Gateway. This feature is enabled by setting an annotation (`web-export`) in the configuration file. ```yaml functions: my_function: handler: index.main annotations: web-export: true ``` Functions with this annotation can be invoked through a URL template with the following parameters. ``` https://{APIHOST}/api/v1/web/{USER_NAMESPACE}/{PACKAGE}/{ACTION_NAME}.{TYPE} ``` - *APIHOST* - platform endpoint e.g. *openwhisk.ng.bluemix.net.* - *USER_NAMESPACE* - this must be an explicit namespace and cannot use the default namespace (_). - *PACKAGE* - action package or `default`. - *ACTION_NAME* - default form `${servicename}-${space}-${name}`. - *TYPE* - `.json`, `.html`, `.text` or `.http`. Return values from the function are used to construct the HTTP response. The following parameters are supported. 1. `headers`: a JSON object where the keys are header-names and the values are string values for those headers (default is no headers). 2. `code`: a valid HTTP status code (default is 200 OK). 3. `body`: a string which is either plain text or a base64 encoded string (for binary data). Here is an example of returning HTML content: ``` function main(args) { var msg = "you didn't tell me who you are." if (args.name) { msg = `hello ${args.name}!` } return {body: `

${msg}

`} } ``` Here is an example of returning binary data: ``` function main() { let png = return { headers: { "Content-Type": "image/png" }, body: png }; } ``` Functions can access request parameters using the following environment variables. 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. **Full details on this feature are available in this [here](https://github.com/apache/incubator-openwhisk/blob/master/docs/webactions.md).** ## Scheduled Invocations Functions can be set up to fire automatically using the [alarm package](https://github.com/openwhisk/openwhisk/blob/master/docs/catalog.md#using-the-alarm-package). This allows you to invoke functions with preset parameters at specific times (*12:00 each day*) or according to a schedule (*every ten minutes*). Scheduled invocation for functions can be configured through the `serverless.yaml` file. The `schedule` event configuration is controlled by a string, based on the UNIX crontab syntax, in the format `cron(X X X X X)`. This can either be passed in as a native string or through the `rate` parameter. ```yaml functions: my_function: handler: index.main events: - schedule: cron(* * * * *) // fires each minute. ``` This above example generates a new trigger (`${service}_crawl_schedule_trigger`) and rule (`${service}_crawl_schedule_rule`) during deployment. Other `schedule` event parameters can be manually configured, e.g trigger or rule names. ```yaml functions: aggregate: handler: statistics.handler events: - schedule: rate: cron(0 * * * *) // call once an hour trigger: triggerName rule: ruleName max: 10000 // max invocations, default: 1000, max: 10000 params: // event params for invocation hello: world ``` ## IBM Message Hub Events IBM Bluemix provides an "Apache Kafka"-as-a-Service called IBM Message Hub. Functions can be connected to fire when messages arrive on Kafka topics. IBM Message Hub instances can be provisioned through the IBM Bluemix platform. OpenWhisk on Bluemix will export Message Hub service credentials bound to a package with the following name: ``` /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 ``` Rather than having to manually define all the properties needed by the Message Hub trigger feed, you can reference a package to use instead. Credentials from the referenced package will be used when executing the trigger feed. Developers only need to add the topic to listen to for each trigger. ```yaml # serverless.yaml functions: index: handler: users.main events: - message_hub: package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 topic: my_kafka_topic ``` The plugin will create a trigger called `${serviceName}_${fnName}_messagehub_${topic}` and a rule called `${serviceName}_${fnName}_messagehub_${topic}_rule` to bind the function to the message hub events. The trigger and rule names created can be set explicitly using the `trigger` and`rule` parameters. Other functions can bind to the same trigger using the inline `trigger` event referencing this trigger name. ```yaml # serverless.yaml functions: index: handler: users.main events: - message_hub: package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 topic: my_kafka_topic trigger: log_events rule: connect_index_to_kafka another: handler: users.another events: - trigger: log_events ``` ### Using Manual Parameters Parameters for the Message Hub event source can be defined explicitly, rather than using pulling credentials from a package. ```yaml # serverless.yaml functions: index: handler: users.main events: - message_hub: topic: my_kafka_topic brokers: afka01-prod01.messagehub.services.us-south.bluemix.net:9093 user: USERNAME password: PASSWORD admin_url: https://kafka-admin-prod01.messagehub.services.us-south.bluemix.net:443 json: true binary_key: true binary_value: true ``` `topic`, `brokers`, `user`, `password` and `admin_url` are mandatory parameters. ## Cloudant DB Events IBM Cloudant provides a hosted NoSQL database, based upon CouchDB, running on IBM Bluemix. Functions can be connected to events fired when the database is updated. These events use the [CouchDB changes feed](http://guide.couchdb.org/draft/notifications.html) to follow database modifications. IBM Cloudant instances can be provisioned through the IBM Bluemix platform. OpenWhisk on Bluemix will export Cloudant service credentials bound to a package with the following name: ``` /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 ``` Rather than having to manually define all the properties needed by the [Cloudant trigger feed](https://github.com/openwhisk/openwhisk-package-cloudant#using-the-cloudant-package), you can reference a package to use instead. Credentials from the referenced package will be used when executing the trigger feed. Developers only need to add the database name to follow for modifications. ```yaml # serverless.yaml functions: index: handler: users.main events: - cloudant: package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 db: my_db_name ``` The plugin will create a trigger called `${serviceName}_${fnName}_cloudant_${topic}` and a rule called `${serviceName}_${fnName}_cloudant_${topic}_rule` to bind the function to the Cloudant update events. The trigger and rule names created can be set explicitly using the `trigger` and`rule` parameters. Other functions can bind to the same trigger using the inline `trigger` event referencing this trigger name. ### Using Manual Parameters Parameters for the Cloudant event source can be defined explicitly, rather than using pulling credentials from a package. ```yaml # serverless.yaml functions: index: handler: users.main events: - cloudant: // basic auth example host: xxx-yyy-zzz-bluemix.cloudant.com username: USERNAME password: PASSWORD db: db_name - cloudant: // iam auth example host: xxx-yyy-zzz-bluemix.cloudant.com iam_api_key: IAM_API_KEY db: db_name ``` `username` and `password` or `iam_api_key` parameters can be used for authentication. ### Adding Optional Parameters The following optional feed parameters are also supported: * `max` - Maximum number of triggers to fire. Defaults to infinite. * `filter` - Filter function defined on a design document. * `query` - Optional query parameters for the filter function. ```yaml # serverless.yaml functions: index: handler: users.main events: - cloudant: ... max: 10000 query: status: new filter: mailbox/by_status ``` ## Custom Event Triggers Functions are connected to event sources in OpenWhisk [using triggers and rules](https://github.com/openwhisk/openwhisk/blob/master/docs/triggers_rules.md). Triggers create a named event stream within the system. Triggers can be fired manually or connected to external data sources, like databases or message queues. Rules set up a binding between triggers and serverless functions. With an active rule, each time a trigger is fired, the function will be executed with the trigger payload. Event binding for functions can be configured through the `serverless.yaml` file. ```yaml functions: my_function: handler: index.main events: - trigger: my_trigger ``` This configuration will create a trigger called `servicename-my_trigger` with an active rule binding `my_function` to this event stream. ### Customising Rules Rule names default to the following format `servicename-trigger-to-action`. These names be explicitly set through configuration. ```yaml functions: my_function: handler: index.main events: - trigger: name: "my_trigger" rule: "rule_name" ``` ### Customing Triggers Triggers can be defined as separate resources in the `serverless.yaml` file. This allows you to set up trigger properties like default parameters. ```yaml functions: my_function: handler: index.main events: - trigger: my_trigger resources: triggers: my_trigger: parameters: hello: world ``` ### Trigger Feeds Triggers can be bound to external event sources using the `feed` property. OpenWhisk [provides a catalogue](https://github.com/openwhisk/openwhisk/blob/master/docs/catalog.md) of third-party event sources bundled as [packages](https://github.com/openwhisk/openwhisk/blob/master/docs/packages.md#creating-and-using-trigger-feeds). This example demonstrates setting up a trigger which uses the `/whisk.system/alarms/alarm` feed. The `alarm` feed will fire a trigger according to a user-supplied cron schedule. ```yaml resources: triggers: alarm_trigger: parameters: hello: world feed: /whisk.system/alarms/alarm feed_parameters: cron: '*/8 * * * * *' ``` ## Commands The following serverless commands are currently implemented for the OpenWhisk provider. - `deploy` - [Deploy functions, triggers and rules for service](https://serverless.com/framework/docs/providers/openwhisk/cli-reference/deploy/). - `invoke`- [Invoke deployed serverless function and show result](https://serverless.com/framework/docs/providers/openwhisk/cli-reference/invoke/). - `invokeLocal`- [Invoke serverless functions locally and show result](https://serverless.com/framework/docs/providers/openwhisk/cli-reference/invoke#invoke-local). - `remove` - [Remove functions, triggers and rules for service](https://serverless.com/framework/docs/providers/openwhisk/cli-reference/remove/). - `logs` - [Display activation logs for deployed function](https://serverless.com/framework/docs/providers/openwhisk/cli-reference/logs/). - `info` - [Display details on deployed functions, triggers and rules](https://serverless.com/framework/docs/providers/openwhisk/cli-reference/info/). ================================================ FILE: compile/apigw/README.md ================================================ # Compile API Gateway Endpoints This plugins compiles the HTTP events bound to functions in `serverless.yaml` to corresponding [OpenWhisk API Gateway endpoint](https://github.com/openwhisk/openwhisk/blob/master/docs/apigateway.md) definitions. ## How it works `Compile HTTP` hooks into the [`package:compileEvents`](/lib/plugins/deploy) lifecycle. It loops over all functions which are defined in `serverless.yaml` looking for the defined events. For each `http` event defined for the function, the corresponding API gateway endpoint definition will be created. ## Examples ```yaml # serverless.yaml functions: index: handler: users.handler events: - http: GET /api/greeting ``` This definition will create a new endpoint, which binds the configured Action (index) to the URL path (/api/greeting) and HTTP method (GET). HTTP operation and path parameters can also be passed object properties on the event object. ```yaml # serverless.yaml functions: index: handler: users.handler events: - http: method: GET basepath: /mypath path: /api/greeting resp: json ``` During deployment the endpoint configuration file will be uploaded to OpenWhisk. Each user has a unique hostname which provides access to the configured API endpoints. Invoking the endpoints on the gateway host will execute functions on-demand. API Gateway hosts serving the API endpoints will be shown during deployment. ```shell $ serverless deploy ... endpoints: GET https://xxx-gws.api-gw.mybluemix.net/api/greeting --> index ``` Calling the configured API endpoints will execute the deployed functions. ````shell $ http get https://xxx-gws.api-gw.mybluemix.net/api/greeting?user="James Thomas" HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Date: Mon, 19 Dec 2016 15:47:53 GMT { "message": "Hello James Thomas!" } ```` ================================================ FILE: compile/apigw/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const crypto = require('crypto'); const { formatApiHost } = require('../../utils'); class OpenWhiskCompileHttpEvents { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('openwhisk'); this.hooks = { 'before:package:compileEvents': this.setup.bind(this), 'before:package:compileFunctions': this.addWebAnnotations.bind(this), 'package:compileEvents': this.compileHttpEvents.bind(this) }; } setup() { // This object will be used to store the endpoint definitions, passed directly to // the OpenWhisk SDK during the deploy process. this.serverless.service.apigw = {}; // Actions and Triggers referenced by Rules must used fully qualified identifiers (including namespace). if (!this.serverless.service.provider.namespace) { return this.provider.props().then(props => { this.serverless.service.provider.namespace = props.namespace; this.serverless.service.provider.host = props.apihost; }); } } generateAuthString() { return crypto.randomBytes(64).toString('hex') } // HTTP events need Web Actions enabled for those functions. Add // annotation 'web-export' if it is not already present. addWebAnnotations() { const names = Object.keys(this.serverless.service.functions) names.forEach(fnName => { const f = this.serverless.service.functions[fnName] const httpEvents = (f.events || []).filter(e => e.http) if (httpEvents.length) { if (!f.annotations) f.annotations = {} f.annotations['web-export'] = true if (!f.annotations.hasOwnProperty('require-whisk-auth')) { f.annotations['require-whisk-auth'] = this.generateAuthString() } } }) return BbPromise.resolve(); } calculateFunctionName(functionName, functionObject) { return functionObject.name || `${this.serverless.service.service}_${functionName}`; } calculateFunctionNameSpace(functionObject) { return functionObject.namespace || this.serverless.service.provider.namespace || '_'; } retrieveAuthKey(functionObject) { const annotations = functionObject.annotations || {} return annotations['require-whisk-auth'] } // // This method takes the rule definitions, parsed from the user's YAML file, // and turns it into the OpenWhisk Rule resource object. // // These resource objects are passed to the OpenWhisk SDK to create the associated Rules // during the deployment process. // // Parameter values will be parsed from the user's YAML definition, either as a value from // the rule definition or the service provider defaults. compileHttpEvent(funcName, funcObj, http) { const options = this.parseHttpEvent(http); options.namespace = this.calculateFunctionNameSpace(funcName, funcObj); const name = this.calculateFunctionName(funcName, funcObj).split('/'); options.action = name.pop(); options.pkge = name.pop() || "default"; const secure_key = this.retrieveAuthKey(funcObj) if (secure_key) { options.secure_key = secure_key; } return options; } parseHttpEvent(httpEvent) { if (httpEvent.path && httpEvent.method) { return { relpath: httpEvent.path, operation: httpEvent.method, responsetype: httpEvent.resp || 'json' }; } else if (typeof httpEvent === 'string') { const method_and_path = httpEvent.trim().split(' '); if (method_and_path.length !== 2) { throw new this.serverless.classes.Error( `Incorrect HTTP event parameter value (${httpEvent}), must be string in form: HTTP_METHOD API_PATH e.g. GET /api/foo`); } return { operation: method_and_path[0], relpath: method_and_path[1], responsetype: httpEvent.resp || 'json' } } throw new this.serverless.classes.Error( `Incorrect HTTP event parameter value (${httpEvent}), must be string ("GET /api/foo") or object ({method: "GET", path: "/api/foo"})`); } addAuthToSwagger(swagger, auth) { if (!auth.key && auth.secret) { throw new this.serverless.classes.Error( "Missing mandatory resources.apigw.auth.key parameter. Must be defined to enable authentication." ) } swagger.security = [{ client_id: [] }] const client_id = { in: "header", name: auth.key, type: "apiKey", "x-key-type": "clientId" } swagger.securityDefinitions = { client_id } if (auth.secret) { swagger.security[0].client_secret = [] swagger.securityDefinitions.client_secret = { in: "header", name: auth.secret, type: "apiKey", "x-key-type": "clientSecret" } } } addOAuthToSwagger(swagger, oauth) { if (!oauth.provider) return const providers_urls = { google: "https://www.googleapis.com/oauth2/v3/tokeninfo", facebook: "https://graph.facebook.com/debug_token", github: "https://api.github.com/user", } if (oauth.provider !== 'app-id' && oauth.provider !== 'google' && oauth.provider !== 'facebook' && oauth.provider !== 'github') { throw new this.serverless.classes.Error( `OAuth defined with invalid provider (${oauth.provider}), must be: app-id, google, facebook, github.` ) } const security = {} security[oauth.provider] = [] swagger.security = [ security ] const definition = { flow: "application", tokenUrl: "", type: "oauth2", "x-provider": { name: oauth.provider }, "x-tokenintrospect": { url: null } } if (oauth.provider === 'app-id') { if (!oauth.tenant) { throw new this.serverless.classes.Error( `OAuth provider app-id defined without tenant parameter` ) } definition['x-provider'].params = { tenantId: oauth.tenant } } else { definition['x-tokenintrospect'].url = providers_urls[oauth.provider] } const securityDefinitions = {} securityDefinitions[oauth.provider] = definition swagger.securityDefinitions = securityDefinitions } addRateLimitToSwagger(swagger, rate_limit) { const rate = rate_limit.rate const unit = rate_limit.unit if (!rate) { throw new this.serverless.classes.Error( "Missing rate limit parameter: rate." ) } if (!unit) { throw new this.serverless.classes.Error( "Missing rate limit parameter: unit." ) } if (unit !== "minute" && unit !== "second" && unit !== "hour" && unit !== "day") { throw new this.serverless.classes.Error( "Invalid rate limit parameter: unit." ) } swagger["x-ibm-rate-limit"] = [{ rate: rate_limit.rate, unit: rate_limit.unit, units: 1 }] } generateSwagger(service, host, options, httpEvents) { const paths = httpEvents.reduce((paths, httpEvent) => { const path = paths[httpEvent.relpath] || {} const operation = httpEvent.operation.toLowerCase() path[operation] = this.compileSwaggerPath(httpEvent, host) paths[httpEvent.relpath] = path return paths }, {}) const cases = httpEvents.map(httpEvent => this.compileSwaggerCaseSwitch(httpEvent, host)) const execute_body = { "operation-switch": { case: cases } } const enabled = options.hasOwnProperty('cors') ? options.cors : true const x_ibm_configuration = { cors: { enabled }, assembly: { execute: [ execute_body ] } } const swagger = { swagger: "2.0", basePath: options.basepath || "/", info: { title: options.name || service, version: "1.0" }, paths, "x-ibm-configuration": x_ibm_configuration } if (options.auth) { this.addAuthToSwagger(swagger, options.auth) } if (options.oauth) { this.addOAuthToSwagger(swagger, options.oauth) } if (options.rate_limit) { this.addRateLimitToSwagger(swagger, options.rate_limit) } return swagger } compileSwaggerPath(httpEvent, host) { const operationId = this.operationId(httpEvent) const pathParameters = this.parsePathParameters(httpEvent.relpath) const responses = { "200": { description: "A successful invocation response" } } const webaction_url = this.webActionUrl(httpEvent, host) const x_ow = { action: httpEvent.action, namespace: httpEvent.namespace, package: httpEvent.pkge, url: webaction_url } const swaggerPath = { operationId, responses, "x-openwhisk": x_ow } if (pathParameters.length) { swaggerPath.parameters = pathParameters.map(this.createPathParameter) } return swaggerPath } parsePathParameters (path) { const regex = /{([^}]+)\}/g const findAllParams = p => { const ids = [] let id = regex.exec(p) while (id) { ids.push(id[1]) id = regex.exec(p) } return ids } return path.split('/') .map(findAllParams) .reduce((sum, el) => sum.concat(el), []) } createPathParameter (name) { return { name: name, in: 'path', description: `Default description for '${name}'`, required: true, type: 'string' } } compileSwaggerCaseSwitch(httpEvent, host) { const pathParameters = this.parsePathParameters(httpEvent.relpath) const webaction_url = this.webActionUrl(httpEvent, host, !!pathParameters.length) const operationId = this.operationId(httpEvent) const header = { set: "message.headers.X-Require-Whisk-Auth", value: httpEvent.secure_key } const execute = [ { "set-variable": { actions: [ header ] } }, { invoke: { "target-url": webaction_url, verb: "keep" } } ] const operations = [ operationId ] const swaggerCaseSwitch = { execute, operations } return swaggerCaseSwitch } webActionUrl(httpEvent, host, has_path_params) { const url = `${formatApiHost(host)}/api/v1/web/${httpEvent.namespace}/${httpEvent.pkge}/${httpEvent.action}.${httpEvent.responsetype}${has_path_params ? '$(request.path)': ''}` return url } operationId(httpEvent) { return `${httpEvent.operation}-${httpEvent.relpath}`.toLowerCase() } compileFunctionHttpEvents(functionName, functionObject) { if (!functionObject.events) return [] const events = functionObject.events .filter(e => e.http) .map(e => this.compileHttpEvent(functionName, functionObject, e.http)) if (events.length && this.options.verbose) { this.serverless.cli.log(`Compiled API Gateway definition (${functionName}): ${JSON.stringify(events)}`); } return events } compileHttpEvents () { this.serverless.cli.log('Compiling API Gateway definitions...'); const allFunctions = this.serverless.service.getAllFunctions() const httpEvents = allFunctions.map( functionName => this.compileFunctionHttpEvents(functionName, this.serverless.service.getFunction(functionName)) ).reduce((a, b) => a.concat(b), []) if (httpEvents.length) { const service = this.serverless.service.service const host = this.serverless.service.provider.host const resources = this.serverless.service.resources || {} const options = resources.apigw || {} this.serverless.service.apigw.swagger = this.generateSwagger(service, host, options, httpEvents) } return BbPromise.resolve(); } } module.exports = OpenWhiskCompileHttpEvents; ================================================ FILE: compile/apigw/tests/index.js ================================================ 'use strict'; const crypto = require('crypto'); const BbPromise = require('bluebird'); const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const OpenWhiskCompileHttpEvents = require('../index'); describe('OpenWhiskCompileHttpEvents', () => { let serverless; let sandbox; let openwhiskCompileHttpEvents; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskCompileHttpEvents = new OpenWhiskCompileHttpEvents(serverless, options); serverless.service.service = 'serviceName'; serverless.service.provider = { namespace: 'testing', apihost: '', auth: '', }; serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskCompileHttpEvents.setup(); }); afterEach(() => { sandbox.restore(); }); describe('#addWebAnnotations()', () => { it('should add annotations when http event present', () => { openwhiskCompileHttpEvents.serverless.service.functions = { a: { events: [ { http: true } ], annotations: {} }, b: { events: [ { http: true } ], annotations: { foo: 'bar' } }, c: { events: [ { http: true } ], annotations: { 'web-export': false } }, d: { events: [ { http: true } ] } } const auth_string = crypto.randomBytes(64).toString('hex'); sandbox.stub(openwhiskCompileHttpEvents, 'generateAuthString', () => auth_string); return openwhiskCompileHttpEvents.addWebAnnotations().then(() => { expect(openwhiskCompileHttpEvents.serverless.service.functions.a.annotations).to.deep.equal({ 'web-export': true, 'require-whisk-auth': auth_string }) expect(openwhiskCompileHttpEvents.serverless.service.functions.b.annotations).to.deep.equal({ foo: 'bar', 'web-export': true, 'require-whisk-auth': auth_string }) expect(openwhiskCompileHttpEvents.serverless.service.functions.c.annotations).to.deep.equal({ 'web-export': true, 'require-whisk-auth': auth_string }) expect(openwhiskCompileHttpEvents.serverless.service.functions.d.annotations).to.deep.equal({ 'web-export': true, 'require-whisk-auth': auth_string }) }) }); it('should not add auth annotation when annotation already present', () => { openwhiskCompileHttpEvents.serverless.service.functions = { a: { events: [ { http: true } ], annotations: { 'require-whisk-auth': false } }, b: { events: [ { http: true } ], annotations: { 'require-whisk-auth': true } }, c: { events: [ { http: true } ], annotations: { 'require-whisk-auth': 'some string' } } } return openwhiskCompileHttpEvents.addWebAnnotations().then(() => { expect(openwhiskCompileHttpEvents.serverless.service.functions.a.annotations).to.deep.equal({ 'web-export': true, 'require-whisk-auth': false }) expect(openwhiskCompileHttpEvents.serverless.service.functions.b.annotations).to.deep.equal({ 'web-export': true, 'require-whisk-auth': true }) expect(openwhiskCompileHttpEvents.serverless.service.functions.c.annotations).to.deep.equal({ 'web-export': true, 'require-whisk-auth': 'some string' }) }) }); it('should ignore annotations when http event not present', () => { openwhiskCompileHttpEvents.serverless.service.functions = { a: { }, b: { events: [] }, c: { events: [], annotations: { hello: 'world', 'web-export': true } } } return openwhiskCompileHttpEvents.addWebAnnotations().then(() => { expect(openwhiskCompileHttpEvents.serverless.service.functions.a.annotations).to.be.equal(undefined) expect(openwhiskCompileHttpEvents.serverless.service.functions.b.annotations).to.be.equal(undefined) expect(openwhiskCompileHttpEvents.serverless.service.functions.c.annotations).to.deep.equal({ hello: 'world', 'web-export': true }) }) }); }) describe('#compileHttpEvents()', () => { it('should return empty swagger if functions has no http events', () => expect(openwhiskCompileHttpEvents.compileHttpEvents().then(() => { expect(openwhiskCompileHttpEvents.serverless.service.apigw).to.deep.equal({}); })).to.eventually.be.fulfilled ); it('should call compileFunctionEvents for each function with events', () => { const stub = sinon.stub(openwhiskCompileHttpEvents, 'compileFunctionHttpEvents').returns([{foo: 'bar'}]); sandbox.stub(openwhiskCompileHttpEvents.serverless.service, 'getAllFunctions', () => ["first", "second", "third"]); sandbox.stub(openwhiskCompileHttpEvents, 'generateSwagger', () => ({"swagger": {}})); const handler = name => ({events: {}}) openwhiskCompileHttpEvents.serverless.service.getFunction = handler; return expect(openwhiskCompileHttpEvents.compileHttpEvents().then(() => { expect(openwhiskCompileHttpEvents.serverless.service.apigw.swagger).to.deep.equal( {swagger: {}} ); expect(stub.calledThrice).to.be.equal(true); })).to.eventually.be.fulfilled; }); }); describe('#compileFunctionHttpEvents()', () => { it('should not call compileHttpEvents when events parameter is missing', () => { const stub = sinon.stub(openwhiskCompileHttpEvents, 'compileHttpEvent') const events = openwhiskCompileHttpEvents.compileFunctionHttpEvents('name', {}) expect(events).to.deep.equal([]); expect(stub.called).to.be.equal(false); }) it('should not call compileHttpEvents when events list contains no events', () => { const stub = sinon.stub(openwhiskCompileHttpEvents, 'compileHttpEvent') const events = openwhiskCompileHttpEvents.compileFunctionHttpEvents('name', { events: [{"trigger": {}}] }) expect(events).to.deep.equal([]); expect(stub.called).to.be.equal(false); }) it('should call compileHttpEvents when events list contains triggers', () => { const stub = sinon.stub(openwhiskCompileHttpEvents, 'compileHttpEvent').returns({}) const events = openwhiskCompileHttpEvents.compileFunctionHttpEvents('name', { events: [ {"http": true}, {"http": true}, {"http": true} ] }) expect(events).to.deep.equal([{}, {}, {}]); expect(stub.calledThrice).to.be.equal(true); }) it('should log event when verbose flag is used', () => { openwhiskCompileHttpEvents.options.verbose = true const log = sandbox.stub(openwhiskCompileHttpEvents.serverless.cli, 'log') const clog = sandbox.stub(openwhiskCompileHttpEvents.serverless.cli, 'consoleLog') const stub = sinon.stub(openwhiskCompileHttpEvents, 'compileHttpEvent').returns({ foo: 'bar' }) openwhiskCompileHttpEvents.compileFunctionHttpEvents('name', { events: [ {"http": true}, {"http": true}, {"http": true} ] }) expect(log.calledOnce).to.be.equal(true); const result = JSON.stringify([{foo: "bar"}, {foo: "bar"}, {foo: "bar"}]) expect(log.args[0][0]).to.be.equal(`Compiled API Gateway definition (name): ${result}`); }) }); describe('#compileHttpEvent()', () => { it('should define http events from string property', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const http = "GET /api/foo/bar" const result = openwhiskCompileHttpEvents.compileHttpEvent('action-name', {}, http); return expect(result).to.deep.equal({ relpath: '/api/foo/bar', operation: 'GET', pkge: 'default', namespace: 'sample_ns', action: 'my-service_action-name', responsetype: 'json' }); }); it('should define http events from string property with explicit package', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const http = "GET /api/foo/bar" const fnObj = { name: 'somePackage/actionName' } const result = openwhiskCompileHttpEvents.compileHttpEvent('action-name', fnObj, http); return expect(result).to.deep.equal({ relpath: '/api/foo/bar', operation: 'GET', pkge: 'somePackage', namespace: 'sample_ns', action: 'actionName', responsetype: 'json' }); }); it('should define http events from object property', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const http = {path: "/api/foo/bar", method: "GET"} const result = openwhiskCompileHttpEvents.compileHttpEvent('action-name', {}, http); return expect(result).to.deep.equal({relpath: '/api/foo/bar', operation: 'GET', action: 'my-service_action-name', namespace: 'sample_ns', pkge: 'default', responsetype: 'json'}); }); it('should add secure auth key if present', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const http = {path: "/api/foo/bar", method: "GET"} const result = openwhiskCompileHttpEvents.compileHttpEvent('action-name', { annotations: { 'require-whisk-auth': 'auth-token' } }, http); return expect(result).to.deep.equal({relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'my-service_action-name', namespace: 'sample_ns', pkge: 'default', responsetype: 'json'}); }); it('should define http events with explicit response type', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const http = {path: "/api/foo/bar", method: "GET", resp: 'http'} const result = openwhiskCompileHttpEvents.compileHttpEvent('action-name', {}, http); return expect(result).to.deep.equal({relpath: '/api/foo/bar', operation: 'GET', action: 'my-service_action-name', namespace: 'sample_ns', pkge: 'default', responsetype: 'http'}); }); it('should throw if http event value invalid', () => { expect(() => openwhiskCompileHttpEvents.compileHttpEvent('', {}, 'OPERATION')) .to.throw(Error, /Incorrect HTTP event/); expect(() => openwhiskCompileHttpEvents.compileHttpEvent('', {}, {})) .to.throw(Error, /Incorrect HTTP event/); expect(() => openwhiskCompileHttpEvents.compileHttpEvent('', {}, {method: true})) .to.throw(Error, /Incorrect HTTP event/); expect(() => openwhiskCompileHttpEvents.compileHttpEvent('', {}, {path: true})) .to.throw(Error, /Incorrect HTTP event/); }); }); describe('#compileSwaggerPath()', () => { it('should define swagger definition from http events', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const result = openwhiskCompileHttpEvents.compileSwaggerPath(httpEvent, host); const expectedResult = { operationId: "get-/api/foo/bar", responses: { "200": { description: "A successful invocation response" } }, "x-openwhisk": { action: "action-name", namespace: "user@host.com_space", package: "default", url: "https://openwhisk.somewhere.com/api/v1/web/user@host.com_space/default/action-name.json" } } return expect(result).to.deep.equal(expectedResult) }); it('should define swagger definition from http events and respect specified protocol on api host', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'http://openwhisk.somewhere.com' const result = openwhiskCompileHttpEvents.compileSwaggerPath(httpEvent, host); const expectedResult = { operationId: "get-/api/foo/bar", responses: { "200": { description: "A successful invocation response" } }, "x-openwhisk": { action: "action-name", namespace: "user@host.com_space", package: "default", url: "http://openwhisk.somewhere.com/api/v1/web/user@host.com_space/default/action-name.json" } } return expect(result).to.deep.equal(expectedResult) }); it('should define swagger definition with path parameters', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/{id}', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'http' } const host = 'openwhisk.somewhere.com' const result = openwhiskCompileHttpEvents.compileSwaggerPath(httpEvent, host); const expectedResult = { operationId: "get-/api/foo/{id}", parameters: [{ name: "id", in: "path", description: "Default description for 'id'", required: true, type: "string" }], responses: { "200": { description: "A successful invocation response" } }, "x-openwhisk": { action: "action-name", namespace: "user@host.com_space", package: "default", url: "https://openwhisk.somewhere.com/api/v1/web/user@host.com_space/default/action-name.http" } } return expect(result).to.deep.equal(expectedResult) }); }); describe('#compileSwaggerCaseSwitch()', () => { it('should define swagger case statement from http events', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const result = openwhiskCompileHttpEvents.compileSwaggerCaseSwitch(httpEvent, host); const expectedResult = { execute: [{ "set-variable": { actions: [{ set: "message.headers.X-Require-Whisk-Auth", value: "auth-token" }] } }, { invoke: { "target-url": "https://openwhisk.somewhere.com/api/v1/web/user@host.com_space/default/action-name.json", "verb": "keep" } } ], operations: [ "get-/api/foo/bar" ] } return expect(result).to.deep.equal(expectedResult) }); it('should define swagger case statement from http events with path parameters', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/{id}', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'http' } const host = 'openwhisk.somewhere.com' const result = openwhiskCompileHttpEvents.compileSwaggerCaseSwitch(httpEvent, host); const expectedResult = { execute: [{ "set-variable": { actions: [{ set: "message.headers.X-Require-Whisk-Auth", value: "auth-token" }] } }, { invoke: { "target-url": "https://openwhisk.somewhere.com/api/v1/web/user@host.com_space/default/action-name.http$(request.path)", "verb": "keep" } } ], operations: [ "get-/api/foo/{id}" ] } return expect(result).to.deep.equal(expectedResult) }); }); describe('#generateSwagger()', () => { it('should generate APIGW swagger with paths and case statements from http event', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = "false" const swaggerPath = openwhiskCompileHttpEvents.compileSwaggerPath(httpEvent, host) const swaggerCase = openwhiskCompileHttpEvents.compileSwaggerCaseSwitch(httpEvent, host) const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); const swaggerCases = result["x-ibm-configuration"] .assembly.execute[0]["operation-switch"].case expect(result.swagger).to.equal("2.0") expect(result.basePath).to.equal("/") expect(result.info.title).to.equal(service) expect(result.info.version).to.equal("1.0") expect(result.paths["/api/foo/bar"].get).to.deep.equal(swaggerPath) expect(swaggerCases[0]).to.deep.equal(swaggerCase) }); it('should generate APIGW swagger with multiple http events on same path', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const gethttpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const posthttpEvent = { relpath: '/api/foo/bar', operation: 'POST', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = "false" const getswaggerPath = openwhiskCompileHttpEvents.compileSwaggerPath(gethttpEvent, host) const getswaggerCase = openwhiskCompileHttpEvents.compileSwaggerCaseSwitch(gethttpEvent, host) const postswaggerPath = openwhiskCompileHttpEvents.compileSwaggerPath(posthttpEvent, host) const postswaggerCase = openwhiskCompileHttpEvents.compileSwaggerCaseSwitch(posthttpEvent, host) const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ gethttpEvent, posthttpEvent ] ); const swaggerCases = result["x-ibm-configuration"] .assembly.execute[0]["operation-switch"].case expect(result.paths["/api/foo/bar"].get).to.deep.equal(getswaggerPath) expect(result.paths["/api/foo/bar"].post).to.deep.equal(postswaggerPath) expect(swaggerCases.length).to.equal(2) expect(swaggerCases[0]).to.deep.equal(getswaggerCase) expect(swaggerCases[1]).to.deep.equal(postswaggerCase) }); it('should generate APIGW swagger with multiple http events on different paths', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const gethttpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const posthttpEvent = { relpath: '/api/foo/ccc', operation: 'POST', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = "false" const getswaggerPath = openwhiskCompileHttpEvents.compileSwaggerPath(gethttpEvent, host) const getswaggerCase = openwhiskCompileHttpEvents.compileSwaggerCaseSwitch(gethttpEvent, host) const postswaggerPath = openwhiskCompileHttpEvents.compileSwaggerPath(posthttpEvent, host) const postswaggerCase = openwhiskCompileHttpEvents.compileSwaggerCaseSwitch(posthttpEvent, host) const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ gethttpEvent, posthttpEvent ] ); const swaggerCases = result["x-ibm-configuration"] .assembly.execute[0]["operation-switch"].case expect(result.paths["/api/foo/bar"].get).to.deep.equal(getswaggerPath) expect(result.paths["/api/foo/ccc"].post).to.deep.equal(postswaggerPath) expect(swaggerCases.length).to.equal(2) expect(swaggerCases[0]).to.deep.equal(getswaggerCase) expect(swaggerCases[1]).to.deep.equal(postswaggerCase) }); it('should generate APIGW swagger with default API gateway options', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = {} const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result["x-ibm-configuration"].cors.enabled).to.equal(true) }); it('should generate APIGW swagger with custom CORS API gateway options', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = { cors: false } const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result["x-ibm-configuration"].cors.enabled).to.equal(false) }); it('should generate APIGW swagger with custom basepath API gateway option', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = { basepath: "/api" } const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result.basePath).to.equal(options.basepath) }); it('should generate APIGW swagger with custom API name option', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = { name: "my-api" } const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result.info.title).to.equal(options.name) }); it('should generate APIGW swagger with custom auth key API gateway options', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = { auth: { key: "some-header-key" } } const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result.security).to.deep.equal([{"client_id": []}]) expect(result.securityDefinitions.client_id).to.deep.equal({"in": "header", type: "apiKey", "x-key-type": "clientId", name: "some-header-key" }) }); it('should generate APIGW swagger with custom auth key and secret API gateway options', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = { auth: { key: "some-header-key", secret: "some-header-secret" } } const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result.security).to.deep.equal([{client_id: [], client_secret: []}]) expect(result.securityDefinitions.client_id).to.deep.equal({"in": "header", type: "apiKey", "x-key-type": "clientId", name: "some-header-key" }) expect(result.securityDefinitions.client_secret).to.deep.equal({"in": "header", type: "apiKey", "x-key-type": "clientSecret", name: "some-header-secret" }) }); it('should generate APIGW swagger with AppID OAuth provider API gateway options', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = { oauth: { provider: "app-id", tenant: "some-id" } } const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result.security).to.deep.equal([{"app-id": []}]) expect(result.securityDefinitions["app-id"]).to.deep.equal({ flow: "application", tokenUrl: "", type: "oauth2", "x-provider": { name: "app-id", params: { tenantId: options.oauth.tenant } }, "x-tokenintrospect": { "url": null } }) }); it('should generate APIGW swagger with Google OAuth provider API gateway options', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = { oauth: { provider: "google" } } const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result.security).to.deep.equal([{"google": []}]) expect(result.securityDefinitions["google"]).to.deep.equal({ flow: "application", tokenUrl: "", type: "oauth2", "x-provider": { name: "google" }, "x-tokenintrospect": { url: "https://www.googleapis.com/oauth2/v3/tokeninfo"} }) }); it('should generate APIGW swagger with Facebook OAuth provider API gateway options', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = { oauth: { provider: "facebook" } } const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result.security).to.deep.equal([{"facebook": []}]) expect(result.securityDefinitions["facebook"]).to.deep.equal({ flow: "application", tokenUrl: "", type: "oauth2", "x-provider": { name: "facebook" }, "x-tokenintrospect": { url: "https://graph.facebook.com/debug_token"} }) }); it('should generate APIGW swagger with Github OAuth provider API gateway options', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = { oauth: { provider: "github" } } const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result.security).to.deep.equal([{"github": []}]) expect(result.securityDefinitions["github"]).to.deep.equal({ flow: "application", tokenUrl: "", type: "oauth2", "x-provider": { name: "github" }, "x-tokenintrospect": { url: "https://api.github.com/user"} }) }); it('should generate APIGW swagger with rate limiting API gateway options', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" const options = { rate_limit: { rate: 1000, unit: "minute"} } const result = openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ] ); expect(result["x-ibm-rate-limit"]).to.deep.equal([{rate: 1000, unit: "minute", units: 1}]) }); it('should throw if API GW auth options are invalid', () => { openwhiskCompileHttpEvents.serverless.service.service = 'my-service' openwhiskCompileHttpEvents.serverless.service.provider = {namespace: "sample_ns"}; const httpEvent = { relpath: '/api/foo/bar', operation: 'GET', secure_key: 'auth-token', action: 'action-name', namespace: 'user@host.com_space', pkge: 'default', responsetype: 'json' } const host = 'openwhisk.somewhere.com' const service = "my-service" let options = { auth: { secret: "something" } } expect(() => openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ])) .to.throw(Error, /Missing mandatory resources.apigw.auth.key/); options = { rate_limit: { } } expect(() => openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ])) .to.throw(Error, /Missing rate limit parameter: rate/); options = { rate_limit: { rate: 1000 } } expect(() => openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ])) .to.throw(Error, /Missing rate limit parameter: unit/); options = { rate_limit: { rate: 1000, unit: 'blah' } } expect(() => openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ])) .to.throw(Error, /Invalid rate limit parameter: unit/); options = { oauth: { provider: 'blah' } } expect(() => openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ])) .to.throw(Error, /OAuth defined with invalid provider/); options = { oauth: { provider: 'app-id' } } expect(() => openwhiskCompileHttpEvents.generateSwagger(service, host, options, [ httpEvent ])) .to.throw(Error, /OAuth provider app-id defined without tenant parameter/); }); }); }); ================================================ FILE: compile/cloudant/README.md ================================================ # Cloudant Events This plugins compiles the `cloudant` events in `serverless.yaml` to corresponding [OpenWhisk Cloudant Trigger Feeds](https://github.com/openwhisk/openwhisk-package-cloudant) definitions. ## How it works `Compile Cloudant` hooks into the [`package:compileEvents`](/lib/plugins/deploy) lifecycle. It loops over all schedule event which are defined in `serverless.yaml`. ### Using Package Parameters IBM Cloudant instances can be provisioned through the IBM Bluemix platform. OpenWhisk on Bluemix will export Cloudant service credentials bound to a package with the following name: ``` /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 ``` Rather than having to manually define all the properties needed by the Cloudant trigger feed, you can reference a package to use instead. Credentials from the referenced package will be used when executing the trigger feed. Developers only need to add the database to listen to for each trigger. ```yaml # serverless.yaml functions: index: handler: users.main events: - cloudant: package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 db: db_name ``` The plugin will create a trigger called `${serviceName}_${fnName}_cloudant_${db}` and a rule called `${serviceName}_${fnName}_cloudant_${db}_rule` to bind the function to the database update events. The trigger and rule names created can be set explicitly using the `trigger` and `rule` parameters. Other functions can bind to the same trigger using the inline `trigger` event referencing this trigger name. ```yaml # serverless.yaml functions: index: handler: users.main events: - cloudant: package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 db: my_db trigger: db_events rule: connect_index_to_db another: handler: users.another events: - trigger: db_events ``` ### Using Manual Parameters Trigger feed parameters for the Cloudant event source can be defined explicitly, rather than using pulling credentials from a package. ```yaml # serverless.yaml functions: index: handler: users.main events: - cloudant: host: xxx-yyy-zzz-bluemix.cloudant.com username: USERNAME password: PASSWORD db: db_name ``` ### Adding Optional Parameters The following optional feed parameters are also supported: * `max` - Maximum number of triggers to fire. Defaults to infinite. * `filter` - Filter function defined on a design document. * `query` - Optional query parameters for the filter function. ```yaml # serverless.yaml functions: index: handler: users.main events: - cloudant: ... max: 10000 query: status: new filter: mailbox/by_status ``` ================================================ FILE: compile/cloudant/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); class OpenWhiskCompileCloudant { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('openwhisk'); this.default_package = '/whisk.system/cloudant' this.hooks = { 'before:package:compileEvents': () => BbPromise.bind(this) .then(this.setup) .then(this.processCloudantEvents) }; } setup() { if (!this.serverless.service.resources) { this.serverless.service.resources = {}; } if (!this.serverless.service.resources.triggers) { this.serverless.service.resources.triggers = {}; } if (!this.serverless.service.resources.rules) { this.serverless.service.resources.rules = {}; } } validateConfig (fnName, config) { if (!config.db) { throw new this.serverless.classes.Error( `Cloudant event property (db) missing on function: ${fnName}` ) } if (!config.package) { const config_properties = ['db', 'password', 'username', 'host'] if (!config.db) { throw new this.serverless.classes.Error( `Cloudant event property (db) missing on function: ${fnName}` ) } if (!config.host) { throw new this.serverless.classes.Error( `Cloudant event property (host) missing on function: ${fnName}` ) } const has_manual_auth = !!config.username && !!config.password const has_iam_auth = !!config.iam_api_key if (!has_manual_auth && !has_iam_auth) { throw new this.serverless.classes.Error( `Cloudant event authentication property (username & password or iam_api_key) missing on function: ${fnName}` ) } } } compileCloudantTrigger (fnName, config) { this.validateConfig(fnName, config) const name = config.trigger || this.defaultCloudantName(fnName, config.db) const feed = `${config.package || this.default_package}/changes` const feed_parameters = { dbname: config.db } if (!config.package) { if (config.iam_api_key) { feed_parameters.iamApiKey = config.iam_api_key } else { feed_parameters.username = config.username feed_parameters.password = config.password } feed_parameters.host = config.host } if (config.max) { feed_parameters.maxTriggers = config.max } if (config.query) { feed_parameters.query_params = config.query } if (config.filter) { feed_parameters.filter = config.filter } return { name, content: { feed, feed_parameters } } } defaultCloudantName (fnName, db) { return `${this.serverless.service.service}_${fnName}_cloudant_${db}` } processCloudantEvent (fnName, config) { const fnObj = this.serverless.service.getFunction(fnName) const trigger = this.compileCloudantTrigger(fnName, config) const rule = config.rule || `${this.defaultCloudantName(fnName, config.db)}_rule` fnObj.events.push({ trigger: { name: trigger.name, rule } }) this.serverless.service.resources.triggers[trigger.name] = trigger.content } processCloudantEvents () { this.serverless.service.getAllFunctions().forEach(name => { const fn = this.serverless.service.getFunction(name) const events = (fn.events || []).filter(e => e.cloudant) events.forEach(e => this.processCloudantEvent(name, e.cloudant)) }) } } module.exports = OpenWhiskCompileCloudant ================================================ FILE: compile/cloudant/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const OpenWhiskCompileCloudant = require('../index'); describe('OpenWhiskCompileCloudant', () => { let serverless; let sandbox; let openwhiskCompileCloudant; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskCompileCloudant = new OpenWhiskCompileCloudant(serverless, options); serverless.service.service = 'serviceName'; serverless.service.provider = { namespace: 'testing', apihost: '', auth: '', }; serverless.cli = { log: () => {} }; openwhiskCompileCloudant.setup() }); afterEach(() => { sandbox.restore(); }); describe('#processCloudantEvents()', () => { it('should call processCloudantEvent for each cloudant event.', () => { const service = openwhiskCompileCloudant.serverless.service; const fns = { first: { events: [{}, {cloudant: {package: 'testing_package', db: 'some_db'}}, {trigger: true}] }, second: { events: [{cloudant: {package: 'another_package', db: 'some_db'}}] }, third: {} } service.getAllFunctions = () => Object.keys(fns) service.getFunction = name => fns[name]; const spy = openwhiskCompileCloudant.processCloudantEvent = sinon.spy() openwhiskCompileCloudant.processCloudantEvents() expect(spy.calledTwice).to.be.equal(true) expect(spy.withArgs("first", {package: 'testing_package', db: 'some_db'}).calledOnce).to.be.equal(true) expect(spy.withArgs("second", {package: 'another_package', db: 'some_db'}).calledOnce).to.be.equal(true) }) }) describe('#processCloudantEvents()', () => { it('should create trigger & rules and update manifest resources.', () => { const cloudant = { package: 'some_package', db: 'testing' } const fnObj = { events: [{cloudant}] } serverless.service.getFunction = () => fnObj openwhiskCompileCloudant.compileCloudantTrigger = () => ({name: 'serviceName_fnName_cloudant_testing', content: { a: 1 }}) openwhiskCompileCloudant.processCloudantEvent("fnName", fnObj.events[0].cloudant) expect(fnObj.events[1]).to.be.deep.equal({ trigger: { name: 'serviceName_fnName_cloudant_testing', rule: 'serviceName_fnName_cloudant_testing_rule' } }) expect(serverless.service.resources.triggers).to.be.deep.equal({serviceName_fnName_cloudant_testing: {a: 1}}) }) }) describe('#compileCloudantTrigger()', () => { it('should throw errors for missing db parameter.', () => { expect(() => openwhiskCompileCloudant.compileCloudantTrigger('testing', {})) .to.throw(Error, 'Cloudant event property (db) missing on function: testing'); }) it('should throw errors for missing host parameters without package', () => { const config = { db: 'dbname', username: 'user', password: 'password' } expect(() => openwhiskCompileCloudant.compileCloudantTrigger('testing', config)) .to.throw(Error, `Cloudant event property (host) missing on function: testing`); }) it('should throw errors for missing username parameter without package', () => { const config = { db: 'dbname', host: 'host.com', password: 'password' } expect(() => openwhiskCompileCloudant.compileCloudantTrigger('testing', config)) .to.throw(Error, `Cloudant event authentication property (username & password or iam_api_key) missing on function: testing`); }) it('should throw errors for missing password parameter without package', () => { const config = { db: 'dbname', host: 'host.com', username: 'username' } expect(() => openwhiskCompileCloudant.compileCloudantTrigger('testing', config)) .to.throw(Error, `Cloudant event authentication property (username & password or iam_api_key) missing on function: testing`); }) it('should throw errors for missing authentication parameters without package', () => { const config = { db: 'dbname', host: 'host.com' } expect(() => openwhiskCompileCloudant.compileCloudantTrigger('testing', config)) .to.throw(Error, `Cloudant event authentication property (username & password or iam_api_key) missing on function: testing`); }) it('should return trigger for cloudant provider using package.', () => { const db = 'my_db', pkge = '/bluemixOrg_bluemixSpace/packageId' const trigger = openwhiskCompileCloudant.compileCloudantTrigger('testing', { db, 'package': pkge }) expect(trigger).to.be.deep.equal({ name: `${serverless.service.service}_testing_cloudant_${db}`, content: { feed: `${pkge}/changes`, feed_parameters: { dbname: `${db}` } } }) }) it('should return trigger for cloudant provider with manual username & password configuration.', () => { const config = { db: 'dbname', username: 'user', password: 'password', host: 'hostname' } const trigger = openwhiskCompileCloudant.compileCloudantTrigger('testing', config) expect(trigger).to.be.deep.equal({ name: `${serverless.service.service}_testing_cloudant_${config.db}`, content: { feed: `/whisk.system/cloudant/changes`, feed_parameters: { username: config.username, password: config.password, host: config.host, dbname: config.db } } }) }) it('should return trigger for cloudant provider with manual iam api key configuration.', () => { const config = { db: 'dbname', iam_api_key: 'api_key', host: 'hostname' } const trigger = openwhiskCompileCloudant.compileCloudantTrigger('testing', config) expect(trigger).to.be.deep.equal({ name: `${serverless.service.service}_testing_cloudant_${config.db}`, content: { feed: `/whisk.system/cloudant/changes`, feed_parameters: { iamApiKey: config.iam_api_key, host: config.host, dbname: config.db } } }) }) it('should return trigger with optional configuration parameters.', () => { const config = { db: 'dbname', username: 'user', password: 'password', host: 'hostname', max: 10000, query: { key: 'value' }, filter: 'some/view' } const trigger = openwhiskCompileCloudant.compileCloudantTrigger('testing', config) expect(trigger).to.be.deep.equal({ name: `${serverless.service.service}_testing_cloudant_${config.db}`, content: { feed: `/whisk.system/cloudant/changes`, feed_parameters: { username: config.username, password: config.password, host: config.host, dbname: config.db, maxTriggers: 10000, filter: 'some/view', query_params: { key: 'value' } } } }) }) }) }); ================================================ FILE: compile/functions/README.md ================================================ # Compile Functions This plugins compiles the functions in `serverless.yaml` to corresponding [OpenWhisk Action](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md) definitions. ## How it works `Compile Functions` hooks into the [`package:compileFunctions`](/lib/plugins/deploy) lifecycle. It loops over all functions which are defined in `serverless.yaml`. Inside the function loop it creates corresponding OpenWhisk Action definition based on the settings (e.g. function `name` property or service `defaults`) which are provided in the `serverless.yaml` file. The function will be called `_` by default but you can specify an alternative name with the help of the functions `name` property. The Action namespace defaults to the service provider namespace but can be set manually, using the `namespace` parameter. The functions `MemorySize` is set to `256`, `Timeout to `60` and `Runtime` to `nodejs`. You can overwrite those defaults by setting corresponding entries in the function definition or server provider properties. At the end all OpenWhisk Action definitions are merged inside the `serverless.service.actions` section. ### Action Rules Action [Rules](https://github.com/openwhisk/openwhisk/blob/master/docs/triggers_rules.md), binding Actions to [Triggers](https://github.com/openwhisk/openwhisk/blob/master/docs/triggers_rules.md), can be defined using the `events` property. ```yaml # serverless.yaml functions: index: handler: users.main events: - triggers: trigger: "myTriggerName" ``` This definition will create a new Rule, which binds the configured Action to the Trigger. More documentation on the Rules configuration can be found in the [`compileRules` plugin](../rules). ================================================ FILE: compile/functions/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const Runtimes = require('./runtimes/index.js') class OpenWhiskCompileFunctions { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('openwhisk'); this.runtimes = new Runtimes(serverless) this.hooks = { 'before:package:createDeploymentArtifacts': () => BbPromise.bind(this) .then(this.excludes) .then(this.disableSeqPackaging), 'before:package:compileFunctions': this.setup.bind(this), 'package:compileFunctions': this.compileFunctions.bind(this), }; } // Ensure we don't bundle provider plugin with service artifact. excludes() { const exclude = this.serverless.service.package.exclude || []; exclude.push("node_modules/serverless-openwhisk/**"); this.serverless.service.package.exclude = exclude; } disableSeqPackaging() { this.serverless.service.getAllFunctions().forEach(functionName => { const functionObject = this.serverless.service.getFunction(functionName); if (functionObject.sequence) { Object.assign(functionObject, { package: { disable: true } }) } }) } setup() { // This object will be used to store the Action resources, passed directly to // the OpenWhisk SDK during the deploy process. this.serverless.service.actions = {}; } calculateFunctionName(functionName, functionObject) { return functionObject.name || `${this.serverless.service.service}_${functionName}`; } calculateFunctionNameSpace(functionName, functionObject) { return functionObject.namespace || this.serverless.service.provider.namespace; } calculateMemorySize(functionObject) { return functionObject.memory || this.serverless.service.provider.memory; } calculateConcurrency(functionObject) { return functionObject.concurrency || this.serverless.service.provider.concurrency; } calculateTimeout(functionObject) { return functionObject.timeout || this.serverless.service.provider.timeout; } calculateOverwrite(functionObject) { let Overwrite = true; if (functionObject.hasOwnProperty('overwrite')) { Overwrite = functionObject.overwrite; } else if (this.serverless.service.provider.hasOwnProperty('overwrite')) { Overwrite = this.serverless.service.provider.overwrite; } return Overwrite; } compileFunctionAction(params) { return { actionName: params.FunctionName, namespace: params.NameSpace, overwrite: params.Overwrite, action: { exec: params.Exec, limits: { timeout: params.Timeout ? (params.Timeout * 1000) : undefined, memory: params.MemorySize, concurrency: params.Concurrency, }, parameters: params.Parameters, annotations: params.Annotations }, }; } // This method takes the function handler definition, parsed from the user's YAML file, // and turns it into the OpenWhisk Action resource object. // // These resource objects are passed to the OpenWhisk SDK to create the associated Actions // during the deployment process. // // Parameter values will be parsed from the user's YAML definition, either as a value from // the function handler definition or the service provider defaults. compileFunction(functionName, functionObject) { return this.runtimes.exec(functionObject).then(Exec => { const FunctionName = this.calculateFunctionName(functionName, functionObject); const NameSpace = this.calculateFunctionNameSpace(functionName, functionObject); const MemorySize = this.calculateMemorySize(functionObject); const Timeout = this.calculateTimeout(functionObject); const Overwrite = this.calculateOverwrite(functionObject); const Concurrency = this.calculateConcurrency(functionObject); // optional action parameters const Parameters = Object.keys(functionObject.parameters || {}) .map(key => ({ key, value: functionObject.parameters[key] })); // optional action annotations const Annotations = this.constructAnnotations(functionObject.annotations); return this.compileFunctionAction( { FunctionName, NameSpace, Overwrite, Exec, Timeout, MemorySize, Concurrency, Parameters, Annotations } ); }); } constructAnnotations (annotations) { if (!annotations) return [] // finalise action parameters when exposing as external HTTP endpoint. // mirrors behaviour from OpenWhisk CLI. if (annotations['web-export']) { annotations['final'] = true } const converted = Object.keys(annotations) .map(key => ({ key, value: annotations[key] })); return converted } logCompiledFunction (name, fn) { const clone = JSON.parse(JSON.stringify(fn)) if (clone.action.exec.code) { clone.action.exec.code = '' } this.serverless.cli.log(`Compiled Function (${name}): ${JSON.stringify(clone)}`); } compileFunctions() { this.serverless.cli.log('Compiling Functions...'); if (!this.serverless.service.actions) { throw new this.serverless.classes.Error( 'Missing Resources section from OpenWhisk Resource Manager template'); } const functionPromises = this.serverless.service.getAllFunctions().map((functionName) => { const functionObject = this.serverless.service.getFunction(functionName); if (!functionObject.handler && !functionObject.sequence) { throw new this.serverless.classes .Error(`Missing "handler" or "sequence" property in function ${functionName}`); } if (functionObject.handler && functionObject.sequence) { throw new this.serverless.classes .Error(`Found both "handler" and "sequence" properties in function ${functionName}, please choose one.`); } const functions = this.serverless.service.actions; const err = () => { throw new this.serverless.classes .Error(`Unable to read handler file in function ${functionName}`); }; let compileFn = this.compileFunction(functionName, functionObject) .then(newFunction => (functions[functionName] = newFunction)) if (this.options.verbose) { compileFn = compileFn.then(fn => this.logCompiledFunction(functionName, fn)) } return compileFn.catch(err); }); return BbPromise.all(functionPromises); } } module.exports = OpenWhiskCompileFunctions; ================================================ FILE: compile/functions/runtimes/base.js ================================================ 'use strict'; const fs = require('fs-extra') const JSZip = require('jszip') const BbPromise = require('bluebird') class BaseRuntime { constructor(serverless) { this.serverless = serverless } match (functionObject) { if (!functionObject.hasOwnProperty('handler')) return false const runtime = this.calculateRuntime(functionObject) return !!(runtime && runtime.startsWith(this.kind)) } exec (functionObject) { const main = this.calculateFunctionMain(functionObject); const kind = this.calculateKind(functionObject); const exec = { main, kind } if (functionObject.hasOwnProperty('image')) { exec.image = functionObject.image } return this.generateActionPackage(functionObject).then(code => Object.assign(exec, { code })) } calculateFunctionMain(functionObject) { const splitted = functionObject.handler.split('.'); if (splitted.length < 2) { return functionObject; } return splitted[splitted.length - 1]; } calculateRuntime(functionObject) { return functionObject.runtime || this.serverless.service.provider.runtime } calculateDefaultRuntime(functionObject) { const runtime = this.calculateRuntime(functionObject) return runtime.includes(':') ? runtime : `${runtime}:default` } calculateKind(functionObject) { if (functionObject.hasOwnProperty('image')) return 'blackbox' return this.calculateDefaultRuntime(functionObject) } isValidFile (handlerFile) { return fs.existsSync(handlerFile) } convertHandlerToPath(functionHandler) { const lastDot = functionHandler.lastIndexOf('.'); if (lastDot === -1) { return functionHandler; } return functionHandler.substring(0, lastDot) + this.extension; } convertHandlerToPathInZip(functionHandler) { let path = this.convertHandlerToPath(functionHandler); while (path.startsWith('../')) { path = path.substring(3); } return path; } generateActionPackage(functionObject) { // Check that handler file exists const handlerFile = this.convertHandlerToPath(functionObject.handler); if (!this.isValidFile(handlerFile)) { throw new this.serverless.classes.Error(`Function handler (${handlerFile}) does not exist.`) } // Generate action package const handlerFileZipPath = this.convertHandlerToPathInZip(functionObject.handler); return this.getArtifactZip(functionObject) .then(zip => this.processActionPackage(handlerFileZipPath, zip)) .then(zip => zip.generateAsync( { type: 'nodebuffer', compression: 'DEFLATE', compressionOptions: { level: 9 } } )) .then(buf => buf.toString('base64')); } getArtifactZip(functionObject) { const artifactPath = this.getArtifactPath(functionObject) const readFile = BbPromise.promisify(fs.readFile); return readFile(artifactPath).then(zipBuffer => JSZip.loadAsync(zipBuffer)) } getArtifactPath(functionObject) { return this.serverless.service.package.individually ? functionObject.package.artifact : this.serverless.service.package.artifact; } } module.exports = BaseRuntime ================================================ FILE: compile/functions/runtimes/binary.js ================================================ 'use strict'; const BaseRuntime = require('./base') class Binary extends BaseRuntime { constructor (serverless) { super(serverless) this.kind = 'binary' } exec (functionObject) { const image = 'openwhisk/dockerskeleton' const kind = 'blackbox' return this.generateActionPackage(functionObject).then(code => ({ image, kind, code })) } processActionPackage (handlerFile, zip) { return zip.file(handlerFile).async('nodebuffer').then(data => { zip.remove(handlerFile) return zip.file('exec', data) }) } calculateKind (functionObject) { return `blackbox` } convertHandlerToPath (functionHandler) { return functionHandler } } module.exports = Binary ================================================ FILE: compile/functions/runtimes/docker.js ================================================ 'use strict'; class Docker { constructor(serverless) { this.serverless = serverless } match (functionObject) { if (!functionObject.hasOwnProperty('handler')) return false return this.calculateRuntime(functionObject) === 'docker' } exec (functionObject) { return { kind: 'blackbox', image: functionObject.handler } } calculateRuntime(functionObject) { return functionObject.runtime || this.serverless.service.provider.runtime } } module.exports = Docker ================================================ FILE: compile/functions/runtimes/index.js ================================================ 'use strict'; const Binary = require('./binary') const Docker = require('./docker') const Node = require('./node') const Python = require('./python') const Swift = require('./swift') const Php = require('./php') const Sequence = require('./sequence') const Java = require('./java') const Ruby = require('./ruby') class Runtimes { constructor(serverless) { this.serverless = serverless; this.runtimes = [ new Binary(serverless), new Docker(serverless), new Node(serverless), new Python(serverless), new Swift(serverless), new Php(serverless), new Sequence(serverless), new Java(serverless), new Ruby(serverless) ]; } exec (functionObj) { const matched = this.runtimes.find(runtime => runtime.match(functionObj)) if (matched) return Promise.resolve(matched.exec(functionObj)) throw new this.serverless.classes.Error( 'This runtime is not currently supported by the OpenWhisk provider plugin.'); } } module.exports = Runtimes ================================================ FILE: compile/functions/runtimes/java.js ================================================ 'use strict'; const fs = require('fs-extra'); const BbPromise = require('bluebird'); const BaseRuntime = require('./base'); const JSZip = require('jszip'); class Java extends BaseRuntime { constructor(serverless) { super(serverless); this.kind = 'java'; this.extension = '.jar'; } convertHandlerToPath(functionHandler) { const lastColon = functionHandler.lastIndexOf(':'); if (lastColon === -1) { return functionHandler; } return functionHandler.substring(0, lastColon); } // Main class has to be defined as an annotation, otherwise it will assume the main class is called 'Main'. calculateFunctionMain(functionObject) { if (functionObject.handler) { const splitted = functionObject.handler.split(':'); if (splitted.length > 1 && splitted[splitted.length - 1]) { return splitted[splitted.length - 1]; } } return 'Main'; } // Ensure zip package used to deploy action has the correct artifacts for the runtime by only // including the deployable JAR file. processActionPackage(handlerFile, zip) { return zip; } } module.exports = Java; ================================================ FILE: compile/functions/runtimes/node.js ================================================ 'use strict'; const BaseRuntime = require('./base') class Node extends BaseRuntime { constructor (serverless) { super(serverless) this.kind = 'nodejs' this.extension = '.js' } calculateRuntime(functionObject) { return super.calculateRuntime(functionObject) || 'nodejs:default' } processActionPackage (handlerFile, zip) { zip.file("package.json", JSON.stringify({main: handlerFile})) return zip } //We're handling a special case here which is that if TypeScript is being used //we won't actually have a ".js" file (this.extension), instead we'll have a "ts" //which should still be considered safe enough, or at least shouldn't //completely stop the deployment process isValidFile(handlerFile) { return super.isValidFile(handlerFile) || this.isValidTypeScriptFile(handlerFile); } //Check for TypeScript version of handler file isValidTypeScriptFile(handlerFile) { //replaces the last occurance of `.js` with `.ts`, case insensitive const typescriptHandlerFile = handlerFile.replace(/\.js$/gi, ".ts"); return super.isValidFile(typescriptHandlerFile); } } module.exports = Node ================================================ FILE: compile/functions/runtimes/php.js ================================================ 'use strict'; const BaseRuntime = require('./base') class Php extends BaseRuntime { constructor (serverless) { super(serverless) this.kind = 'php' this.extension = '.php' } processActionPackage (handlerFile, zip) { return zip.file(handlerFile).async('nodebuffer').then(data => { zip.remove(handlerFile) return zip.file('index.php', data) }) } } module.exports = Php ================================================ FILE: compile/functions/runtimes/python.js ================================================ 'use strict'; const BaseRuntime = require('./base') class Python extends BaseRuntime { constructor (serverless) { super(serverless) this.kind = 'python' this.extension = '.py' } processActionPackage (handlerFile, zip) { return zip.file(handlerFile).async('nodebuffer').then(data => { zip.remove(handlerFile) return zip.file('__main__.py', data) }) } calculateDefaultRuntime (functionObject) { return this.calculateRuntime(functionObject) } } module.exports = Python ================================================ FILE: compile/functions/runtimes/ruby.js ================================================ 'use strict'; const BaseRuntime = require('./base') class Ruby extends BaseRuntime { constructor (serverless) { super(serverless) this.kind = 'ruby' this.extension = '.rb' } processActionPackage (handlerFile, zip) { return zip.file(handlerFile).async('nodebuffer').then(data => { zip.remove(handlerFile) return zip.file('main.rb', data) }) } } module.exports = Ruby ================================================ FILE: compile/functions/runtimes/sequence.js ================================================ 'use strict'; class Sequence { constructor(serverless) { this.serverless = serverless } match (functionObject) { return functionObject.hasOwnProperty('sequence') } exec (functionObject) { // sequence action names must be fully qualified. // use default namespace if this is missing. const components = functionObject.sequence.map(name => { if (name.startsWith('/')) { return name } const func = this.serverless.service.getFunction(name) return `/_/${func.name}` }) return { kind: 'sequence', components } } } module.exports = Sequence ================================================ FILE: compile/functions/runtimes/swift.js ================================================ 'use strict'; const fs = require('fs-extra') const BaseRuntime = require('./base') const JSZip = require("jszip") class Swift extends BaseRuntime { constructor (serverless) { super(serverless) this.kind = 'swift' this.extension = '.swift' } convertHandlerToPath (functionHandler) { if (this.isZipFile(functionHandler)) { return functionHandler } return super.convertHandlerToPath(functionHandler) } calculateFunctionMain(functionObject) { if (this.isZipFile(functionObject.handler)) { return 'main' } return super.calculateFunctionMain(functionObject) } isZipFile (path) { return path.endsWith('.zip') } readHandlerFile (path) { const contents = fs.readFileSync(path) const encoding = this.isZipFile(path) ? 'base64' : 'utf8' return contents.toString(encoding) } exec (functionObject) { const main = this.calculateFunctionMain(functionObject); const kind = this.calculateKind(functionObject); const handlerPath = this.convertHandlerToPath(functionObject.handler) if (!this.isValidFile(handlerPath)) { throw new this.serverless.classes.Error(`Function handler (${handlerPath}) does not exist.`) } const code = this.readHandlerFile(handlerPath) const binary = this.isZipFile(handlerPath) const exec = { main, kind, code, binary } if (functionObject.hasOwnProperty('image')) { exec.image = functionObject.image } return Promise.resolve(exec) } } module.exports = Swift ================================================ FILE: compile/functions/runtimes/tests/all.js ================================================ 'use strict'; require('./index'); require('./base'); require('./node'); require('./docker'); require('./python'); require('./swift'); require('./php'); require('./binary'); require('./sequence'); require('./java'); require('./ruby'); ================================================ FILE: compile/functions/runtimes/tests/base.js ================================================ 'use strict'; const expect = require('chai').expect; const BaseRuntime = require('../base'); describe('Base', () => { const base = new BaseRuntime(); base.extension = '.js'; describe('#calculateFunctionMain()', () => { it('should extract the main function for a given file handler', () => { const functionObject = { handler: 'index.main' }; const result = base.calculateFunctionMain(functionObject); expect(result).to.equal('main'); }); it('should return the input for a given file handler without exported function', () => { const functionObject = { handler: 'index' }; const result = base.calculateFunctionMain(functionObject); expect(result).to.equal(functionObject); }); it('should extract the main function for a given path handler', () => { const functionObject = { handler: 'myFunction@0.1.0/index.main' }; const result = base.calculateFunctionMain(functionObject); expect(result).to.equal('main'); }); it('should extract the main function for a given relative path handler', () => { const functionObject = { handler: '../myFunction/index.main' }; const result = base.calculateFunctionMain(functionObject); expect(result).to.equal('main'); }); }); describe('#convertHandlerToPathInZip()', () => { it('should extract the path in zip for a given file handler', () => { const result = base.convertHandlerToPathInZip('index.main'); expect(result).to.equal('index.js'); }); it('should return the input for a given file handler without exported function', () => { const result = base.convertHandlerToPathInZip('index'); expect(result).to.equal('index'); }); it('should extract the path in zip for a given path handler', () => { const result = base.convertHandlerToPathInZip('myFunction@0.1.0/index.main'); expect(result).to.equal('myFunction@0.1.0/index.js'); }); it('should extract the path in zip for a given relative path handler', () => { const result = base.convertHandlerToPathInZip('../myFunction@0.1.0/index.main'); expect(result).to.equal('myFunction@0.1.0/index.js'); }); }); describe('#convertHandlerToPath()', () => { it('should extract the path for a given file handler', () => { const result = base.convertHandlerToPath('index.main'); expect(result).to.equal('index.js'); }); it('should return the input for a given file handler without exported function', () => { const result = base.convertHandlerToPath('index'); expect(result).to.equal('index'); }); it('should extract the path for a given path handler', () => { const result = base.convertHandlerToPath('myFunction@0.1.0/index.main'); expect(result).to.equal('myFunction@0.1.0/index.js'); }); it('should extract the path for a given relative path handler', () => { const result = base.convertHandlerToPath('../myFunction@0.1.0/index.main'); expect(result).to.equal('../myFunction@0.1.0/index.js'); }); }); }); ================================================ FILE: compile/functions/runtimes/tests/binary.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const Binary = require('../binary'); const JSZip = require("jszip"); const fs = require('fs-extra'); describe('Binary', () => { let serverless; let node; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {}, getProvider: sandbox.spy()}; serverless.service.provider = { name: 'openwhisk' }; node = new Binary(serverless); }); afterEach(() => { sandbox.restore(); }); describe('#match()', () => { it('should match with explicit runtime', () => { serverless.service.provider.runtime = 'nodejs'; expect(node.match({runtime: 'binary', handler: 'bin_file'})).to.equal(true) }); it('should match with provider runtime', () => { serverless.service.provider.runtime = 'binary'; expect(node.match({handler: 'bin_file'})).to.equal(true) }); it('should not match when wrong explicit runtime', () => { expect(node.match({runtime: 'nodejs', handler: 'bin_file'})).to.equal(false) }); it('should not match when wrong provider runtime', () => { serverless.service.provider.runtime = 'nodejs'; expect(node.match({handler: 'bin_file'})).to.equal(false) }); it('should not match default runtime', () => { expect(node.match({handler: 'bin_file'})).to.equal(false) }); it('should not match when missing handler', () => { expect(node.match({})).to.equal(false) }); }); describe('#exec()', () => { it('should return binary exec definition', () => { const fileContents = 'zip file contents'; const handler = 'bin_file'; const exec = { image: 'openwhisk/dockerskeleton', kind: 'blackbox', code: new Buffer(fileContents) }; sandbox.stub(node, 'generateActionPackage', (functionObj) => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(node.exec({ handler, runtime: 'binary'})) .to.eventually.deep.equal(exec); }) }); describe('#generateActionPackage()', () => { it('should throw error for missing handler file', () => { expect(() => node.generateActionPackage({handler: 'does_not_exist'})) .to.throw(Error, 'Function handler (does_not_exist) does not exist.'); }) it('should read service artifact and add package.json for handler', () => { node.serverless.service.package = {artifact: '/path/to/zip_file.zip'}; node.isValidFile = () => true const zip = new JSZip(); zip.file("handler", "blah blah blah"); return zip.generateAsync({type:"nodebuffer"}).then(zipped => { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return node.generateActionPackage({handler: 'handler'}).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { expect(zip.file("handler")).to.be.equal(null) return zip.file("exec").async("string").then(code => { expect(code).to.be.equal('blah blah blah') }) }) }) }); }) it('should handle service artifact for individual function handler', () => { const functionObj = {handler: 'handler', package: { artifact: '/path/to/zip_file.zip'}} node.serverless.service.package = {individually: true}; node.isValidFile = () => true const zip = new JSZip(); zip.file("handler", "blah blah blah"); return zip.generateAsync({type:"nodebuffer"}).then(zipped => { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return node.generateActionPackage(functionObj).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { expect(zip.file("handler")).to.be.equal(null) return zip.file("exec").async("string").then(code => { expect(code).to.be.equal('blah blah blah') }) }) }) }); }) }) }); ================================================ FILE: compile/functions/runtimes/tests/docker.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const Docker = require('../docker'); describe('Docker', () => { let serverless; let docker; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {}, getProvider: sandbox.spy()}; serverless.service.provider = { name: 'openwhisk' }; docker = new Docker(serverless); }); afterEach(() => { sandbox.restore(); }); describe('#match()', () => { it('should match with explicit runtime', () => { serverless.service.provider.runtime = 'nodejs'; expect(docker.match({runtime: 'docker', handler: 'repo/image'})).to.equal(true) }); it('should match with provider runtime', () => { serverless.service.provider.runtime = 'docker'; expect(docker.match({handler: 'repo/image'})).to.equal(true) }); it('should not match when wrong explicit runtime', () => { expect(docker.match({runtime: 'nodejs', handler: 'repo/image'})).to.equal(false) }); it('should not match when wrong provider runtime', () => { serverless.service.provider.runtime = 'nodejs'; expect(docker.match({handler: 'repo/image'})).to.equal(false) }); it('should not match default runtime', () => { expect(docker.match({handler: 'repo/image'})).to.equal(false) }); it('should not match when missing handler', () => { expect(docker.match({})).to.equal(false) }); }); describe('#exec()', () => { it('should return docker definition for docker image handler', () => { const handler = 'repo/image' const exec = { kind: 'blackbox', image: 'repo/image' }; expect(docker.exec({ runtime: 'docker', handler })).to.deep.equal(exec); }); }); }); ================================================ FILE: compile/functions/runtimes/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const Runtimes = require('../index'); describe('Runtimes', () => { let serverless; let runtimes; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {}, getProvider: sandbox.spy()}; runtimes = new Runtimes(serverless); }); afterEach(() => { sandbox.restore(); }); describe('#exec()', () => { it('should throw error for unknown runtime', () => { expect(() => runtimes.exec({})) .to.throw(Error, /This runtime is not currently supported/); }); it('should execute and return thenable exec for thenable matching runtime', () => { const result = { foo: 'bar' } const match = sinon.stub().returns(true) const exec = sinon.stub().returns(Promise.resolve(result)) runtimes.runtimes = [{ match, exec }] return runtimes.exec({}).then(resp => { expect(resp).to.deep.equal(result) expect(match.called).to.equal(true) expect(exec.called).to.equal(true) }) }); it('should execute and return thenable exec for non-thenable matching runtime', () => { const result = { foo: 'bar' } const match = sinon.stub().returns(true) const exec = sinon.stub().returns(result) runtimes.runtimes = [{ match, exec }] return runtimes.exec({}).then(resp => { expect(resp).to.deep.equal(result) expect(match.called).to.equal(true) expect(exec.called).to.equal(true) }) }); }); }); ================================================ FILE: compile/functions/runtimes/tests/java.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const Java = require('../java'); const JSZip = require('jszip'); const fs = require('fs-extra'); describe('Java', () => { let serverless; let java; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = { classes: { Error }, service: {}, getProvider: sandbox.spy() }; serverless.service.provider = { name: 'openwhisk' }; java = new Java(serverless); }); afterEach(() => { sandbox.restore(); }); describe('#match()', () => { it('should match with explicit runtime', () => { serverless.service.provider.runtime = 'python'; expect(java.match({ runtime: 'java', handler: 'file:func' })).to.equal(true); }); it('should match with provider runtime', () => { serverless.service.provider.runtime = 'java'; expect(java.match({ handler: 'file:func' })).to.equal(true); }); it('should not match when wrong explicit runtime', () => { expect(java.match({ runtime: 'python', handler: 'file:func' })).to.equal(false); }); it('should not match when wrong provider runtime', () => { serverless.service.provider.runtime = 'python'; expect(java.match({ handler: 'file:func' })).to.equal(false); }); it('should not match when missing handler', () => { expect(java.match({})).to.equal(false); }); }); describe('#calculateFunctionMain()', () => { it('should return Main when no main class is defined', () => { expect(java.calculateFunctionMain({})).to.equal('Main'); }); it('should return Main when no main class is defined, but the file is', () => { expect(java.calculateFunctionMain({ handler: 'target/my-jar.jar' })).to.equal('Main'); }); it('should return Main when no main class is defined, but there is a colon', () => { expect(java.calculateFunctionMain({ handler: 'target/my-jar.jar:' })).to.equal('Main'); }); it('should return the provided class when a main class is defined', () => { expect( java.calculateFunctionMain({ handler: 'target/my-jar.jar:my.main.class.Name' }) ).to.equal('my.main.class.Name'); }); }); describe('#exec()', () => { it('should return java exec definition', () => { const fileContents = 'some file contents'; const handler = 'target/my-jar.jar:my.main.class.Name'; const exec = { main: 'my.main.class.Name', kind: 'java:default', code: new Buffer(fileContents), }; sandbox.stub(java, 'generateActionPackage', functionObj => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(java.exec({ handler, runtime: 'java' })).to.eventually.deep.equal(exec); }); it('should return java exec definition with custom image', () => { const fileContents = 'some file contents'; const handler = 'target/my-jar.jar:my.main.class.Name'; const exec = { main: 'my.main.class.Name', kind: 'blackbox', image: 'foo', code: new Buffer(fileContents), }; sandbox.stub(java, 'generateActionPackage', functionObj => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(java.exec({ handler, runtime: 'java', image: 'foo' })).to.eventually.deep.equal( exec ); }); }); describe('#convertHandlerToPath()', () => { it('should return file path passed', () => { expect(java.convertHandlerToPath('target/my-jar.jar')).to.be.equal('target/my-jar.jar'); }); it('should return file path passed, excluding the colon', () => { expect(java.convertHandlerToPath('target/my-jar.jar:')).to.be.equal('target/my-jar.jar'); }); it('should return file path passed without the class', () => { expect(java.convertHandlerToPath('target/my-jar.jar:my.main.class.Name')).to.be.equal( 'target/my-jar.jar' ); }); }); describe('#generateActionPackage()', () => { it('should throw error for missing handler file', () => { expect(() => java.generateActionPackage({ handler: './does_not_exist/my-jar.jar:my.main.class.Name' }) ).to.throw(Error, 'Function handler (./does_not_exist/my-jar.jar) does not exist.'); }); it('should create zip file with the Java jar file for the action', () => { java.serverless.service.package = { artifact: '/path/to/zip_file.zip' }; java.isValidFile = () => true; const zip = new JSZip(); const source = 'binary file contents'; zip.file('target/my-jar.jar', source); return zip.generateAsync({ type: 'nodebuffer' }).then(zipped => { sandbox.stub(fs, 'readFile', (path, cb) => { cb(null, zipped); }); return java .generateActionPackage({ handler: 'target/my-jar.jar:my.main.class.Name' }) .then(data => JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => zip .file('target/my-jar.jar') .async('string') .then(contents => { expect(contents).to.be.equal(source); }) ) ); }); }); }); }); ================================================ FILE: compile/functions/runtimes/tests/node.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const Node = require('../node'); const BaseRuntime = require('../base'); const JSZip = require("jszip"); const fs = require('fs-extra'); describe('Node', () => { let serverless; let node; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {}, getProvider: sandbox.spy()}; serverless.service.provider = { name: 'openwhisk' }; node = new Node(serverless); }); afterEach(() => { sandbox.restore(); }); describe('#match()', () => { it('should match with explicit runtime', () => { serverless.service.provider.runtime = 'python'; expect(node.match({runtime: 'nodejs', handler: 'file.func'})).to.equal(true) }); it('should match with provider runtime', () => { serverless.service.provider.runtime = 'nodejs'; expect(node.match({handler: 'file.func'})).to.equal(true) }); it('should match with default runtime', () => { expect(node.match({handler: 'file.func'})).to.equal(true) }); it('should not match when wrong explicit runtime', () => { expect(node.match({runtime: 'python', handler: 'file.func'})).to.equal(false) }); it('should not match when wrong provider runtime', () => { serverless.service.provider.runtime = 'python'; expect(node.match({handler: 'file.func'})).to.equal(false) }); it('should not match when missing handler', () => { expect(node.match({})).to.equal(false) }); }); describe('#exec()', () => { it('should return default nodejs exec definition', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; const exec = { main: 'some_func', kind: 'nodejs:default', code: new Buffer(fileContents) }; sandbox.stub(node, 'generateActionPackage', (functionObj) => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(node.exec({ handler })) .to.eventually.deep.equal(exec); }) it('should return custom nodejs exec definition', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; const exec = { main: 'some_func', kind: 'nodejs:6', code: new Buffer(fileContents) }; sandbox.stub(node, 'generateActionPackage', (functionObj) => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(node.exec({ handler, runtime: 'nodejs:6' })) .to.eventually.deep.equal(exec); }) it('should support using custom image', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; const exec = { main: 'some_func', image: 'blah', kind: 'blackbox', code: new Buffer(fileContents) }; sandbox.stub(node, 'generateActionPackage', (functionObj) => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(node.exec({ handler, image: 'blah', runtime: 'nodejs:6' })) .to.eventually.deep.equal(exec); }) }); describe('#isValidTypeScriptFile()', () => { it('should report valid file path when a js path is passed that has a ts file instead', () => { //We need to mock the node's `super` call, which is why we're using BaseRuntime sandbox.stub(BaseRuntime.prototype, 'isValidFile', (path) => { expect(path).to.equal('valid_typescript_handler_wrong_extension.ts'); return true; }); expect(node.isValidTypeScriptFile('valid_typescript_handler_wrong_extension.js')).to.equal(true) }); }); describe('#isValidFile()', () => { it('should still allow a js file to be used for handler', () => { //We need to mock the node's `super` call, which is why we're using BaseRuntime sandbox.stub(BaseRuntime.prototype, 'isValidFile', (path) => { expect(path).to.equal('valid_js_handler.js'); return true; }); expect(node.isValidFile('valid_js_handler.js')).to.equal(true) }); }); describe('#generateActionPackage()', () => { it('should throw error for missing handler file', () => { expect(() => node.generateActionPackage({handler: 'does_not_exist.main'})) .to.throw(Error, 'Function handler (does_not_exist.js) does not exist.'); }) it('should read service artifact and add package.json for handler', () => { node.serverless.service.package = {artifact: '/path/to/zip_file.zip'}; node.isValidFile = () => true const zip = new JSZip(); zip.file("handler.js", "function main() { return {}; }"); zip.file("package.json", '{"main": "index.js"}') return zip.generateAsync({type:"nodebuffer"}).then(zipped => { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return node.generateActionPackage({handler: 'handler.main'}).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { return zip.file("package.json").async("string").then(package_json => { expect(package_json).to.be.equal('{"main":"handler.js"}') }) }) }) }); }) it('should read service artifact and add package.json for relative path handler', () => { node.serverless.service.package = { artifact: '/path/to/zip_file.zip' }; node.isValidFile = () => true; const zip = new JSZip(); zip.file('folder/handler.js', 'function main() { return {}; }'); zip.file('folder/package.json', '{"main": "index.js"}'); return zip.generateAsync({ type: 'nodebuffer' }).then(zipped => { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return node.generateActionPackage({ handler: '../folder/handler.main' }).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(actionPackage => { return actionPackage.file('package.json').async('string').then(packageJson => { expect(packageJson).to.be.equal('{"main":"folder/handler.js"}'); }); }); }); }); }); it('should handle service artifact for individual function handler', () => { const functionObj = {handler: 'handler.main', package: { artifact: '/path/to/zip_file.zip'}} node.serverless.service.package = {individually: true}; node.isValidFile = () => true const zip = new JSZip(); zip.file("handler.js", "function main() { return {}; }"); zip.file("package.json", '{"main": "index.js"}') return zip.generateAsync({type:"nodebuffer"}).then(zipped => { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return node.generateActionPackage(functionObj).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { return zip.file("package.json").async("string").then(package_json => { expect(package_json).to.be.equal('{"main":"handler.js"}') }) }) }) }); }) }) }); ================================================ FILE: compile/functions/runtimes/tests/php.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const Php = require('../php'); const JSZip = require("jszip"); const fs = require('fs-extra'); describe('Php', () => { let serverless; let php; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {}, getProvider: sandbox.spy()}; serverless.service.provider = { name: 'openwhisk' }; php = new Php(serverless); }); afterEach(() => { sandbox.restore(); }); describe('#match()', () => { it('should match with explicit runtime', () => { serverless.service.provider.runtime = 'python'; expect(php.match({runtime: 'php', handler: 'file.func'})).to.equal(true) }); it('should match with provider runtime', () => { serverless.service.provider.runtime = 'php'; expect(php.match({handler: 'file.func'})).to.equal(true) }); it('should not match when wrong explicit runtime', () => { expect(php.match({runtime: 'python', handler: 'file.func'})).to.equal(false) }); it('should not match when wrong provider runtime', () => { serverless.service.provider.runtime = 'python'; expect(php.match({handler: 'file.func'})).to.equal(false) }); it('should not match when missing handler', () => { expect(php.match({})).to.equal(false) }); }); describe('#exec()', () => { it('should return php exec definition', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; const exec = { main: 'some_func', kind: 'php:default', code: new Buffer(fileContents) }; sandbox.stub(php, 'generateActionPackage', (functionObj) => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(php.exec({ handler, runtime: 'php'})) .to.eventually.deep.equal(exec); }) it('should support using custom image', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; const exec = { main: 'some_func', image: 'blah', kind: 'blackbox', code: new Buffer(fileContents) }; sandbox.stub(php, 'generateActionPackage', (functionObj) => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(php.exec({ handler, image: 'blah', runtime: 'php:7.1' })) .to.eventually.deep.equal(exec); }) }); describe('#generateActionPackage()', () => { it('should throw error for missing handler file', () => { expect(() => php.generateActionPackage({handler: 'does_not_exist.main'})) .to.throw(Error, 'Function handler (does_not_exist.php) does not exist.'); }) it('should read service artifact and add index.php for handler', () => { php.serverless.service.package = {artifact: '/path/to/zip_file.zip'}; php.isValidFile = () => true const zip = new JSZip(); const source = ' { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return php.generateActionPackage({handler: 'handler.main'}).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { expect(zip.file("handler.php")).to.be.equal(null) return zip.file("index.php").async("string").then(main => { expect(main).to.be.equal(source) }) }) }) }); }) it('should handle service artifact for individual function handler', () => { const functionObj = {handler: 'handler.main', package: { artifact: '/path/to/zip_file.zip'}} php.serverless.service.package = {individually: true}; php.isValidFile = () => true const zip = new JSZip(); const source = ' { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return php.generateActionPackage(functionObj).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { expect(zip.file("handler.php")).to.be.equal(null) return zip.file("index.php").async("string").then(main => { expect(main).to.be.equal(source) }) }) }) }); }); }) }); ================================================ FILE: compile/functions/runtimes/tests/python.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const Python = require('../python'); const JSZip = require("jszip"); const fs = require('fs-extra'); describe('Python', () => { let serverless; let node; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {}, getProvider: sandbox.spy()}; serverless.service.provider = { name: 'openwhisk' }; node = new Python(serverless); }); afterEach(() => { sandbox.restore(); }); describe('#match()', () => { it('should match with explicit runtime', () => { serverless.service.provider.runtime = 'nodejs'; expect(node.match({runtime: 'python', handler: 'file.func'})).to.equal(true) }); it('should match with provider runtime', () => { serverless.service.provider.runtime = 'python'; expect(node.match({handler: 'file.func'})).to.equal(true) }); it('should not match when wrong explicit runtime', () => { expect(node.match({runtime: 'nodejs', handler: 'file.func'})).to.equal(false) }); it('should not match when wrong provider runtime', () => { serverless.service.provider.runtime = 'nodejs'; expect(node.match({handler: 'file.func'})).to.equal(false) }); it('should not match default runtime', () => { expect(node.match({handler: 'file.func'})).to.equal(false) }); it('should not match when missing handler', () => { expect(node.match({})).to.equal(false) }); }); describe('#exec()', () => { it('should return python exec definition', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; const exec = { main: 'some_func', kind: 'python', code: new Buffer(fileContents) }; sandbox.stub(node, 'generateActionPackage', (functionObj) => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(node.exec({ handler, runtime: 'python'})) .to.eventually.deep.equal(exec); }) it('should support using custom image', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; const exec = { main: 'some_func', image: 'blah', kind: 'blackbox', code: new Buffer(fileContents) }; sandbox.stub(node, 'generateActionPackage', (functionObj) => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(node.exec({ handler, image: 'blah', runtime: 'python' })) .to.eventually.deep.equal(exec); }) }); describe('#generateActionPackage()', () => { it('should throw error for missing handler file', () => { expect(() => node.generateActionPackage({handler: 'does_not_exist.main'})) .to.throw(Error, 'Function handler (does_not_exist.py) does not exist.'); }) it('should read service artifact and add __main__.py for handler', () => { node.serverless.service.package = {artifact: '/path/to/zip_file.zip'}; node.isValidFile = () => true const zip = new JSZip(); zip.file("handler.py", "def main(dict):\n\treturn {}"); return zip.generateAsync({type:"nodebuffer"}).then(zipped => { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return node.generateActionPackage({handler: 'handler.main'}).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { expect(zip.file("handler.py")).to.be.equal(null) return zip.file("__main__.py").async("string").then(package_json => { expect(package_json).to.be.equal('def main(dict):\n\treturn {}') }) }) }) }); }) it('should handle service artifact for individual function handler', () => { const functionObj = {handler: 'handler.main', package: { artifact: '/path/to/zip_file.zip'}} node.serverless.service.package = {individually: true}; node.isValidFile = () => true const zip = new JSZip(); zip.file("handler.py", "def main(dict):\n\treturn {}"); return zip.generateAsync({type:"nodebuffer"}).then(zipped => { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return node.generateActionPackage(functionObj).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { expect(zip.file("handler.py")).to.be.equal(null) return zip.file("__main__.py").async("string").then(package_json => { expect(package_json).to.be.equal('def main(dict):\n\treturn {}') }) }) }) }); }) }) }); ================================================ FILE: compile/functions/runtimes/tests/ruby.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const Ruby = require('../ruby'); const JSZip = require('jszip'); const fs = require('fs-extra'); describe('Ruby', () => { let serverless; let ruby; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {}, getProvider: sandbox.spy()}; serverless.service.provider = { name: 'openwhisk' }; ruby = new Ruby(serverless); }); afterEach(() => { sandbox.restore(); }); describe('#match()', () => { it('should match with explicit runtime', () => { serverless.service.provider.runtime = 'python'; expect(ruby.match({runtime: 'ruby', handler: 'file.func'})).to.equal(true) }); it('should match with provider runtime', () => { serverless.service.provider.runtime = 'ruby'; expect(ruby.match({handler: 'file.func'})).to.equal(true) }); it('should not match when wrong explicit runtime', () => { expect(ruby.match({runtime: 'python', handler: 'file.func'})).to.equal(false) }); it('should not match when wrong provider runtime', () => { serverless.service.provider.runtime = 'python'; expect(ruby.match({handler: 'file.func'})).to.equal(false) }); it('should not match when missing handler', () => { expect(ruby.match({})).to.equal(false) }); }); describe('#exec()', () => { it('should return ruby exec definition', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; const exec = { main: 'some_func', kind: 'ruby:default', code: new Buffer(fileContents) }; sandbox.stub(ruby, 'generateActionPackage', (functionObj) => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(ruby.exec({ handler, runtime: 'ruby'})) .to.eventually.deep.equal(exec); }) it('should support using custom image', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; const exec = { main: 'some_func', image: 'blah', kind: 'blackbox', code: new Buffer(fileContents) }; sandbox.stub(ruby, 'generateActionPackage', (functionObj) => { expect(functionObj.handler).to.equal(handler); return Promise.resolve(new Buffer(fileContents)); }); return expect(ruby.exec({ handler, image: 'blah', runtime: 'ruby:7.1' })) .to.eventually.deep.equal(exec); }) }); describe('#generateActionPackage()', () => { it('should throw error for missing handler file', () => { expect(() => ruby.generateActionPackage({handler: 'does_not_exist.main'})) .to.throw(Error, 'Function handler (does_not_exist.rb) does not exist.'); }) it('should read service artifact and add main.rb for handler', () => { ruby.serverless.service.package = {artifact: '/path/to/zip_file.zip'}; ruby.isValidFile = () => true const zip = new JSZip(); const source = 'def main(args)\nname = args["name"] || "stranger"\ngreeting = "Hello #{name}!"\n{ "greeting" => greeting }\nend' zip.file("handler.rb", source); return zip.generateAsync({type:"nodebuffer"}).then(zipped => { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return ruby.generateActionPackage({handler: 'handler.main'}).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { expect(zip.file("handler.rb")).to.be.equal(null) return zip.file("main.rb").async("string").then(main => { expect(main).to.be.equal(source) }) }) }) }); }) it('should handle service artifact for individual function handler', () => { const functionObj = {handler: 'handler.main', package: { artifact: '/path/to/zip_file.zip'}} ruby.serverless.service.package = {individually: true}; ruby.isValidFile = () => true const zip = new JSZip(); const source = 'def main(args)\nname = args["name"] || "stranger"\ngreeting = "Hello #{name}!"\n{ "greeting" => greeting }\nend' zip.file("handler.rb", source); return zip.generateAsync({type:"nodebuffer"}).then(zipped => { sandbox.stub(fs, 'readFile', (path, cb) => { expect(path).to.equal('/path/to/zip_file.zip'); cb(null, zipped); }); return ruby.generateActionPackage(functionObj).then(data => { return JSZip.loadAsync(new Buffer(data, 'base64')).then(zip => { expect(zip.file("handler.rb")).to.be.equal(null) return zip.file("main.rb").async("string").then(main => { expect(main).to.be.equal(source) }) }) }) }); }); }) }); ================================================ FILE: compile/functions/runtimes/tests/sequence.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const Sequence = require('../sequence'); describe('Sequence', () => { let serverless; let sequence; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {}, getProvider: sandbox.spy()}; serverless.service.provider = { name: 'openwhisk' }; sequence = new Sequence(serverless); }); afterEach(() => { sandbox.restore(); }); describe('#match()', () => { it('should match function object with sequence property', () => { expect(sequence.match({sequence: true})).to.equal(true) }); it('should ignore function object without sequence property', () => { expect(sequence.match({})).to.equal(false) }); }); describe('#exec()', () => { it('should return sequence definition for sequence function', () => { const exec = { kind: 'sequence', components: ["/_/one", "/a/two", "/a/b/three"] }; sequence.serverless.service.getFunction = () => ({name: 'one'}); sequence.serverless.service.provider.namespace = 'namespace'; expect(sequence.exec({ sequence: ["one", "/a/two", "/a/b/three"] })).to.deep.equal(exec); }); }); }); ================================================ FILE: compile/functions/runtimes/tests/swift.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const Swift = require('../swift'); const JSZip = require("jszip"); const fs = require('fs-extra'); describe('Swift', () => { let serverless; let node; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {}, getProvider: sandbox.spy()}; serverless.service.provider = { name: 'openwhisk' }; node = new Swift(serverless); }); afterEach(() => { sandbox.restore(); }); describe('#match()', () => { it('should match with explicit runtime', () => { serverless.service.provider.runtime = 'nodejs'; expect(node.match({runtime: 'swift', handler: 'file.func'})).to.equal(true) }); it('should match with provider runtime', () => { serverless.service.provider.runtime = 'swift'; expect(node.match({handler: 'file.func'})).to.equal(true) }); it('should not match when wrong explicit runtime', () => { expect(node.match({runtime: 'nodejs', handler: 'file.func'})).to.equal(false) }); it('should not match when wrong provider runtime', () => { serverless.service.provider.runtime = 'nodejs'; expect(node.match({handler: 'file.func'})).to.equal(false) }); it('should not match default runtime', () => { expect(node.match({handler: 'file.func'})).to.equal(false) }); it('should not match when missing handler', () => { expect(node.match({})).to.equal(false) }); }); describe('#exec()', () => { it('should return swift exec with source file handler', () => { const fileContents = 'some file contents'; const handler = 'handler.some_func'; node.isValidFile = () => true sandbox.stub(fs, 'readFileSync', (path) => { expect(path).to.equal('handler.swift'); return Buffer.from(fileContents) }); const exec = { main: 'some_func', binary: false, kind: 'swift:default', code: fileContents }; return expect(node.exec({ handler, runtime: 'swift'})) .to.eventually.deep.equal(exec); }) it('should return swift exec with zip file handler', () => { const handler = 'my_file.zip'; node.isValidFile = () => true const zip = new JSZip(); const source = 'binary file contents' zip.file("exec", source); return zip.generateAsync({type:"nodebuffer"}).then(zipped => { sandbox.stub(fs, 'readFileSync', (path) => { expect(path).to.equal(handler); return zipped }); const b64 = zipped.toString('base64') const exec = { main: 'main', binary: true, kind: 'swift:default', code: b64 }; return expect(node.exec({ handler, runtime: 'swift'})) .to.eventually.deep.equal(exec); }) }) }); describe('#convertHandlerToPath()', () => { it('should return file path for swift function handlers', () => { expect(node.convertHandlerToPath('file.func')).to.be.equal('file.swift') }) it('should return file path for zip files', () => { expect(node.convertHandlerToPath('my_file.zip')).to.be.equal('my_file.zip') }) }) }); ================================================ FILE: compile/functions/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const OpenWhiskCompileFunctions = require('../index'); describe('OpenWhiskCompileFunctions', () => { let serverless; let openwhiskCompileFunctions; let sandbox; const openwhiskResourcesMockObject = { first: { actionName: 'first', namespace: '', action: { exec: { kind: 'nodejs:default', code: 'function main() {};' }, }, }, second: { actionName: 'second', namespace: '', action: { exec: { kind: 'nodejs:default', code: 'function main() {};' }, }, }, }; beforeEach(() => { sandbox = sinon.sandbox.create(); const options = { stage: 'dev', region: 'us-east-1', }; serverless = {classes: {Error}, service: {}, getProvider: sandbox.spy()}; openwhiskCompileFunctions = new OpenWhiskCompileFunctions(serverless, options); serverless.service.service = 'serviceName'; serverless.service.provider = { namespace: '', apihost: '', auth: '', }; serverless.service.provider = { name: 'openwhisk' }; serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskCompileFunctions.setup(); }); afterEach(() => { sandbox.restore(); }); describe('#disableSeqPackaging()', () => { it('should add disable flag to sequences', () => { const fns = { first: { handler: 'foo.js' }, second: { handler: 'foo.js' }, seq: { sequence: [ 'first', 'second' ] } } openwhiskCompileFunctions.serverless.service.getAllFunctions = () => Object.keys(fns) openwhiskCompileFunctions.serverless.service.getFunction = name => fns[name]; openwhiskCompileFunctions.disableSeqPackaging() expect(fns.seq.package.disable).to.be.true }); }); describe('#constructAnnotations()', () => { it('should handle missing annotations', () => { expect(openwhiskCompileFunctions.constructAnnotations()) .to.deep.equal([]); }) it('should handle empty annotations', () => { expect(openwhiskCompileFunctions.constructAnnotations({})) .to.deep.equal([]); }) it('should handle annotations present', () => { expect(openwhiskCompileFunctions.constructAnnotations({ hello: 'world', foo: 'bar' })).to.deep.equal([ { key: 'hello', value: 'world' }, { key: 'foo', value: 'bar' } ]); }) it('should add final annotations if web-export is present', () => { expect(openwhiskCompileFunctions.constructAnnotations({ hello: 'world', foo: 'bar', "web-export": true })).to.deep.equal([ { key: 'hello', value: 'world' }, { key: 'foo', value: 'bar' }, { key: 'web-export', value: true }, { key: 'final', value: true } ]); }) }) describe('#calculateFunctionNameSpace()', () => { it('should return namespace from function object', () => { expect(openwhiskCompileFunctions .calculateFunctionNameSpace('testing', { namespace: 'testing' }) ).to.equal('testing'); }); it('should return namespace from service provider', () => { openwhiskCompileFunctions.serverless.service.provider = { namespace: 'testing' }; expect(openwhiskCompileFunctions.calculateFunctionNameSpace('testing', {})) .to.equal('testing'); }); }); describe('#logCompiledFunction()', () => { it('should log function contents with code to console.', () => { const log = sandbox.stub(openwhiskCompileFunctions.serverless.cli, 'log') const clog = sandbox.stub(openwhiskCompileFunctions.serverless.cli, 'consoleLog') openwhiskCompileFunctions.logCompiledFunction('first', openwhiskResourcesMockObject.first) expect(log.calledOnce).to.be.equal(true); const clone = JSON.parse(JSON.stringify(openwhiskResourcesMockObject.first)) clone.action.exec.code = '' expect(log.args[0][0]).to.be.equal(`Compiled Function (first): ${JSON.stringify(clone)}`) }); it('should log function contents without code to console.', () => { const log = sandbox.stub(openwhiskCompileFunctions.serverless.cli, 'log') const clog = sandbox.stub(openwhiskCompileFunctions.serverless.cli, 'consoleLog') const clone = JSON.parse(JSON.stringify(openwhiskResourcesMockObject.first)) delete clone.action.exec.code openwhiskCompileFunctions.logCompiledFunction('first', clone) expect(log.calledOnce).to.be.equal(true); expect(log.args[0][0]).to.be.equal(`Compiled Function (first): ${JSON.stringify(clone)}`) }); }); describe('#compileFunctions()', () => { it('should create action function with parsed parameters', () => { let functionObject = { handler: "foo.js", name: "name", namespace: "namespace", overwrite: "overwrite", memory: 123, concurrency: 456, timeout: 789, parameters: { hello: "world", foo: "bar" }, annotations: { hello: "world", foo: "bar" } }; openwhiskCompileFunctions.serverless.service.getAllFunctions = () => ['service_name']; openwhiskCompileFunctions.serverless.service.getFunction = () => functionObject; sandbox.stub(openwhiskCompileFunctions, 'runtimes', { exec: () => Promise.resolve() }); return openwhiskCompileFunctions.compileFunctions().then(functionActions => { let functionAction = functionActions[0]; expect(functionAction.actionName).to.be.equal(functionObject.name); expect(functionAction.namespace).to.be.equal(functionObject.namespace); expect(functionAction.overwrite).to.be.equal(functionObject.overwrite); expect(functionAction.action.limits.memory).to.be.equal(functionObject.memory); expect(functionAction.action.limits.concurrency).to.be.equal(functionObject.concurrency); expect(functionAction.action.limits.timeout).to.be.equal(functionObject.timeout * 1000); let paramsAndAnnotations = [ { key: 'hello', value: 'world' }, { key: 'foo', value: 'bar' } ]; expect(functionAction.action.parameters).to.deep.equal(paramsAndAnnotations); expect(functionAction.action.annotations).to.deep.equal(paramsAndAnnotations); }); }); it('should not add implicit limits parameters', () => { let functionObject = { handler: "foo.js", name: "name" }; openwhiskCompileFunctions.serverless.service.getAllFunctions = () => ['service_name']; openwhiskCompileFunctions.serverless.service.getFunction = () => functionObject; sandbox.stub(openwhiskCompileFunctions, 'runtimes', { exec: () => Promise.resolve() }); return openwhiskCompileFunctions.compileFunctions().then(functionActions => { let functionAction = functionActions[0]; expect(functionAction.actionName).to.be.equal(functionObject.name); expect(functionAction.action.limits.memory).to.be.undefined; expect(functionAction.action.limits.concurrency).to.be.undefined; expect(functionAction.action.limits.timeout).to.be.undefined; }); }); it('should throw an error if the resource section is not available', () => { openwhiskCompileFunctions.serverless.service.actions = null; expect(() => openwhiskCompileFunctions.compileFunctions()) .to.throw(Error, /Missing Resources section/); }); it('should throw an error if function definition has handler and sequence', () => { const f = { sequence: true, handler: true }; openwhiskCompileFunctions.serverless.service.getAllFunctions = () => ['service_name']; openwhiskCompileFunctions.serverless.service.getFunction = () => f; expect(() => openwhiskCompileFunctions.compileFunctions()) .to.throw(Error, /both "handler" and "sequence" properties/); }); it('should throw an error if function definition is missing a handler or sequence', () => { openwhiskCompileFunctions.serverless.service.getAllFunctions = () => ['service_name']; openwhiskCompileFunctions.serverless.service.getFunction = () => ({}); expect(() => openwhiskCompileFunctions.compileFunctions()) .to.throw(Error, /Missing "handler" or "sequence"/); }); it('should throw an error if unable to read function handler file', () => { openwhiskCompileFunctions.serverless.service.getAllFunctions = () => ['service_name']; const missing = { handler: 'missing.handler' }; openwhiskCompileFunctions.serverless.service.getFunction = () => missing; sandbox.stub(openwhiskCompileFunctions, 'compileFunction', () => Promise.reject()); return expect(openwhiskCompileFunctions.compileFunctions()).to.be.rejected; }); it('should create corresponding function resources', () => { const keys = Object.keys(openwhiskResourcesMockObject); const handler = function (name) { return { handler: `${name}.handler` }; }; openwhiskCompileFunctions.serverless.service.getAllFunctions = () => keys; openwhiskCompileFunctions.serverless.service.getFunction = name => handler(name); const log = sandbox.stub(openwhiskCompileFunctions, 'logCompiledFunction') const mock = openwhiskResourcesMockObject; sandbox.stub( openwhiskCompileFunctions, 'compileFunction', name => Promise.resolve(mock[name])); const f = openwhiskCompileFunctions.serverless.service.actions; return openwhiskCompileFunctions.compileFunctions().then(() => { expect(f).to.deep.equal(openwhiskResourcesMockObject) expect(log.called).to.be.equal(false); }); }); it('should log compiled functions with verbose flag', () => { const keys = Object.keys(openwhiskResourcesMockObject); const handler = function (name) { return { handler: `${name}.handler` }; }; openwhiskCompileFunctions.options.verbose = true; openwhiskCompileFunctions.serverless.service.getAllFunctions = () => keys; openwhiskCompileFunctions.serverless.service.getFunction = name => handler(name); const log = sandbox.stub(openwhiskCompileFunctions, 'logCompiledFunction') const mock = openwhiskResourcesMockObject; sandbox.stub( openwhiskCompileFunctions, 'compileFunction', name => Promise.resolve(mock[name])); const f = openwhiskCompileFunctions.serverless.service.actions; return openwhiskCompileFunctions.compileFunctions().then(() => { expect(log.calledTwice).to.be.equal(true); }); }); }); }); ================================================ FILE: compile/message_hub/README.md ================================================ # Compile Triggers This plugins compiles the `message_hub` events in `serverless.yaml` to corresponding [OpenWhisk Message Hub Trigger Feeds](https://github.com/openwhisk/openwhisk-package-kafka) definitions. ## How it works `Compile Message Hub` hooks into the [`package:compileEvents`](/lib/plugins/deploy) lifecycle. It loops over all schedule event which are defined in `serverless.yaml`. ### Using Package Parameters IBM Message Hub instances can be provisioned through the IBM Bluemix platform. OpenWhisk on Bluemix will export Message Hub service credentials bound to a package with the following name: ``` /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 ``` Rather than having to manually define all the properties needed by the Message Hub trigger feed, you can reference a package to use instead. Credentials from the referenced package will be used when executing the trigger feed. Developers only need to add the topic to listen to for each trigger. ```yaml # serverless.yaml functions: index: handler: users.main events: - message_hub: package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 topic: my_kafka_topic ``` The plugin will create a trigger called `${serviceName}_${fnName}_messagehub_${topic}` and a rule called `${serviceName}_${fnName}_messagehub_${topic}_rule` to bind the function to the message hub events. The trigger and rule names created can be set explicitly using the `trigger` and `rule` parameters. Other functions can bind to the same trigger using the inline `trigger` event referncing this trigger name. ```yaml # serverless.yaml functions: index: handler: users.main events: - message_hub: package: /${BLUEMIX_ORG}_${BLUEMIX_SPACE}/Bluemix_${SERVICE_NAME}_Credentials-1 topic: my_kafka_topic trigger: log_events rule: connect_index_to_kafka another: handler: users.another events: - trigger: log_events ``` ### Using Manual Parameters Trigger feed parameters for the Message Hub event source can be defined explicitly, rather than using pulling credentials from a package. ```yaml # serverless.yaml functions: index: handler: users.main events: - message_hub: topic: my_kafka_topic brokers: afka01-prod01.messagehub.services.us-south.bluemix.net:9093 user: USERNAME password: PASSWORD admin_url: https://kafka-admin-prod01.messagehub.services.us-south.bluemix.net:443 json: true binary_key: true binary_value: true ``` `topic`, `brokers`, `user`, `password` and `admin_url` are mandatory parameters. ================================================ FILE: compile/message_hub/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const config_properties = ['user', 'password', 'brokers', 'topic', 'admin_url'] class OpenWhiskCompileMessageHub { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('openwhisk'); this.default_package = '/whisk.system/messaging' this.hooks = { 'before:package:compileEvents': () => BbPromise.bind(this) .then(this.setup) .then(this.processMessageHubEvents) }; } setup() { if (!this.serverless.service.resources) { this.serverless.service.resources = {}; } if (!this.serverless.service.resources.triggers) { this.serverless.service.resources.triggers = {}; } if (!this.serverless.service.resources.rules) { this.serverless.service.resources.rules = {}; } } validateConfig (fnName, config) { if (!config.topic) { throw new this.serverless.classes.Error( `Message Hub event property (topic) missing on function: ${fnName}` ) } if (!config.package) { config_properties.forEach(prop => { if (!config[prop]) { throw new this.serverless.classes.Error( `Message Hub event property (${prop}) missing on function: ${fnName}` ) } }) } } compileMessageHubTrigger (fnName, config) { this.validateConfig(fnName, config) const name = config.trigger || this.defaultMessageHubName(fnName, config.topic) const feed = `${config.package || this.default_package}/messageHubFeed` const feed_parameters = { topic: config.topic, isJSONData: config.json || false, isBinaryKey: config.binary_key || false, isBinaryValue: config.binary_value || false } if (!config.package) { feed_parameters.user = config.user feed_parameters.password = config.password feed_parameters.kafka_brokers_sasl = Array.isArray(config.brokers) ? config.brokers.join(',') : config.brokers feed_parameters.kafka_admin_url = config.admin_url } return { name, content: { feed, feed_parameters } } } defaultMessageHubName (fnName, topic) { return `${this.serverless.service.service}_${fnName}_messagehub_${topic}` } processMessageHubEvent (fnName, config) { const fnObj = this.serverless.service.getFunction(fnName) const trigger = this.compileMessageHubTrigger(fnName, config) const rule = config.rule || `${this.defaultMessageHubName(fnName, config.topic)}_rule` fnObj.events.push({ trigger: { name: trigger.name, rule } }) this.serverless.service.resources.triggers[trigger.name] = trigger.content } processMessageHubEvents () { this.serverless.service.getAllFunctions().forEach(name => { const fn = this.serverless.service.getFunction(name) const events = (fn.events || []).filter(e => e.message_hub) events.forEach(e => this.processMessageHubEvent(name, e.message_hub)) }) } } module.exports = OpenWhiskCompileMessageHub ================================================ FILE: compile/message_hub/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const OpenWhiskCompileMessageHub = require('../index'); describe('OpenWhiskCompileMessageHub', () => { let serverless; let sandbox; let openwhiskCompileMessageHub; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskCompileMessageHub = new OpenWhiskCompileMessageHub(serverless, options); serverless.service.service = 'serviceName'; serverless.service.provider = { namespace: 'testing', apihost: '', auth: '', }; serverless.cli = { log: () => {} }; openwhiskCompileMessageHub.setup() }); afterEach(() => { sandbox.restore(); }); describe('#processMessageHubEvents()', () => { it('should call processMessageHubEvent for each message hub event.', () => { const service = openwhiskCompileMessageHub.serverless.service; const fns = { first: { events: [{}, {message_hub: {package: 'testing_package', topic: 'some_topic'}}, {trigger: true}] }, second: { events: [{message_hub: {package: 'another_package', topic: 'some_topic'}}] }, third: {} } service.getAllFunctions = () => Object.keys(fns) service.getFunction = name => fns[name]; const spy = openwhiskCompileMessageHub.processMessageHubEvent = sinon.spy() openwhiskCompileMessageHub.processMessageHubEvents() expect(spy.calledTwice).to.be.equal(true) expect(spy.withArgs("first", {package: 'testing_package', topic: 'some_topic'}).calledOnce).to.be.equal(true) expect(spy.withArgs("second", {package: 'another_package', topic: 'some_topic'}).calledOnce).to.be.equal(true) }) }) describe('#processMessageHubEvents()', () => { it('should create trigger & rules and update manifest resources.', () => { const message_hub = { package: 'some_package', topic: 'testing' } const fnObj = { events: [{message_hub}] } serverless.service.getFunction = () => fnObj openwhiskCompileMessageHub.compileMessageHubTrigger = () => ({name: 'serviceName_fnName_messagehub_testing', content: { a: 1 }}) openwhiskCompileMessageHub.processMessageHubEvent("fnName", fnObj.events[0].message_hub) expect(fnObj.events[1]).to.be.deep.equal({ trigger: { name: 'serviceName_fnName_messagehub_testing', rule: 'serviceName_fnName_messagehub_testing_rule' } }) expect(serverless.service.resources.triggers).to.be.deep.equal({serviceName_fnName_messagehub_testing: {a: 1}}) }) }) describe('#compileMessageHubTrigger()', () => { it('should throw errors for missing topic parameter.', () => { expect(() => openwhiskCompileMessageHub.compileMessageHubTrigger('testing', {})) .to.throw(Error, 'Message Hub event property (topic) missing on function: testing'); }) it('should throw errors for missing mandatory parameters without package', () => { const config = { topic: 'topic', user: 'user', password: 'password', admin_url: 'url', brokers: 'brokers' } Object.keys(config).forEach(key => { const cloned = Object.assign({}, config) cloned[key] = '' expect(() => openwhiskCompileMessageHub.compileMessageHubTrigger('testing', cloned)) .to.throw(Error, `Message Hub event property (${key}) missing on function: testing`); }) }) it('should return trigger for message hub provider using package.', () => { const topic = 'my_topic', pkge = '/bluemixOrg_bluemixSpace/packageId' const trigger = openwhiskCompileMessageHub.compileMessageHubTrigger('testing', { topic, 'package': pkge }) expect(trigger).to.be.deep.equal({ name: `${serverless.service.service}_testing_messagehub_${topic}`, content: { feed: `${pkge}/messageHubFeed`, feed_parameters: { topic: `${topic}`, isJSONData: false, isBinaryKey: false, isBinaryValue: false } } }) }) it('should return trigger for message hub provider using package with options.', () => { const topic = 'my_topic', pkge = '/bluemixOrg_bluemixSpace/packageId' const trigger = openwhiskCompileMessageHub.compileMessageHubTrigger('testing', { json: true, binary_value: true, binary_key: true, topic, 'package': pkge }) expect(trigger).to.be.deep.equal({ name: `${serverless.service.service}_testing_messagehub_${topic}`, content: { feed: `${pkge}/messageHubFeed`, feed_parameters: { topic: `${topic}`, isJSONData: true, isBinaryKey: true, isBinaryValue: true } } }) }) it('should return trigger with minimum message hub config properties.', () => { const config = { topic: 'topic', user: 'user', password: 'password', admin_url: 'url', brokers: 'brokers' } const trigger = openwhiskCompileMessageHub.compileMessageHubTrigger('testing', config) expect(trigger).to.be.deep.equal({ name: `${serverless.service.service}_testing_messagehub_${config.topic}`, content: { feed: `/whisk.system/messaging/messageHubFeed`, feed_parameters: { kafka_brokers_sasl: config.brokers, user: config.user, password: config.password, topic: config.topic, kafka_admin_url: config.admin_url, isJSONData: false, isBinaryKey: false, isBinaryValue: false } } }) }) it('should return trigger with optional message hub config properties.', () => { const config = { json: true, binary_key: true, binary_value: true, topic: 'topic', user: 'user', password: 'password', admin_url: 'url', brokers: ['a', 'b', 'c'] } const trigger = openwhiskCompileMessageHub.compileMessageHubTrigger('testing', config) expect(trigger).to.be.deep.equal({ name: `${serverless.service.service}_testing_messagehub_${config.topic}`, content: { feed: `/whisk.system/messaging/messageHubFeed`, feed_parameters: { kafka_brokers_sasl: config.brokers.join(','), user: config.user, password: config.password, topic: config.topic, kafka_admin_url: config.admin_url, isJSONData: true, isBinaryKey: true, isBinaryValue: true } } }) }) }) }); ================================================ FILE: compile/packages/README.md ================================================ # Compile Packages This plugins compiles the packages in `serverless.yaml` to corresponding [OpenWhisk Packages](https://github.com/openwhisk/openwhisk/blob/master/docs/packages.md) definitions. ## How it works `Compile Packages` hooks into the [`package:compileEvents`](/lib/plugins/deploy) lifecycle. It loops over all packages which are defined in `serverless.yaml`. ### Implicit Packages Actions 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 ``` ### Binding Packages OpenWhisk also supports "binding" external packages into your workspace. Bound packages can have default parameters set for shared actions. For example, binding the `/whisk.system/cloudant` package into a new package allows you to set default values for the `username`, `password` and `dbname` properties. Actions from this package can then be invoked with having to pass these parameters in. Define packages explicitly with a `binding` parameter to use this behaviour. ```yaml resources: packages: mySamples: binding: /whisk.system/cloudant parameters: username: bernie password: sanders dbname: vermont ``` For more details on package binding, please see the documentation [here](https://github.com/apache/incubator-openwhisk/blob/master/docs/packages.md#creating-and-using-package-bindings). ================================================ FILE: compile/packages/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); class OpenWhiskCompilePackages { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('openwhisk'); this.hooks = { 'before:package:compileEvents': () => BbPromise.bind(this) .then(this.setup) .then(this.renameManifestPackages) .then(this.mergeActionPackages), 'package:compileEvents': this.compilePackages.bind(this), }; } setup() { // This object will be used to store the Packages resource, passed directly to // the OpenWhisk SDK during the deploy process. this.serverless.service.packages = {}; } renameManifestPackages() { const resources = this.serverless.service.resources if (!resources || !resources.packages) return; const manifestPackages = resources.packages; Object.keys(manifestPackages).forEach(packageKey => { const pack = manifestPackages[packageKey]; if (pack.name && pack.name !== packageKey) { // move the package under the new name manifestPackages[pack.name] = pack; delete manifestPackages[packageKey]; } }) } mergeActionPackages() { const actionPackages = this.getActionPackages(); if (!actionPackages.length) return; if (!this.serverless.service.resources) { this.serverless.service.resources = {}; } if (!this.serverless.service.resources.packages) { this.serverless.service.resources.packages = {}; } const manifestPackages = this.serverless.service.resources.packages; actionPackages.forEach(pkge => { manifestPackages[pkge] = manifestPackages[pkge] || {} }) } getActionPackages() { const actionPackages = new Set(); this.serverless.service.getAllFunctions() .map(name => this.serverless.service.getFunction(name)) .filter(func => func.name) .forEach(func => { const id = func.name.match(/^(.+)\/.+$/) if (id) actionPackages.add(id[1]) }); return [...actionPackages]; } calculatePackageName(packageName, packageObject) { return packageObject.name || packageName; } compilePackage(name, params) { const effectiveName = this.calculatePackageName(name, params); const pkge = { name: effectiveName, overwrite: true, package: {} }; pkge.namespace = params.namespace || this.serverless.service.provider.namespace; if (params.hasOwnProperty('overwrite')) { pkge.overwrite = params.overwrite; } else if (this.serverless.service.provider.hasOwnProperty('overwrite')) { pkge.overwrite = params.overwrite; } if (params.hasOwnProperty('shared')) { pkge.package.publish = params.shared; } if (params.parameters) { pkge.package.parameters = Object.keys(params.parameters).map( key => ({ key, value: params.parameters[key] }) ); } if (params.annotations) { pkge.package.annotations = Object.keys(params.annotations).map( key => ({ key, value: params.annotations[key] }) ); } if (params.binding) { // package identifier must be in format: /namespace/package const to_bind = params.binding.match(/^\/(.+)\/(.+)$/) if (!to_bind) { throw new this.serverless.classes.Error(`Invalid Package Binding (${params.binding}). Must be in form: /namespace/package`); } pkge.package.binding = { name: to_bind[2], namespace: to_bind[1] } } if (this.options.verbose) { this.serverless.cli.log(`Compiled Package (${name}): ${JSON.stringify(pkge)}`); } return pkge; } compilePackages() { this.serverless.cli.log('Compiling Packages...'); const manifestResources = this.serverless.service.resources; const owPackages = this.serverless.service.packages; if (!owPackages) { throw new this.serverless.classes.Error( 'Missing Packages section from OpenWhisk Resource Manager template'); } if (manifestResources && manifestResources.packages) { Object.keys(manifestResources.packages).forEach(pkge => { owPackages[pkge] = this.compilePackage(pkge, manifestResources.packages[pkge]); }); } return BbPromise.resolve(); } } module.exports = OpenWhiskCompilePackages; ================================================ FILE: compile/packages/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const OpenWhiskCompilePackages = require('../index'); describe('OpenWhiskCompilePackages', () => { let serverless; let sandbox; let openwhiskCompilePackages; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskCompilePackages = new OpenWhiskCompilePackages(serverless, options); serverless.service.service = 'serviceName'; serverless.service.provider = { namespace: 'testing', apihost: '', auth: '', }; serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskCompilePackages.setup(); }); afterEach(() => { sandbox.restore(); }); describe('#renameManifestPackages()', () => { it('should handle config without resource packages', () => { openwhiskCompilePackages.serverless.service.resources = null openwhiskCompilePackages.renameManifestPackages(); openwhiskCompilePackages.serverless.service.resources = {} openwhiskCompilePackages.renameManifestPackages(); openwhiskCompilePackages.serverless.service.resources.packages = {} openwhiskCompilePackages.renameManifestPackages(); }) it('should rename packages with explicit names', () => { openwhiskCompilePackages.serverless.service.resources.packages = { 'first' : { name: 'firstchanged', parameters: { hello: 'world first' } }, 'second' : { parameters: { hello: 'world second' } } }; const expected = { 'firstchanged' : { name: 'firstchanged', parameters: { hello: 'world first' } }, 'second' : { parameters: { hello: 'world second' } } }; openwhiskCompilePackages.renameManifestPackages(); expect(openwhiskCompilePackages.serverless.service.resources.packages) .to.deep.equal(expected); }) }) describe('#getActionPackages()', () => { it('should return no package names for functions without name property', () => { const service = openwhiskCompilePackages.serverless.service; service.getAllFunctions = () => ["first", "second", "third"]; const handler = name => ({}) service.getFunction = handler; expect(openwhiskCompilePackages.getActionPackages()).to.deep.equal([]) }) it('should return no package names for functions with name missing package', () => { const service = openwhiskCompilePackages.serverless.service; service.getAllFunctions = () => ["first", "second", "third"]; const handler = name => ({name: "foo"}) service.getFunction = handler; expect(openwhiskCompilePackages.getActionPackages()).to.deep.equal([]) }) it('should return package names for functions with name including package', () => { const service = openwhiskCompilePackages.serverless.service; service.getAllFunctions = () => ["first", "second", "third"]; const handler = name => ({name: `${name}/${name}`}) service.getFunction = handler; expect(openwhiskCompilePackages.getActionPackages()).to.deep.equal(["first", "second", "third"]) service.getAllFunctions = () => ["first", "first", "first"]; expect(openwhiskCompilePackages.getActionPackages()).to.deep.equal(["first"]) }) }) describe('#calculatePackageName()', () => { it('should return package name from object key', () => { expect(openwhiskCompilePackages.calculatePackageName('a', { parameters: 'p' })).to.equal('a'); }) it('should return package name from name property', () => { expect(openwhiskCompilePackages.calculatePackageName('a', { name: 'b' })).to.equal('b'); }) }) describe('#mergeActionPackages()', () => { it('should set up packages from action names', () => { openwhiskCompilePackages.serverless.service.resources = {}; const output = {first: {}, second: {}, third: {}}; sandbox.stub(openwhiskCompilePackages, 'getActionPackages', () => ["first", "second", "third"]); openwhiskCompilePackages.mergeActionPackages(); expect(openwhiskCompilePackages.serverless.service.resources.packages).to.deep.equal(output) }); it('should ignore packages already defined in resources', () => { const packages = {first: 1, second: 2, third: 3}; openwhiskCompilePackages.serverless.service.resources = { packages }; sandbox.stub(openwhiskCompilePackages, 'getActionPackages', () => ["first", "second", "third"]); openwhiskCompilePackages.mergeActionPackages(); expect(openwhiskCompilePackages.serverless.service.resources.packages).to.deep.equal(packages) }) }) describe('#compilePackages()', () => { it('should throw an error if the resource section is not available', () => { openwhiskCompilePackages.serverless.service.packages = null; expect(() => openwhiskCompilePackages.compilePackages()) .to.throw(Error, /Missing Packages section/); }); it('should return empty packages if manifest has no packages', () => expect(openwhiskCompilePackages.compilePackages()).to.eventually.fulfilled ); it('should call compilePackage for each package definition', () => { const packages = { a: {}, b: {}, c: {} }; const stub = sinon.stub(openwhiskCompilePackages, 'compilePackage'); openwhiskCompilePackages.serverless.service.resources.packages = packages; openwhiskCompilePackages.serverless.service.packages = {}; return expect(openwhiskCompilePackages.compilePackages().then(() => { expect(stub.calledThrice).to.be.equal(true); Object.keys(packages).forEach( key => expect(stub.calledWith(key, packages[key])).to.be.equal(true) ); })).to.eventually.be.fulfilled; }); it('should update package definitions from manifest values', () => { const pkge = { shared: true, overwrite: false, namespace: 'another_ns', annotations: { foo: 'bar' }, parameters: { hello: 'world' } }; const expected = { name: 'sample', overwrite: false, namespace: 'another_ns', package: { publish: true, parameters: [{ key: 'hello', value: 'world' }], annotations: [{ key: 'foo', value: 'bar' }] }, }; openwhiskCompilePackages.serverless.service.resources.packages = { sample: pkge }; return expect(openwhiskCompilePackages.compilePackages().then(() => expect(openwhiskCompilePackages.serverless.service.packages) .to.deep.equal({ sample: expected }) )).to.eventually.be.fulfilled; }); it('should merge packages with explicit names', () => { openwhiskCompilePackages.serverless.service.resources.packages = { 'first' : { name: 'firstchanged', parameters: { hello: 'world first' } }, 'second' : { parameters: { hello: 'world second' } } }; sandbox.stub(openwhiskCompilePackages, 'getActionPackages', () => ['firstchanged', 'second', 'third']); const expected = { firstchanged: { name: 'firstchanged', overwrite: true, package: { parameters: [{ key: 'hello', value: 'world first' }] }, namespace: 'testing' }, second: { name: 'second', overwrite: true, package: { parameters: [{ key: 'hello', value: 'world second' }] }, namespace: 'testing' }, third: { name: 'third', overwrite: true, package: {}, namespace: 'testing' } }; // Simulate hooks openwhiskCompilePackages.setup(); openwhiskCompilePackages.renameManifestPackages(); openwhiskCompilePackages.mergeActionPackages(); return expect(openwhiskCompilePackages.compilePackages().then(() => { expect(openwhiskCompilePackages.serverless.service.packages) .to.deep.equal(expected) })).to.eventually.be.fulfilled; }); }); describe('#compilePackage()', () => { it('should define packages without a body', () => { const testing = { name: 'testing', namespace: 'testing', overwrite: true, package: {} }; const result = openwhiskCompilePackages.compilePackage('testing', {}); return expect(result).to.deep.equal(testing); }); it('should define packages with manifest params', () => { const params = { shared: true, overwrite: false, namespace: 'another_ns', annotations: { foo: 'bar' }, parameters: { hello: 'world' }, binding: '/whisk.system/utils' }; const expected = { name: 'testing', overwrite: false, namespace: 'another_ns', package: { publish: true, annotations: [{ key: 'foo', value: 'bar' }], parameters: [{ key: 'hello', value: 'world' }], binding: { namespace: 'whisk.system', name: 'utils' } }, }; const result = openwhiskCompilePackages.compilePackage('testing', params); return expect(result).to.deep.equal(expected); }); it('should throw an error for invalid binding identifier', () => { const params = { binding: 'external_ns/external_package' }; expect(() => openwhiskCompilePackages.compilePackage('testing', params)) .to.throw(Error, /Invalid Package Binding/); params.binding = 'incorrect'; expect(() => openwhiskCompilePackages.compilePackage('testing', params)) .to.throw(Error, /Invalid Package Binding/); }); it('should log packages to console when verbose flag is set', () => { openwhiskCompilePackages.options.verbose = true; const log = sandbox.stub(openwhiskCompilePackages.serverless.cli, 'log'); sandbox.stub(openwhiskCompilePackages.serverless.cli, 'consoleLog'); const params = { name: 'myPackage', overwrite: true, namespace: 'testing' }; const result = openwhiskCompilePackages.compilePackage('myPackage', params); expect(log.calledOnce).to.be.equal(true); expect(log.args[0][0]).to.be.equal(`Compiled Package (myPackage): ${JSON.stringify(result)}`); }); it('should rename packages when name parameter is present', () => { const params = { name: 'customname', shared: true, overwrite: false, namespace: 'another_ns', parameters: { hello: 'world' } }; const expected = { name: 'customname', overwrite: false, namespace: 'another_ns', package: { publish: true, parameters: [{ key: 'hello', value: 'world' }] }, }; const result = openwhiskCompilePackages.compilePackage('testing', params); return expect(result).to.deep.equal(expected); }); }); }); ================================================ FILE: compile/rules/README.md ================================================ # Compile Rules This plugins compiles the triggers bound to functions in `serverless.yaml` to corresponding [OpenWhisk Rules](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md) definitions. ## How it works `Compile Rules` hooks into the [`package:compileEvents`](/lib/plugins/deploy) lifecycle. It loops over all functions which are defined in `serverless.yaml` looking for the defined events. For each `trigger` event defined for the function, the corresponding `rule` will be created. ## Examples ```yaml # serverless.yaml functions: index: handler: users.handler events: - trigger: "my_trigger" ``` This definition will create a new Rule, called `my-service_my_trigger_to_index`, which binds the configured Action (index) to the Trigger (my_trigger). Triggers can be defined within the `serverless.yaml` file, see the documentation in the [`compileTriggers` plugin](../triggers). Triggers that aren't explicitly defined will be automatically created. If the event trigger value is a string, containing the trigger name, the rule name will be automatically generated in the form: `servicename-trigger-to-action`. If the trigger event value is an object, you can set the rule name explicitly. ```yaml # serverless.yaml functions: index: handler: users.handler events: - trigger: name: "my_trigger" rule: "rule_name" ``` At the end all OpenWhisk Rule definitions are merged inside the `serverless.service.rules` section. ================================================ FILE: compile/rules/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); class OpenWhiskCompileRules { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('openwhisk'); this.hooks = { 'before:package:compileEvents': this.setup.bind(this), 'package:compileEvents': this.compileRules.bind(this), }; } setup() { // This object will be used to store the Rule resources, passed directly to // the OpenWhisk SDK during the deploy process. this.serverless.service.rules = {}; if (this.serverless.service.provider.namespace) { return Promise.resolve(); } // Actions and Triggers referenced by Rules must used fully qualified identifiers (including namespace). return this.provider.props().then(props => { this.serverless.service.provider.namespace = props.namespace || '_'; }); } calculateFunctionName(functionName, functionObject) { const namespace = this.calculateFunctionNameSpace(functionObject); const name = functionObject.name || `${this.serverless.service.service}_${functionName}`; return `/${namespace}/${name}` } calculateFunctionNameSpace(functionObject) { return functionObject.namespace || this.serverless.service.provider.namespace } calculateTriggerName(triggerName) { let namespace = this.serverless.service.provider.namespace; const resources = this.serverless.service.resources; if (resources.triggers && resources.triggers[triggerName]) { namespace = resources.triggers[triggerName].namespace || namespace; } return `/${namespace}/${triggerName}`; } generateDefaultRuleName(functionName, triggerName) { return `${this.serverless.service.service}_${triggerName}_to_${functionName}` } // // This method takes the rule definitions, parsed from the user's YAML file, // and turns it into the OpenWhisk Rule resource object. // // These resource objects are passed to the OpenWhisk SDK to create the associated Rules // during the deployment process. // // Parameter values will be parsed from the user's YAML definition, either as a value from // the rule definition or the service provider defaults. compileRule(funcName, funcObj, trigger) { const namespace = this.calculateFunctionNameSpace(funcObj); const action = this.calculateFunctionName(funcName, funcObj); const defaultRuleName = this.generateDefaultRuleName(funcName, trigger); if (typeof trigger === 'string') { return { ruleName: defaultRuleName, overwrite: true, trigger: this.calculateTriggerName(trigger), action, namespace }; } if (!trigger.hasOwnProperty('rule')) { throw new this.serverless.classes.Error( `Missing mandatory rule property from Event Trigger definition for Function: ${funcName}`); } if (!trigger.hasOwnProperty('name')) { throw new this.serverless.classes.Error( `Missing mandatory name property from Event Trigger definition for Function: ${funcName}`); } const ruleName = trigger.rule || defaultRuleName; let overwrite = true if(trigger.hasOwnProperty('overwrite')) { overwrite = trigger.overwrite; } return { ruleName, overwrite, trigger: this.calculateTriggerName(trigger.name), action, namespace }; } compileFunctionRules(functionName, functionObject) { if (!functionObject.events) return [] const events = functionObject.events .filter(e => e.trigger) .map(e => this.compileRule(functionName, functionObject, e.trigger)) if (events.length && this.options.verbose) { this.serverless.cli.log(`Compiled Rule (${functionName}): ${JSON.stringify(events)}`); } return events } compileRules() { this.serverless.cli.log('Compiling Rules...'); const manifestResources = this.serverless.service.resources; const owRules = this.serverless.service.rules; if (!owRules) { throw new this.serverless.classes.Error( 'Missing Rules section from OpenWhisk Resource Manager template'); } const allFunctions = this.serverless.service.getAllFunctions() const functionRules = allFunctions.map( functionName => this.compileFunctionRules(functionName, this.serverless.service.getFunction(functionName)) ).reduce((a, b) => a.concat(b), []) functionRules.forEach(rule => { owRules[rule.ruleName] = rule; }) return BbPromise.resolve(); } } module.exports = OpenWhiskCompileRules; ================================================ FILE: compile/rules/tests/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const OpenWhiskCompileRules = require('../index'); describe('OpenWhiskCompileRules', () => { let serverless; let sandbox; let openwhiskCompileRules; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskCompileRules = new OpenWhiskCompileRules(serverless, options); serverless.service.service = 'serviceName'; serverless.service.provider = { namespace: 'testing', apihost: '', auth: '', }; serverless.cli = { consoleLog: () => {}, log: () => {} }; }); afterEach(() => { sandbox.restore(); }); describe('#setup()', () => { it('should not call provider props if namespace in defaults', () => { openwhiskCompileRules.serverless.getProvider = () => ({props: sinon.assert.fail}); openwhiskCompileRules.setup(); }) it('should use provider props if namespace available', () => { openwhiskCompileRules.serverless.service.provider.namespace = null; const props = () => BbPromise.resolve({namespace: 'sample_ns'}) openwhiskCompileRules.provider = { props }; return openwhiskCompileRules.setup().then(() => { expect(openwhiskCompileRules.serverless.service.provider.namespace).to.equal("sample_ns") }); }) it('should use default namespace if provider namespace missing', () => { openwhiskCompileRules.serverless.service.provider.namespace = null; const props = () => BbPromise.resolve({}) openwhiskCompileRules.provider = { props }; return openwhiskCompileRules.setup().then(() => { expect(openwhiskCompileRules.serverless.service.provider.namespace).to.equal("_") }); }) }); describe('#compileRules()', () => { beforeEach(() => { openwhiskCompileRules.setup(); }); it('should throw an error if the resource section is not available', () => { openwhiskCompileRules.serverless.service.rules = null; expect(() => openwhiskCompileRules.compileRules()) .to.throw(Error, /Missing Rules section/); }); it('should return empty rules if functions has no triggers', () => expect(openwhiskCompileRules.compileRules().then(() => { expect(openwhiskCompileRules.serverless.service.rules).to.deep.equal({}); })).to.eventually.be.fulfilled ); it('should call compileFunctionRule and update rules for each function with events', () => { const stub = sinon.stub(openwhiskCompileRules, 'compileFunctionRules').returns([{ruleName: 'ruleName'}]); openwhiskCompileRules.serverless.service.rules = {}; sandbox.stub(openwhiskCompileRules.serverless.service, 'getAllFunctions', () => ["first", "second", "third"]); const handler = name => ({events: {}}) openwhiskCompileRules.serverless.service.getFunction = handler; return expect(openwhiskCompileRules.compileRules().then(() => { expect(openwhiskCompileRules.serverless.service.rules).to.deep.equal({ 'ruleName': {ruleName: 'ruleName'} }); expect(stub.calledThrice).to.be.equal(true); })).to.eventually.be.fulfilled; }); }); describe('#compileFunctionRules()', () => { beforeEach(() => { openwhiskCompileRules.setup(); }); it('should not call compileRule when events parameter is missing', () => { const stub = sinon.stub(openwhiskCompileRules, 'compileRule') const rules = openwhiskCompileRules.compileFunctionRules('name', {}) expect(rules).to.deep.equal([]); expect(stub.called).to.be.equal(false); }) it('should not call compileRule when events list contains no triggers', () => { const stub = sinon.stub(openwhiskCompileRules, 'compileRule') const rules = openwhiskCompileRules.compileFunctionRules('name', { events: [{"api": {}}] }) expect(rules).to.deep.equal([]); expect(stub.called).to.be.equal(false); }) it('should call compileRule when events list contains triggers', () => { const stub = sinon.stub(openwhiskCompileRules, 'compileRule').returns({}) const rules = openwhiskCompileRules.compileFunctionRules('name', { events: [ {"trigger": {}}, {"trigger": {}}, {"trigger": {}}, ] }) expect(rules).to.deep.equal([{}, {}, {}]); expect(stub.calledThrice).to.be.equal(true); }) it('should log rules when verbose flag is used', () => { openwhiskCompileRules.options.verbose = true const log = sandbox.stub(openwhiskCompileRules.serverless.cli, 'log') const clog = sandbox.stub(openwhiskCompileRules.serverless.cli, 'consoleLog') const stub = sinon.stub(openwhiskCompileRules, 'compileRule').returns({ foo: 'bar' }) openwhiskCompileRules.compileFunctionRules('name', { events: [ {"trigger": true}, {"trigger": true}, {"trigger": true} ] }) expect(log.calledOnce).to.be.equal(true); const result = JSON.stringify([{foo: 'bar'}, {foo: 'bar'}, {foo: 'bar'}]); expect(log.args[0][0]).to.be.equal(`Compiled Rule (name): ${result}`); }) }); describe('#compileRule()', () => { beforeEach(() => { openwhiskCompileRules.setup(); }); it('should define rules from trigger string', () => { openwhiskCompileRules.serverless.service.service = 'my-service' openwhiskCompileRules.serverless.service.provider = {namespace: "sample_ns"}; const funcObj = {} const trigger = "some-trigger" const testing = { ruleName: 'my-service_some-trigger_to_action-name', action: '/sample_ns/my-service_action-name', trigger: '/sample_ns/some-trigger', namespace: 'sample_ns', overwrite: true }; const result = openwhiskCompileRules.compileRule('action-name', {}, trigger); return expect(result).to.deep.equal(testing); }); it('should define rules from trigger object', () => { openwhiskCompileRules.serverless.service.service = 'my-service' openwhiskCompileRules.serverless.service.provider = {namespace: "sample_ns"}; const funcObj = { namespace: 'custom_ns' } const trigger = {name: "custom_trigger_name", rule: "custom_rule_name", overwrite: false} const testing = { ruleName: 'custom_rule_name', action: '/custom_ns/my-service_action-name', trigger: '/sample_ns/custom_trigger_name', namespace: 'custom_ns', overwrite: false }; const result = openwhiskCompileRules.compileRule('action-name', funcObj, trigger); return expect(result).to.deep.equal(testing); }); it('should throw if trigger missing rule', () => { expect(() => openwhiskCompileRules.compileRule('', {}, {name: ''})) .to.throw(Error, /Missing mandatory rule property from Event Trigger/); }); it('should throw if trigger missing name', () => { expect(() => openwhiskCompileRules.compileRule('', {}, {rule: ''})) .to.throw(Error, /Missing mandatory name property from Event Trigger/); }); /* it('should define rules with manifest params', () => { const params = { overwrite: true, namespace: 'another_ns', parameters: { hello: 'world' } }; const expected = { ruleName: 'testing', overwrite: true, namespace: 'another_ns', parameters: [{ key: 'hello', value: 'world' }], }; const result = openwhiskCompileRules.compileRule('testing', params); return expect(result).to.deep.equal(expected); }); it('should define rules with feed manifest params', () => { const feedName = '/ns/package/feed'; const params = { feed: feedName, feed_parameters: { hello: 'world' } }; const expected = { ruleName: 'myRule', overwrite: true, namespace: 'testing', feed: { feedName: 'package/feed', namespace: 'ns', rule: '/testing/myRule', params: params.feed_parameters, }, }; const result = openwhiskCompileRules.compileRule('myRule', params); return expect(result).to.deep.equal(expected); }); */ }); }); ================================================ FILE: compile/schedule/README.md ================================================ # Compile Triggers This plugins compiles the schedule events in `serverless.yaml` to corresponding [OpenWhisk Alarm Trigger Feeds](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md) definitions. ## How it works `Compile Schedule` hooks into the [`package:compileEvents`](/lib/plugins/deploy) lifecycle. It loops over all schedule event which are defined in `serverless.yaml`. ### Implicit Schedule Definition Alarm triggers referenced in the function event configuration don't have to explicitly defined the triggers and rules in the `resources` section. The plugin will set up these resources for creation without any further configuration. ```yaml # serverless.yaml functions: index: handler: users.main events: - schedule: cron(* * * * *) // fires every minute ``` The plugin will create a trigger called `${serviceName}_${fnName}_alarmTrigger` and a rule called `${serviceName}_${fnName}_alarmRule` to bind the function to the cron events. ### Explicit Schedule Definition Adding extra properties for the alarm event can be handled by defining an object as the `schedule` value rather than the cron string. ```yaml # serverless.yaml functions: index: handler: users.main events: - schedule: rate: cron(* * * * *) trigger: custom_trigger_name rule: custom_rule_name max: 10000 // maximum event fires, defaults to 1000 params: hello: world ``` `rate` is the only mandatory property. All OpenWhisk Schedule events are merged inside the `serverless.service.triggers` and `serverless.service.rules` section. ================================================ FILE: compile/schedule/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); class OpenWhiskCompileSchedules { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('openwhisk'); this.feed = '/whisk.system/alarms/alarm' this.hooks = { 'before:package:compileEvents': () => BbPromise.bind(this) .then(this.setup) .then(this.processScheduleEvents) }; } setup() { if (!this.serverless.service.resources) { this.serverless.service.resources = {}; } if (!this.serverless.service.resources.triggers) { this.serverless.service.resources.triggers = {}; } if (!this.serverless.service.resources.rules) { this.serverless.service.resources.rules = {}; } } // correct rate syntax is: cron(* * * * *) parseScheduleRate (rate) { const cron = rate.match(/cron\((.*)\)/) if (!cron || !cron[1] || cron[1].split(' ').length !== 5) { throw new this.serverless.classes.Error( [`Schedule event rate property value is invalid: ${rate}`, 'The correct syntax should be "cron(_ _ _ _ _)"'].join('\n') ) } return cron[1] } compileScheduleTrigger (fnName, schedule) { const name = schedule.trigger || `${this.serverless.service.service}_${fnName}_schedule_trigger` const cron = this.parseScheduleRate(schedule.rate || schedule) const trigger_payload = JSON.stringify(schedule.params || {}) const feed_parameters = { cron, trigger_payload } if (schedule.max) { feed_parameters.maxTriggers = schedule.max } return { name, content: { feed: this.feed, feed_parameters } } } defaultScheduleRuleName (triggerName, fnName) { return `${this.serverless.service.service}_${fnName}_schedule_rule` } processScheduleEvent (fnName, schedule) { const fnObj = this.serverless.service.getFunction(fnName) const trigger = this.compileScheduleTrigger(fnName, schedule) const rule = schedule.rule || this.defaultScheduleRuleName(trigger.name, fnName) fnObj.events.push({ trigger: { name: trigger.name, rule } }) this.serverless.service.resources.triggers[trigger.name] = trigger.content } processScheduleEvents () { this.serverless.service.getAllFunctions().forEach(name => { const fn = this.serverless.service.getFunction(name) const events = (fn.events || []).filter(e => e.schedule) events.forEach(e => this.processScheduleEvent(name, e.schedule)) }) } } module.exports = OpenWhiskCompileSchedules; ================================================ FILE: compile/schedule/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const OpenWhiskCompileSchedules = require('../index'); describe('OpenWhiskCompileSchedules', () => { let serverless; let sandbox; let openwhiskCompileSchedules; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskCompileSchedules = new OpenWhiskCompileSchedules(serverless, options); serverless.service.service = 'serviceName'; serverless.service.provider = { namespace: 'testing', apihost: '', auth: '', }; serverless.cli = { log: () => {} }; openwhiskCompileSchedules.setup() }); afterEach(() => { sandbox.restore(); }); describe('#processScheduleEvents()', () => { it('should update create schedule trigger and update manifest resources.', () => { const fnObj = { events: [{schedule: "cron(* * * * *)"}] } serverless.service.getFunction = () => fnObj openwhiskCompileSchedules.compileScheduleTrigger = () => ({name: 'serviceName_fnName_schedule_trigger', content: { a: 1 }}) openwhiskCompileSchedules.processScheduleEvent("fnName", fnObj.events[0].schedule) expect(fnObj.events[1]).to.be.deep.equal({ trigger: { name: 'serviceName_fnName_schedule_trigger', rule: 'serviceName_fnName_schedule_rule' } }) expect(serverless.service.resources.triggers).to.be.deep.equal({serviceName_fnName_schedule_trigger: {a: 1}}) }) }) describe('#processScheduleEvents()', () => { it('should call processEventSchedule for each schedule event.', () => { const service = openwhiskCompileSchedules.serverless.service; const fns = { first: { events: [{}, {schedule: "cron(* * * * *)"}, {trigger: true}] }, second: { events: [{schedule: "cron(* * * * *)"}] }, third: {} } service.getAllFunctions = () => Object.keys(fns) service.getFunction = name => fns[name]; const spy = openwhiskCompileSchedules.processScheduleEvent = sinon.spy() openwhiskCompileSchedules.processScheduleEvents() expect(spy.calledTwice).to.be.equal(true) expect(spy.withArgs("first", "cron(* * * * *)").calledOnce).to.be.equal(true) expect(spy.withArgs("second", "cron(* * * * *)").calledOnce).to.be.equal(true) }) }) describe('#compileScheduleTrigger()', () => { it('should throw errors for incorrect rate definition.', () => { let name = 'my_fn' expect(() => openwhiskCompileSchedules.compileScheduleTrigger(name, 'ron(* * * * *)')) .to.throw(Error, /Schedule event rate property value is invalid/); expect(() => openwhiskCompileSchedules.compileScheduleTrigger(name, '* * * * *')) .to.throw(Error, /Schedule event rate property value is invalid/); expect(() => openwhiskCompileSchedules.compileScheduleTrigger(name, 'cron(* * * *)')) .to.throw(Error, /Schedule event rate property value is invalid/); expect(() => openwhiskCompileSchedules.compileScheduleTrigger(name, 'cron(* * * * * *)')) .to.throw(Error, /Schedule event rate property value is invalid/); }) it('should return default trigger for simple schedule event.', () => { let name = 'my_fn', rate = 'cron(* * * * *)' const trigger = openwhiskCompileSchedules.compileScheduleTrigger(name, rate) expect(trigger).to.be.deep.equal({ name: `${serverless.service.service}_${name}_schedule_trigger`, content: { feed: '/whisk.system/alarms/alarm', feed_parameters: { cron: '* * * * *', trigger_payload: "{}" } } }) }) it('should return trigger for object schedule event.', () => { const name = 'my_fn' const rate = { rate: 'cron(* * * * *)', trigger: 'trigger_name', max: 500, params: { hello: 'world' } } const trigger = openwhiskCompileSchedules.compileScheduleTrigger(name, rate) expect(trigger).to.be.deep.equal({ name: `trigger_name`, content: { feed: '/whisk.system/alarms/alarm', feed_parameters: { cron: '* * * * *', trigger_payload: JSON.stringify(rate.params), maxTriggers: 500 } } }) }) }) /* // should throw for incorrect format... describe('#getEventSchedules()', () => { it('should return all names for simple triggers registered on functions', () => { const service = openwhiskCompileSchedules.serverless.service; service.getAllFunctions = () => ["first", "second", "third"]; const handler = name => ({events: [{schedule: "cron(* * * * *)"}, {schedule: "cron(* * * * *)"}]}) service.getFunction = handler; expect(openwhiskCompileSchedules.getEventSchedules()).to.deep.equal(["blah", "foo"]) }) /* it('should return all names for complex triggers registered on functions', () => { const service = openwhiskCompileSchedules.serverless.service; service.getAllFunctions = () => ["first", "second", "third"]; const handler = name => ({events: [{trigger: {name: "blah"}}, {trigger: {name: "foo"}}]}) service.getFunction = handler; expect(openwhiskCompileSchedules.getEventSchedules()).to.deep.equal(["blah", "foo"]) }) }) */ /* describe('#mergeEventSchedules()', () => { it('should set up non-existant triggers', () => { openwhiskCompileSchedules.serverless.service.resources = {}; const output = {first: {}, second: {}, third: {}}; sandbox.stub(openwhiskCompileSchedules, 'getEventSchedules', () => ["first", "second", "third"]); openwhiskCompileSchedules.mergeEventSchedules(); expect(openwhiskCompileSchedules.serverless.service.resources.triggers).to.deep.equal(output) }); it('should ignore existing triggers', () => { const triggers = {first: 1, second: 2, third: 3}; openwhiskCompileSchedules.serverless.service.resources = { triggers }; sandbox.stub(openwhiskCompileSchedules, 'getEventSchedules', () => ["first", "second", "third"]); openwhiskCompileSchedules.mergeEventSchedules(); expect(openwhiskCompileSchedules.serverless.service.resources.triggers).to.deep.equal(triggers) }) }) */ }); ================================================ FILE: compile/servicebindings/README.md ================================================ # Service Bindings This plugin binds IBM Cloud platform service credentials to actions and packages in `serverless.yaml`. ## How it works `Compile Service Bindings` hooks into the [`package:compileEvents`](/lib/plugins/deploy) lifecycle. ***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 ``` ================================================ FILE: compile/servicebindings/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); class OpenWhiskCompileServiceBindings { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('openwhisk'); this.hooks = { 'package:compileEvents': this.compileServiceBindings.bind(this) }; } calculateFunctionName(name, props) { return props.name || `${this.serverless.service.service}_${name}`; } parseServiceBindings(name, properties) { const bindings = properties.bind || [] const servicebindings = bindings.filter(b => b.service) .map(b => Object.assign(b.service, { action: name } )) const serviceNames = new Set() for (let sb of servicebindings) { if (!sb.hasOwnProperty('name')) { throw new Error(`service binding missing name parameter: ${JSON.stringify(sb)}`) } if (serviceNames.has(sb.name)) { throw new Error(`multiple bindings for same service not supported: ${sb.name}`) } serviceNames.add(sb.name) } return servicebindings } compileFnServiceBindings() { return this.serverless.service.getAllFunctions() .map(name => { const fnObj = this.serverless.service.getFunction(name) const fnName = this.calculateFunctionName(name, fnObj) return this.parseServiceBindings(fnName, fnObj) }) .filter(sbs => sbs.length > 0) } compilePkgServiceBindings() { const manifestResources = this.serverless.service.resources || {} const packages = manifestResources.packages || {} return Object.keys(packages) .map(name => this.parseServiceBindings(name, packages[name])) .filter(sbs => sbs.length > 0) } compileServiceBindings() { this.serverless.cli.log('Compiling Service Bindings...'); const fnServiceBindings = this.compileFnServiceBindings() const pkgServiceBindings = this.compilePkgServiceBindings() this.serverless.service.bindings = { fns: fnServiceBindings, packages: pkgServiceBindings } return BbPromise.resolve(); } } module.exports = OpenWhiskCompileServiceBindings; ================================================ FILE: compile/servicebindings/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const OpenWhiskCompileServiceBindings = require('../index'); describe('OpenWhiskCompileServiceBindings', () => { let serverless; let sandbox; let openwhiskCompileServiceBindings; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskCompileServiceBindings = new OpenWhiskCompileServiceBindings(serverless, options); serverless.service.service = 'serviceName'; serverless.service.provider = { namespace: 'testing', apihost: '', auth: '', }; serverless.cli = { consoleLog: () => {}, log: () => {} }; }); afterEach(() => { sandbox.restore(); }); describe('#parseServiceBindings()', () => { it('should return empty array when missing service bindings', () => { const action = 'fnName' expect(openwhiskCompileServiceBindings.parseServiceBindings(action, {})).to.deep.equal([]) expect(openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: []})).to.deep.equal([]) expect(openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: [{}]})).to.deep.equal([]) expect(openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: [{blah: {}}]})).to.deep.equal([]) }) it('should return array with single service binding property', () => { const action = 'fnName' const service = { name: 'my-service', instance: 'my-instance', key: 'mykey' } const response = { action: `fnName`, name: 'my-service', instance: 'my-instance', key: 'mykey' } const result = openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: [{ service }]}) expect(result).to.deep.equal([response]) }) it('should return array with multiple service binding properties', () => { const action = 'fnName' const service_a = { action: `serviceName_fnName`, name: 'my-service-a', instance: 'my-instance-a', key: 'mykey' } const service_b = { action: `serviceName_fnName`, name: 'my-service-b', instance: 'my-instance-b', key: 'mykey' } const service_c = { action: `serviceName_fnName`, name: 'my-service-c', instance: 'my-instance-c', key: 'mykey' } const services = [{ service: service_a }, { service: service_b }, { service: service_c } ] const result = openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: services}) expect(result).to.deep.equal([service_a, service_b, service_c]) }) it('should throw an error if service binding is missing name', () => { const service = { instance: 'my-instance-a', key: 'mykey' } const action = 'fnName' const services = [{ service }] expect(() => openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: services})) .to.throw(Error, /service binding missing name parameter/); }); it('should throw an error if multiple bindings for same service name', () => { const action = 'fnName' const service = { name: 'my-service', instance: 'my-instance-a', key: 'mykey' } const services = [{ service }, { service }] expect(() => openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: services})) .to.throw(Error, /multiple bindings for same service not supported/); }); }) describe('#compileServiceBindings()', () => { it('should return service bindings for simple functions', () => { const fns = { a: { bind: [{ service: { name: 'service-name-a' } }] }, b: { bind: [{ service: { name: 'service-name-b', instance: 'instance-name' } }] }, c: { bind: [{ service: { name: 'service-name-a' } }, { service: { name: 'service-name-b' } }] }, d: { }, } const service = openwhiskCompileServiceBindings.serverless.service service.getAllFunctions = () => Object.keys(fns) service.getFunction = name => fns[name] const services = [ [{ action: 'serviceName_a', name: 'service-name-a' }], [{ action: 'serviceName_b', name: 'service-name-b', instance: 'instance-name' }], [{ action: 'serviceName_c', name: 'service-name-a' }, { action: 'serviceName_c', name: 'service-name-b' }] ] return openwhiskCompileServiceBindings.compileServiceBindings().then(result => { expect(service.bindings.fns).to.deep.equal(services) expect(service.bindings.packages).to.deep.equal([]) }) }) it('should return service bindings for functions with explicit name', () => { const fns = { a: { name: 'some_name', bind: [{ service: { name: 'service-name-a' } }] } } const service = openwhiskCompileServiceBindings.serverless.service service.getAllFunctions = () => Object.keys(fns) service.getFunction = name => fns[name] const services = [ [{ action: 'some_name', name: 'service-name-a' }] ] return openwhiskCompileServiceBindings.compileServiceBindings().then(result => { expect(service.bindings.fns).to.deep.equal(services) expect(service.bindings.packages).to.deep.equal([]) }) }) it('should return service bindings for packages', () => { const service = openwhiskCompileServiceBindings.serverless.service service.resources.packages = { a: { bind: [{ service: { name: 'service-name-a' } }] }, b: { bind: [{ service: { name: 'service-name-b', instance: 'instance-name' } }] }, c: { bind: [{ service: { name: 'service-name-a' } }, { service: { name: 'service-name-b' } }] }, d: { }, } const services = [ [{ action: 'a', name: 'service-name-a' }], [{ action: 'b', name: 'service-name-b', instance: 'instance-name' }], [{ action: 'c', name: 'service-name-a' }, { action: 'c', name: 'service-name-b' }] ] return openwhiskCompileServiceBindings.compileServiceBindings().then(() => { expect(service.bindings.packages).to.deep.equal(services); expect(service.bindings.fns).to.deep.equal([]); }); }); it('should return service bindings for functions & packages', () => { const service = openwhiskCompileServiceBindings.serverless.service; service.resources.packages = { a: { bind: [{ service: { name: 'service-name-a' } }] } }; const fns = { b: { bind: [{ service: { name: 'service-name-b', instance: 'instance-name' } }] }, } service.getAllFunctions = () => Object.keys(fns) service.getFunction = name => fns[name] const services = { packages: [[{ action: 'a', name: 'service-name-a' }]], fns: [[{ action: 'serviceName_b', name: 'service-name-b', instance: 'instance-name' }]] } return openwhiskCompileServiceBindings.compileServiceBindings().then(() => { expect(service.bindings).to.deep.equal(services); }); }) }) }); ================================================ FILE: compile/triggers/README.md ================================================ # Compile Triggers This plugins compiles the triggers in `serverless.yaml` to corresponding [OpenWhisk Triggers](https://github.com/openwhisk/openwhisk/blob/master/docs/actions.md) definitions. ## How it works `Compile Triggers` hooks into the [`package:compileEvents`](/lib/plugins/deploy) lifecycle. It loops over all triggers which are defined in `serverless.yaml`. ### Implicit Trigger Definition Triggers referenced from function event configuration don't have to be explicitly defined in the `resources` section. The plugin will set up these resources for creation without any further configuration. ```yaml # serverless.yaml functions: index: handler: users.main events: - triggers: trigger: "myTriggerName" ``` This function handler includes a reference to a trigger that will be created during deployment. ### Explicit Trigger Definition Specifying triggers in the `resources` section allows users to override default parameters used when creating triggers implicitly. The trigger will be identified by the `triggers` property identifier, using the namespace from service provider defaults (unless set manually using the `namespace` property). Default parameters for each trigger can be set using the `parameters` property. Connecting triggers to event feeds is supported through the `feed` and `feed_parameters` properties, as shown in the example below. ```yaml # serverless.yaml resources: triggers: myTrigger: parameters: hello: world feed: /whisk.system/alarms/alarm feed_parameters: cron: '*/8 * * * * *' ``` At the end all OpenWhisk Trigger definitions are merged inside the `serverless.service.triggers` section. ================================================ FILE: compile/triggers/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); class OpenWhiskCompileTriggers { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('openwhisk'); this.hooks = { 'before:package:compileEvents': () => BbPromise.bind(this) .then(this.setup) .then(this.mergeEventTriggers), 'package:compileEvents': this.compileTriggers.bind(this), }; } setup() { // This object will be used to store the Trigger resources, passed directly to // the OpenWhisk SDK during the deploy process. this.serverless.service.triggers = {}; } mergeEventTriggers() { const triggers = this.getEventTriggers(); if (!triggers.length) return; if (!this.serverless.service.resources) { this.serverless.service.resources = {}; } if (!this.serverless.service.resources.triggers) { this.serverless.service.resources.triggers = {}; } const manifestTriggers = this.serverless.service.resources.triggers || {}; triggers.forEach(trigger => { manifestTriggers[trigger] = manifestTriggers[trigger] || {} }) } getEventTriggers() { const eventTriggers = new Set(); this.serverless.service.getAllFunctions() .map(name => this.serverless.service.getFunction(name)) .filter(func => func.events) .forEach(func => func.events.forEach(event => { if (event.trigger) { eventTriggers.add(event.trigger.name || event.trigger) } })); return [...eventTriggers]; } // Trigger identifiers are composed of a namespace and a name. // The name may optionally include a package identifier. // // Valid examples shown here: // // /james.thomas@uk.ibm.com/myPackage/myTrigger // /james.thomas@uk.ibm.com/myTrigger compileTriggerFeed(trigger, feed, params) { const feedPathParts = feed.split('/').filter(i => i); const namespace = feedPathParts.splice(0, 1).join(); const feedName = feedPathParts.join('/'); return { trigger, feedName, namespace, params }; } // // This method takes the trigger definitions, parsed from the user's YAML file, // and turns it into the OpenWhisk Trigger resource object. // // These resource objects are passed to the OpenWhisk SDK to create the associated Triggers // during the deployment process. // // Parameter values will be parsed from the user's YAML definition, either as a value from // the trigger definition or the service provider defaults. compileTrigger(name, params) { const trigger = { triggerName: name, overwrite: true }; trigger.namespace = params.namespace || this.serverless.service.provider.namespace; if (params.hasOwnProperty('overwrite')) { trigger.overwrite = params.overwrite; } else if (this.serverless.service.provider.hasOwnProperty('overwrite')) { trigger.overwrite = params.overwrite; } if (params.parameters) { trigger.parameters = Object.keys(params.parameters).map( key => ({ key, value: params.parameters[key] }) ); } // binding triggers to event feeds is sent as a separate API request // once triggers have been created. if (params.feed) { trigger.feed = this.compileTriggerFeed( `/${trigger.namespace}/${trigger.triggerName}`, params.feed, params.feed_parameters ); } if (this.options.verbose) { this.serverless.cli.log(`Compiled Trigger (${name}): ${JSON.stringify(trigger)}`); } return trigger; } compileTriggers() { this.serverless.cli.log('Compiling Triggers & Feeds...'); const manifestResources = this.serverless.service.resources; const owTriggers = this.serverless.service.triggers; if (!owTriggers) { throw new this.serverless.classes.Error( 'Missing Triggers section from OpenWhisk Resource Manager template'); } if (manifestResources && manifestResources.triggers) { Object.keys(manifestResources.triggers).forEach(trigger => { owTriggers[trigger] = this.compileTrigger(trigger, manifestResources.triggers[trigger]); }); } return BbPromise.resolve(); } } module.exports = OpenWhiskCompileTriggers; ================================================ FILE: compile/triggers/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); const sinon = require('sinon'); const OpenWhiskCompileTriggers = require('../index'); describe('OpenWhiskCompileTriggers', () => { let serverless; let sandbox; let openwhiskCompileTriggers; beforeEach(() => { sandbox = sinon.sandbox.create(); serverless = {classes: {Error}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskCompileTriggers = new OpenWhiskCompileTriggers(serverless, options); serverless.service.service = 'serviceName'; serverless.service.provider = { namespace: 'testing', apihost: '', auth: '', }; serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskCompileTriggers.setup(); }); afterEach(() => { sandbox.restore(); }); describe('#getEventTriggers()', () => { it('should return all names for simple triggers registered on functions', () => { const service = openwhiskCompileTriggers.serverless.service; service.getAllFunctions = () => ["first", "second", "third"]; const handler = name => ({events: [{trigger: "blah"}, {trigger: "foo"}]}) service.getFunction = handler; expect(openwhiskCompileTriggers.getEventTriggers()).to.deep.equal(["blah", "foo"]) }) it('should return all names for complex triggers registered on functions', () => { const service = openwhiskCompileTriggers.serverless.service; service.getAllFunctions = () => ["first", "second", "third"]; const handler = name => ({events: [{trigger: {name: "blah"}}, {trigger: {name: "foo"}}]}) service.getFunction = handler; expect(openwhiskCompileTriggers.getEventTriggers()).to.deep.equal(["blah", "foo"]) }) }) describe('#mergeEventTriggers()', () => { it('should set up non-existant triggers', () => { openwhiskCompileTriggers.serverless.service.resources = {}; const output = {first: {}, second: {}, third: {}}; sandbox.stub(openwhiskCompileTriggers, 'getEventTriggers', () => ["first", "second", "third"]); openwhiskCompileTriggers.mergeEventTriggers(); expect(openwhiskCompileTriggers.serverless.service.resources.triggers).to.deep.equal(output) }); it('should ignore existing triggers', () => { const triggers = {first: 1, second: 2, third: 3}; openwhiskCompileTriggers.serverless.service.resources = { triggers }; sandbox.stub(openwhiskCompileTriggers, 'getEventTriggers', () => ["first", "second", "third"]); openwhiskCompileTriggers.mergeEventTriggers(); expect(openwhiskCompileTriggers.serverless.service.resources.triggers).to.deep.equal(triggers) }) }) describe('#compileTriggers()', () => { it('should throw an error if the resource section is not available', () => { openwhiskCompileTriggers.serverless.service.triggers = null; expect(() => openwhiskCompileTriggers.compileTriggers()) .to.throw(Error, /Missing Triggers section/); }); it('should return empty triggers if manifest has no triggers', () => expect(openwhiskCompileTriggers.compileTriggers()).to.eventually.fulfilled ); it('should call compileTrigger for each trigger definition', () => { const triggers = { a: {}, b: {}, c: {} }; const stub = sinon.stub(openwhiskCompileTriggers, 'compileTrigger'); openwhiskCompileTriggers.serverless.service.resources.triggers = triggers; openwhiskCompileTriggers.serverless.service.triggers = {}; return expect(openwhiskCompileTriggers.compileTriggers().then(() => { expect(stub.calledThrice).to.be.equal(true); Object.keys(triggers).forEach( key => expect(stub.calledWith(key, triggers[key])).to.be.equal(true) ); })).to.eventually.be.fulfilled; }); it('should update trigger definitions from manifest values', () => { const trigger = { overwrite: true, namespace: 'another_ns', parameters: { hello: 'world' } }; const expected = { triggerName: 'sample', overwrite: true, namespace: 'another_ns', parameters: [{ key: 'hello', value: 'world' }], }; openwhiskCompileTriggers.serverless.service.resources.triggers = { sample: trigger }; return expect(openwhiskCompileTriggers.compileTriggers().then(() => expect(openwhiskCompileTriggers.serverless.service.triggers) .to.deep.equal({ sample: expected }) )).to.eventually.be.fulfilled; }); }); describe('#compileTriggerFeed()', () => { it('should define trigger feed without parameters', () => { const expected = { trigger: '/ns/testing', feedName: 'package/action', namespace: 'ns', params: {}, }; const result = openwhiskCompileTriggers.compileTriggerFeed('/ns/testing', '/ns/package/action', {}); return expect(result).to.deep.equal(expected); }); it('should define trigger feed with parameters', () => { const expected = { trigger: '/ns/testing', feedName: 'package/action', namespace: 'ns', params: { hello: 'world', }, }; const result = openwhiskCompileTriggers .compileTriggerFeed('/ns/testing', '/ns/package/action', { hello: 'world' }); return expect(result).to.deep.equal(expected); }); }); describe('#compileTrigger()', () => { it('should define triggers without a body', () => { const testing = { triggerName: 'testing', namespace: 'testing', overwrite: false }; const result = openwhiskCompileTriggers.compileTrigger('testing', testing); return expect(result).to.deep.equal(testing); }); it('should define triggers with manifest params', () => { const params = { overwrite: true, namespace: 'another_ns', parameters: { hello: 'world' } }; const expected = { triggerName: 'testing', overwrite: true, namespace: 'another_ns', parameters: [{ key: 'hello', value: 'world' }], }; const result = openwhiskCompileTriggers.compileTrigger('testing', params); return expect(result).to.deep.equal(expected); }); it('should define triggers with feed manifest params', () => { const feedName = '/ns/package/feed'; const params = { feed: feedName, feed_parameters: { hello: 'world' } }; const expected = { triggerName: 'myTrigger', overwrite: true, namespace: 'testing', feed: { feedName: 'package/feed', namespace: 'ns', trigger: '/testing/myTrigger', params: params.feed_parameters, }, }; const result = openwhiskCompileTriggers.compileTrigger('myTrigger', params); return expect(result).to.deep.equal(expected); }); it('should log triggers to console when verbose flag is set', () => { openwhiskCompileTriggers.options.verbose = true const log = sandbox.stub(openwhiskCompileTriggers.serverless.cli, 'log') const clog = sandbox.stub(openwhiskCompileTriggers.serverless.cli, 'consoleLog') const feedName = '/ns/package/feed'; const params = { feed: feedName, feed_parameters: { hello: 'world' } }; const expected = { triggerName: 'myTrigger', overwrite: true, namespace: 'testing', feed: { feedName: 'package/feed', namespace: 'ns', trigger: '/testing/myTrigger', params: params.feed_parameters, }, }; const result = openwhiskCompileTriggers.compileTrigger('myTrigger', params); expect(log.calledOnce).to.be.equal(true); expect(log.args[0][0]).to.be.equal(`Compiled Trigger (myTrigger): ${JSON.stringify(result)}`) }); }); }); ================================================ FILE: configCredentials/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const path = require('path'); const fse = require('fs-extra'); class OpenWhiskConfigCredentials { constructor(serverless, options) { this.serverless = serverless; this.options = options; // Note: we're not setting the provider here as this plugin should also be // run when the CWD is not an AWS service // this will be merged with the core config commands this.commands = { config: { commands: { credentials: { lifecycleEvents: [ 'config', ], options: { apihost: { usage: 'OpenWhisk platform API hostname.', shortcut: 'h', required: true, }, auth: { usage: 'User authentication credentials for the provider', shortcut: 'a', required: true, } }, }, }, }, }; this.hooks = { 'config:credentials:config': () => BbPromise.bind(this) .then(this.configureCredentials), }; } configureCredentials() { // sanitize this.options.provider = this.options.provider.toLowerCase(); this.options.profile = this.options.profile ? this.options.profile : 'default'; // resolve if provider option is not 'aws' if (this.options.provider !== 'openwhisk') { return BbPromise.resolve(); } // validate if (!this.options.apihost || !this.options.auth) { throw new this.serverless.classes.Error('Please include --auth and --apihost options for Apache OpenwWhisk.'); } this.serverless.cli.log('Setting up Apache OpenWhisk...'); this.serverless.cli.log('Saving your credentials in "~/.wskprops"...'); // locate home directory on user's machine const env = process.env; const home = env.HOME || env.USERPROFILE || (env.HOMEPATH ? ((env.HOMEDRIVE || 'C:/') + env.HOMEPATH) : null); if (!home) { throw new this.serverless.classes .Error('Can\'t find home directory on your local file system.'); } // check if ~/.wskprops exists const credsPath = path.join(home, '.wskprops'); if (this.serverless.utils.fileExistsSync(credsPath)) { // check if credentials files contains anything const credsFile = this.serverless.utils.readFileSync(credsPath); // if credentials file exists w/ auth, exit if (credsFile.length && credsFile.indexOf(`AUTH`) > -1) { this.serverless.cli.log( `Failed! ~/.wskprops exists and already has credentials.`); return BbPromise.resolve(); } } // write credentials file with 'default' profile this.serverless.utils.writeFile( credsPath, `APIHOST=${this.options.apihost} AUTH=${this.options.auth}`); this.serverless.cli.log( 'Success! Your Apache OpenWhisk credentials were stored in the configuration file (~/.wskprops).' ); return BbPromise.resolve(); } } module.exports = OpenWhiskConfigCredentials; ================================================ FILE: configCredentials/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const BbPromise = require('bluebird'); const fs = require('fs'); const fse = require('fs-extra'); const os = require('os'); const path = require('path'); const crypto = require('crypto'); const OpenWhiskConfigCredentials = require('../index'); const OpenWhiskProvider = require('../../provider/openwhiskProvider'); const getTmpDirPath = () => path.join(os.tmpdir(), 'tmpdirs-serverless', 'serverless', crypto.randomBytes(8).toString('hex')); const getTmpFilePath = (fileName) => path.join(getTmpDirPath(), fileName); describe('OpenWhiskConfigCredentials', () => { let openwhiskConfigCredentials; const serverless = { cli: { log: () => {} }, utils: {} }; beforeEach(() => { const options = { provider: 'openwhisk', apihost: 'openwhisk.ng.bluemix.net', auth: 'user:pass', }; openwhiskConfigCredentials = new OpenWhiskConfigCredentials(serverless, options); }); describe('#constructor()', () => { it('should have the command "config"', () => { expect(openwhiskConfigCredentials.commands.config).to.not.equal(undefined); }); it('should have the sub-command "credentials"', () => { expect(openwhiskConfigCredentials.commands.config.commands.credentials).to.not.equal(undefined); }); it('should have no lifecycle event', () => { expect(openwhiskConfigCredentials.commands.config.lifecycleEvents).to.equal(undefined); }); it('should have the lifecycle event "config" for the "credentials" sub-command', () => { expect(openwhiskConfigCredentials.commands.config.commands.credentials.lifecycleEvents) .to.deep.equal(['config']); }); it('should have the req. options "apihost" and "auth" for the "credentials" sub-command', () => { // eslint-disable-next-line no-unused-expressions expect(openwhiskConfigCredentials.commands.config.commands.credentials.options.apihost.required) .to.be.true; // eslint-disable-next-line no-unused-expressions expect(openwhiskConfigCredentials.commands.config.commands.credentials.options.auth.required) .to.be.true; }); it('should have a "config:credentials:config" hook', () => { expect(openwhiskConfigCredentials.hooks['config:credentials:config']).to.not.equal(undefined); }); it('should run promise chain in order for "config:credentials:config" hook', () => { const openwhiskConfigCredentialsStub = sinon .stub(openwhiskConfigCredentials, 'configureCredentials').returns(BbPromise.resolve()); return openwhiskConfigCredentials.hooks['config:credentials:config']().then(() => { expect(openwhiskConfigCredentialsStub.calledOnce).to.equal(true); openwhiskConfigCredentials.configureCredentials.restore(); }); }); }); describe('#configureCredentials()', () => { let homeDir; let tmpDirPath; let credentialsFilePath; beforeEach(() => { // create a new tmpDir for the homeDir path tmpDirPath = getTmpDirPath(); fse.mkdirsSync(tmpDirPath); // create the .openwhisk/credetials directory and file credentialsFilePath = path.join(tmpDirPath, '.wskprops'); fse.ensureFileSync(credentialsFilePath); // save the homeDir so that we can reset this later on homeDir = os.homedir(); process.env.HOME = tmpDirPath; process.env.HOMEPATH = tmpDirPath; process.env.USERPROFILE = tmpDirPath; }); it('should lowercase the provider option', () => { openwhiskConfigCredentials.options.provider = 'SOMEPROVIDER'; return openwhiskConfigCredentials.configureCredentials().then(() => { expect(openwhiskConfigCredentials.options.provider).to.equal('someprovider'); }); }); it('should resolve if the provider option is not "openwhisk"', (done) => { openwhiskConfigCredentials.options.provider = 'invalid-provider'; openwhiskConfigCredentials.configureCredentials().then(() => done()); }); it('should throw an error if the "apihost" and "auth" options are not given', () => { openwhiskConfigCredentials.options.apihost = false; openwhiskConfigCredentials.options.auth = false; expect(() => openwhiskConfigCredentials.configureCredentials()).to.throw(Error); }); it('should reject if credentials file exists and has credentials', () => { serverless.utils.fileExistsSync = () => true; serverless.utils.readFileSync = () => "AUTH"; const lines = [] serverless.cli.log = log => lines.push(log) return openwhiskConfigCredentials.configureCredentials().then(() => { expect(lines[2]).to.equal('Failed! ~/.wskprops exists and already has credentials.'); }); }); it('should write to empty credentials file', () => { openwhiskConfigCredentials.options.apihost = 'my-apihost'; openwhiskConfigCredentials.options.auth = 'my-auth'; let args serverless.utils.fileExistsSync = () => true; serverless.utils.readFileSync = () => ""; serverless.utils.writeFile = (path, content) => args = {path, content}; return openwhiskConfigCredentials.configureCredentials().then(() => { expect(args.content).to.equal('APIHOST=my-apihost\nAUTH=my-auth'); }); }); afterEach(() => { // recover the homeDir process.env.HOME = homeDir; process.env.HOMEPATH = homeDir; process.env.USERPROFILE = homeDir; }); }); }); ================================================ FILE: deploy/README.md ================================================ # Deploy This plugin (re)deploys the service to OpenWhisk. ## How it works `Deploy` starts by hooking into the [`deploy:initializeResources`](/lib/plugins/deploy) lifecycle. It fetches the user credentials for the OpenWhisk service being used, storing them under `serverless.service.defaults`. ### User Credentials The plugin attempts to parse configuration settings from the `.wskprops` file in the user's home directory, or from the file path specified in the `WSK_CONFIG_FILE` environment variable. These settings can be set manually using the following environment parameters. - **OW_AUTH** - Authentication key for OpenWhisk provider. - **OW_APIHOST** - API endpoint for OpenWhisk provider. - **OW_NAMESPACE** - User namespace for OpenWhisk resources. If both the properties file and environment parameters are missing one of these values, an error will be thrown. **Note:** Other plugins (e.g. the `Compile Functions` plugin) use these `defaults` property when compiling resource definitions and using the OpenWhisk APIs. ### Deploying Functions, Triggers, Rules and Feeds Next up it hooks into the [`deploy:deploy`](/lib/plugins/deploy) lifecycle and deploys the previously created resource definitions for Actions, Triggers, Feeds and Rules using the OpenWhisk APIs. Resources are deployed in the following order: - **Actions** - **Triggers** - **Feeds** - **Rules** Failure to deploy a single resource at any stage will cause the entire deployment to halt with the error message from the failed deployment. ================================================ FILE: deploy/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const initializeResources = require('./lib/initializeResources'); const deployPackages = require('./lib/deployPackages'); const deployFunctions = require('./lib/deployFunctions'); const deployRules = require('./lib/deployRules'); const deployTriggers = require('./lib/deployTriggers'); const deployFeeds = require('./lib/deployFeeds'); const deployApiGw = require('./lib/deployApiGw'); const deployServiceBindings = require('./lib/deployServiceBindings'); class OpenWhiskDeploy { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('openwhisk'); Object.assign( this, initializeResources, deployPackages, deployFunctions, deployApiGw, deployRules, deployTriggers, deployFeeds, deployServiceBindings ); this.hooks = { 'deploy:initializeResources': () => BbPromise.bind(this).then(this.initializeResources), 'deploy:deploy': () => BbPromise.bind(this) .then(this.deployPackages) .then(this.deployFunctions) .then(this.deploySequences) .then(this.deployRoutes) .then(this.deployTriggers) .then(this.deployFeeds) .then(this.deployRules) .then(this.configureServiceBindings) .then(() => this.serverless.cli.log('Deployment successful!')), }; } } module.exports = OpenWhiskDeploy; ================================================ FILE: deploy/lib/deployApiGw.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { deployRouteSwagger(route) { return this.provider.client().then(ow => { if (this.options.verbose) { this.serverless.cli.log(`Deploying API Gateway Route: ${JSON.stringify(route)}`); } return ow.routes.create(route) .then(() => { if (this.options.verbose) { this.serverless.cli.log(`Deployed API Gateway Route: ${JSON.stringify(route)}`); } }).catch(err => { throw new this.serverless.classes.Error( `Failed to deploy API Gateway route due to error: ${err.message}` ); }) }); }, replaceDefaultNamespace(swagger) { return this.provider.client() .then(ow => ow.actions.list()) .then(allActions => { for(let path in swagger.paths) { for(let verb in swagger.paths[path]) { const operation = swagger.paths[path][verb] if (operation['x-openwhisk'].namespace === '_') { const swaggerAction = operation['x-openwhisk'] const action = allActions.find(item => item.name === swaggerAction.action) swaggerAction.namespace = action.namespace swaggerAction.url = swaggerAction.url.replace(/web\/_/, `web/${action.namespace}`) const id = operation.operationId const stmts = swagger["x-ibm-configuration"].assembly.execute[0]['operation-switch'].case const stmt = stmts.find(stmt => stmt.operations[0] === id) const invoke = stmt.execute[stmt.execute.length -1].invoke invoke['target-url'] = invoke['target-url'].replace(/web\/_/, `web/${action.namespace}`) } } } return swagger }) }, deployRoutes() { const apigw = this.serverless.service.apigw; if (!apigw.swagger) { return BbPromise.resolve(); } this.serverless.cli.log('Deploying API Gateway definitions...'); return this.replaceDefaultNamespace(apigw.swagger) .then(swagger => this.deployRouteSwagger({ swagger })) } }; ================================================ FILE: deploy/lib/deployFeeds.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { deleteFeed(feed) { return new Promise((resolve, reject) => { this.provider.client().then(ow => ow.feeds.delete(feed).then(() => resolve(feed)).catch(() => resolve(feed)) ); }) }, deployFeed(feed) { return this.provider.client().then(ow => { if (this.options.verbose) { this.serverless.cli.log(`Deploying Feed: ${feed.feedName}`); } return ow.feeds.create(feed) .then(() => { if (this.options.verbose) { this.serverless.cli.log(`Deployed Feed: ${feed.feedName}`); } }).catch(err => { throw new this.serverless.classes.Error( `Failed to deploy feed (${feed.feedName}) due to error: ${err.message}` ); }) }); }, deployFeeds() { const feeds = this.getFeeds() if (feeds.length) { this.serverless.cli.log('Binding Feeds To Triggers...'); } const deleteAndDeployFeeds = feeds.map(feed => { return this.deleteFeed(feed).then(() => this.deployFeed(feed)) }) return BbPromise.all(deleteAndDeployFeeds) }, getFeeds() { const triggers = this.serverless.service.triggers; return Object.keys(triggers).map(t => triggers[t].feed).filter(f => f); } }; ================================================ FILE: deploy/lib/deployFunctions.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { deployFunctionHandler(functionHandler) { return this.provider.client().then(ow => { if (this.options.verbose) { this.serverless.cli.log(`Deploying Function: ${functionHandler.actionName}`); } return ow.actions.create(functionHandler) .then(() => { if (this.options.verbose) { this.serverless.cli.log(`Deployed Function: ${functionHandler.actionName}`); } }) .catch(err => { throw new this.serverless.classes.Error( `Failed to deploy function (${functionHandler.actionName}) due to error: ${err.message}` ); })} ); }, deploySequences() { const sequences = this.filterActions(true) if (sequences.length) { this.serverless.cli.log('Deploying Sequences...'); } return this.deployActions(sequences); }, deployFunctions() { this.serverless.cli.log('Deploying Functions...'); return this.deployActions(this.filterActions()) }, deployActions(names) { const actions = this.serverless.service.actions; return BbPromise.all( names.map(a => this.deployFunctionHandler(actions[a])) ); }, filterActions(sequence) { const actions = this.serverless.service.actions; const kind = action => action.action.exec.kind const match = action => (kind(action) === 'sequence') === !!sequence return Object.keys(actions).filter(a => match(actions[a])) }, }; ================================================ FILE: deploy/lib/deployPackages.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { deployPackage(pkge) { return this.provider.client().then(ow => { if (this.options.verbose) { this.serverless.cli.log(`Deploying Package: ${pkge.name}`); } return ow.packages.create(pkge) .then(() => { if (this.options.verbose) { this.serverless.cli.log(`Deployed Package: ${pkge.name}`); } }).catch(err => { throw new this.serverless.classes.Error( `Failed to deploy package (${pkge.name}) due to error: ${err.message}` ); }); }); }, deployPackages() { const pkges = this.getPackages(); if (pkges.length) { this.serverless.cli.log('Deploying Packages...'); } return BbPromise.all( pkges.map(p => this.deployPackage(p)) ); }, getPackages() { const pkges = this.serverless.service.packages; return Object.keys(this.serverless.service.packages).map(p => pkges[p]); } }; ================================================ FILE: deploy/lib/deployRules.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { deployRule(rule) { return this.provider.client().then(ow => { if (this.options.verbose) { this.serverless.cli.log(`Deploying Rule: ${rule.ruleName}`); } return ow.rules.create(rule) .then(() => { if (this.options.verbose) { this.serverless.cli.log(`Deployed Rule: ${rule.ruleName}`); } }).catch(err => { throw new this.serverless.classes.Error( `Failed to deploy rule (${rule.ruleName}) due to error: ${err.message}` ); }) }); }, enableRule(rule) { return this.provider.client().then(ow => ow.rules.enable(rule).catch(err => { throw new this.serverless.classes.Error( `Failed to enable rule (${rule.ruleName}) due to error: ${err.message}` ); }) ); }, deployRules() { const rules = this.getRules(); if (rules.length) { this.serverless.cli.log('Deploying Rules...'); } return BbPromise.all(rules.map(r => this.deployRule(r).then(() => this.enableRule(r)))); }, getRules() { const rules = this.serverless.service.rules; return Object.keys(this.serverless.service.rules).map(r => rules[r]); } }; ================================================ FILE: deploy/lib/deployServiceBindings.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const { spawn } = require('child_process'); module.exports = { configureServiceBinding(binding) { if (this.options.verbose) { this.serverless.cli.log(`Configuring Service Binding: ${JSON.stringify(binding)}`); } return new Promise((resolve, reject) => { const args = ['wsk', 'service', 'bind', binding.name, binding.action] if (binding.instance) { args.push("--instance", binding.instance) } if (binding.key) { args.push("--keyname", binding.key) } const bx = spawn('bx', args); const stdout = [] const stderr = [] bx.stdout.on('data', data => { stdout.push(data.toString()) }); bx.stderr.on('data', (data) => { stderr.push(data.toString()) }); bx.on('error', (err) => { if (err.code === 'ENOENT') { const err = new this.serverless.classes.Error( 'Unable to execute `bx wsk service bind` command. Is IBM Cloud CLI installed?' ) return reject(err) } reject(err.message) }); bx.on('close', (code) => { if (code === 2) { const err = new this.serverless.classes.Error( 'Unable to execute `bx wsk service bind` command. Is IBM Cloud Functions CLI plugin installed?' ) return reject(err) } if (code > 0) { const errmsg = (stderr[0] || '').split('\n')[0] const err = new this.serverless.classes.Error(`Failed to configure service binding (${JSON.stringify(binding)})\n ${errmsg}`); return reject(err) } if (this.options.verbose) { this.serverless.cli.log(`Configured Service Binding: ${JSON.stringify(binding)}`); } resolve() }); }); }, configureServiceBindings() { const bindings = this.getServiceBindings(); if (bindings.fns.length || bindings.packages.length) { this.serverless.cli.log('Configuring Service Bindings...'); } return BbPromise.all( bindings.packages.map(sbs => BbPromise.mapSeries(sbs, sb => this.configureServiceBinding(sb))) ).then(() => BbPromise.all( bindings.fns.map(sbs => BbPromise.mapSeries(sbs, sb => this.configureServiceBinding(sb))) )); }, getServiceBindings() { return this.serverless.service.bindings || { fns: [], packages: [] } ; } }; ================================================ FILE: deploy/lib/deployTriggers.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { deployTrigger(trigger) { return this.provider.client().then(ow => { if (this.options.verbose) { this.serverless.cli.log(`Deploying Trigger: ${trigger.triggerName}`); } return ow.triggers.create(trigger) .then(() => { if (this.options.verbose) { this.serverless.cli.log(`Deployed Trigger: ${trigger.triggerName}`); } }).catch(err => { throw new this.serverless.classes.Error( `Failed to deploy trigger (${trigger.triggerName}) due to error: ${err.message}` ); }); }); }, deployTriggers() { const triggers = this.getTriggers(this.serverless.service.triggers); if(triggers.length) { this.serverless.cli.log('Deploying Triggers...'); } return BbPromise.all( triggers.map(t => this.deployTrigger(t)) ); }, getTriggers(triggers) { const feedMask = { feed: undefined }; return Object.keys(triggers) .map(t => { const trigger = triggers[t]; if (trigger.feed) { Object.assign(trigger, { trigger: { annotations: [{ key: 'feed', value: `/${trigger.feed.namespace}/${trigger.feed.feedName}`, }], }, }); } return Object.assign(trigger, feedMask); }); }, }; ================================================ FILE: deploy/lib/initializeResources.js ================================================ 'use strict'; // This class ensures that all the required authentication credentials // are available, either from the user's .wskprops file or environment // parameters. module.exports = { initializeResources() { this.serverless.cli.log('Initialising Resources...'); const ParamNames = ['auth', 'apihost', 'namespace']; const Defaults = this.serverless.service.provider; return this.provider.props() .then(props => { Object.assign(Defaults, props); ParamNames.forEach(key => { if (!Defaults[key]) { const envName = `OW_${key.toUpperCase()}`; throw new this.serverless.classes.Error( `OpenWhisk required configuration parameter ${envName} missing or blank. ` + 'Must be present in .wskprops as environment variable.' ); } }); }); }, }; ================================================ FILE: deploy/lib/validate.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { validate () { if (!this.serverless.config.servicePath) { throw new this.serverless.classes.Error('This command can only be run inside a service'); } this.options.stage = this.options.stage || (this.serverless.service.provider && this.serverless.service.provider.stage) || 'dev'; this.options.region = this.options.region || (this.serverless.service.provider && this.serverless.service.provider.region) || 'us-east-1'; return BbPromise.resolve(); } }; ================================================ FILE: deploy/tests/all.js ================================================ 'use strict'; require('./validate'); require('./initializeResources'); require('./deployFunctions'); require('./deployRules'); require('./deployPackages'); require('./deployTriggers'); require('./deployFeeds'); require('./deployApiGw'); require('./deployServiceBindings'); require('./index'); ================================================ FILE: deploy/tests/deployApiGw.js ================================================ 'use strict'; const expect = require('chai').expect; const OpenWhiskDeploy = require('../index'); const sinon = require('sinon'); const chaiAsPromised = require('chai-as-promised'); const fs = require('fs'); require('chai').use(chaiAsPromised); describe('deployHttpEvents', () => { let serverless; let openwhiskDeploy; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); const CLI = function () { this.log = function () {};}; serverless = {classes: {Error, CLI}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskDeploy = new OpenWhiskDeploy(serverless, options); openwhiskDeploy.serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskDeploy.serverless.service.service = 'my-service' openwhiskDeploy.serverless.service.provider = { namespace: 'testing', apihost: 'openwhisk.org', auth: 'user:pass', }; openwhiskDeploy.provider = { client: () => {} } }); afterEach(() => { sandbox.restore(); }); describe('#replaceDefaultNamespace()', async () => { it('should return same swagger doc without default namespace', async () => { const source = fs.readFileSync('./deploy/tests/resources/swagger.json', 'utf-8') const swagger = JSON.parse(source) sandbox.stub(openwhiskDeploy.provider, 'client', () => { const list = params => { return Promise.resolve(); }; return Promise.resolve({ actions: { list } }); }); let result = await openwhiskDeploy.replaceDefaultNamespace(swagger) expect(result).to.be.deep.equal(swagger) }) it('should replace default namespace in swagger doc', async () => { const without_default_ns = fs.readFileSync('./deploy/tests/resources/swagger.json', 'utf-8') const with_default_ns = fs.readFileSync('./deploy/tests/resources/swagger_default_ns.json', 'utf-8') const source = JSON.parse(with_default_ns) const converted = JSON.parse(without_default_ns) const actions = [{"name":"hello","namespace":"user@host.com_dev"}] sandbox.stub(openwhiskDeploy.provider, 'client', () => { const list = params => { return Promise.resolve(actions); }; return Promise.resolve({ actions: { list } }); }); let result = await openwhiskDeploy.replaceDefaultNamespace(source) expect(result).to.be.deep.equal(converted) }) it('should return same swagger doc including path params', async () => { const without_default_ns = fs.readFileSync('./deploy/tests/resources/swagger_ns_paths.json', 'utf-8') const with_default_ns = fs.readFileSync('./deploy/tests/resources/swagger_paths.json', 'utf-8') const source = JSON.parse(with_default_ns) const converted = JSON.parse(without_default_ns) const actions = [{"name":"hello","namespace":"user@host.com_dev"}] sandbox.stub(openwhiskDeploy.provider, 'client', () => { const list = params => { return Promise.resolve(actions); }; return Promise.resolve({ actions: { list } }); }); let result = await openwhiskDeploy.replaceDefaultNamespace(source) expect(result).to.be.deep.equal(converted) }) }) /** describe('#configRouteSwagger()', () => { it('should update swagger with CORS config parameter', () => { const source = fs.readFileSync('./deploy/tests/resources/swagger.json', 'utf-8') const swagger = JSON.parse(source) const options = { cors: false } let result = openwhiskDeploy.configRouteSwagger(swagger, options) expect(result['x-ibm-configuration'].cors.enabled).to.be.equal(false) swagger['x-ibm-configuration'].cors.enabled = false options.cors = true result = openwhiskDeploy.configRouteSwagger(swagger, options) expect(result['x-ibm-configuration'].cors.enabled).to.be.equal(true) }) it('should maintain existing swagger config parameters', () => { const source = fs.readFileSync('./deploy/tests/resources/swagger.json', 'utf-8') const swagger = JSON.parse(source) swagger['x-ibm-configuration'].test = 'value' const options = { cors: false } let result = openwhiskDeploy.configRouteSwagger(swagger, options) expect(result['x-ibm-configuration'].test).to.be.equal('value') }) it('should leave swagger the same without config parameters', () => { const source = fs.readFileSync('./deploy/tests/resources/swagger.json', 'utf-8') const swagger = JSON.parse(source) const options = { } const result = openwhiskDeploy.configRouteSwagger(swagger, options) expect(result).to.be.deep.equal(swagger) }) }) */ /** describe('#updateRouteConfig()', () => { it('should retrieve and deploy updated api gw route swagger to openwhisk', () => { const source = fs.readFileSync('./deploy/tests/resources/swagger.json', 'utf-8') const swagger = JSON.parse(source) sandbox.stub(openwhiskDeploy.provider, 'client', () => { const get = params => { expect(params).to.be.deep.equal({basepath: '/my-service'}); return Promise.resolve({ apis: [{value: {apidoc:swagger}}]}); }; const create = params => { expect(params.swagger).to.be.deep.equal(swagger); return Promise.resolve({}); }; return Promise.resolve({ routes: { get: get, create: create } }); }); return expect(openwhiskDeploy.updateRoutesConfig('/my-service', {random: false})) .to.eventually.be.fulfilled; }); }); */ describe('#deployRouteSwagger()', () => { it('should deploy api gw route handler to openwhisk', () => { sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { expect(params).to.be.deep.equal({foo: 'bar'}); return Promise.resolve(); }; return Promise.resolve({ routes: { create } }); }); return expect(openwhiskDeploy.deployRouteSwagger({foo: 'bar'})) .to.eventually.be.fulfilled; }); it('should reject when function handler fails to deploy with error message', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = () => Promise.reject(err); return Promise.resolve({ routes: { create } }); }); return expect(openwhiskDeploy.deployRouteSwagger({relpath: '/foo/bar'})) .to.eventually.be.rejectedWith( new RegExp(`${err.message}`) ); }); it('should log function deploy information with verbose flag', () => { openwhiskDeploy.options.verbose = true const log = sandbox.stub(openwhiskDeploy.serverless.cli, 'log') const clog = sandbox.stub(openwhiskDeploy.serverless.cli, 'consoleLog') sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { return Promise.resolve(); }; return Promise.resolve({ routes: { create } }); }); return openwhiskDeploy.deployRouteSwagger({foo: 'bar'}).then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0]).to.be.equal('Deploying API Gateway Route: ' + JSON.stringify({foo: 'bar'})) expect(log.args[1][0]).to.be.equal('Deployed API Gateway Route: ' + JSON.stringify({foo: 'bar'})) }) }); }); }); ================================================ FILE: deploy/tests/deployFeeds.js ================================================ 'use strict'; const expect = require('chai').expect; const OpenWhiskDeploy = require('../index'); const sinon = require('sinon'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('deployFeeds', () => { let serverless; let openwhiskDeploy; let sandbox; const mockFeedObject = { feeds: { myFeed: { feedName: 'myFeed', }, }, }; beforeEach(() => { sandbox = sinon.sandbox.create(); const CLI = function () { this.log = function () {};}; serverless = {classes: {Error, CLI}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskDeploy = new OpenWhiskDeploy(serverless, options); openwhiskDeploy.serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskDeploy.serverless.service.provider = { namespace: 'testing', apihost: 'openwhisk.org', auth: 'user:pass', }; openwhiskDeploy.provider = { client: () => {} } }); afterEach(() => { sandbox.restore(); }); describe('#deployFeeds()', () => { it('should call deleteFeed & deployFeed for each registered trigger feed', () => { const deployStub = sandbox.stub(openwhiskDeploy, 'deployFeed', () => Promise.resolve()); const deleteStub = sandbox.stub(openwhiskDeploy, 'deleteFeed', () => Promise.resolve()); const triggerWithFeed = { feed: { feedName: 'blah' } }; openwhiskDeploy.serverless.service.triggers = { myTrigger: triggerWithFeed, anotherTrigger: {}, finalTrigger: triggerWithFeed }; return openwhiskDeploy.deployFeeds().then(() => { expect(deployStub.calledTwice).to.be.equal(true); expect(deployStub.calledWith(triggerWithFeed.feed)).to.be.equal(true); expect(deleteStub.calledTwice).to.be.equal(true); expect(deleteStub.calledWith(triggerWithFeed.feed)).to.be.equal(true); }); }); it('should not log anything for empty feeds', () => { openwhiskDeploy.serverless.service.triggers = {}; const log = sandbox.stub(openwhiskDeploy.serverless.cli, 'log'); return openwhiskDeploy.deployFeeds().then(() => { console.log(log.called) expect(log.called).to.be.equal(false); }); }) }); describe('#deployFeed()', () => { it('should deploy feed to openwhisk', () => { const stub = sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { expect(params).to.be.deep.equal(mockFeedObject.feeds.myFeed); return Promise.resolve(); }; return Promise.resolve({ feeds: { create } }); }); return openwhiskDeploy.deployFeed(mockFeedObject.feeds.myFeed).then(() => { expect(stub.called).to.be.true }) }); it('should log function deploy information with verbose flag', () => { openwhiskDeploy.options.verbose = true const log = sandbox.stub(openwhiskDeploy.serverless.cli, 'log') const clog = sandbox.stub(openwhiskDeploy.serverless.cli, 'consoleLog') sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { return Promise.resolve(); }; return Promise.resolve({ feeds: { create } }); }); return openwhiskDeploy.deployFeed(mockFeedObject.feeds.myFeed).then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0]).to.be.equal('Deploying Feed: myFeed') expect(log.args[1][0]).to.be.equal('Deployed Feed: myFeed') }) }); it('should reject when function handler fails to deploy with error message', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = () => Promise.reject(err); return Promise.resolve({ feeds: { create } }); }); return expect(openwhiskDeploy.deployFeed(mockFeedObject.feeds.myFeed)) .to.eventually.be.rejectedWith( new RegExp(`${mockFeedObject.feeds.myFeed.feedName}.*${err.message}`) ); }); }); describe('#deleteFeed()', () => { it('should delete feed from openwhisk', () => { const stub = sandbox.stub(openwhiskDeploy.provider, 'client', () => { const _delete = params => { expect(params).to.be.deep.equal(mockFeedObject.feeds.myFeed); return Promise.resolve(); }; return Promise.resolve({ feeds: { "delete": _delete } }); }); return openwhiskDeploy.deleteFeed(mockFeedObject.feeds.myFeed).then(() => { expect(stub.called).to.be.true }) }); it('should handle errors from non-existant feeds', () => { const err = { message: 'some reason' }; const stub = sandbox.stub(openwhiskDeploy.provider, 'client', () => { const _delete = () => Promise.reject(err); return Promise.resolve({ feeds: { "delete": _delete } }); }); return openwhiskDeploy.deleteFeed(mockFeedObject.feeds.myFeed).then(() => { expect(stub.called).to.be.true }) }); }); }); ================================================ FILE: deploy/tests/deployFunctions.js ================================================ 'use strict'; const expect = require('chai').expect; const OpenWhiskDeploy = require('../index'); const sinon = require('sinon'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('deployFunctions', () => { let serverless; let openwhiskDeploy; let sandbox; const fileContents = `function main() { return {payload: 'Hello world'}; }`; const mockFunctionObject = { actionName: 'serviceName_functionName', namespace: 'namespace', action: { exec: { kind: 'nodejs:default', code: fileContents }, limits: { timeout: 60 * 1000, memory: 256 }, parameters: [{ key: 'foo', value: 'bar' }], }, }; beforeEach(() => { sandbox = sinon.sandbox.create(); const CLI = function () { this.log = function () {};}; serverless = {classes: {Error, CLI}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskDeploy = new OpenWhiskDeploy(serverless, options); openwhiskDeploy.serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskDeploy.serverless.service.provider = { namespace: 'testing', apihost: 'openwhisk.org', auth: 'user:pass', }; openwhiskDeploy.provider = { client: () => {} } }); afterEach(() => { sandbox.restore(); }); describe('#deployFunctionHandler()', () => { it('should deploy function handler to openwhisk', () => { sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { expect(params).to.be.deep.equal({ actionName: mockFunctionObject.actionName, namespace: mockFunctionObject.namespace, action: mockFunctionObject.action, }); return Promise.resolve(); }; return Promise.resolve({ actions: { create } }); }); return expect(openwhiskDeploy.deployFunctionHandler(mockFunctionObject)) .to.eventually.be.fulfilled; }); it('should log function deploy information with verbose flag', () => { openwhiskDeploy.options.verbose = true const log = sandbox.stub(openwhiskDeploy.serverless.cli, 'log') const clog = sandbox.stub(openwhiskDeploy.serverless.cli, 'consoleLog') sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { return Promise.resolve(); }; return Promise.resolve({ actions: { create } }); }); return openwhiskDeploy.deployFunctionHandler(mockFunctionObject).then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0]).to.be.equal('Deploying Function: serviceName_functionName') expect(log.args[1][0]).to.be.equal('Deployed Function: serviceName_functionName') }) }); it('should reject when function handler fails to deploy with error message', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = () => Promise.reject(err); return Promise.resolve({ actions: { create } }); }); return expect(openwhiskDeploy.deployFunctionHandler(mockFunctionObject)) .to.eventually.be.rejectedWith( new RegExp(`${mockFunctionObject.actionName}.*${err.message}`) ); }); }); }); ================================================ FILE: deploy/tests/deployPackages.js ================================================ 'use strict'; const expect = require('chai').expect; const OpenWhiskDeploy = require('../index'); const sinon = require('sinon'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('deployPackages', () => { let serverless; let openwhiskDeploy; let sandbox; const mockPackageObject = { packages: { myPackage: { name: 'myPackage', namespace: 'myNamespace' }, }, }; beforeEach(() => { sandbox = sinon.sandbox.create(); const CLI = function () { this.log = function () {};}; serverless = {classes: {Error, CLI}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskDeploy = new OpenWhiskDeploy(serverless, options); openwhiskDeploy.serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskDeploy.serverless.service.provider = { namespace: 'testing', apihost: 'openwhisk.org', auth: 'user:pass', }; openwhiskDeploy.provider = { client: () => {} } }); afterEach(() => { sandbox.restore(); }); describe('#deployPackage()', () => { it('should deploy package to openwhisk', () => { sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { expect(params).to.be.deep.equal(mockPackageObject.packages.myPackage); return Promise.resolve(); }; return Promise.resolve({ packages: { create } }); }); return expect(openwhiskDeploy.deployPackage(mockPackageObject.packages.myPackage)) .to.eventually.be.fulfilled; }); it('should reject when function handler fails to deploy with error message', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = () => Promise.reject(err); return Promise.resolve({ packages: { create } }); }); return expect(openwhiskDeploy.deployPackage(mockPackageObject.packages.myPackage)) .to.eventually.be.rejectedWith( new RegExp(`${mockPackageObject.packages.myPackage.name}.*${err.message}`) ); }); it('should log package deploy information with verbose flag', () => { openwhiskDeploy.options.verbose = true const log = sandbox.stub(openwhiskDeploy.serverless.cli, 'log') const clog = sandbox.stub(openwhiskDeploy.serverless.cli, 'consoleLog') sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { return Promise.resolve(); }; return Promise.resolve({ packages: { create } }); }); return openwhiskDeploy.deployPackage(mockPackageObject.packages.myPackage).then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0]).to.be.equal('Deploying Package: myPackage') expect(log.args[1][0]).to.be.equal('Deployed Package: myPackage') }) }) }); }); ================================================ FILE: deploy/tests/deployRules.js ================================================ 'use strict'; const expect = require('chai').expect; const OpenWhiskDeploy = require('../index'); const sinon = require('sinon'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('deployRules', () => { let serverless; let openwhiskDeploy; let sandbox; const mockRuleObject = { rules: { myRule: { ruleName: 'myRule', namepspace: 'myNamespace', action: 'myAction', trigger: 'myTrigger', }, }, }; beforeEach(() => { sandbox = sinon.sandbox.create(); const CLI = function () { this.log = function () {};}; serverless = {classes: {Error, CLI}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskDeploy = new OpenWhiskDeploy(serverless, options); openwhiskDeploy.serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskDeploy.serverless.service.provider = { namespace: 'testing', apihost: 'openwhisk.org', auth: 'user:pass', }; openwhiskDeploy.provider = { client: () => {} } }); afterEach(() => { sandbox.restore(); }); describe('#deployRule()', () => { it('should deploy function handler to openwhisk', () => { sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { expect(params).to.be.deep.equal(mockRuleObject.rules.myRule); return Promise.resolve(); }; return Promise.resolve({ rules: { create } }); }); return expect(openwhiskDeploy.deployRule(mockRuleObject.rules.myRule)) .to.eventually.be.fulfilled; }); it('should reject when function handler fails to deploy with error message', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = () => Promise.reject(err); return Promise.resolve({ rules: { create } }); }); return expect(openwhiskDeploy.deployRule(mockRuleObject.rules.myRule)) .to.eventually.be.rejectedWith( new RegExp(`${mockRuleObject.rules.myRule.ruleName}.*${err.message}`) ); }); it('should log function deploy information with verbose flag', () => { openwhiskDeploy.options.verbose = true const log = sandbox.stub(openwhiskDeploy.serverless.cli, 'log') const clog = sandbox.stub(openwhiskDeploy.serverless.cli, 'consoleLog') sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { return Promise.resolve(); }; return Promise.resolve({ rules: { create } }); }); return openwhiskDeploy.deployRule(mockRuleObject.rules.myRule).then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0]).to.be.equal('Deploying Rule: myRule') expect(log.args[1][0]).to.be.equal('Deployed Rule: myRule') }) }); }); describe('#enableRule()', () => { it('should call enable rule on openwhisk', () => { sandbox.stub(openwhiskDeploy.provider, 'client', () => { const enable = params => { expect(params).to.be.deep.equal(mockRuleObject.rules.myRule); return Promise.resolve(); }; return Promise.resolve({ rules: { enable } }); }); return expect(openwhiskDeploy.enableRule(mockRuleObject.rules.myRule)) .to.eventually.be.fulfilled; }); it('should reject when enable rule fails with error message', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskDeploy.provider, 'client', () => { const enable = () => Promise.reject(err); return Promise.resolve({ rules: { enable } }); }); return expect(openwhiskDeploy.enableRule(mockRuleObject.rules.myRule)) .to.eventually.be.rejectedWith( new RegExp(`${mockRuleObject.rules.myRule.ruleName}.*${err.message}`) ); }); }); describe('#deployRules()', () => { it('should call deployRule & enableRule for each rule', () => { const deployStub = sandbox.stub(openwhiskDeploy, 'deployRule', () => Promise.resolve()); const enableStub = sandbox.stub(openwhiskDeploy, 'enableRule', () => Promise.resolve()); openwhiskDeploy.serverless.service.rules = { hello: {}, foo: {} }; return openwhiskDeploy.deployRules().then(() => { expect(deployStub.calledTwice).to.be.equal(true); expect(deployStub.calledWith({})).to.be.equal(true); expect(enableStub.calledTwice).to.be.equal(true); expect(enableStub.calledWith({})).to.be.equal(true); }); }); it('should not log anything for empty feeds', () => { openwhiskDeploy.serverless.service.rules = {}; const log = sandbox.stub(openwhiskDeploy.serverless.cli, 'log'); return openwhiskDeploy.deployRules().then(() => { console.log(log.called) expect(log.called).to.be.equal(false); }); }) }); }); ================================================ FILE: deploy/tests/deployServiceBindings.js ================================================ 'use strict'; const expect = require('chai').expect; const OpenWhiskDeploy = require('../index'); const sinon = require('sinon'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('deployServiceBindings', () => { let serverless; let openwhiskDeploy; let sandbox; const mockPackageObject = { packages: { myPackage: { name: 'myPackage', namespace: 'myNamespace' }, }, }; beforeEach(() => { sandbox = sinon.sandbox.create(); const CLI = function () { this.log = function () {};}; serverless = {classes: {Error, CLI}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskDeploy = new OpenWhiskDeploy(serverless, options); openwhiskDeploy.serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskDeploy.serverless.service.provider = { namespace: 'testing', apihost: 'openwhisk.org', auth: 'user:pass', }; openwhiskDeploy.provider = { client: () => {} } }); afterEach(() => { sandbox.restore(); }); describe('#configureServiceBindings()', () => { it('should call binding command for each binding and return when all finish', () => { const bindings = [[{name: 'a'}, {name: 'a'}, {name: 'a'}]] openwhiskDeploy.serverless.service.bindings = { fns: bindings, packages: bindings } sandbox.stub(openwhiskDeploy, 'configureServiceBinding', () => { return Promise.resolve(); }); return openwhiskDeploy.configureServiceBindings().then(results => { expect(results.length).to.equal(bindings.length) }) }); it('should reject when function handler fails to deploy with error message', () => { const bindings = [[{name: 'a'}, {name: 'a'}, {name: 'a'}]] openwhiskDeploy.serverless.service.bindings = { fns: bindings, packages: bindings } const err = { message: 'some reason' }; sandbox.stub(openwhiskDeploy, 'configureServiceBinding', () => { return Promise.reject(err); }); return expect(openwhiskDeploy.configureServiceBindings()) .to.eventually.be.rejectedWith(new RegExp(`${err.message}`)); }); }); }); ================================================ FILE: deploy/tests/deployTriggers.js ================================================ 'use strict'; const expect = require('chai').expect; const OpenWhiskDeploy = require('../index'); const sinon = require('sinon'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('deployTriggers', () => { let serverless; let openwhiskDeploy; let sandbox; const mockTriggerObject = { triggers: { myTrigger: { triggerName: 'myTrigger', namespace: 'myNamespace', }, feedTrigger: { triggerName: 'feedTrigger', namespace: 'feedNamespace', trigger: { annotations: [ { key: 'feed', value: '/whisk.system/alarms/alarm' } ] }, }, }, serviceTriggers: { myTrigger: { triggerName: 'myTrigger', namespace: 'myNamespace', }, feedTrigger: { triggerName: 'feedTrigger', namespace: 'feedNamespace', feed: { trigger : '/feedNamespace/feedTrigger', feedName: 'alarms/alarm', namespace: 'whisk.system', params: { cron: '* * * * *', trigger_payload: {} } } }, }, owTriggers: [ { triggerName: 'myTrigger', namespace: 'myNamespace', feed: undefined }, { triggerName: 'feedTrigger', namespace: 'feedNamespace', trigger: { annotations: [ { key: 'feed', value: '/whisk.system/alarms/alarm' } ], }, feed: undefined }, ], }; beforeEach(() => { sandbox = sinon.sandbox.create(); const CLI = function () { this.log = function () {}; }; serverless = { classes: { Error, CLI }, service: { provider: {}, resources: {}, getAllFunctions: () => [] }, getProvider: sandbox.spy() }; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskDeploy = new OpenWhiskDeploy(serverless, options); openwhiskDeploy.serverless.cli = { consoleLog: () => {}, log: () => {} }; openwhiskDeploy.serverless.service.provider = { namespace: 'testing', apihost: 'openwhisk.org', auth: 'user:pass', }; openwhiskDeploy.provider = { client: () => {} }; }); afterEach(() => { sandbox.restore(); }); describe('#deployTrigger()', () => { it('should deploy trigger to openwhisk', () => { sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { expect(params).to.be.deep.equal(mockTriggerObject.triggers.myTrigger); return Promise.resolve(); }; return Promise.resolve({ triggers: { create } }); }); return expect(openwhiskDeploy.deployTrigger(mockTriggerObject.triggers.myTrigger)) .to.eventually.be.fulfilled; }); it('should reject when function handler fails to deploy with error message', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = () => Promise.reject(err); return Promise.resolve({ triggers: { create } }); }); return expect(openwhiskDeploy.deployTrigger(mockTriggerObject.triggers.myTrigger)) .to.eventually.be.rejectedWith( new RegExp(`${mockTriggerObject.triggers.myTrigger.triggerName}.*${err.message}`) ); }); it('should log function deploy information with verbose flag', () => { openwhiskDeploy.options.verbose = true; const log = sandbox.stub(openwhiskDeploy.serverless.cli, 'log'); const clog = sandbox.stub(openwhiskDeploy.serverless.cli, 'consoleLog'); sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => Promise.resolve(); return Promise.resolve({ triggers: { create } }); }); return openwhiskDeploy.deployTrigger(mockTriggerObject.triggers.myTrigger).then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0]).to.be.equal('Deploying Trigger: myTrigger'); expect(log.args[1][0]).to.be.equal('Deployed Trigger: myTrigger'); }); }); it('should deploy trigger with feed annotation to openwhisk', () => { sandbox.stub(openwhiskDeploy.provider, 'client', () => { const create = params => { expect(params).to.be.deep.equal(mockTriggerObject.triggers.feedTrigger); return Promise.resolve(); }; return Promise.resolve({ triggers: { create } }); }); return expect(openwhiskDeploy.deployTrigger(mockTriggerObject.triggers.feedTrigger)) .to.eventually.be.fulfilled; }); it('should change the trigger format to match the ow.', () => { expect(openwhiskDeploy.getTriggers(mockTriggerObject.serviceTriggers)) .to.be.deep.equal(mockTriggerObject.owTriggers) }); }); }); ================================================ FILE: deploy/tests/index.js ================================================ 'use strict'; const OpenWhiskDeploy = require('../index'); const expect = require('chai').expect; const BbPromise = require('bluebird'); const sinon = require('sinon'); describe('OpenWhiskDeploy', () => { const CLI = function () { this.log = function () {};}; const serverless = {classes: {Error, CLI}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; const openwhiskDeploy = new OpenWhiskDeploy(serverless, options); openwhiskDeploy.serverless.cli = new serverless.classes.CLI(); describe('#constructor()', () => { it('should have hooks', () => expect(openwhiskDeploy.hooks).to.be.not.empty); it('should run "deploy:initializeResources" hook promise chain in order', () => { const initializeResourcesStub = sinon .stub(openwhiskDeploy, 'initializeResources').returns(BbPromise.resolve()); return openwhiskDeploy.hooks['deploy:initializeResources']().then(() => { expect(initializeResourcesStub.calledOnce).to.be.equal(true); openwhiskDeploy.initializeResources.restore(); }); }); it('should run "deploy:deploy" promise chain in order', () => { const deployPackagesStub = sinon .stub(openwhiskDeploy, 'deployPackages').returns(BbPromise.resolve()); const deployFunctionsStub = sinon .stub(openwhiskDeploy, 'deployFunctions').returns(BbPromise.resolve()); const deploySequencesStub = sinon .stub(openwhiskDeploy, 'deploySequences').returns(BbPromise.resolve()); const deployRulesStub = sinon .stub(openwhiskDeploy, 'deployRules').returns(BbPromise.resolve()); const deployTriggersStub = sinon .stub(openwhiskDeploy, 'deployTriggers').returns(BbPromise.resolve()); const deployFeedsStub = sinon .stub(openwhiskDeploy, 'deployFeeds').returns(BbPromise.resolve()); const deployRoutesStub = sinon .stub(openwhiskDeploy, 'deployRoutes').returns(BbPromise.resolve()); return openwhiskDeploy.hooks['deploy:deploy']().then(() => { expect(deployPackagesStub.calledOnce).to.be.equal(true); expect(deployFunctionsStub.calledOnce).to.be.equal(true); expect(deploySequencesStub.calledOnce).to.be.equal(true); expect(deployRoutesStub.calledOnce).to.be.equal(true); expect(deployRulesStub.calledOnce).to.be.equal(true); expect(deployTriggersStub.calledOnce).to.be.equal(true); expect(deployFeedsStub.calledOnce).to.be.equal(true); openwhiskDeploy.deployFunctions.restore(); }); }); }); }); ================================================ FILE: deploy/tests/initializeResources.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const chaiAsPromised = require('chai-as-promised'); const OpenWhiskDeploy = require('../index'); require('chai').use(chaiAsPromised); describe('#initializeResources()', () => { let serverless; let sandbox; let openwhiskDeploy; beforeEach(() => { sandbox = sinon.sandbox.create(); const CLI = function () { this.log = function () {};}; serverless = {classes: {Error, CLI}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskDeploy = new OpenWhiskDeploy(serverless, options); openwhiskDeploy.provider = {props: () => {}}; serverless.cli = { log: () => {} }; }); afterEach(() => { sandbox.restore(); }); it('should instantiate openwhisk resources from openwhisk authentication properties', () => { const mockObject = { apihost: 'blah.blah.com', auth: 'another_user:another_pass', namespace: 'user@user.com', }; sandbox.stub(openwhiskDeploy.provider, 'props', () => Promise.resolve(mockObject)); return openwhiskDeploy.initializeResources().then(() => { expect(openwhiskDeploy.serverless.service.provider).to.deep.equal(mockObject); }); }); it('should throw error when parameter (OW_AUTH) is missing', () => { const mockObject = { apihost: 'blah.blah.com', namespace: 'user@user.com', }; sandbox.stub(openwhiskDeploy.provider, 'props', () => Promise.resolve(mockObject)); return expect(openwhiskDeploy.initializeResources()).to.be.rejectedWith(/OW_AUTH/); }); it('should throw error when parameter (OW_APIHOST) is missing', () => { const mockObject = { auth: 'user:pass', namespace: 'user@user.com', }; sandbox.stub(openwhiskDeploy.provider, 'props', () => Promise.resolve(mockObject)); return expect(openwhiskDeploy.initializeResources()).to.be.rejectedWith(/OW_APIHOST/); }); it('should throw error when parameter (OW_NAMESPACE) is missing', () => { const mockObject = { auth: 'user:pass', apihost: 'blah.blah.com', }; sandbox.stub(openwhiskDeploy.provider, 'props', () => Promise.resolve(mockObject)); return expect(openwhiskDeploy.initializeResources()).to.be.rejectedWith(/OW_NAMESPACE/); }); }); ================================================ FILE: deploy/tests/resources/swagger.json ================================================ { "swagger": "2.0", "basePath": "/testing", "info": { "title": "testing", "version": "1.0" }, "paths": { "/testing": { "get": { "operationId": "getTesting", "responses": { "200": { "description": "A successful invocation response" } }, "x-openwhisk": { "action": "hello", "namespace": "user@host.com_dev", "package": "default", "url": "https://openwhisk/api/v1/web/user@host.com_dev/default/hello.json" } } } }, "x-ibm-configuration": { "assembly": { "execute": [ { "operation-switch": { "case": [ { "execute": [ { "set-variable": { "actions": [ { "set": "message.headers.X-Require-Whisk-Auth", "value": "3b08f67e-c7fa-4998-9096-ffa355932a3d" } ] } }, { "invoke": { "target-url": "https://openwhisk/api/v1/web/user@host.com_dev/default/hello.json", "verb": "keep" } } ], "operations": [ "getTesting" ] } ], "otherwise": [], "title": "whisk-invoke" } } ] }, "cors": { "enabled": true } } } ================================================ FILE: deploy/tests/resources/swagger_default_ns.json ================================================ { "swagger": "2.0", "basePath": "/testing", "info": { "title": "testing", "version": "1.0" }, "paths": { "/testing": { "get": { "operationId": "getTesting", "responses": { "200": { "description": "A successful invocation response" } }, "x-openwhisk": { "action": "hello", "namespace": "_", "package": "default", "url": "https://openwhisk/api/v1/web/_/default/hello.json" } } } }, "x-ibm-configuration": { "assembly": { "execute": [ { "operation-switch": { "case": [ { "execute": [ { "set-variable": { "actions": [ { "set": "message.headers.X-Require-Whisk-Auth", "value": "3b08f67e-c7fa-4998-9096-ffa355932a3d" } ] } }, { "invoke": { "target-url": "https://openwhisk/api/v1/web/_/default/hello.json", "verb": "keep" } } ], "operations": [ "getTesting" ] } ], "otherwise": [], "title": "whisk-invoke" } } ] }, "cors": { "enabled": true } } } ================================================ FILE: deploy/tests/resources/swagger_ns_paths.json ================================================ { "swagger": "2.0", "basePath": "/testing", "info": { "title": "testing", "version": "1.0" }, "paths": { "/testing": { "get": { "operationId": "getTesting", "responses": { "200": { "description": "A successful invocation response" } }, "x-openwhisk": { "action": "hello", "namespace": "user@host.com_dev", "package": "default", "url": "https://openwhisk/api/v1/web/user@host.com_dev/default/hello.http" } } } }, "x-ibm-configuration": { "assembly": { "execute": [ { "operation-switch": { "case": [ { "execute": [ { "set-variable": { "actions": [ { "set": "message.headers.X-Require-Whisk-Auth", "value": "3b08f67e-c7fa-4998-9096-ffa355932a3d" } ] } }, { "invoke": { "target-url": "https://openwhisk/api/v1/web/user@host.com_dev/default/hello.http$(request.path)", "verb": "keep" } } ], "operations": [ "getTesting" ] } ], "otherwise": [], "title": "whisk-invoke" } } ] }, "cors": { "enabled": true } } } ================================================ FILE: deploy/tests/resources/swagger_paths.json ================================================ { "swagger": "2.0", "basePath": "/testing", "info": { "title": "testing", "version": "1.0" }, "paths": { "/testing": { "get": { "operationId": "getTesting", "responses": { "200": { "description": "A successful invocation response" } }, "x-openwhisk": { "action": "hello", "namespace": "_", "package": "default", "url": "https://openwhisk/api/v1/web/_/default/hello.http" } } } }, "x-ibm-configuration": { "assembly": { "execute": [ { "operation-switch": { "case": [ { "execute": [ { "set-variable": { "actions": [ { "set": "message.headers.X-Require-Whisk-Auth", "value": "3b08f67e-c7fa-4998-9096-ffa355932a3d" } ] } }, { "invoke": { "target-url": "https://openwhisk/api/v1/web/_/default/hello.http$(request.path)", "verb": "keep" } } ], "operations": [ "getTesting" ] } ], "otherwise": [], "title": "whisk-invoke" } } ] }, "cors": { "enabled": true } } } ================================================ FILE: deploy/tests/validate.js ================================================ 'use strict'; const expect = require('chai').expect; const OpenWhiskDeploy = require('../index'); describe('#validate()', () => { let serverless; let openwhiskDeploy; beforeEach(() => { const CLI = function () { this.log = function () {};}; serverless = {classes: {Error, CLI}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: () => {}}; openwhiskDeploy = new OpenWhiskDeploy(serverless); openwhiskDeploy.serverless.config = { servicePath: true }; openwhiskDeploy.serverless.service.environment = { vars: {}, stages: { dev: { vars: {}, regions: { 'us-east-1': { vars: {}, }, }, }, }, }; openwhiskDeploy.serverless.service.functions = { first: { handler: true, }, }; openwhiskDeploy.options = { stage: 'dev', region: 'us-east-1', }; }); it('should throw error if stage does not exist in service', () => { openwhiskDeploy.options.stage = 'prod'; expect(() => openwhiskDeploy.validate()).to.throw(Error); }); it('should throw error if region does not exist in service', () => { openwhiskDeploy.options.region = 'us-west-2'; expect(() => openwhiskDeploy.validate()).to.throw(Error); }); it('should throw error if not inside service (servicePath not defined)', () => { openwhiskDeploy.serverless.config.servicePath = false; expect(() => openwhiskDeploy.validate()).to.throw(Error); }); it('should throw error if region vars object does not exist', () => { openwhiskDeploy.serverless.service.environment.stages.dev.regions['us-east-1'] = {}; expect(() => openwhiskDeploy.validate()).to.throw(Error); }); }); ================================================ FILE: deployFunction/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const fs = require('fs-extra'); const path = require('path') const CompileFunctions = require('../compile/functions/') class OpenWhiskDeployFunction { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.provider = this.serverless.getProvider('openwhisk'); // Temporary hack until we have a better way to access existing plugins. const is_package_plugin = plugin => plugin.hasOwnProperty('packageFunction') this.pkg = serverless.pluginManager.getPlugins().find(is_package_plugin) this.compileFunctions = new CompileFunctions(serverless, options) this.hooks = { 'deploy:function:initialize': () => BbPromise.bind(this) .then(this.validate), 'deploy:function:packageFunction': () => BbPromise.bind(this) .then(this.packageFunction) .then(this.compileFunction), 'deploy:function:deploy': () => BbPromise.bind(this) .then(this.deployFunction) .then(this.cleanup) }; } validate () { if (!this.serverless.config.servicePath) { throw new this.serverless.classes.Error('This command can only be run inside a service'); } this.options.stage = this.options.stage || (this.serverless.service.provider && this.serverless.service.provider.stage) || 'dev'; this.options.region = this.options.region || (this.serverless.service.provider && this.serverless.service.provider.region) || 'us-east-1'; return BbPromise.resolve(); } compileFunction () { const functionObject = this.serverless.service.getFunction(this.options.function); return this.compileFunctions.compileFunction(this.options.function, functionObject).then(action => this.action = action) } packageFunction () { this.serverless.cli.log(`Packaging function: ${this.options.function}...`); const functionObject = this.serverless.service.getFunction(this.options.function); // sequences do not need packaging, no files to deploy if (functionObject.sequence) { return BbPromise.resolve(); } this.serverless.service.package.individually = true return this.pkg.packageFunction(this.options.function); } deployFunction (data) { this.serverless.cli.log(`Deploying function: ${this.options.function}...`); return this.provider.client().then(ow => ow.actions.create(this.action).then(() => { this.serverless.cli.log(`Successfully deployed function: ${this.options.function}`); }).catch(err => { throw new this.serverless.classes.Error( `Failed to deploy function (${this.options.function}) due to error: ${err.message}` ); }) ); } cleanup () { if (this.serverless.config.servicePath) { const serverlessTmpDirPath = path.join(this.serverless.config.servicePath, '.serverless'); if (this.serverless.utils.dirExistsSync(serverlessTmpDirPath)) { fs.removeSync(serverlessTmpDirPath); } } return BbPromise.resolve(); } } module.exports = OpenWhiskDeployFunction; ================================================ FILE: deployFunction/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); const sinon = require('sinon'); const path = require('path'); const fs = require('fs'); const OpenWhiskDeployFunction = require('../index'); const BbPromise = require('bluebird'); require('chai').use(chaiAsPromised); describe('OpenWhiskDeployFunction', () => { let serverless; let openwhiskDeployFunction; let sandbox beforeEach(() => { sandbox = sinon.sandbox.create(); const CLI = function () { this.log = function () {};}; serverless = {pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => {}, provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()}; const options = { stage: 'dev', region: 'us-east-1', }; openwhiskDeployFunction = new OpenWhiskDeployFunction(serverless, options); openwhiskDeployFunction.serverless.cli = new serverless.classes.CLI(); openwhiskDeployFunction.serverless.service.provider = { namespace: 'testing', apihost: 'openwhisk.org', auth: 'user:pass', }; openwhiskDeployFunction.provider = {client: () => {}} }); afterEach(() => { sandbox.restore(); }); describe('#constructor()', () => { it('should have hooks', () => expect(openwhiskDeployFunction.hooks).to.be.not.empty); }); describe('hooks', () => { it('should run "deploy:function:initialize" promise chain in order', () => { const validateStub = sinon .stub(openwhiskDeployFunction, 'validate').returns(BbPromise.resolve()); return openwhiskDeployFunction.hooks['deploy:function:initialize']().then(() => { expect(validateStub.calledOnce).to.equal(true); openwhiskDeployFunction.validate.restore() }); }); it('should run "deploy:function:packageFunction" promise chain in order', () => { const packageFunctionStub = sinon .stub(openwhiskDeployFunction, 'packageFunction').returns(BbPromise.resolve()); const compileFunctionStub = sinon .stub(openwhiskDeployFunction, 'compileFunction').returns(BbPromise.resolve()); return openwhiskDeployFunction.hooks['deploy:function:packageFunction']().then(() => { expect(packageFunctionStub.calledOnce).to.equal(true); expect(compileFunctionStub.calledOnce).to.equal(true); expect(compileFunctionStub.calledAfter(packageFunctionStub)) .to.equal(true); openwhiskDeployFunction.packageFunction.restore(); openwhiskDeployFunction.compileFunction.restore(); }); }); it('should run "deploy:function:deploy" promise chain in order', () => { const deployFunctionStub = sinon .stub(openwhiskDeployFunction, 'deployFunction').returns(BbPromise.resolve()); const cleanupStub = sinon .stub(openwhiskDeployFunction, 'cleanup').returns(BbPromise.resolve()); return openwhiskDeployFunction.hooks['deploy:function:deploy']().then(() => { expect(deployFunctionStub.calledOnce).to.equal(true); expect(cleanupStub.calledAfter(deployFunctionStub)) .to.equal(true); openwhiskDeployFunction.deployFunction.restore(); openwhiskDeployFunction.cleanup.restore(); }); }); }); describe('#packageFunction()', () => { it('should not package sequence actions', () => { const fObj = {sequence: ['a', 'b', 'c']} const spy = sinon.spy() openwhiskDeployFunction.pkg = { packageFunction: spy } const getFunctionStub = sinon.stub(openwhiskDeployFunction.serverless.service, "getFunction").returns(fObj) return openwhiskDeployFunction.packageFunction().then(() => { expect(spy.called).to.be.false }) }) }) describe('#compileFunction()', () => { it('should store compiled function on instance', () => { const fObj = {handler: "file.main"} const action = {name: "action"} const getFunctionStub = sinon.stub(openwhiskDeployFunction.serverless.service, "getFunction").returns(fObj) const compileFunctionStub = sinon.stub(openwhiskDeployFunction.compileFunctions, "compileFunction").returns(BbPromise.resolve(action)) return openwhiskDeployFunction.compileFunction().then(() => { expect(openwhiskDeployFunction.action).to.be.equal(action); getFunctionStub.restore(); compileFunctionStub.restore(); }) }) }) describe('#deployFunction()', () => { it('should deploy function to openwhisk', () => { openwhiskDeployFunction.action = {actionName: "sample"} sandbox.stub(openwhiskDeployFunction.provider, 'client', () => { const create = params => { expect(params).to.be.deep.equal(openwhiskDeployFunction.action); return Promise.resolve(); }; return Promise.resolve({ actions: { create } }); }); return expect(openwhiskDeployFunction.deployFunction()) .to.eventually.be.fulfilled; }); it('should reject when function handler fails to deploy with error message', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskDeployFunction.provider, 'client', () => { const create = () => Promise.reject(err); return Promise.resolve({ actions: { create } }); }); return expect(openwhiskDeployFunction.deployFunction()) .to.eventually.be.rejected; }); }); }); ================================================ FILE: index.js ================================================ 'use strict'; /* NOTE: this plugin is used to add all the differnet provider related plugins at once. This way only one plugin needs to be added to the service in order to get access to the whole provider implementation. */ const CompileFunctions = require('./compile/functions/index.js'); const CompileTriggers = require('./compile/triggers/index.js'); const CompileRules = require('./compile/rules/index.js'); const CompilePackages = require('./compile/packages/index.js'); const CompileHttpEvents = require('./compile/apigw/index.js'); const CompileSchedule = require('./compile/schedule/index.js'); const CompileMessageHub = require('./compile/message_hub/index.js'); const CompileCloudant = require('./compile/cloudant/index.js'); const CompileServiceBindings = require('./compile/servicebindings/index.js'); const Deploy = require('./deploy/index.js'); const Invoke = require('./invoke/index.js'); const InvokeLocal = require('./invokeLocal/index.js'); const Remove = require('./remove/index.js'); const Logs = require('./logs/index.js'); const Info = require('./info/index.js'); const DeployFunction = require('./deployFunction/index.js'); const OpenwhiskProvider = require('./provider/openwhiskProvider.js'); const ConfigCredentials = require('./configCredentials/index.js') class Index { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.serverless.pluginManager.addPlugin(OpenwhiskProvider); this.serverless.pluginManager.addPlugin(CompilePackages); this.serverless.pluginManager.addPlugin(CompileFunctions); this.serverless.pluginManager.addPlugin(CompileHttpEvents); this.serverless.pluginManager.addPlugin(CompileRules); this.serverless.pluginManager.addPlugin(CompileTriggers); this.serverless.pluginManager.addPlugin(CompileSchedule); this.serverless.pluginManager.addPlugin(CompileMessageHub); this.serverless.pluginManager.addPlugin(CompileCloudant); this.serverless.pluginManager.addPlugin(CompileServiceBindings); this.serverless.pluginManager.addPlugin(Remove); this.serverless.pluginManager.addPlugin(Invoke); this.serverless.pluginManager.addPlugin(InvokeLocal); this.serverless.pluginManager.addPlugin(Deploy); this.serverless.pluginManager.addPlugin(Logs); this.serverless.pluginManager.addPlugin(Info); this.serverless.pluginManager.addPlugin(DeployFunction); this.serverless.pluginManager.addPlugin(ConfigCredentials); } } module.exports = Index; ================================================ FILE: info/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const chalk = require('chalk'); const { formatApiHost } = require('../utils'); class OpenWhiskInfo { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.provider = this.serverless.getProvider('openwhisk'); this.hooks = { 'info:info': () => BbPromise.bind(this) .then(this.validate) .then(this.info), 'after:deploy:deploy': () => BbPromise.bind(this) .then(() => { if (this.options.noDeploy) { return BbPromise.resolve(); } this.consoleLog(''); this.failsafe = true; return BbPromise.bind(this) .then(this.validate) .then(this.info) }) }; } validate() { if (!this.serverless.config.servicePath) { throw new this.serverless.classes.Error('This command can only be run inside a service.'); } return this.provider.props().then(props => { this.props = props; this.props.apihost = formatApiHost(props.apihost); return this.provider.client(); }).then(client => { this.client = client; }) } info () { this.consoleLog(`${chalk.yellow.underline('Service Information')}`); return BbPromise.bind(this) .then(this.showServiceInfo) .then(this.showPackagesInfo) .then(this.showActionsInfo) .then(this.showTriggersInfo) .then(this.showRulesInfo) .then(this.showRoutesInfo) .then(this.showWebActionsInfo); } showServiceInfo () { this.consoleLog(`platform:\t${this.props.apihost}`); this.consoleLog(`namespace:\t${this.props.namespace || '_'}`); this.consoleLog(`service:\t${this.serverless.service.service}\n`); } showActionsInfo () { this.consoleLog(`${chalk.yellow('actions:')}`); return this.client.actions.list().then(actions => { this._actions = actions; if (!actions.length) return console.log('**no actions deployed**\n'); const names = actions.map(action => { const pkge = action.namespace.match(/\/(.+)/) if (!pkge) return action.name return `${pkge[1]}/${action.name}` }).join(' '); this.consoleLog(names + '\n') }) } showWebActionsInfo () { this.consoleLog(`${chalk.yellow('endpoints (web actions):')}`); const web_actions = this._actions.filter(action => { const annotations = action.annotations || [] return annotations.some(a => a.key === 'web-export' && a.value === true) }) if (!web_actions.length) { this.consoleLog('**no web actions deployed**\n'); return Promise.resolve(); } const extractNsAndPkge = id => { let [ns, pkge] = id.split('/') pkge = pkge || 'default' return { ns, pkge } } return this.provider.props().then(props => { web_actions.forEach(action => { const nsPkge = extractNsAndPkge(action.namespace) this.consoleLog(`${formatApiHost(props.apihost)}/api/v1/web/${nsPkge.ns}/${nsPkge.pkge}/${action.name}`) }) }) } showResourcesInfo (resource) { this.consoleLog(`${chalk.yellow(`${resource}:`)}`); return this.client[resource].list().then(resources => { if (!resources.length) return console.log(`**no ${resource} deployed**\n`); const names = resources.map(r => r.name).join(' '); this.consoleLog(names + '\n') }) } showPackagesInfo () { return this.showResourcesInfo('packages') } showTriggersInfo () { return this.showResourcesInfo('triggers') } showRulesInfo () { return this.showResourcesInfo('rules') } showRoutesInfo () { this.consoleLog(`${chalk.yellow('endpoints (api-gw):')}`) let operation = this.client.routes.list().then(routes => { if (!routes.apis.length) return console.log('**no routes deployed**\n') routes.apis.forEach(api => this.logApiEndPoints(api.value)) this.consoleLog('') }) operation = operation.catch(err => { this.consoleLog(`${chalk.red('**failed to fetch routes**')}`) if(err.message.match(/status code 400/) && err.message.match(/expired/)) { this.consoleLog(`${chalk.red('**api gateway key is wrong or has expired! if it has expired, please refresh with wsk bluemix login**\n')}`) } if (!this.failsafe) throw err }) return operation } logEndPoint (baseUrl, path, method, actionName) { if (!path.startsWith('/')) { path = `/${path}` } if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1) } this.consoleLog(`${method.toUpperCase()} ${baseUrl}${path} --> ${actionName}`) } logApiEndPoints (api) { const paths = api.apidoc.paths Object.keys(paths).forEach(path => { const methods = Object.keys(paths[path]) methods.forEach(method => { const operation = paths[path][method] let actionName = 'unknown' if (operation.hasOwnProperty('x-openwhisk')) { actionName = operation['x-openwhisk'].action; } else if (operation.hasOwnProperty('x-ibm-op-ext')) { actionName = operation['x-ibm-op-ext'].actionName; } this.logEndPoint(api.gwApiUrl, path, method, actionName) }) }) } consoleLog (message) { console.log(message) } } module.exports = OpenWhiskInfo; ================================================ FILE: info/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); const sinon = require('sinon'); const path = require('path'); const os = require('os'); const OpenWhiskInfo = require('../'); const BbPromise = require('bluebird'); const chalk = require('chalk'); const moment = require('moment'); const Credentials = require('../../provider/credentials') require('chai').use(chaiAsPromised); describe('OpenWhiskInfo', () => { let sandbox; const CLI = function () { this.log = function () {};}; const serverless = {pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => {}, provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; const options = { stage: 'dev', region: 'us-east-1', function: 'first' }; const openwhiskInfo = new OpenWhiskInfo(serverless, options); openwhiskInfo.client = { routes: { list: () => {} }, rules: { list: () => {} }, triggers: { list: () => {} }, packages: { list: () => {} }, actions: { list: () => {} } }; serverless.service.service = "my_service"; beforeEach(() => { sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); }); describe('#constructor()', () => { it('should have hooks', () => expect(openwhiskInfo.hooks).to.be.not.empty); it('should run promise chain in order', () => { const validateStub = sinon .stub(openwhiskInfo, 'validate').returns(BbPromise.resolve()); const infoStub = sinon .stub(openwhiskInfo, 'info').returns(BbPromise.resolve()); return openwhiskInfo.hooks['info:info']().then(() => { expect(validateStub.calledOnce).to.be.equal(true); expect(infoStub.calledAfter(validateStub)).to.be.equal(true); openwhiskInfo.validate.restore(); openwhiskInfo.info.restore(); }); }); }); describe('#info()', () => { it('should show title and call display functions', () => { const log = sandbox.stub(openwhiskInfo, 'consoleLog') const service = sandbox.stub(openwhiskInfo, 'showServiceInfo') const action = sandbox.stub(openwhiskInfo, 'showActionsInfo') const packages = sandbox.stub(openwhiskInfo, 'showPackagesInfo') const triggers = sandbox.stub(openwhiskInfo, 'showTriggersInfo') const rules = sandbox.stub(openwhiskInfo, 'showRulesInfo') const routes = sandbox.stub(openwhiskInfo, 'showRoutesInfo') const web_actions = sandbox.stub(openwhiskInfo, 'showWebActionsInfo') return openwhiskInfo.info().then(() => { expect(service.calledOnce).to.be.equal(true); expect(packages.calledOnce).to.be.equal(true); expect(action.calledOnce).to.be.equal(true); expect(triggers.calledOnce).to.be.equal(true); expect(rules.calledOnce).to.be.equal(true); expect(routes.calledOnce).to.be.equal(true); expect(web_actions.calledOnce).to.be.equal(true); expect(log.args[0][0].match(/Service Information/)).to.be.ok; }); }); }); describe('#showServiceInfo()', () => { it('should show service, platform and call display functions', () => { const log = sandbox.stub(openwhiskInfo, 'consoleLog') openwhiskInfo.props = { apihost: 'some_end_point', namespace: 'custom_ns' }; openwhiskInfo.showServiceInfo() expect(log.calledThrice).to.be.equal(true); expect(log.args[0][0].match(/platform:\tsome_end_point/)).to.be.ok; expect(log.args[1][0].match(/namespace:\tcustom_ns/)).to.be.ok; expect(log.args[2][0].match(/service:\tmy_service/)).to.be.ok; }); }); describe('#showPackagesInfo()', () => { it('should show package names returned', () => { const log = sandbox.stub(openwhiskInfo, 'consoleLog') sandbox.stub(openwhiskInfo.client.packages, 'list').returns(BbPromise.resolve([ {name: 'first'}, {name: 'second'}, {name: 'third'} ])); return expect(openwhiskInfo.showPackagesInfo().then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0].match(/packages:/)).to.be.ok; expect(log.args[1][0].match(/first second third/)).to.be.ok; })); }) }) describe('#showActionsInfo()', () => { it('should show action names returned', () => { const log = sandbox.stub(openwhiskInfo, 'consoleLog') sandbox.stub(openwhiskInfo.client.actions, 'list').returns(BbPromise.resolve([ {name: 'first', namespace: 't'}, {name: 'second', namespace: "t"}, {name: 'third', namespace: "t"} ])); return expect(openwhiskInfo.showActionsInfo().then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0].match(/actions:/)).to.be.ok; expect(log.args[1][0].match(/first second third/)).to.be.ok; })); }) it('should show package action names returned', () => { const log = sandbox.stub(openwhiskInfo, 'consoleLog') sandbox.stub(openwhiskInfo.client.actions, 'list').returns(BbPromise.resolve([ {name: 'first', namespace: "testing"}, {name: 'second', namespace: 'user@host.com/somePackage'}, {name: 'third', namespace: "testing"} ])); return expect(openwhiskInfo.showActionsInfo().then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0].match(/actions:/)).to.be.ok; expect(log.args[1][0].match(/first somePackage\/second third/)).to.be.ok; })); }) }) describe('#showTriggersInfo()', () => { it('should show trigger names returned', () => { const log = sandbox.stub(openwhiskInfo, 'consoleLog') sandbox.stub(openwhiskInfo.client.triggers, 'list').returns(BbPromise.resolve([ {name: 'first'}, {name: 'second'}, {name: 'third'} ])); return expect(openwhiskInfo.showTriggersInfo().then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0].match(/triggers:/)).to.be.ok; expect(log.args[1][0].match(/first second third/)).to.be.ok; })); }) }) describe('#showRulesInfo()', () => { it('should show rules names returned', () => { const log = sandbox.stub(openwhiskInfo, 'consoleLog') sandbox.stub(openwhiskInfo.client.rules, 'list').returns(BbPromise.resolve([ {name: 'first'}, {name: 'second'}, {name: 'third'} ])); return expect(openwhiskInfo.showRulesInfo().then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0].match(/rules:/)).to.be.ok; expect(log.args[1][0].match(/first second third/)).to.be.ok; })); }) }) describe('#showRoutesInfo()', () => { it('should show routes returned', () => { const endpoint = { "x-ibm-op-ext": { "actionName": "my_service-dev-hello" } } const apis = [{ value: { gwApiUrl: 'https://api-gateway.com/service_name', apidoc: { paths: { "/api/hello": { get: endpoint }, "/api/foobar": { post: endpoint } } } } }, { value: { gwApiUrl: 'https://api-gateway.com/service_name', apidoc: { paths: { "/api/foo/1": { get: endpoint }, "/api/bar/2": { post: endpoint } } } } }] const log = sandbox.stub(openwhiskInfo, 'consoleLog') sandbox.stub(openwhiskInfo.client.routes, 'list').returns(BbPromise.resolve({ apis })); return expect(openwhiskInfo.showRoutesInfo().then(() => { expect(log.args[0][0].match(/endpoints \(api-gw\):/)).to.be.ok; expect(log.args[1][0].match(/GET https:\/\/api-gateway.com\/service_name\/api\/hello/)).to.be.ok; expect(log.args[2][0].match(/POST https:\/\/api-gateway.com\/service_name\/api\/foobar/)).to.be.ok; expect(log.args[3][0].match(/GET https:\/\/api-gateway.com\/service_name\/api\/foo\/1/)).to.be.ok; expect(log.args[4][0].match(/POST https:\/\/api-gateway.com\/service_name\/api\/bar\/2/)).to.be.ok; })); }) it('should show api v2 routes returned', () => { const endpoint = { "x-openwhisk": { "action": "my_service-dev-hello" } } const apis = [{ value: { gwApiUrl: 'https://api-gateway.com/service_name/api', apidoc: { paths: { "hello": { get: endpoint }, "foobar": { post: endpoint } } } } }, { value: { gwApiUrl: 'https://api-gateway.com/service_name/api', apidoc: { paths: { "foo/1": { get: endpoint }, "bar/2": { post: endpoint } } } } }] const log = sandbox.stub(openwhiskInfo, 'consoleLog') sandbox.stub(openwhiskInfo.client.routes, 'list').returns(BbPromise.resolve({ apis })); return expect(openwhiskInfo.showRoutesInfo().then(() => { expect(log.args[0][0].match(/endpoints \(api-gw\):/)).to.be.ok; expect(log.args[1][0].match(/GET https:\/\/api-gateway.com\/service_name\/api\/hello/)).to.be.ok; expect(log.args[2][0].match(/POST https:\/\/api-gateway.com\/service_name\/api\/foobar/)).to.be.ok; expect(log.args[3][0].match(/GET https:\/\/api-gateway.com\/service_name\/api\/foo\/1/)).to.be.ok; expect(log.args[4][0].match(/POST https:\/\/api-gateway.com\/service_name\/api\/bar\/2/)).to.be.ok; })); }) it('should display error message in failsafe mode', () => { openwhiskInfo.failsafe = true; const log = sandbox.stub(openwhiskInfo, 'consoleLog') sandbox.stub(openwhiskInfo.client.routes, 'list').returns(BbPromise.resolve(false)); return expect(openwhiskInfo.showRoutesInfo().then(() => { expect(log.calledTwice).to.be.equal(true); expect(log.args[0][0].match(/endpoints \(api-gw\):/)).to.be.ok; expect(log.args[1][0].match(/failed to fetch routes/)).to.be.ok; })); }) it('should display error message about expired key in failsafe mode', () => { openwhiskInfo.failsafe = true; const log = sandbox.stub(openwhiskInfo, 'consoleLog') sandbox.stub(openwhiskInfo.client.routes, 'list').returns(BbPromise.reject(new Error('status code 400 blah blah expired'))); return expect(openwhiskInfo.showRoutesInfo().then(() => { expect(log.calledThrice).to.be.equal(true); expect(log.args[0][0].match(/endpoints \(api-gw\):/)).to.be.ok; expect(log.args[1][0].match(/failed to fetch routes/)).to.be.ok; expect(log.args[2][0].match(/expired/)).to.be.ok; })); }) it('should return error about expired key without failsafe mode', () => { openwhiskInfo.failsafe = false; const log = sandbox.stub(openwhiskInfo, 'consoleLog') sandbox.stub(openwhiskInfo.client.routes, 'list').returns(BbPromise.reject(new Error('status code 400 blah blah expired'))); return expect(openwhiskInfo.showRoutesInfo()) .to.eventually.be.rejected; }) }) describe('#showWebActionsInfo()', () => { it('should show web action routes returned', () => { const apihost = 'openwhisk.ng.bluemix.net' openwhiskInfo.provider = { props: () => Promise.resolve({ apihost }) } const log = sandbox.stub(openwhiskInfo, 'consoleLog') openwhiskInfo._actions = [ {name: 'first', namespace: 'user_name', annotations: [{key: 'web-export', value: true}, {key: 'a', value: 'b'}]}, {name: 'second', namespace: 'user_name', annotations: [{key: 'web-export', value: false}]}, {name: 'third'}, {name: 'fourth', namespace: 'user_name', annotations: [{key: 'web-export', value: true}]}, {name: 'fifth', annotations: []}, {name: 'sixth', namespace: 'user_name/custom_package', annotations: [{key: 'web-export', value: true}]}, ]; return expect(openwhiskInfo.showWebActionsInfo().then(() => { expect(log.callCount).to.be.equal(4); expect(log.args[0][0].match(/endpoints \(web actions\):/)).to.be.ok; expect(log.args[1][0].match(/https:\/\/openwhisk.ng.bluemix.net\/api\/v1\/web\/user_name\/default\/first/)).to.be.ok; expect(log.args[2][0].match(/https:\/\/openwhisk.ng.bluemix.net\/api\/v1\/web\/user_name\/default\/fourth/)).to.be.ok; expect(log.args[3][0].match(/https:\/\/openwhisk.ng.bluemix.net\/api\/v1\/web\/user_name\/custom_package\/sixth/)).to.be.ok; })); }) it('should show web action routes returned with apihost that include a protocol', () => { const apihost = 'http://localhost' openwhiskInfo.provider = { props: () => Promise.resolve({ apihost }) } const log = sandbox.stub(openwhiskInfo, 'consoleLog') openwhiskInfo._actions = [ {name: 'first', namespace: 'user_name', annotations: [{key: 'web-export', value: true}, {key: 'a', value: 'b'}]}, {name: 'second', namespace: 'user_name', annotations: [{key: 'web-export', value: false}]}, {name: 'third'}, {name: 'fourth', namespace: 'user_name', annotations: [{key: 'web-export', value: true}]}, {name: 'fifth', annotations: []}, {name: 'sixth', namespace: 'user_name/custom_package', annotations: [{key: 'web-export', value: true}]}, ]; return expect(openwhiskInfo.showWebActionsInfo().then(() => { expect(log.callCount).to.be.equal(4); expect(log.args[0][0].match(/endpoints \(web actions\):/)).to.be.ok; expect(log.args[1][0].match(/http:\/\/localhost\/api\/v1\/web\/user_name\/default\/first/)).to.be.ok; expect(log.args[2][0].match(/http:\/\/localhost\/api\/v1\/web\/user_name\/default\/fourth/)).to.be.ok; expect(log.args[3][0].match(/http:\/\/localhost\/api\/v1\/web\/user_name\/custom_package\/sixth/)).to.be.ok; })); }) }) }); ================================================ FILE: invoke/README.md ================================================ # Invoke This plugin invokes an OpenWhisk Action. ## How it works `Invoke` hooks into the [`invoke:invoke`](/lib/plugins/invoke) lifecycle. It will send the HTTP POST request to the Action endpoint to trigger the function activation. The output of the function is fetched and will be prompted on the console. ================================================ FILE: invoke/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const chalk = require('chalk'); const path = require('path'); const stdin = require('get-stdin'); const fse = require('fs-extra'); const CmdLineParamsOptions = { type: ['blocking', 'nonblocking'], log: ['result', 'response'], }; class OpenWhiskInvoke { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.provider = this.serverless.getProvider('openwhisk'); this.hooks = { 'invoke:invoke': () => BbPromise.bind(this) .then(this.validate) .then(this.invoke) .then(this.log), }; } validate() { if (!this.serverless.config.servicePath) { throw new this.serverless.classes.Error('This command can only be run inside a service.'); } this.serverless.service.getFunction(this.options.function); return new Promise((resolve, reject) => { if (this.options.data) { resolve(); } else if (this.options.path) { const absolutePath = path.isAbsolute(this.options.path) ? this.options.path : path.join(this.serverless.config.servicePath, this.options.path); if (!this.serverless.utils.fileExistsSync(absolutePath)) { throw new this.serverless.classes.Error('The file you provided does not exist.'); } this.options.data = this.readFileSync(absolutePath); console.log(typeof this.options.data) if (this.options.data == null) { throw new this.serverless.classes.Error( 'The file path provided must point to a JSON file with a top-level JSON object definition.' ); } resolve(); } else { return this.getStdin().then(input => { this.options.data = input || '{}'; resolve(); }); } }).then(() => { try { this.options.data = JSON.parse(this.options.data); if (this.options.data == null || typeof this.options.data !== 'object') throw new this.serverless.classes.Error('Data parameter must be a JSON object') } catch (exception) { throw new this.serverless.classes.Error( `Error parsing data parameter as JSON: ${exception}` ); } }).then(() => { this.validateParamOptions(); return this.provider.client().then(client => { this.client = client; }); }); } readFileSync(path) { return fse.readFileSync(path); } getStdin() { return stdin() } // ensure command-line parameter values is a valid option. validateParamOptions() { Object.keys(CmdLineParamsOptions).forEach(key => { if (!this.options[key]) { this.options[key] = CmdLineParamsOptions[key][0]; } else if (!CmdLineParamsOptions[key].find(i => i === this.options[key])) { const options = CmdLineParamsOptions[key].join(' or '); throw new this.serverless.classes.Error( `Invalid ${key} parameter value, must be either ${options}.` ); } }); } invoke() { const functionObject = this.serverless.service.getFunction(this.options.function); const options = { blocking: this.isBlocking(), actionName: functionObject.name || `${this.serverless.service.service}_${this.options.function}`, }; if (functionObject.namespace) { options.namespace = functionObject.namespace; } if (this.options.data) { options.params = this.options.data; } return this.client.actions.invoke(options) .catch(err => this.formatErrMsg(err)); } formatErrMsg (err) { let err_msg = `Failed to invoke function service (${this.options.function}) due to error:` const base_err = err.error if (base_err.response && base_err.response.result && typeof base_err.response.result.error === 'string') { err.message = base_err.response.result.error err_msg = `Failed to invoke function service (${this.options.function}) due to application error:` const logs_msg = ` Check logs for activation: ${base_err.activationId}` throw new this.serverless.classes.Error(`${err_msg}\n\n ${err.message}\n\n ${logs_msg}`) } throw new this.serverless.classes.Error(`${err_msg}\n\n ${err.message}`) } isBlocking() { return this.options.type === 'blocking'; } isLogResult() { return this.options.log === 'result'; } log(invocationReply) { if (this.options.verbose || this.options.v) { this.logDetails(invocationReply) } let color = 'white'; // error response indicated in-blocking call boolean parameter, success. if (this.isBlocking() && !invocationReply.response.success) { color = 'red'; } let result = invocationReply; // blocking invocation result available as 'response.result' parameter if (this.isBlocking() && this.isLogResult()) { result = invocationReply.response.result; } this.consoleLog(chalk[color](JSON.stringify(result, null, 4))); return BbPromise.resolve(); } logDetails(invocationReply) { const id = `/${invocationReply.namespace}/${invocationReply.name}` const actv = invocationReply.activationId const find_time = (annotations, key) => (annotations.find(el => el.key === key) || {value: 0}).value const annotations = invocationReply.annotations || [] const waitTime = find_time(annotations, 'waitTime') const initTime = find_time(annotations, 'initTime') const field = (name, label) => `${chalk.blue(name)} (${chalk.yellow(label)})` const time = (name, value, color = 'blue') => `${chalk[color](name)}: ${chalk.green(value + 'ms')}` const duration = (duration, init = 0, wait) => `${time('duration', duration)} (${time('init', init, 'magenta')}, ${time('wait', wait, 'magenta')})` this.consoleLog(`${chalk.green('=>')} ${field('action', id)} ${field('activation', actv)} ${duration(invocationReply.duration, initTime, waitTime)}`) } consoleLog(msg) { console.log(msg); // eslint-disable-line no-console } } module.exports = OpenWhiskInvoke; ================================================ FILE: invoke/tests/index.js ================================================ 'use strict'; const chalk = require('chalk'); const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); const sinon = require('sinon'); const path = require('path'); const os = require('os'); const OpenWhiskInvoke = require('../'); const BbPromise = require('bluebird'); const fs = require('fs-extra'); require('chai').use(chaiAsPromised); describe('OpenWhiskInvoke', () => { let sandbox; const CLI = function () { this.log = function () {};}; const serverless = {config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => {}, provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; const options = { stage: 'dev', region: 'us-east-1', function: 'first', }; const openwhiskInvoke = new OpenWhiskInvoke(serverless, options); beforeEach(() => { openwhiskInvoke.provider = {client: () => Promise.resolve({})} sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); }); describe('#constructor()', () => { it('should have hooks', () => expect(openwhiskInvoke.hooks).to.be.not.empty); it('should run promise chain in order', () => { const validateStub = sinon .stub(openwhiskInvoke, 'validate').returns(BbPromise.resolve()); const invokeStub = sinon .stub(openwhiskInvoke, 'invoke').returns(BbPromise.resolve()); const logStub = sinon .stub(openwhiskInvoke, 'log').returns(BbPromise.resolve()); return openwhiskInvoke.hooks['invoke:invoke']().then(() => { expect(validateStub.calledOnce).to.be.equal(true); expect(invokeStub.calledAfter(validateStub)).to.be.equal(true); expect(logStub.calledAfter(invokeStub)).to.be.equal(true); openwhiskInvoke.validate.restore(); openwhiskInvoke.invoke.restore(); openwhiskInvoke.log.restore(); }); }); }); describe('#validate()', () => { beforeEach(() => { serverless.config.servicePath = true; serverless.service.environment = { vars: {}, stages: { dev: { vars: {}, regions: { 'us-east-1': { vars: {}, }, }, }, }, }; serverless.service.functions = { first: { handler: true, }, }; serverless.service.getFunction = name => serverless.service.functions[name]; }); it('it should parse data parameter as JSON if provided', () => { serverless.config.servicePath = path.join(os.tmpdir(), (new Date).getTime().toString()); const data = { testProp: 'testValue', }; openwhiskInvoke.options.data = '{"hello": "world"}'; return openwhiskInvoke.validate().then(() => { expect(openwhiskInvoke.options.data).to.deep.equal({hello: "world"}); openwhiskInvoke.options.data = null; }); }); it('it should parse stdin as JSON data without explicit options', () => { const data = '{"hello": "world"}'; sinon.stub(openwhiskInvoke, 'getStdin').returns(BbPromise.resolve(data)); serverless.config.servicePath = path.join(os.tmpdir(), (new Date).getTime().toString()); return openwhiskInvoke.validate().then(() => { expect(openwhiskInvoke.options.data).to.deep.equal({hello: "world"}); openwhiskInvoke.options.data = null; }); }); it('it should throw if file is not parsed as JSON object (invalid)', () => { serverless.config.servicePath = path.join(os.tmpdir(), (new Date).getTime().toString()); openwhiskInvoke.options.data = '{"hello": "world"'; return expect(openwhiskInvoke.validate()).to.eventually.be.rejectedWith('Error parsing') }); it('it should throw if file is not parsed as JSON object (number)', () => { serverless.config.servicePath = path.join(os.tmpdir(), (new Date).getTime().toString()); openwhiskInvoke.options.data = '1'; return expect(openwhiskInvoke.validate()).to.eventually.be.rejectedWith('Error parsing') }); it('it should parse file if file path is provided', () => { serverless.config.servicePath = path.join(os.tmpdir(), (new Date).getTime().toString()); const data = { testProp: 'testValue', }; openwhiskInvoke.serverless.utils = {fileExistsSync: () => true}; openwhiskInvoke.readFileSync = () => JSON.stringify(data); openwhiskInvoke.options.path = 'data.json'; openwhiskInvoke.options.data = null; return openwhiskInvoke.validate().then(() => { expect(openwhiskInvoke.options.data).to.deep.equal(data); openwhiskInvoke.options.path = false; serverless.config.servicePath = true; }); }); it('it should throw if file is not parsed as JSON object', () => { serverless.config.servicePath = path.join(os.tmpdir(), (new Date).getTime().toString()); openwhiskInvoke.serverless.utils = {fileExistsSync: () => true}; openwhiskInvoke.options.path = 'data.txt'; openwhiskInvoke.readFileSync = () => 'testing'; return expect(openwhiskInvoke.validate()).to.eventually.be.rejectedWith('Error parsing') }); it('it should throw if type parameter is not valid value', () => { openwhiskInvoke.options.type = 'random'; openwhiskInvoke.options.path = null; openwhiskInvoke.options.data = null; return expect(openwhiskInvoke.validate()).to.eventually.be.rejectedWith('blocking or nonblocking') }); it('it should throw if log parameter is not valid value', () => { openwhiskInvoke.options.type = 'blocking'; openwhiskInvoke.options.log = 'random'; openwhiskInvoke.options.path = null; openwhiskInvoke.options.data = '{}'; return expect(openwhiskInvoke.validate()).to.eventually.be.rejectedWith('result or response') }); it('it should throw error if service path is not set', () => { serverless.config.servicePath = false; expect(() => openwhiskInvoke.validate()).to.throw(Error); }); it('it should throw error if file path does not exist', () => { serverless.config.servicePath = path.join(os.tmpdir(), (new Date).getTime().toString()); openwhiskInvoke.serverless.utils = {fileExistsSync: () => false}; openwhiskInvoke.options.path = 'some/path'; openwhiskInvoke.options.data = null; return expect(openwhiskInvoke.validate()).to.eventually.be.rejectedWith('does not exist') }); }); describe('#invoke()', () => { let invokeStub; beforeEach(() => { openwhiskInvoke.serverless.service.functions = { first: { namespace: 'sample', handler: true, }, }; openwhiskInvoke.serverless.service.service = 'new-service'; openwhiskInvoke.options = { stage: 'dev', function: 'first', data: { a: 1 }, }; openwhiskInvoke.client = { actions: { invoke: () => {} } }; }); afterEach(() => { invokeStub.restore(); }); it('should invoke with correct params', () => { invokeStub = sinon.stub(openwhiskInvoke.client.actions, 'invoke') .returns(BbPromise.resolve()); return openwhiskInvoke.invoke().then(() => { expect(invokeStub.calledOnce).to.be.equal(true); expect(invokeStub.args[0][0]).to.be.deep.equal({ actionName: 'new-service_first', blocking: false, namespace: 'sample', params: { a: 1 }, }); }); } ); it('should reject when sdk client fails', () => { invokeStub = sinon.stub(openwhiskInvoke.client.actions, 'invoke').returns(BbPromise.reject()); return expect(openwhiskInvoke.invoke()).to.be.eventually.rejected; }); }); describe('#log()', () => { it('should log activation response result', () => { const log = sandbox.stub(openwhiskInvoke, 'consoleLog'); openwhiskInvoke.options.log = 'result' openwhiskInvoke.options.type = 'blocking' const result = {success: true, result: { hello: "world"} }; return openwhiskInvoke.log({response: result}).then(() => { expect(log.calledOnce).to.be.equal(true); const msg = chalk.white(JSON.stringify(result.result, null, 4)); console.log(msg) expect(log.args[0][0]).to.be.equal(msg); }); }); it('should log verbose activation response result', () => { const log = sandbox.stub(openwhiskInvoke, 'consoleLog'); openwhiskInvoke.options.log = 'result' openwhiskInvoke.options.type = 'blocking' openwhiskInvoke.options.v = true const input = { activationId: 12345, name: 'blah', namespace: 'workspace', duration: 100, annotations: [ { key: "waitTime", value: 33 } ], response: { success: true, result: { hello: "world"} } }; return openwhiskInvoke.log(input).then(() => { expect(log.calledTwice).to.be.equal(true); const msg = chalk.white(JSON.stringify(input.response.result, null, 4)); const field = (name, label) => `${chalk.blue(name)} (${chalk.yellow(label)})` const time = (name, value, color = 'blue') => `${chalk[color](name)}: ${chalk.green(value + 'ms')}` const duration = (duration, init = 0, wait) => `${time('duration', duration)} (${time('init', init, 'magenta')}, ${time('wait', wait, 'magenta')})` const output = `${chalk.green('=>')} ${field('action', '/workspace/blah')} ${field('activation', 12345)} ${duration(100, undefined, 33)}` expect(log.args[0][0]).to.be.equal(output); expect(log.args[1][0]).to.be.equal(msg); }); }); it('should log verbose activation coldstart response result', () => { const log = sandbox.stub(openwhiskInvoke, 'consoleLog'); openwhiskInvoke.options.log = 'result' openwhiskInvoke.options.type = 'blocking' openwhiskInvoke.options.v = true const input = { activationId: 12345, name: 'blah', namespace: 'workspace', duration: 100, annotations: [ { key: "waitTime", value: 33 }, { key: "initTime", value: 63 } ], response: { success: true, result: { hello: "world"} } }; return openwhiskInvoke.log(input).then(() => { expect(log.calledTwice).to.be.equal(true); const msg = chalk.white(JSON.stringify(input.response.result, null, 4)); const field = (name, label) => `${chalk.blue(name)} (${chalk.yellow(label)})` const time = (name, value, color = 'blue') => `${chalk[color](name)}: ${chalk.green(value + 'ms')}` const duration = (duration, init = 0, wait) => `${time('duration', duration)} (${time('init', init, 'magenta')}, ${time('wait', wait, 'magenta')})` const output = `${chalk.green('=>')} ${field('action', '/workspace/blah')} ${field('activation', 12345)} ${duration(100, 63, 33)}` expect(log.args[0][0]).to.be.equal(output); expect(log.args[1][0]).to.be.equal(msg); }); }); }); }); ================================================ FILE: invokeLocal/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const _ = require('lodash'); const path = require('path'); const chalk = require('chalk'); const stdin = require('get-stdin'); const spawn = require('child_process').spawn; class OpenWhiskInvokeLocal { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.provider = this.serverless.getProvider('openwhisk'); this.hooks = { 'invoke:local:invoke': () => BbPromise.bind(this) .then(this.validate) .then(this.mergePackageParams) .then(this.loadEnvVars) .then(this.invokeLocal), }; } validate() { if (!this.serverless.config.servicePath) { throw new this.serverless.classes.Error('This command can only be run inside a service.'); } this.options.functionObj = this.serverless.service.getFunction(this.options.function); return new BbPromise(resolve => { if (this.options.data) { resolve(); } else if (this.options.path) { const absolutePath = path.isAbsolute(this.options.path) ? this.options.path : path.join(this.serverless.config.servicePath, this.options.path); if (!this.serverless.utils.fileExistsSync(absolutePath)) { throw new this.serverless.classes.Error('The file you provided does not exist.'); } this.options.data = this.serverless.utils.readFileSync(absolutePath); resolve(); } else { stdin().then(input => { this.options.data = input; resolve(); }); } }).then(() => { const params = this.options.functionObj.parameters || {}; let data = {}; try { if(typeof this.options.data === 'object') { data = this.options.data; } else { data = JSON.parse(this.options.data); } } catch (exception) { // do nothing if it's a simple string or object already } this.options.data = Object.assign(params, data); }); } mergePackageParams() { const functionObj = this.serverless.service.getFunction(this.options.function) || {} const name = functionObj.name || '' const id = name.match(/^(.+)\/.+$/) if (id) { const pgke = id[1] const manifestPackages = this.serverless.service.resources.packages || {}; const packageDetails = manifestPackages[pgke] || {} const packageParams = packageDetails.parameters || {} this.options.data = Object.assign(packageParams, this.options.data); } return BbPromise.resolve(); } loadEnvVars() { return this.provider.props().then(props => { const envVars = { __OW_API_KEY: props.auth, __OW_API_HOST: props.apihost, __OW_ACTION_NAME: this.calculateFunctionName(this.options.function, this.options.functionObj), __OW_NAMESPACE: this.calculateFunctionNameSpace(this.options.functionObj) }; _.merge(process.env, envVars); return BbPromise.resolve(); }) } calculateFunctionName(functionName, functionObject) { const namespace = this.calculateFunctionNameSpace(functionObject); const name = functionObject.name || `${this.serverless.service.service}_${functionName}`; return `/${namespace}/${name}` } calculateFunctionNameSpace(functionObject) { return functionObject.namespace || this.serverless.service.provider.namespace || '_'; } invokeLocal() { const runtime = this.options.functionObj.runtime || this.serverless.service.provider.runtime || 'nodejs:default'; const handler = this.options.functionObj.handler; const handlerPath = handler.split('.')[0]; const handlerName = handler.split('.')[1]; if (runtime.startsWith('nodejs')) { return this.invokeLocalNodeJs( handlerPath, handlerName, this.options.data); } else if (runtime.startsWith('python')) { return this.invokeLocalPython( handlerPath, handlerName, this.options.data); } throw new this.serverless.classes .Error('You can only invoke Node.js or Python functions locally.'); } invokeLocalNodeJs(handlerPath, handlerName, params) { let action, result; try { /* * we need require() here to load the handler from the file system * which the user has to supply by passing the function name */ action = require(path // eslint-disable-line global-require .join(this.serverless.config.servicePath, handlerPath))[handlerName]; } catch (error) { this.serverless.cli.consoleLog(error); process.exit(0); } try { let result = action(params) return Promise.resolve(result).then(result => { this.serverless.cli.consoleLog(JSON.stringify(result, null, 4)); }).catch(err => { const errorResult = { errorMessage: err.message || err, errorType: err.constructor.name, }; this.serverless.cli.consoleLog(chalk.red(JSON.stringify(errorResult, null, 4))); process.exitCode = 1; }) } catch (err) { const errorResult = { errorMessage: err.message, errorType: err.constructor.name, }; this.serverless.cli.consoleLog(chalk.red(JSON.stringify(errorResult, null, 4))); process.exitCode = 1; } } invokeLocalPython(handlerPath, handlerName, params) { if (process.env.VIRTUAL_ENV) { process.env.PATH = `${process.env.VIRTUAL_ENV}/bin:${process.env.PATH}`; } return new BbPromise(resolve => { const python = spawn( path.join(__dirname, 'invoke.py'), [handlerPath, handlerName], { env: process.env }); python.stdout.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); python.stderr.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); python.stdin.write(JSON.stringify(params || {})); python.stdin.end(); python.on('close', () => resolve()); }); } } module.exports = OpenWhiskInvokeLocal; ================================================ FILE: invokeLocal/invoke.py ================================================ #!/usr/bin/env python import argparse import json import sys from time import time from importlib import import_module parser = argparse.ArgumentParser( prog='invoke', description='Runs a Lambda entry point (handler) with an optional event', ) parser.add_argument('handler_path', help=('Path to the module containing the handler function,' ' omitting ".py". IE: "path/to/module"')) parser.add_argument('handler_name', help='Name of the handler function') if __name__ == '__main__': args = parser.parse_args() # this is needed because you need to import from where you've executed sls sys.path.append('.') module = import_module(args.handler_path.replace('/', '.')) handler = getattr(module, args.handler_name) event = json.load(sys.stdin) result = handler(event) sys.stdout.write(json.dumps(result, indent=4)) ================================================ FILE: invokeLocal/tests/fixture/handlerWithError.js ================================================ 'use strict'; module.exports.withObj = () => { return { message: 'hello' } }; module.exports.withError = () => { throw new Error('failed') }; module.exports.withPromise = () => { return Promise.resolve({ message: 'hello' }) }; module.exports.withRejectedPromise = () => { return Promise.reject(new Error('failed')) }; ================================================ FILE: invokeLocal/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const path = require('path'); const OpenWhiskInvokeLocal = require('../index'); const OpenWhiskProvider = require('../../provider/openwhiskProvider'); const BbPromise = require('bluebird'); const os = require('os'); const crypto = require('crypto'); const getTmpDirPath = () => path.join(os.tmpdir(), 'tmpdirs-serverless', 'serverless', crypto.randomBytes(8).toString('hex')); const getTmpFilePath = (fileName) => path.join(getTmpDirPath(), fileName); describe('OpenWhiskInvokeLocal', () => { const CLI = function () { this.consoleLog = function () {};}; const serverless = { config: () => {}, utils: {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: { environment: {}, getFunction: () => {}, provider: {}, resources: {}, getAllFunctions: () => [] } }; serverless.setProvider = () => {} const provider = new OpenWhiskProvider(serverless) serverless.getProvider = () => provider; const options = { stage: 'dev', region: 'us-east-1', function: 'first', }; const openwhiskInvokeLocal = new OpenWhiskInvokeLocal(serverless, options); describe('#constructor()', () => { it('should have hooks', () => expect(openwhiskInvokeLocal.hooks).to.be.not.empty); it('should set the provider variable to an instance of OpenWhiskProvider', () => expect(openwhiskInvokeLocal.provider).to.be.instanceof(OpenWhiskProvider)); it('should run promise chain in order', () => { const validateStub = sinon .stub(openwhiskInvokeLocal, 'validate').returns(BbPromise.resolve()); const loadEnvVarsStub = sinon .stub(openwhiskInvokeLocal, 'loadEnvVars').returns(BbPromise.resolve()); const invokeLocalStub = sinon .stub(openwhiskInvokeLocal, 'invokeLocal').returns(BbPromise.resolve()); return openwhiskInvokeLocal.hooks['invoke:local:invoke']().then(() => { expect(validateStub.calledOnce).to.be.equal(true); expect(loadEnvVarsStub.calledAfter(validateStub)).to.be.equal(true); expect(invokeLocalStub.calledAfter(loadEnvVarsStub)).to.be.equal(true); openwhiskInvokeLocal.validate.restore(); openwhiskInvokeLocal.loadEnvVars.restore(); openwhiskInvokeLocal.invokeLocal.restore(); }); }); it('should set an empty options object if no options are given', () => { const openwhiskInvokeWithEmptyOptions = new OpenWhiskInvokeLocal(serverless); expect(openwhiskInvokeWithEmptyOptions.options).to.deep.equal({}); }); }); describe('#validate()', () => { beforeEach(() => { serverless.config.servicePath = true; serverless.service.environment = { vars: {}, stages: { dev: { vars: {}, regions: { 'us-east-1': { vars: {}, }, }, }, }, }; serverless.service.functions = { first: { handler: true, }, }; openwhiskInvokeLocal.options.data = null; openwhiskInvokeLocal.options.path = false; serverless.service.getFunction = () => serverless.service.functions.first }); it('should ignore data if it is a simple string', () => { openwhiskInvokeLocal.options.data = 'simple-string'; return openwhiskInvokeLocal.validate().then(() => { expect(openwhiskInvokeLocal.options.data).to.deep.equal({}); }); }); it('should ignore data if it is an array', () => { openwhiskInvokeLocal.options.data = '[]'; return openwhiskInvokeLocal.validate().then(() => { expect(openwhiskInvokeLocal.options.data).to.deep.equal({}); }); }); it('should parse data if it is a json string', () => { openwhiskInvokeLocal.options.data = '{"key": "value"}'; return openwhiskInvokeLocal.validate().then(() => { expect(openwhiskInvokeLocal.options.data).to.deep.equal({ key: 'value' }); }); }); it('it should parse file if relative file path is provided', () => { serverless.config.servicePath = getTmpDirPath(); const data = { testProp: 'testValue', }; serverless.utils.fileExistsSync = () => true; serverless.utils.readFileSync = path => JSON.stringify(data); openwhiskInvokeLocal.options.path = 'data.json'; return openwhiskInvokeLocal.validate().then(() => { expect(openwhiskInvokeLocal.options.data).to.deep.equal(data); }); }); it('it should parse file if absolute file path is provided', () => { serverless.config.servicePath = getTmpDirPath(); const data = { event: { testProp: 'testValue', }, }; serverless.utils.fileExistsSync = () => true; serverless.utils.readFileSync = path => JSON.stringify(data); const dataFile = path.join(serverless.config.servicePath, 'data.json'); openwhiskInvokeLocal.options.path = dataFile; return openwhiskInvokeLocal.validate().then(() => { expect(openwhiskInvokeLocal.options.data).to.deep.equal(data); }); }); it('it should accept file path containing javascript object', () => { serverless.config.servicePath = getTmpDirPath(); const data = { event: { testProp: 'testValue', }, }; serverless.utils.fileExistsSync = () => true; serverless.utils.readFileSync = path => data; const dataFile = path.join(serverless.config.servicePath, 'data.json'); openwhiskInvokeLocal.options.path = dataFile; return openwhiskInvokeLocal.validate().then(() => { expect(openwhiskInvokeLocal.options.data).to.deep.equal(data); }); }); it('it should throw error if service path is not set', () => { serverless.config.servicePath = false; expect(() => openwhiskInvokeLocal.validate()).to.throw(Error); }); it('it should reject error if file path does not exist', () => { serverless.config.servicePath = getTmpDirPath(); openwhiskInvokeLocal.options.path = 'some/path'; return openwhiskInvokeLocal.validate().catch((err) => { expect(err).to.be.instanceOf(Error); }); }); it('should resolve if path is not given', () => { openwhiskInvokeLocal.options.path = false; return openwhiskInvokeLocal.validate() }); it('should use parameters from function object', () => { serverless.service.functions.first.parameters = { foo: 'bar', nums: 1, arr: ['foo', 'bar'] } return openwhiskInvokeLocal.validate().then(() => { expect(openwhiskInvokeLocal.options.data).to.deep.equal(serverless.service.functions.first.parameters); }); }); it('should merge parameters from function object and data parameters', () => { serverless.service.functions.first.parameters = { foo: 'bar', nums: 1, arr: ['foo', 'bar'] } openwhiskInvokeLocal.options.data = JSON.stringify({ foo: 'foo', bar: 'foo' }) return openwhiskInvokeLocal.validate().then(() => { expect(openwhiskInvokeLocal.options.data).to.deep.equal({ foo: 'foo', bar: 'foo', nums: 1, arr: ['foo', 'bar'] }); }); }); }); describe('#loadEnvVars()', () => { beforeEach(() => { serverless.config.servicePath = true; serverless.service.provider = { namespace: 'testing_ns', environment: { providerVar: 'providerValue', }, }; openwhiskInvokeLocal.options = { region: 'us-east-1', functionObj: { name: 'serviceName-dev-hello', environment: { functionVar: 'functionValue', }, }, }; serverless.getProvider()._props = {apihost: 'endpoint', auth: 'user:pass'} }); it('it should expected env vars', () => openwhiskInvokeLocal .loadEnvVars().then(() => { expect(process.env.__OW_API_KEY).to.equal('user:pass'); expect(process.env.__OW_API_HOST).to.equal('endpoint'); expect(process.env.__OW_ACTION_NAME).to.equal('/testing_ns/serviceName-dev-hello'); expect(process.env.__OW_NAMESPACE).to.equal('testing_ns'); }) ); }); describe('#mergePackageParams()', () => { beforeEach(() => { serverless.config.servicePath = true; serverless.service.provider = { namespace: 'testing_ns', environment: { providerVar: 'providerValue', }, }; serverless.service.functions.first.name = 'mypackage/first' serverless.service.functions.first.parameters = { foo: 'bar', nums: 1, arr: ['foo', 'bar'] } openwhiskInvokeLocal.options = { data: { foo: 'bar', nums: 1, arr: ['foo', 'bar'] }}; }); it('it should ignore implicit packages without parameters', () => openwhiskInvokeLocal .mergePackageParams().then(() => { expect(openwhiskInvokeLocal.options.data).to.deep.equal(serverless.service.functions.first.parameters); }) ); it('it should merge implicit packages with parameters', () => { serverless.service.resources.packages = { mypackage: { parameters: { hello: 'world', foo: 'baz' } } } return openwhiskInvokeLocal.mergePackageParams().then(() => { const merged = Object.assign(serverless.service.resources.packages.mypackage.parameters, openwhiskInvokeLocal.options.data) expect(openwhiskInvokeLocal.options.data).to.deep.equal(merged); }) }); }); describe('#invokeLocal()', () => { let invokeLocalNodeJsStub, invokeLocalPythonStub; beforeEach(() => { invokeLocalNodeJsStub = sinon.stub(openwhiskInvokeLocal, 'invokeLocalNodeJs').returns(BbPromise.resolve()); invokeLocalPythonStub = sinon.stub(openwhiskInvokeLocal, 'invokeLocalPython').returns(BbPromise.resolve()); openwhiskInvokeLocal.serverless.service.service = 'new-service'; openwhiskInvokeLocal.options = { stage: 'dev', function: 'first', functionObj: { handler: 'handler.hello', name: 'hello', }, data: {}, }; }); afterEach(() => { invokeLocalNodeJsStub.restore(); invokeLocalPythonStub.restore(); }); it('should call invokeLocalNodeJs when no runtime is set', () => openwhiskInvokeLocal.invokeLocal() .then(() => { expect(invokeLocalNodeJsStub.calledOnce).to.be.equal(true); expect(invokeLocalNodeJsStub.calledWithExactly( 'handler', 'hello', {} )).to.be.equal(true); openwhiskInvokeLocal.invokeLocalNodeJs.restore(); }) ); it('should call invokeLocalNodeJs when nodejs runtime is set', () => { openwhiskInvokeLocal.options.functionObj.runtime = 'nodejs:6'; openwhiskInvokeLocal.invokeLocal() .then(() => { expect(invokeLocalNodeJsStub.calledOnce).to.be.equal(true); expect(invokeLocalNodeJsStub.calledWithExactly( 'handler', 'hello', {} )).to.be.equal(true); openwhiskInvokeLocal.invokeLocalNodeJs.restore(); }) }); it('should call invokeLocalPython when python runtime is set', () => { openwhiskInvokeLocal.options.functionObj.runtime = 'python'; openwhiskInvokeLocal.invokeLocal() .then(() => { expect(invokeLocalPythonStub.calledOnce).to.be.equal(true); expect(invokeLocalPythonStub.calledWithExactly( 'handler', 'hello', {} )).to.be.equal(true); openwhiskInvokeLocal.invokeLocalPython.restore(); }) }); it('throw error when using invalid runtime', () => { openwhiskInvokeLocal.options.functionObj.runtime = 'wrong'; expect(() => openwhiskInvokeLocal.invokeLocal()).to.throw(Error); delete openwhiskInvokeLocal.options.functionObj.runtime; }); }); describe('#invokeLocalNodeJs', () => { beforeEach(() => { openwhiskInvokeLocal.options = { functionObj: { name: '', }, }; serverless.cli = new CLI(serverless); sinon.stub(serverless.cli, 'consoleLog'); }); afterEach(() => { serverless.cli.consoleLog.restore(); }); it('should print message for non-promise return', () => { openwhiskInvokeLocal.serverless.config.servicePath = __dirname; return openwhiskInvokeLocal.invokeLocalNodeJs('fixture/handlerWithError', 'withObj').then(() => { expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"message": "hello"'); }) }); it('should print message for promise return', () => { openwhiskInvokeLocal.serverless.config.servicePath = __dirname; return openwhiskInvokeLocal.invokeLocalNodeJs('fixture/handlerWithError', 'withPromise').then(() => { expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"message": "hello"'); }) }); it('should exit with error exit code for thrown errors', () => { process.exitCode = -1 openwhiskInvokeLocal.serverless.config.servicePath = __dirname; openwhiskInvokeLocal.invokeLocalNodeJs('fixture/handlerWithError', 'withError'); expect(process.exitCode).to.be.equal(1); expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('"errorMessage": "failed"'); }); it('should exit with error exit code for rejected promises', () => { process.exitCode = -1 openwhiskInvokeLocal.serverless.config.servicePath = __dirname; return openwhiskInvokeLocal.invokeLocalNodeJs('fixture/handlerWithError', 'withRejectedPromise').then(() => { expect(process.exitCode).to.be.equal(1); expect(serverless.cli.consoleLog.lastCall.args[0]).to.contain('errorMessage'); }) }); }); }); ================================================ FILE: logs/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const chalk = require('chalk'); const moment = require('moment'); const path = require('path'); class OpenWhiskLogs { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.provider = this.serverless.getProvider('openwhisk'); this.previous_activations = new Set() this.hooks = { 'logs:logs': () => BbPromise.bind(this) .then(this.validate) .then(this.functionLogs) }; } validate () { if (!this.serverless.config.servicePath) { throw new this.serverless.classes.Error('This command can only be run inside a service.'); } this.serverless.service.getFunction(this.options.function); this.options.stage = this.options.stage || (this.serverless.service.provider && this.serverless.service.provider.stage) || 'dev'; this.options.region = this.options.region || (this.serverless.service.provider && this.serverless.service.provider.region) || 'us-east-1'; this.options.interval = this.options.interval || 1000; if (this.options.filter) { this.options.filter = new RegExp(this.options.filter, 'i'); } if (this.options.startTime) { this.options.startTime = moment(this.options.startTime) } return this.provider.client().then(client => { this.client = client; }); } functionLogs () { return BbPromise.bind(this) .then(this.retrieveInvocationLogs) .then(this.filterFunctionLogs) .then(this.showFunctionLogs) .then(() => { if (this.options.tail) { this.timeout = setTimeout(() => { this.functionLogs() }, this.options.interval) } }) } retrieveInvocationLogs () { const functionObject = this.serverless.service.getFunction(this.options.function); const options = { docs: true, limit: 100, namespace: '_' }; return this.client.activations.list(options) .catch(err => { throw new this.serverless.classes.Error( `Failed to retrieve activation logs due to error:` + ` ${err.message}` ); }); } // activation log annotations have a { key: 'path, value: 'namespace/actioname' } member. hasPathAnnotationWithName (annotations, name) { return annotations.filter(an => an.key === 'path') .map(an => an.value.split('/').slice(1).join('/')) .some(value => value === name) } filterFunctionLogs (logs) { const functionObject = this.serverless.service.getFunction(this.options.function); const actionName = functionObject.name || `${this.serverless.service.service}_${this.options.function}` // skip activations for other actions or that we have seen before const filtered = logs.filter(log => this.hasPathAnnotationWithName((log.annotations || []), actionName) && !this.previous_activations.has(log.activationId)) // allow regexp filtering of log messages if (this.options.filter) { filtered.forEach(log => { log.logs = log.logs.filter(logLine => logLine.match(this.options.filter)) }) } // filter those logs based upon start time if (this.options.startTime) { filtered.forEach(log => { log.logs = log.logs.filter(logLine => { const timestamp = logLine.split(" ")[0] return this.options.startTime.isBefore(moment(timestamp)) }) }) } return BbPromise.resolve(filtered); } showFunctionLogs (logs) { if (!this.options.tail && !logs.length) { this.consoleLog(`There's no log data for function "${this.options.function}" available right now…`) return BbPromise.resolve(); } logs.filter(log => log.logs.length) .reverse() .map((log, idx, arr) => { if (this.timeout && idx === 0) console.log('') this.previous_activations.add(log.activationId) this.consoleLog(this.formatActivationLine(log)) log.logs.map(this.formatLogLine).forEach(this.consoleLog) if (idx != (arr.length - 1)) { this.consoleLog('') } }) return BbPromise.resolve(); } formatActivationLine (activation) { return `${chalk.blue('activation')} (${chalk.yellow(activation.activationId)}):` } formatLogLine (logLine) { const items = logLine.split(' ').filter(item => item !== '') const format = 'YYYY-MM-DD HH:mm:ss.SSS' const timestamp = chalk.green(moment(items[0]).utc().format(format)) let contents = items.slice(2).join(' ') if (items[1] === 'stderr:') { contents = chalk.red(contents) } return `${timestamp} ${contents}` } consoleLog(msg) { console.log(msg); // eslint-disable-line no-console } } module.exports = OpenWhiskLogs; ================================================ FILE: logs/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const chaiAsPromised = require('chai-as-promised'); const sinon = require('sinon'); const path = require('path'); const os = require('os'); const OpenWhiskLogs = require('../'); const BbPromise = require('bluebird'); const chalk = require('chalk'); const moment = require('moment'); require('chai').use(chaiAsPromised); describe('OpenWhiskLogs', () => { let sandbox; const CLI = function () { this.log = function () {};}; const serverless = {config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: name => (serverless.service.functions[name]), provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; const options = { stage: 'dev', region: 'us-east-1', function: 'first', }; const openwhiskLogs = new OpenWhiskLogs(serverless, options); beforeEach(() => { sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); }); describe('#constructor()', () => { it('should have hooks', () => expect(openwhiskLogs.hooks).to.be.not.empty); it('should run promise chain in order', () => { const validateStub = sinon .stub(openwhiskLogs, 'validate').returns(BbPromise.resolve()); const retrieveInvocationLogsStub = sinon .stub(openwhiskLogs, 'retrieveInvocationLogs').returns(BbPromise.resolve()); const filterFunctionLogsStub = sinon .stub(openwhiskLogs, 'filterFunctionLogs').returns(BbPromise.resolve()); const showFunctionLogsStub = sinon .stub(openwhiskLogs, 'showFunctionLogs').returns(BbPromise.resolve()); return openwhiskLogs.hooks['logs:logs']().then(() => { expect(validateStub.calledOnce).to.be.equal(true); expect(retrieveInvocationLogsStub.calledAfter(validateStub)).to.be.equal(true); expect(filterFunctionLogsStub.calledAfter(retrieveInvocationLogsStub)).to.be.equal(true); expect(showFunctionLogsStub.calledAfter(filterFunctionLogsStub)).to.be.equal(true); openwhiskLogs.validate.restore(); openwhiskLogs.retrieveInvocationLogs.restore(); openwhiskLogs.filterFunctionLogs.restore(); openwhiskLogs.showFunctionLogs.restore(); }); }); }); describe('#functionLogs', () => { let clock; beforeEach(() => { openwhiskLogs.serverless.service.functions = { first: { handler: true, }, }; openwhiskLogs.serverless.service.service = 'new-service'; openwhiskLogs.options = { function: 'first' }; clock = sinon.useFakeTimers(); }); afterEach(() => { clock.restore(); }); it('should not tail logs unless option is set', () => { const retrieveInvocationLogsStub = sinon .stub(openwhiskLogs, 'retrieveInvocationLogs').returns(BbPromise.resolve([])); return openwhiskLogs.functionLogs().then(() => { expect(clock.timers).to.be.equal(undefined) openwhiskLogs.retrieveInvocationLogs.restore(); }); }) it('should support tailing logs', () => { const retrieveInvocationLogsStub = sinon .stub(openwhiskLogs, 'retrieveInvocationLogs').returns(BbPromise.resolve([])); openwhiskLogs.options.tail = true openwhiskLogs.options.interval = 100 return openwhiskLogs.functionLogs().then(() => { expect(clock.timers['1'].createdAt).to.be.equal(0) expect(clock.timers['1'].delay).to.be.equal(100) openwhiskLogs.retrieveInvocationLogs.restore(); }); }) }) describe('#retrieveLogs()', () => { let activationsStub, clock; beforeEach(() => { openwhiskLogs.serverless.service.functions = { first: { handler: true, }, }; openwhiskLogs.serverless.service.service = 'new-service'; openwhiskLogs.options = { function: 'first' }; openwhiskLogs.client = { activations: { list: () => {} } }; clock = sinon.useFakeTimers(); }); afterEach(() => { activationsStub.restore(); clock.restore(); }); it('should invoke with correct params', () => { activationsStub = sinon.stub(openwhiskLogs.client.activations, 'list') .returns(BbPromise.resolve()); return openwhiskLogs.retrieveInvocationLogs().then(() => { expect(activationsStub.calledOnce).to.be.equal(true); expect(activationsStub.args[0][0]).to.be.deep.equal({ docs: true, limit: 100, namespace: '_' }); }); } ); it('should reject when sdk client fails', () => { activationsStub = sinon.stub(openwhiskLogs.client.activations, 'list').returns(BbPromise.reject()); return expect(openwhiskLogs.retrieveInvocationLogs()).to.be.eventually.rejected; }); }); describe('#filterFunctionLogs()', () => { beforeEach(() => { openwhiskLogs.serverless.service.functions = { first: { handler: true, }, }; openwhiskLogs.serverless.service.service = 'new-service'; openwhiskLogs.options = { function: 'first' }; }); it('should filter out different function logs', () => { const logs = [ { name: "new-service_first", annotations: [ {key: "path", value: 'user@host.com_dev/new-service_first'} ] }, { name: "new-service_first", annotations: [ {key: "path", value: 'user@host.com_dev/new-service_first'} ] }, { name: "new-service_second", annotations: [ {key: "path", value: 'user@host.com_dev/new-service_second'} ] }, { name: "new-service_third", annotations: [ {key: "path", value: "user@host.com_dev/new-second_third"} ] }, { name: "new-service_first", annotations: [ {key: "path", value: 'user@host.com_dev/new-service_first'} ] } ] return openwhiskLogs.filterFunctionLogs(logs).then(logs => { expect(logs.length).to.be.equal(3) logs.forEach(log => expect(log.name).to.be.equal('new-service_first')) }) }); it('should filter out different function logs with package function', () => { openwhiskLogs.serverless.service.functions.first.name = 'packagename/funcname' const logs = [ { name: "funcname", annotations:[{ key:"path", value:"user@host.com_dev/packagename/funcname"} ]}, { name: "funcname", annotations:[{ key:"path", value:"user@host.com_dev/packagename/funcname"} ]}, { name: "new-service_second", annotations: [ {key: "path", value: 'user@host.com_dev/new-service_second'} ] }, { name: "new-service_third", annotations: [ {key: "path", value: "user@host.com_dev/new-second_third"} ] }, { name: "funcname", annotations:[{ key:"path", value:"user@host.com_dev/packagename/funcname"} ]} ] return openwhiskLogs.filterFunctionLogs(logs).then(logs => { expect(logs.length).to.be.equal(3) logs.forEach(log => expect(log.name).to.be.equal('funcname')) }) }); it('should filter out logs lines based upon contents', () => { openwhiskLogs.options.startTime = moment('2001-01-01') const logs = [{name: "new-service_first", annotations: [ {key: "path", value: 'user@host.com_dev/new-service_first' }], logs: ["2001-01-02 matching line", "2001-01-01 another matching line", "2000-12-31 should not match"]}] return openwhiskLogs.filterFunctionLogs(logs).then(logs => { expect(logs.length).to.be.equal(1) expect(logs[0].logs).to.be.deep.equal(["2001-01-02 matching line"]) delete openwhiskLogs.options.startTime }) }); it('should filter out logs lines based upon contents', () => { openwhiskLogs.options.filter = new RegExp('matching', 'i') const logs = [ {name: "new-service_first", annotations: [ {key: "path", value: 'user@host.com_dev/new-service_first' }], logs: ["matching line", "another matching line", "should not match"]}, {name: "new-service_first", annotations: [ {key: "path", value: 'user@host.com_dev/new-service_first' }], logs: ["does not match"]} ] return openwhiskLogs.filterFunctionLogs(logs).then(logs => { expect(logs.length).to.be.equal(2) expect(logs[0].logs).to.be.deep.equal(["matching line", "another matching line"]) expect(logs[1].logs).to.be.deep.equal([]) delete openwhiskLogs.options.filter }) }); it('should filter already seen log messages', () => { openwhiskLogs.previous_activations = new Set([1, 2, 3, 4, 5]) const logs = [ {activationId: 1, annotations: [ {key: "path", value: 'user@host.com_dev/new-service_first' }], name: "new-service_first"}, {activationId: 5, annotations: [ {key: "path", value: 'user@host.com_dev/new-service_first' }], name: "new-service_first"}, {activationId: 6, annotations: [ {key: "path", value: 'user@host.com_dev/new-service_first' }], name: "new-service_first"} ] return openwhiskLogs.filterFunctionLogs(logs).then(logs => { expect(logs.length).to.be.equal(1) expect(logs[0].name).to.be.equal('new-service_first') expect(logs[0].activationId).to.be.equal(6) }) }); }); describe('#showFunctionLogs()', () => { let logStub beforeEach(() => { openwhiskLogs.serverless.service.functions = { first: { handler: true, }, }; openwhiskLogs.serverless.service.service = 'new-service'; openwhiskLogs.options = { function: 'first' }; }); afterEach(() => { logStub.restore(); }) it('should return no logs message for zero activations', () => { logStub = sinon.stub(openwhiskLogs, 'consoleLog') return openwhiskLogs.showFunctionLogs([]).then(() => { expect(logStub.calledOnce).to.be.equal(true); expect(logStub.args[0][0]).to.be.deep.equal(`There's no log data for function "first" available right now…`); }); }); it('should return log messages for activations', () => { logStub = sinon.stub(openwhiskLogs, 'consoleLog') openwhiskLogs.previous_activations = new Set() const activation = { activationId: 12345, logs: [ "2016-11-21T11:08:05.980285407Z stdout: this is the message", "2016-11-21T11:08:05.980285407Z stderr: this is an error" ]} return openwhiskLogs.showFunctionLogs([activation]).then(() => { expect(logStub.calledThrice).to.be.equal(true); expect(logStub.args[0][0]).to.be.deep.equal(`${chalk.blue('activation')} (${chalk.yellow(12345)}):`); expect(logStub.args[1][0]).to.be.deep.equal(`${chalk.green('2016-11-21 11:08:05.980')} this is the message`); expect(logStub.args[2][0]).to.be.deep.equal(`${chalk.green('2016-11-21 11:08:05.980')} ${chalk.red('this is an error')}`); expect(openwhiskLogs.previous_activations.size).to.be.equal(1) expect(openwhiskLogs.previous_activations.has(12345)).to.be.equal(true) }); }); }); }); ================================================ FILE: package.json ================================================ { "name": "serverless-openwhisk", "version": "0.18.4", "description": "OpenWhisk support for the Serverless Framework", "main": "index.js", "directories": { "test": "tests" }, "scripts": { "test": "istanbul cover -x '**/*.test.js' node_modules/mocha/bin/_mocha tests/all -- -R spec --recursive", "report": "istanbul report lcovonly && codecov" }, "repository": { "type": "git", "url": "git+https://github.com/serverless/serverless-openwhisk.git" }, "keywords": [ "serverless", "framework", "openwhisk" ], "author": "James Thomas ", "license": "MIT", "bugs": { "url": "https://github.com/serverless/serverless-openwhisk/issues" }, "engines": { "node": ">=6.0" }, "homepage": "https://github.com/serverless/serverless-openwhisk#readme", "dependencies": { "@ibm-functions/iam-token-manager": "^1.0.3", "bluebird": "^3.4.6", "chalk": "^1.1.3", "fs-extra": "^1.0.0", "get-stdin": "^5.0.1", "jszip": "^3.1.3", "jws": "^3.2.2", "lodash": "^4.17.11", "moment": "^2.16.0", "openwhisk": "^3.19.0" }, "devDependencies": { "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "coveralls": "^3.0.3", "istanbul": "^0.4.4", "mocha": "^6.1.2", "mocha-lcov-reporter": "^1.2.0", "mock-require": "^1.3.0", "proxyquire": "^1.7.10", "sinon": "^1.17.5" } } ================================================ FILE: provider/cliTokenManager.js ================================================ 'use strict'; "use strict"; const jws = require('jws'); const { exec } = require('child_process'); const { readFileSync } = require('fs'); const path = require('path'); // Configuration file location for IBM Cloud CLI. // This will contain the current IAM tokens for the user. const DEFAULT_CONFIG_LOCATION = `.bluemix/config.json` // This class handles retrieving authentication tokens for IAM namespaces on IBM Cloud Functions. // Tokens are parsed from the configuration file used by the IBM Cloud CLI. // If tokens have expired, the CLI command `ibmcloud iam oauth-tokens` is executed. // This will automatically refresh the tokens in the configuration. module.exports = class CliTokenManager { constructor(_exec = exec, _readFile = readFileSync) { this.exec = _exec this.readFile = _readFile this.refresh_command = 'ibmcloud iam oauth-tokens' } getAuthHeader () { const to_header = token => `Bearer ${token}` const token = this.readTokenFromConfig() if (this.isTokenExpired(token)) { return this.refreshToken().then(to_header) } return Promise.resolve(to_header(token)) } refreshToken () { return new Promise((resolve, reject) => { this.exec(this.refresh_command, error => { if (error) { const err_message = `IAM token from IBM Cloud CLI configuration file (.bluemix/config.json) has expired. ` + `Refresh failed using CLI command (ibmcloud iam oauth-tokens). Check error message for details: ${error}` return reject(new Error(err_message)) } resolve(this.readTokenFromConfig()) }); }) } // IAM Tokens stored under the IAMToken field in configuration. readTokenFromConfig (configPath = CliTokenManager.configFilePath()) { const contents = this.readFile(configPath, 'utf-8') const config = JSON.parse(contents) const [prefix, token] = config.IAMToken.split(' ') return token } isTokenExpired (token) { const decoded = jws.decode(token, { json: true }) const expiry_time = decoded.payload.exp const now = Math.floor(Date.now() / 1000) return expiry_time <= now } // Support both platforms for configuration files. static configFilePath (config_file = DEFAULT_CONFIG_LOCATION) { const home_dir = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; const config_path = path.format({ dir: home_dir, base: config_file }); return config_path } } ================================================ FILE: provider/credentials.js ================================================ 'use strict'; const path = require('path'); const fs = require('fs-extra'); const ENV_PARAMS = ['OW_APIHOST', 'OW_AUTH', 'OW_NAMESPACE', 'OW_APIGW_ACCESS_TOKEN', 'OW_IAM_NAMESPACE_API_KEY']; function getWskPropsFile() { const Home = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; return process.env.WSK_CONFIG_FILE || path.format({ dir: Home, base: '.wskprops' }); } function readWskPropsFile() { const wskFilePath = getWskPropsFile(); return new Promise(resolve => { fs.readFile(wskFilePath, 'utf8', (err, data) => { resolve(err ? '' : data); }); }); } function getWskProps() { return readWskPropsFile().then(data => { if (!data) return {}; const wskProps = data.trim().split('\n') .map(line => line.split('=')) .reduce((params, keyValue) => { params[keyValue[0].toLowerCase()] = keyValue[1]; // eslint-disable-line no-param-reassign return params; }, {}); return wskProps; }); } function getWskEnvProps() { const envProps = {}; ENV_PARAMS.forEach((envName) => { if (process.env[envName]) envProps[envName.slice(3).toLowerCase()] = process.env[envName]; }); return envProps; } module.exports = { getWskProps() { return getWskProps() .then(props => Object.assign(props, getWskEnvProps())); }, ENV_PARAMS, }; ================================================ FILE: provider/openwhiskProvider.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const openwhisk = require('openwhisk') const IamTokenManager = require('@ibm-functions/iam-token-manager') const CliTokenManager = require('./cliTokenManager') const Credentials = require('./credentials'); const constants = { providerName: 'openwhisk', }; const credentials = ['apihost', 'auth']; class OpenwhiskProvider { static getProviderName() { return constants.providerName; } constructor(serverless) { this.serverless = serverless; this.provider = this; this.serverless.setProvider(constants.providerName, this); this.sdk = openwhisk } // Returns OpenWhisk SDK client configured with authentication credentials. // Auto-detects use of IAM namespaces when using IBM Cloud Functions and adds // external auth handler to client. client() { if (this._client) return BbPromise.resolve(this._client) const ignore_certs = this.serverless.service.provider.ignore_certs || false return this.props().then(props => { if (props.hasOwnProperty('iam_namespace_api_key')) { const auth_handler = new IamTokenManager({ iamApikey: props.iam_namespace_api_key }); this._client = openwhisk({ apihost: props.apihost, auth_handler, namespace: props.namespace }); } else if (this.isIBMCloudIAMProps(props)) { const auth_handler = new CliTokenManager() this._client = openwhisk({ apihost: props.apihost, auth_handler, namespace: props.namespace }); } else { this.hasValidCreds(props) this._client = openwhisk({ apihost: props.apihost, api_key: props.auth, namespace: props.namespace, ignore_certs, apigw_token: props.apigw_access_token }); } return this._client }) } props() { if (this._props) return BbPromise.resolve(this._props) return Credentials.getWskProps().then(wskProps => { this._props = wskProps; return this._props; }) } hasValidCreds(creds) { credentials.forEach(prop => { if (!creds[prop]) { throw new Error(`Missing mandatory openwhisk configuration property: OW_${prop.toUpperCase()}.` + ' Check .wskprops file or set environment variable?'); } }); return creds; } // Auto-detect whether ~/.wskprops uses IBM Cloud IAM namespace (and therefore requires IAM auth handler). // Namespace will be IAM NS ID rather than default namespace. Api host will end with ibm.com hostname. isIBMCloudIAMProps (props) { return props.namespace !== '_' && props.apihost.endsWith('cloud.ibm.com') } } module.exports = OpenwhiskProvider; ================================================ FILE: provider/tests/cliTokenManager.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const fs = require('fs-extra'); const chaiAsPromised = require('chai-as-promised'); const CliTokenManager = require('../cliTokenManager.js'); require('chai').use(chaiAsPromised); describe('CliTokenManager', () => { describe('#getAuthHeader()', () => { it('should return bearer token from configuration', () => { const cliTokenManager = new CliTokenManager() const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM' cliTokenManager.readTokenFromConfig = () => token cliTokenManager.isTokenExpired = () => false const header = `Bearer ${token}` return cliTokenManager.getAuthHeader().then(result => { expect(result).to.equal(header); }) }); it('should return refreshed bearer token when token is expired', () => { const cliTokenManager = new CliTokenManager() const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM' cliTokenManager.readTokenFromConfig = () => null cliTokenManager.isTokenExpired = () => true cliTokenManager.refreshToken = () => Promise.resolve(token) const header = `Bearer ${token}` return cliTokenManager.getAuthHeader().then(result => { expect(result).to.equal(header); }) }); }) describe('#readTokenFromConfig()', () => { it('should return bearer token from default configuration file', () => { const readFile = (path, format) => { expect(path).to.equal(config_path) expect(format).to.equal('utf-8') return JSON.stringify({ IAMToken: `Bearer ${config_token}`}) } const cliTokenManager = new CliTokenManager(null, readFile) const config_token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM' const config_path = `~/.bluemix/config.json` const token = cliTokenManager.readTokenFromConfig(config_path) expect(token).to.equal(config_token) }); }); describe('#isTokenExpired()', () => { it('should return true for expired JWT tokens', () => { const cliTokenManager = new CliTokenManager() // created from http://jwtbuilder.jamiekurtz.com/ // JWT expired in 2000. const expired_token = 'eyJraWQiOiIyMDE5MDIwNCIsImFsZyI6IlJTMjU2In0.eyJpYW1faWQiOiJJQk1pZC0yNzAwMDJQUzIxIiwiaWQiOiJJQk1pZC0yNzAwMDJQUzIxIiwicmVhbG1pZCI6IklCTWlkIiwiaWRlbnRpZmllciI6IjI3MDAwMlBTMjEiLCJnaXZlbl9uYW1lIjoiSmFtZXMiLCJmYW1pbHlfbmFtZSI6IlRob21hcyIsIm5hbWUiOiJKYW1lcyBUaG9tYXMiLCJlbWFpbCI6ImphbWVzLnRob21hc0B1ay5pYm0uY29tIiwic3ViIjoiamFtZXMudGhvbWFzQHVrLmlibS5jb20iLCJhY2NvdW50Ijp7InZhbGlkIjp0cnVlLCJic3MiOiI4ZDYzZmIxY2M1ZTk5ZTg2ZGQ3MjI5ZGRkZmExNjY0OSJ9LCJpYXQiOjE1NjM0NDAyMzEsImV4cCI6MTU2MzQ0MzgzMSwiaXNzIjoiaHR0cHM6Ly9pYW0uY2xvdWQuaWJtLmNvbS9pZGVudGl0eSIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInNjb3BlIjoiaWJtIG9wZW5pZCIsImNsaWVudF9pZCI6ImJ4IiwiYWNyIjoxLCJhbXIiOlsicHdkIl19.DhgBTV_dxtSirpSoe-H_xXfxBKYIrxFqiu4eVluTq78Sqp9FCCQoMSuJBD0ysHsD-0sIp5yHq03-0DnAdldnD2YkFRwrDXY-9uG5cJGB1vH3l6X6BaWprGG-AcswqeTklnjCrRqIiUr5EU9odZAfwbDPYdoE21gudS2kMZoVgezJsUtYz2tJH-I-1JfbBPuTLLuhWVr4ZPP2GzOvI7xpWBVwMYmUviLrxD_-Gq2vJyly1rNBYA4VZKf1G46yT790EqRz9N3o18bmKUxDCP6ur2oVHwGNQy15fn8LsiylHf4s9p9yPuLtgExN6FcdMfPU8hUT1UWfaWssjpetk3crjA' const expired = cliTokenManager.isTokenExpired(expired_token) expect(expired).to.equal(true) }); it('should return false for non-expired JWT tokens', () => { const cliTokenManager = new CliTokenManager() // created from http://jwtbuilder.jamiekurtz.com/ - example JWT expires in 2100. // I won't be around when this unit test starts failing... const expired_token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1NjM0NTM2OTYsImV4cCI6NDExOTUxMTI5NiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.WNqaMqKIqkKXT731uGV8jnJmNj74qYUSiZeLLYl6ME0' const expired = cliTokenManager.isTokenExpired(expired_token) expect(expired).to.equal(false) }); }); describe('#configFilePath()', () => { it('should return default config location', () => { const default_path = `${process.env['HOME']}/.bluemix/config.json` expect(CliTokenManager.configFilePath()).to.equal(default_path) }); }); describe('#refreshToken()', () => { it('should return current token once command has executed', () => { const cliTokenManager = new CliTokenManager() const token = 'eyj0exaioijkv1qilcjhbgcioijiuzi1nij9.eyj1c2vyswqioijimdhmodzhzi0znwrhltq4zjitogzhyi1jzwyzota0njywymqifq.-xn_h82phvtcma9vdohrczxh-x5mb11y1537t3rgzcm' cliTokenManager.readTokenFromConfig = () => token cliTokenManager.exec = (cmd, cb) => { expect(cmd).to.equal(cliTokenManager.refresh_command) setTimeout(() => cb(), 0) } return cliTokenManager.refreshToken().then(_token => { expect(_token).to.equal(token) }) }); it('should throw error when refresh token command fails', () => { const cliTokenManager = new CliTokenManager() cliTokenManager.exec = (_, cb) => { setTimeout(() => cb(new Error("cmd failed")), 0) } return expect(cliTokenManager.refreshToken()).to.eventually.be.rejectedWith(/^IAM token from IBM Cloud CLI/); }); }); }); ================================================ FILE: provider/tests/credentials.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const fs = require('fs-extra'); const chaiAsPromised = require('chai-as-promised'); const Credentials = require('../credentials'); require('chai').use(chaiAsPromised); describe('#getWskProps()', () => { let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); }); afterEach(() => { ['WSK_CONFIG_FILE', ...Credentials.ENV_PARAMS].forEach(param => process.env[param] = ''); sandbox.restore(); }); describe('should instantiate openwhisk resources from the properties file', () => { const mockObject = { apihost: 'openwhisk.ng.bluemix.net', auth: 'user:pass', namespace: 'blah@provider.com_dev', apigw_access_token: 'blahblahblahkey1234', iam_namespace_api_key: 'some-api-key-value', }; const wskProps = 'APIHOST=openwhisk.ng.bluemix.net\nNAMESPACE=blah@provider.com_dev\n' + 'AUTH=user:pass\nAPIGW_ACCESS_TOKEN=blahblahblahkey1234\nIAM_NAMESPACE_API_KEY=some-api-key-value'; it('when the default is used', () => { const home = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; sandbox.stub(fs, 'readFile', (path, encoding, cb) => { expect(path.match(home).length).to.equal(1); expect(path.match('.wskprops').length).to.equal(1); cb(null, wskProps); }); return expect(Credentials.getWskProps()).to.eventually.deep.equal(mockObject); }); it('when a path is specified', () => { const propsFilePath = '/different/place/file_name'; process.env.WSK_CONFIG_FILE = propsFilePath; sandbox.stub(fs, 'readFile', (path, encoding, cb) => { expect(path).to.equal(propsFilePath); cb(null, wskProps); }); return expect(Credentials.getWskProps()).to.eventually.deep.equal(mockObject); }); }); it('should instantiate openwhisk resources from environment variables', () => { const mockObject = { apihost: 'blah.blah.com', auth: 'another_user:another_pass', namespace: 'user@user.com', apigw_access_token: 'some_access_token', iam_namespace_api_key: 'some_api_key' }; sandbox.stub(fs, 'readFile', (path, encoding, cb) => { cb(true); }); process.env.OW_APIHOST = 'blah.blah.com'; process.env.OW_AUTH = 'another_user:another_pass'; process.env.OW_NAMESPACE = 'user@user.com'; process.env.OW_APIGW_ACCESS_TOKEN = 'some_access_token'; process.env.OW_IAM_NAMESPACE_API_KEY = 'some_api_key'; return expect(Credentials.getWskProps()).to.eventually.deep.equal(mockObject); }); it('should overwrite properties files resource variables with environment variables', () => { const mockObject = { apihost: 'blah.blah.com', auth: 'another_user:another_pass', namespace: 'user@user.com', apigw_access_token: 'some_access_token', iam_namespace_api_key: 'some_api_key' }; const wskProps = 'APIHOST=openwhisk.ng.bluemix.net\nNAMESPACE=blah@provider.com_dev\nAUTH=user:pass\nAPIGW_ACCESS_TOKEN=hello\nIAM_NAMESPACE_API_KEY=old_key'; sandbox.stub(fs, 'readFile', (path, encoding, cb) => { cb(null, wskProps); }); process.env.OW_APIHOST = 'blah.blah.com'; process.env.OW_AUTH = 'another_user:another_pass'; process.env.OW_NAMESPACE = 'user@user.com'; process.env.OW_APIGW_ACCESS_TOKEN = 'some_access_token'; process.env.OW_IAM_NAMESPACE_API_KEY = 'some_api_key'; return expect(Credentials.getWskProps()).to.eventually.deep.equal(mockObject); }); }); ================================================ FILE: provider/tests/index.js ================================================ 'use strict'; require('./openwhiskProvider'); require('./credentials'); require('./cliTokenManager'); ================================================ FILE: provider/tests/openwhiskProvider.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const openwhisk = require('openwhisk'); const chaiAsPromised = require('chai-as-promised'); const BbPromise = require('bluebird'); require('chai').use(chaiAsPromised); const OpenwhiskProvider = require('../openwhiskProvider'); const Credentials = require('../credentials'); const CliTokenManager = require('../cliTokenManager.js'); describe('OpenwhiskProvider', () => { let openwhiskProvider; let serverless; let sandbox; const options = { stage: 'dev', region: 'us-east-1', }; beforeEach(() => { sandbox = sinon.sandbox.create(); const CLI = function () { this.log = function () {};}; const serverless = {setProvider: () => {}, config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => ({}), provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; openwhiskProvider = new OpenwhiskProvider(serverless, options); openwhiskProvider.serverless.cli = new serverless.classes.CLI(); }); afterEach(() => { sandbox.restore(); }); describe('#getProviderName()', () => { it('should return the provider name', () => { expect(OpenwhiskProvider.getProviderName()).to.equal('openwhisk'); }); }); describe('#constructor()', () => { it('should set Serverless instance', () => { expect(typeof openwhiskProvider.serverless).to.not.equal('undefined'); }); it('should set OpenWhisk instance', () => { expect(typeof openwhiskProvider.sdk).to.not.equal('undefined'); }); it('should set the provider property', () => { expect(openwhiskProvider.provider).to.equal(openwhiskProvider); }); }); describe('#client()', () => { it('should return pre-configured openwhisk client', () => { openwhiskProvider._client = null const creds = {apihost: 'some_api', auth: 'user:pass'} sandbox.stub(openwhiskProvider, "props").returns(BbPromise.resolve(creds)) return openwhiskProvider.client().then(client => { expect(client.actions.client.options).to.be.deep.equal({apigwToken: undefined, apigwSpaceGuid: undefined, namespace: undefined, apiKey: creds.auth, ignoreCerts: false, apiVersion: 'v1', cert: undefined, key: undefined, api: `https://${creds.apihost}/api/v1/`, authHandler: undefined, noUserAgent: undefined}) expect(typeof openwhiskProvider._client).to.not.equal('undefined'); }) }) it('should allow ignore_certs options for openwhisk client', () => { openwhiskProvider._client = null const creds = {apihost: 'some_api', auth: 'user:pass'} sandbox.stub(openwhiskProvider, "props").returns(BbPromise.resolve(creds)) openwhiskProvider.serverless.service.provider.ignore_certs = true return openwhiskProvider.client().then(client => { expect(client.actions.client.options).to.be.deep.equal({apigwToken: undefined, apigwSpaceGuid: undefined, namespace: undefined, apiKey: creds.auth, ignoreCerts: true, apiVersion: 'v1', cert: undefined, key: undefined, api: `https://${creds.apihost}/api/v1/`, authHandler: undefined, noUserAgent: undefined}) expect(typeof openwhiskProvider._client).to.not.equal('undefined'); }) }) it('should allow apigw_access_token option for openwhisk client', () => { openwhiskProvider._client = null const creds = {apihost: 'some_api', auth: 'user:pass', apigw_access_token: 'token'} sandbox.stub(openwhiskProvider, "props").returns(BbPromise.resolve(creds)) return openwhiskProvider.client().then(client => { expect(client.actions.client.options).to.be.deep.equal({apigwToken: 'token', apigwSpaceGuid: 'user', namespace: undefined, apiKey: creds.auth, ignoreCerts: false, apiVersion: 'v1', cert: undefined, key: undefined, api: `https://${creds.apihost}/api/v1/`, authHandler: undefined, noUserAgent: undefined}) expect(typeof openwhiskProvider._client).to.not.equal('undefined'); }) }) it('should cache client instance', () => { openwhiskProvider._client = {} return openwhiskProvider.client().then(client => { expect(client).to.be.equal(openwhiskProvider._client) }) }) it('should support client auth using IBM Cloud IAM API key', () => { openwhiskProvider._client = null const API_KEY = 'some-key-value'; const creds = {iam_namespace_api_key: API_KEY, apihost: 'some_api', namespace: 'a34dd39e-e3de-4160-bbab-59ac345678ed'} sandbox.stub(openwhiskProvider, "props").returns(BbPromise.resolve(creds)) return openwhiskProvider.client().then(client => { expect(client.actions.client.options.namespace).to.be.deep.equal(creds.namespace) expect(client.actions.client.options.api).to.be.deep.equal(`https://${creds.apihost}/api/v1/`) expect(typeof client.actions.client.options.authHandler).to.not.equal('undefined') expect(client.actions.client.options.authHandler.iamApikey).to.be.deep.equal(API_KEY) }) }) it('should support client auth using IBM Cloud CLI configuration file', () => { openwhiskProvider._client = null const API_KEY = 'some-key-value'; const creds = {apihost: 'region.functions.cloud.ibm.com', namespace: 'a34dd39e-e3de-4160-bbab-59ac345678ed'} sandbox.stub(openwhiskProvider, "props").returns(BbPromise.resolve(creds)) return openwhiskProvider.client().then(client => { expect(client.actions.client.options.namespace).to.be.deep.equal(creds.namespace) expect(client.actions.client.options.api).to.be.deep.equal(`https://${creds.apihost}/api/v1/`) expect(client.actions.client.options.authHandler instanceof CliTokenManager).to.be.equal(true) }) }) }) describe('#props()', () => { it('should return promise that resolves with provider credentials', () => { openwhiskProvider._props = null const creds = {apihost: 'some_api', auth: 'user:pass', namespace: 'namespace'} sandbox.stub(Credentials, "getWskProps").returns(BbPromise.resolve(creds)) return openwhiskProvider.props().then(props => { expect(props).to.be.deep.equal({auth: creds.auth, namespace: creds.namespace, apihost: creds.apihost}) expect(typeof openwhiskProvider._props).to.not.equal('undefined'); }) }); it('should return cached provider credentials', () => { openwhiskProvider._props = {} const stub = sandbox.stub(Credentials, "getWskProps") return openwhiskProvider.props().then(props => { expect(props).to.be.equal(openwhiskProvider._props) expect(stub.called).to.be.equal(false) }) }); it('should reject promise when getWskProps rejects', () => { sandbox.stub(Credentials, "getWskProps").returns(BbPromise.reject()) return expect(openwhiskProvider.props()).to.eventually.be.rejected; }); }); describe('#hasValidCreds()', () => { it('should throw error when parameter (AUTH) is missing', () => { const mockObject = { apihost: 'blah.blah.com', namespace: 'user@user.com', }; return expect(() => openwhiskProvider.hasValidCreds(mockObject)).to.throw(/OW_AUTH/); }); it('should throw error when parameter (APIHOST) is missing', () => { const mockObject = { auth: 'user:pass', namespace: 'user@user.com', }; return expect(() => openwhiskProvider.hasValidCreds(mockObject)).to.throw(/OW_APIHOST/); }); }) }) ================================================ FILE: remove/README.md ================================================ # Remove This plugin removes the Action from OpenWhisk. ## How it works `Remove` hooks into the [`remove:remove`](/lib/plugins/remove) lifecycle. It will send the HTTP DELETE request to the Action endpoint to trigger the function removal. ================================================ FILE: remove/index.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const validate = require('./lib/validate'); const removePackages = require('./lib/removePackages'); const removeFunctions = require('./lib/removeFunctions'); const removeTriggers = require('./lib/removeTriggers'); const removeRules = require('./lib/removeRules'); const removeFeeds = require('./lib/removeFeeds'); const removeRoutes = require('./lib/removeRoutes'); const setupResources = require('./lib/setupResources'); const util = require('./lib/util'); class OpenWhiskRemove { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.provider = this.serverless.getProvider('openwhisk'); Object.assign(this, validate, setupResources, removePackages, removeFunctions, removeTriggers, removeRules, removeFeeds, removeRoutes, util); this.hooks = { 'remove:remove': () => BbPromise.bind(this) .then(this.validate) .then(this.setupResources) .then(this.removeRoutes) .then(this.removeRules) .then(this.removeFunctions) .then(this.removePackages) .then(this.removeTriggers) .then(this.removeFeeds) .then(() => this.serverless.cli.log('Resource removal successful!')), }; } } module.exports = OpenWhiskRemove; ================================================ FILE: remove/lib/removeFeeds.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { removeFeed (feed) { const onProvider = ow => ow.feeds.delete(feed); const errMsgTemplate = `Failed to remove feed (${feed.feedName}) due to error:` return this.handleOperationFailure(onProvider, errMsgTemplate); }, removeTriggerFeed(triggerName, params) { return this.provider.props().then(props => { const triggerNamespace = params.namespace || `${props.namespace}`; const trigger = `/${triggerNamespace}/${triggerName}`; // split feed identifier into namespace & name const feedPathParts = params.feed.split('/').filter(i => i); const namespace = feedPathParts.splice(0, 1).join(); const feedName = feedPathParts.join('/'); return this.removeFeed({ trigger, feedName, namespace }); }); }, removeFeeds() { const resources = this.serverless.service.resources; if (!resources || !resources.triggers) { return BbPromise.resolve(); } const triggers = resources.triggers; const feeds = Object.keys(triggers).filter(t => triggers[t].feed) if (feeds.length) { this.serverless.cli.log('Removing Feeds...'); } return BbPromise.all( feeds.map(t => this.removeTriggerFeed(t, triggers[t])) ); }, }; ================================================ FILE: remove/lib/removeFunctions.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { removeFunctionHandler(functionHandler) { const onProvider = ow => ow.actions.delete(functionHandler); const errMsgTemplate = `Failed to delete function service (${functionHandler.actionName}) due to error:`; return this.handleOperationFailure(onProvider, errMsgTemplate); }, removeFunction(functionName) { const functionObject = this.serverless.service.getFunction(functionName); const FunctionHandler = {}; FunctionHandler.actionName = functionObject.name || `${this.serverless.service.service}_${functionName}`; if (functionObject.namespace) { FunctionHandler.namespace = functionObject.namespace; } return this.removeFunctionHandler(FunctionHandler); }, removeFunctions() { this.serverless.cli.log('Removing Functions...'); return BbPromise.all( this.serverless.service.getAllFunctions().map(f => this.removeFunction(f)) ); }, }; ================================================ FILE: remove/lib/removePackages.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { removePackageHandler(pkge) { const onProvider = ow => ow.packages.delete(pkge); const errMsgTemplate = `Failed to delete package (${pkge.name}) due to error:`; return this.handleOperationFailure(onProvider, errMsgTemplate); }, removePackage(name) { const packageObject = this.serverless.service.resources.packages[name]; const pkge = { name }; if (packageObject.namespace) { pkge.namespace = packageObject.namespace; } return this.removePackageHandler(pkge); }, removePackages() { const resources = this.serverless.service.resources; if (!resources || !resources.packages) { return BbPromise.resolve(); } const packages = Object.keys(resources.packages) if (packages.length) { this.serverless.cli.log('Removing Packages...'); } return BbPromise.all( packages.map(p => this.removePackage(p)) ); } }; ================================================ FILE: remove/lib/removeRoutes.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { hasRoutes() { return this.serverless.service.getAllFunctions() .map(f => this.serverless.service.getFunction(f)) .some(fnObj => { const events = fnObj.events || [] return events.some(event => event.http) }) }, basePath() { const resources = this.serverless.service.resources || {} const options = resources.apigw || {} return options.basepath || "/" }, removeRoutes() { if (!this.hasRoutes()) { return Promise.resolve(); } this.serverless.cli.log('Removing API Gateway definitions...'); const basepath = this.basePath() const onProvider = ow => ow.routes.delete({ basepath }); const errMsgTemplate = `Failed to unbind API Gateway routes (${basepath}) due to error:`; return this.handleOperationFailure(onProvider, errMsgTemplate); } }; ================================================ FILE: remove/lib/removeRules.js ================================================ 'use strict'; const BbPromise = require('bluebird'); const Util = require('./util.js'); module.exports = { removeRule (ruleName) { const onProvider = ow => ow.rules.delete({ ruleName }); const errMsgTemplate = `Failed to delete rule (${ruleName}) due to error:`; return this.handleOperationFailure(onProvider, errMsgTemplate); }, removeRules() { if (this.serverless.service.rules.length) { this.serverless.cli.log('Removing Rules...'); } return BbPromise.all( this.serverless.service.rules.map(r => this.removeRule(r)) ); } }; ================================================ FILE: remove/lib/removeTriggers.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { removeTriggerHandler(Trigger) { const onProvider = ow => ow.triggers.delete(Trigger); const errMsgTemplate = `Failed to delete event trigger (${Trigger.triggerName}) due to error:`; return this.handleOperationFailure(onProvider, errMsgTemplate); }, removeTrigger(triggerName) { const triggerObject = this.serverless.service.resources.triggers[triggerName]; const Trigger = { triggerName }; if (triggerObject.namespace) { Trigger.namespace = triggerObject.namespace; } return this.removeTriggerHandler(Trigger); }, removeTriggers() { const resources = this.serverless.service.resources; if (!resources || !resources.triggers) { return BbPromise.resolve(); } const triggers = Object.keys(resources.triggers) if (triggers.length) { this.serverless.cli.log('Removing Triggers...'); } return BbPromise.all( triggers.map(t => this.removeTrigger(t)) ); } }; ================================================ FILE: remove/lib/setupResources.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { initializeTriggers () { if (!this.serverless.service.resources) { this.serverless.service.resources = {}; } if (!this.serverless.service.resources.triggers) { this.serverless.service.resources.triggers = {}; } const triggers = this.getEventTriggers() const manifestTriggers = this.serverless.service.resources.triggers || {}; triggers.forEach(trigger => { manifestTriggers[trigger] = manifestTriggers[trigger] || {} }) }, getEventTriggers() { const triggers = new Set(); this.serverless.service.getAllFunctions() .forEach(name => { const func = this.serverless.service.getFunction(name); const events = func.events || [] events.forEach(event => { if (event.schedule) { triggers.add(event.schedule.name || `${this.serverless.service.service}_${name}_schedule_trigger`) } else if (event.message_hub) { triggers.add(event.message_hub.name || `${this.serverless.service.service}_${name}_messagehub_${event.message_hub.topic}`) } else if (event.cloudant) { triggers.add(event.cloudant.name || `${this.serverless.service.service}_${name}_cloudant_${event.cloudant.db}`) } else if (event.trigger) { triggers.add(event.trigger.name || event.trigger) } }) }) return [...triggers]; }, initializeRules() { const allFunctions = this.serverless.service.getAllFunctions() const rules = allFunctions.map( functionName => this.getRuleNames(functionName, this.serverless.service.getFunction(functionName)) ).reduce((a, b) => a.concat(b), []) this.serverless.service.rules = rules; }, getRuleName(funcName, funcObj, trigger) { const defaultRuleName = this.generateDefaultRuleName(funcName, trigger); if (typeof trigger === 'string') { return defaultRuleName } return trigger.rule || defaultRuleName; }, getScheduleRuleName(funcName, funcObj, schedule) { return schedule.rule || `${this.serverless.service.service}_${funcName}_schedule_rule` }, getMessageHubRuleName(funcName, funcObj, config) { return config.rule || `${this.serverless.service.service}_${funcName}_messagehub_${config.topic}_rule` }, getCloudantRuleName(funcName, funcObj, config) { return config.rule || `${this.serverless.service.service}_${funcName}_cloudant_${config.db}_rule` }, getRuleNames(functionName, functionObject) { if (!functionObject.events) return [] const triggerRules = functionObject.events .filter(e => e.trigger) .map(e => this.getRuleName(functionName, functionObject, e.trigger)) const scheduleRules = functionObject.events .filter(e => e.schedule) .map(e => this.getScheduleRuleName(functionName, functionObject, e.schedule)) const messageHubRules = functionObject.events .filter(e => e.message_hub) .map(e => this.getMessageHubRuleName(functionName, functionObject, e.message_hub)) const cloudantRules = functionObject.events .filter(e => e.cloudant) .map(e => this.getCloudantRuleName(functionName, functionObject, e.cloudant)) return triggerRules.concat(scheduleRules, messageHubRules, cloudantRules) }, generateDefaultRuleName(functionName, triggerName) { return `${this.serverless.service.service}_${triggerName}_to_${functionName}` }, setupResources () { this.serverless.cli.log('Setting up resources...'); this.initializeRules(); this.initializeTriggers(); return BbPromise.resolve(); } }; ================================================ FILE: remove/lib/util.js ================================================ 'use strict'; module.exports = { handleOperationFailure (onProvider, errMsgTemplate) { return new Promise((resolve, reject) => { this.provider.client().then(onProvider).then(resolve).catch(err => { this.serverless.cli.log(`${errMsgTemplate}: ${err.message}`); resolve(); }); }) } }; ================================================ FILE: remove/lib/validate.js ================================================ 'use strict'; const BbPromise = require('bluebird'); module.exports = { validate() { if (!this.serverless.config.servicePath) { throw new this.serverless.classes.Error('This command can only be run inside a service.'); } this.options.stage = this.options.stage || (this.serverless.service.provider && this.serverless.service.provider.stage) || 'dev'; this.options.region = this.options.region || (this.serverless.service.provider && this.serverless.service.provider.region) || 'us-east-1'; return BbPromise.resolve(); }, }; ================================================ FILE: remove/tests/all.js ================================================ 'use strict'; require('./index'); require('./removePackages'); require('./removeFunctions'); require('./removeTriggers'); require('./removeRules'); require('./removeRoutes'); require('./removeFeeds'); require('./setupResources'); ================================================ FILE: remove/tests/index.js ================================================ 'use strict'; const expect = require('chai').expect; const BbPromise = require('bluebird'); const sinon = require('sinon'); const OpenWhiskRemove = require('../'); describe('OpenWhiskRemove', () => { const options = { stage: 'dev', region: 'us-east-1', }; const CLI = function () { this.log = function () {};}; const serverless = {setProvider: () => {}, config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => ({}), provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; const openwhiskRemove = new OpenWhiskRemove(serverless, options); openwhiskRemove.serverless.cli = new serverless.classes.CLI(); describe('#constructor()', () => { it('should have hooks', () => expect(openwhiskRemove.hooks).to.be.not.empty); it('should have access to the serverless instance', () => { expect(openwhiskRemove.serverless).to.deep.equal(serverless); }); it('should run promise chain in order', () => { const validateStub = sinon .stub(openwhiskRemove, 'validate').returns(BbPromise.resolve()); sinon.stub(openwhiskRemove, 'removePackages').returns(BbPromise.resolve()); sinon.stub(openwhiskRemove, 'removeFunctions').returns(BbPromise.resolve()); sinon.stub(openwhiskRemove, 'removeTriggers').returns(BbPromise.resolve()); sinon.stub(openwhiskRemove, 'removeRules').returns(BbPromise.resolve()); sinon.stub(openwhiskRemove, 'removeRoutes').returns(BbPromise.resolve()); return openwhiskRemove.hooks['remove:remove']() .then(() => { expect(validateStub.calledOnce).to.be.equal(true); openwhiskRemove.validate.restore(); openwhiskRemove.removePackages.restore(); openwhiskRemove.removeFunctions.restore(); openwhiskRemove.removeRoutes.restore(); openwhiskRemove.removeTriggers.restore(); openwhiskRemove.removeRules.restore(); }); }); }); }); ================================================ FILE: remove/tests/removeFeeds.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const OpenWhiskRemove = require('../index'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('OpenWhiskRemove', () => { const CLI = function () { this.log = function () {};}; const serverless = {setProvider: () => {}, config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => ({}), provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; let openwhiskRemove; let sandbox; const mockFeedObject = { feedName: 'someFeed', }; beforeEach(() => { const options = { stage: 'dev', region: 'us-east-1', }; openwhiskRemove = new OpenWhiskRemove(serverless, options); openwhiskRemove.serverless.cli = new serverless.classes.CLI(); openwhiskRemove.serverless.service.service = 'helloworld'; openwhiskRemove.provider = {props: () => {}, client: () => {}}; process.env.OW_NAMESPACE = 'default'; sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); delete process.env.OW_NAMESPACE; }); describe('#removeFeeds()', () => { it('should call removeFeed for each trigger feed', () => { const stub = sandbox.stub(openwhiskRemove, 'removeTriggerFeed', () => Promise.resolve()); const triggers = { first: { feed: mockFeedObject }, second: { feed: mockFeedObject }, third: {}, }; openwhiskRemove.serverless.service.resources = { triggers }; return openwhiskRemove.removeFeeds().then(() => { expect(stub.calledTwice).to.be.equal(true); expect(stub.calledWith('first', triggers.first)).to.be.equal(true); expect(stub.calledWith('second', triggers.second)).to.be.equal(true); }); }); }); describe('#removeTriggerFeed()', () => { it('should call removeFeed with correct feed parameters', () => { const stub = sandbox.stub(openwhiskRemove, 'removeFeed', () => Promise.resolve()); sandbox.stub(openwhiskRemove.provider, 'props', () => Promise.resolve({namespace: 'default'})); const trigger = { feed: '/whisk.system/alarms/alarm', feed_parameters: { a: 1 } }; const feed = { feedName: 'alarms/alarm', namespace: 'whisk.system', trigger: '/default/myTrigger' }; return openwhiskRemove.removeTriggerFeed('myTrigger', trigger).then(() => { expect(stub.calledOnce).to.be.equal(true); expect(stub.calledWith(feed)).to.be.equal(true); }); }); it('should call removeFeed with custom trigger namespace', () => { sandbox.stub(openwhiskRemove.provider, 'props', () => Promise.resolve({})); const stub = sandbox.stub(openwhiskRemove, 'removeFeed', () => Promise.resolve()); const trigger = { namespace: 'custom', feed: '/whisk.system/alarms/alarm', feed_parameters: { a: 1 } }; const feed = { feedName: 'alarms/alarm', namespace: 'whisk.system', trigger: '/custom/myTrigger' }; return openwhiskRemove.removeTriggerFeed('myTrigger', trigger).then(() => { expect(stub.calledOnce).to.be.equal(true); expect(stub.calledWith(feed)).to.be.equal(true); }); }); }); describe('#removeFeed()', () => { it('should remove feed from openwhisk', () => { sandbox.stub(openwhiskRemove.provider, 'client', () => { const stub = params => { expect(params).to.be.deep.equal({ feedName: 'some_feed', namespace: 'test', trigger: 'myTrigger', }); return Promise.resolve(); }; return Promise.resolve({ feeds: { delete: stub } }); }); return expect(openwhiskRemove.removeFeed( { feedName: 'some_feed', namespace: 'test', trigger: 'myTrigger' } )).to.eventually.be.fulfilled; }); it('should reject when feed removal fails to be removed with error message', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskRemove.provider, 'client', () => Promise.resolve( { feeds: { delete: () => Promise.reject(err) } } )); const log = sandbox.stub(openwhiskRemove.serverless.cli, "log"); const result = openwhiskRemove.removeFeed({feedName: 'test'}).then(() => { expect(log.called).to.be.equal(true); expect(log.args[0][0].match(/Failed to remove feed \(test\)/)).to.be.ok; }) return expect(result).to.eventually.be.fulfilled; }); }); }); ================================================ FILE: remove/tests/removeFunctions.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const OpenWhiskRemove = require('../index'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('OpenWhiskRemove', () => { const CLI = function () { this.log = function () {};}; const serverless = {setProvider: () => {}, config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => ({}), provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; let openwhiskRemove; let sandbox; const mockFunctionObject = { actionName: 'serviceName_functionName', namespace: 'namespace', }; beforeEach(() => { const options = { stage: 'dev', region: 'us-east-1', }; openwhiskRemove = new OpenWhiskRemove(serverless, options); openwhiskRemove.serverless.cli = new serverless.classes.CLI(); openwhiskRemove.serverless.service.service = 'helloworld'; openwhiskRemove.provider = {client: () => {}}; sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); }); describe('#removeFunction()', () => { it('should call removeFunctionHandler with default params', () => { const stub = sandbox.stub(openwhiskRemove, 'removeFunctionHandler', () => Promise.resolve()); const retValue = { name: 'name', namespace: 'namespace' }; sandbox.stub(openwhiskRemove.serverless.service, 'getFunction', () => retValue); const functionName = 'testing'; return openwhiskRemove.removeFunction(functionName).then(() => { expect(stub.calledOnce).to.be.equal(true); expect(stub.calledWith({ actionName: 'name', namespace: 'namespace' })).to.be.equal(true); }); }); it('should call removeFunctionHandler without functionObject name or namespace', () => { const stub = sandbox.stub(openwhiskRemove, 'removeFunctionHandler', () => Promise.resolve()); const nothing = {}; sandbox.stub(openwhiskRemove.serverless.service, 'getFunction', () => nothing); const functionName = 'testing'; return openwhiskRemove.removeFunction(functionName).then(() => { expect(stub.calledOnce).to.be.equal(true); expect(stub.calledWith({ actionName: 'helloworld_testing' })).to.be.equal(true); }); }); }); describe('#removeFunctionHandler()', () => { it('should remove function handler from openwhisk', () => { sandbox.stub(openwhiskRemove.provider, 'client', () => { const stub = params => { expect(params).to.be.deep.equal({ actionName: mockFunctionObject.actionName, namespace: mockFunctionObject.namespace, }); return Promise.resolve(); }; return Promise.resolve({ actions: { delete: stub } }); }); return expect(openwhiskRemove.removeFunctionHandler(mockFunctionObject)) .to.eventually.be.fulfilled; }); it('should still resolve when function handler fails to be removed', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskRemove.provider, 'client', () => Promise.resolve( { actions: { delete: () => Promise.reject(err) } } )); const log = sandbox.stub(openwhiskRemove.serverless.cli, "log"); const result = openwhiskRemove.removeFunctionHandler(mockFunctionObject).then(() => { expect(log.called).to.be.equal(true); expect(log.args[0][0].match(/Failed to delete function service \(serviceName_functionName\)/)).to.be.ok; }) return expect(result).to.eventually.be.fulfilled; }); }); }); ================================================ FILE: remove/tests/removePackages.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const OpenWhiskRemove = require('../index'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('OpenWhiskRemove', () => { const CLI = function () { this.log = function () {};}; const serverless = {setProvider: () => {}, config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => ({}), provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; let openwhiskRemove; let sandbox; const mockPackageObject = { name: 'somePackage', namespace: 'namespace', }; beforeEach(() => { const options = { stage: 'dev', region: 'us-east-1', }; openwhiskRemove = new OpenWhiskRemove(serverless, options); openwhiskRemove.serverless.cli = new serverless.classes.CLI(); openwhiskRemove.serverless.service.service = 'helloworld'; openwhiskRemove.provider = {client: () => {}}; sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); }); describe('#removePackage()', () => { it('should call removePackageHandler with default params', () => { const stub = sandbox.stub(openwhiskRemove, 'removePackageHandler', () => Promise.resolve()); const packages = { myPackage: {} }; openwhiskRemove.serverless.service.resources = { packages }; const name = 'myPackage'; return openwhiskRemove.removePackage(name).then(() => { expect(stub.calledOnce).to.be.equal(true); expect(stub.calledWith({ name: 'myPackage' })).to.be.equal(true); }); }); it('should call removePackageHandler with custom namespace', () => { const stub = sandbox.stub(openwhiskRemove, 'removePackageHandler', () => Promise.resolve()); const packages = { myPackage: { namespace: 'myNamespace' } }; openwhiskRemove.serverless.service.resources = { packages }; const name = 'myPackage'; return openwhiskRemove.removePackage(name).then(() => { expect(stub.calledOnce).to.be.equal(true); expect(stub.calledWith({ name: 'myPackage', namespace: 'myNamespace' })) .to.be.equal(true); }); }); }); describe('#removeFunctionHandler()', () => { it('should remove function handler from openwhisk', () => { sandbox.stub(openwhiskRemove.provider, 'client', () => { const stub = params => { expect(params).to.be.deep.equal({ name: mockPackageObject.name, namespace: mockPackageObject.namespace, }); return Promise.resolve(); }; return Promise.resolve({ packages: { delete: stub } }); }); return expect(openwhiskRemove.removePackageHandler(mockPackageObject)) .to.eventually.be.fulfilled; }); it('should resolve even when function handler fails to be removed', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskRemove.provider, 'client', () => Promise.resolve( { packages: { delete: () => Promise.reject(err) } } )); const log = sandbox.stub(openwhiskRemove.serverless.cli, "log"); const result = openwhiskRemove.removePackageHandler(mockPackageObject).then(() => { expect(log.called).to.be.equal(true); expect(log.args[0][0].match(/Failed to delete package \(somePackage\)/)).to.be.ok; }) return expect(result).to.eventually.be.fulfilled; }); }); }); ================================================ FILE: remove/tests/removeRoutes.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const OpenWhiskRemove = require('../index'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('OpenWhiskRemove', () => { const CLI = function () { this.log = function () {};}; const serverless = {setProvider: () => {}, config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => ({}), provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; let openwhiskRemove; let sandbox; const mockFunctionObject = { actionName: 'serviceName_functionName', namespace: 'namespace', }; beforeEach(() => { const options = { stage: 'dev', region: 'us-east-1', }; openwhiskRemove = new OpenWhiskRemove(serverless, options); openwhiskRemove.serverless.cli = new serverless.classes.CLI(); openwhiskRemove.serverless.service.service = 'helloworld'; openwhiskRemove.provider = {client: () => {}}; sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); }); describe('#removeRoutes()', () => { it('should not remove routes when http events not defined.', () => { const fnDefs = { none: { }, has_event: { events: [{trigger: true}, {schedule: true}] } } openwhiskRemove.serverless.service.getAllFunctions = () => Object.keys(fnDefs) openwhiskRemove.serverless.service.getFunction = (name) => fnDefs[name] const stub = sinon.stub().returns(Promise.resolve()) sandbox.stub(openwhiskRemove.provider, 'client', () => { return Promise.resolve({ routes: { delete: stub } }); }); return openwhiskRemove.removeRoutes().then(() => { expect(stub.called).to.be.equal(false); }) }); it('should remove service api gw routes from openwhisk', () => { const fnDefs = { has_event: { events: [{http: true}] } } openwhiskRemove.serverless.service.getAllFunctions = () => Object.keys(fnDefs) openwhiskRemove.serverless.service.getFunction = (name) => fnDefs[name] const stub = sinon.stub().returns(Promise.resolve()) sandbox.stub(openwhiskRemove.provider, 'client', () => { return Promise.resolve({ routes: { delete: stub } }); }); const result = openwhiskRemove.removeRoutes().then(() => { expect(stub.called).to.be.equal(true); expect(stub.args[0][0]).to.be.deep.equal({ basepath: '/' }); }) return expect(result).to.eventually.be.fulfilled; }); it('should still resolve when api gw routes fail to be removed', () => { const fnDefs = { has_event: { events: [{http: true}] } } openwhiskRemove.serverless.service.getAllFunctions = () => Object.keys(fnDefs) openwhiskRemove.serverless.service.getFunction = (name) => fnDefs[name] const err = { message: 'some reason' }; sandbox.stub(openwhiskRemove.provider, 'client', () => Promise.resolve( { routes: { delete: () => Promise.reject(err) } } )); const log = sandbox.stub(openwhiskRemove.serverless.cli, "log"); const result = openwhiskRemove.removeRoutes().then(() => { expect(log.called).to.be.equal(true); expect(log.args[1][0].match(/Failed to unbind API Gateway routes \(\/\)/)).to.be.ok; }) return expect(result).to.eventually.be.fulfilled; }); }); }); ================================================ FILE: remove/tests/removeRules.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const OpenWhiskRemove = require('../index'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('OpenWhiskRemove', () => { const CLI = function () { this.log = function () {};}; const serverless = {setProvider: () => {}, config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => ({}), provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; let openwhiskRemove; let sandbox; const mockRuleObject = { myRule: 'myTrigger', }; beforeEach(() => { const options = { stage: 'dev', region: 'us-east-1', }; openwhiskRemove = new OpenWhiskRemove(serverless, options); openwhiskRemove.serverless.cli = new serverless.classes.CLI(); openwhiskRemove.serverless.service.service = 'helloworld'; openwhiskRemove.provider = {client: () => {}}; sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); }); describe('#removeRules()', () => { it('should call removeRule for each rule', () => { openwhiskRemove.serverless.service.rules = ["first", "second"] const stub = sandbox.stub(openwhiskRemove, 'removeRule', () => Promise.resolve()); return openwhiskRemove.removeRules().then(() => { expect(stub.calledTwice).to.be.equal(true); expect(stub.calledWith('first')).to.be.equal(true); expect(stub.calledWith('second')).to.be.equal(true); }); }); }); describe('#removeRule()', () => { it('should remove rule handler from openwhisk', () => { sandbox.stub(openwhiskRemove.provider, 'client', () => { const stub = params => { expect(params).to.be.deep.equal({ ruleName: 'myRule', }); return Promise.resolve(); }; return Promise.resolve({ rules: { delete: stub } }); }); return expect(openwhiskRemove.removeRule('myRule')) .to.eventually.be.fulfilled; }); it('should resolve even if function handler fails to be removed', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskRemove.provider, 'client', () => Promise.resolve( { rules: { delete: () => Promise.reject(err) } } )); const log = sandbox.stub(openwhiskRemove.serverless.cli, "log"); const result = openwhiskRemove.removeRule('myRule').then(() => { expect(log.called).to.be.equal(true); expect(log.args[0][0].match(/Failed to delete rule \(myRule\)/)).to.be.ok; }) return expect(result) .to.eventually.be.fulfilled; }); }); }); ================================================ FILE: remove/tests/removeTriggers.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const OpenWhiskRemove = require('../index'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('OpenWhiskRemove', () => { const CLI = function () { this.log = function () {};}; const serverless = {setProvider: () => {}, config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => ({}), provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; let openwhiskRemove; let sandbox; const mockTriggerObject = { triggerName: 'someTrigger', namespace: 'namespace', }; beforeEach(() => { const options = { stage: 'dev', region: 'us-east-1', }; openwhiskRemove = new OpenWhiskRemove(serverless, options); openwhiskRemove.serverless.cli = new serverless.classes.CLI(); openwhiskRemove.serverless.service.service = 'helloworld'; openwhiskRemove.provider = {client: () => {}}; sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); }); describe('#removeTrigger()', () => { it('should call removeTriggerHandler with default params', () => { const stub = sandbox.stub(openwhiskRemove, 'removeTriggerHandler', () => Promise.resolve()); const triggers = { myTrigger: {} }; openwhiskRemove.serverless.service.resources = { triggers }; const triggerName = 'myTrigger'; return openwhiskRemove.removeTrigger(triggerName).then(() => { expect(stub.calledOnce).to.be.equal(true); expect(stub.calledWith({ triggerName: 'myTrigger' })).to.be.equal(true); }); }); it('should call removeTriggerHandler with custom namespace', () => { const stub = sandbox.stub(openwhiskRemove, 'removeTriggerHandler', () => Promise.resolve()); const triggers = { myTrigger: { namespace: 'myNamespace' } }; openwhiskRemove.serverless.service.resources = { triggers }; const triggerName = 'myTrigger'; return openwhiskRemove.removeTrigger(triggerName).then(() => { expect(stub.calledOnce).to.be.equal(true); expect(stub.calledWith({ triggerName: 'myTrigger', namespace: 'myNamespace' })) .to.be.equal(true); }); }); }); describe('#removeFunctionHandler()', () => { it('should remove function handler from openwhisk', () => { sandbox.stub(openwhiskRemove.provider, 'client', () => { const stub = params => { expect(params).to.be.deep.equal({ triggerName: mockTriggerObject.triggerName, namespace: mockTriggerObject.namespace, }); return Promise.resolve(); }; return Promise.resolve({ triggers: { delete: stub } }); }); return expect(openwhiskRemove.removeTriggerHandler(mockTriggerObject)) .to.eventually.be.fulfilled; }); it('should resolve even when function handler fails to be removed', () => { const err = { message: 'some reason' }; sandbox.stub(openwhiskRemove.provider, 'client', () => Promise.resolve( { triggers: { delete: () => Promise.reject(err) } } )); const log = sandbox.stub(openwhiskRemove.serverless.cli, "log"); const result = openwhiskRemove.removeTriggerHandler(mockTriggerObject).then(() => { expect(log.called).to.be.equal(true); expect(log.args[0][0].match(/Failed to delete event trigger \(someTrigger\)/)).to.be.ok; }) return expect(result).to.eventually.be.fulfilled; }); }); }); ================================================ FILE: remove/tests/setupResources.js ================================================ 'use strict'; const expect = require('chai').expect; const sinon = require('sinon'); const OpenWhiskRemove = require('../index'); const chaiAsPromised = require('chai-as-promised'); require('chai').use(chaiAsPromised); describe('OpenWhiskRemove', () => { const CLI = function () { this.log = function () {};}; const serverless = {setProvider: () => {}, config: () => {}, pluginManager: { getPlugins: () => []}, classes: {Error, CLI}, service: {getFunction: () => ({}), provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sinon.spy()}; let openwhiskRemove; let sandbox; beforeEach(() => { const options = { stage: 'dev', region: 'us-east-1', }; openwhiskRemove = new OpenWhiskRemove(serverless, options); openwhiskRemove.serverless.cli = new serverless.classes.CLI(); openwhiskRemove.serverless.service.service = 'helloworld'; openwhiskRemove.provider = {client: () => {}}; sandbox = sinon.sandbox.create(); }); afterEach(() => { sandbox.restore(); }); describe('#initializeRules()', () => { it('should set up empty rules when configuration missing', () => { openwhiskRemove.serverless.service.rules = null; const stub = sandbox.stub(openwhiskRemove.serverless.service, "getAllFunctions").returns([]) openwhiskRemove.initializeRules(); expect(stub.called).to.be.equal(true); expect(openwhiskRemove.serverless.service.rules).to.deep.equal([]); }); it('should set up configured rules', () => { const rules = { "first": {events: [{trigger: 'trigger'}]}, "second": {events: [{trigger: { name: 'trigger', rule: 'rule_name'}}]} } openwhiskRemove.serverless.service.rules = null; sandbox.stub(openwhiskRemove.serverless.service, "getAllFunctions").returns(["first", "second"]) sandbox.stub(openwhiskRemove.serverless.service, "getFunction", id => rules[id]) openwhiskRemove.initializeRules(); expect(openwhiskRemove.serverless.service.rules).to.deep.equal(["helloworld_trigger_to_first", "rule_name"]); }); }); describe('#initializeTriggers()', () => { it('should set up triggers without configured rules', () => { openwhiskRemove.serverless.service.resources.triggers = null sandbox.stub(openwhiskRemove, "getEventTriggers").returns([]) openwhiskRemove.initializeTriggers(); expect(openwhiskRemove.serverless.service.resources.triggers).to.deep.equal({}); }); it('should set up triggers', () => { openwhiskRemove.serverless.service.resources.triggers = {existing: {}}; sandbox.stub(openwhiskRemove, "getEventTriggers").returns(["first", "second"]) openwhiskRemove.initializeTriggers(); expect(openwhiskRemove.serverless.service.resources.triggers).to.deep.equal({existing: {}, first: {}, second: {}}); }); }); }); ================================================ FILE: tests/all.js ================================================ // OpenWhisk Plugins Tests require('../compile/servicebindings/tests'); require('../compile/packages/tests'); require('../compile/triggers/tests'); require('../compile/rules/tests'); require('../compile/apigw/tests'); require('../compile/functions/tests'); require('../compile/functions/runtimes/tests/all.js'); require('../compile/schedule/tests'); require('../compile/message_hub/tests'); require('../compile/cloudant/tests'); require('../configCredentials/tests'); require('../deploy/tests'); require('../deploy/tests/all'); require('../deployFunction/tests'); require('../invoke/tests'); require('../invokeLocal/tests'); require('../provider/tests'); require('../remove/tests'); require('../remove/tests/all'); require('../logs/tests'); require('../info/tests'); ================================================ FILE: tools/travis/build.sh ================================================ #!/bin/bash SCRIPTDIR=$(cd $(dirname "$0") && pwd) HOMEDIR="$SCRIPTDIR/../../../" # Start Serverless related runs ------------- cd $HOMEDIR; touch $HOMEDIR./serverless-openwhisk/serverless.yml; echo $'provider: \n name: openwhisk \n ignore_certs: true \n' > $HOMEDIR./serverless-openwhisk/serverless.yml; cat $HOMEDIR./serverless-openwhisk/serverless.yml; npm install --global serverless $HOMEDIR./serverless-openwhisk cd $HOMEDIR./serverless-openwhisk npm run test exitstatus=$? rm $HOMEDIR/./serverless-openwhisk/serverless.yml exit $exitstatus ================================================ FILE: tools/travis/setup.sh ================================================ #!/bin/bash SCRIPTDIR=$(cd $(dirname "$0") && pwd) HOMEDIR="$SCRIPTDIR/../../../" # install node and npm sudo apt-get -y install nodejs npm npm install -g codecov npm install ================================================ FILE: utils/index.js ================================================ 'use strict'; function formatApiHost(apihost) { if (apihost && !(apihost.startsWith('http://') || apihost.startsWith('https://'))) { // assume https unless explicitly declared return `https://${apihost}`; } else { return apihost; } } module.exports = { formatApiHost };