From 6f60f3eef840e483cd643bf2eb711d8d16596b0c Mon Sep 17 00:00:00 2001 From: Gary Yeap Date: Fri, 14 Sep 2018 14:28:04 +0800 Subject: [PATCH] Add v2 interface support for nodes env (#836) --- environments/nodejs/builder/Dockerfile | 11 +++ environments/nodejs/builder/build.sh | 3 + environments/nodejs/package.json | 8 +- environments/nodejs/server.js | 105 ++++++++++++++++--------- examples/nodejs/README.md | 95 +++++++++++----------- examples/nodejs/README_V1.md | 105 +++++++++++++++++++++++++ examples/nodejs/hello-callback.js | 2 +- examples/nodejs/index.js | 1 + examples/nodejs/multi-entry.js | 13 +++ examples/nodejs/package.json | 30 +++++++ examples/nodejs/stock.js | 41 ---------- 11 files changed, 289 insertions(+), 125 deletions(-) create mode 100644 environments/nodejs/builder/Dockerfile create mode 100755 environments/nodejs/builder/build.sh create mode 100644 examples/nodejs/README_V1.md create mode 100644 examples/nodejs/index.js create mode 100644 examples/nodejs/multi-entry.js create mode 100644 examples/nodejs/package.json delete mode 100644 examples/nodejs/stock.js diff --git a/environments/nodejs/builder/Dockerfile b/environments/nodejs/builder/Dockerfile new file mode 100644 index 00000000..e99cc6d4 --- /dev/null +++ b/environments/nodejs/builder/Dockerfile @@ -0,0 +1,11 @@ +ARG BUILDER_IMAGE=fission/builder +FROM ${BUILDER_IMAGE} +# default variant is the official alpine node image (much smaller than the standard image) +FROM node:8-alpine + +ARG NODE_ENV +ENV NODE_ENV $NODE_ENV + +COPY --from=0 /builder /builder +ADD build.sh /usr/local/bin/build +RUN chmod +x /usr/local/bin/build diff --git a/environments/nodejs/builder/build.sh b/environments/nodejs/builder/build.sh new file mode 100755 index 00000000..af57903a --- /dev/null +++ b/environments/nodejs/builder/build.sh @@ -0,0 +1,3 @@ +#!/bin/sh +cd ${SRC_PKG} +npm install && cp -r ${SRC_PKG} ${DEPLOY_PKG} diff --git a/environments/nodejs/package.json b/environments/nodejs/package.json index 0bddaa6c..2d9ebb4f 100644 --- a/environments/nodejs/package.json +++ b/environments/nodejs/package.json @@ -6,6 +6,10 @@ { "name": "Soam Vasani", "email": "soamvasani@platform9.com" + }, + { + "name": "Gary Yeap", + "email": "contact@garyyeap.com" } ], "description": "NodeJS run container for the fission framework", @@ -20,7 +24,7 @@ "morgan": "*", "mz": "~2.7.0", "request": "^2.81.0", - "request-promise-native": "^1.0.3", - "underscore": ">=1.8.3" + "underscore": ">=1.8.3", + "request-promise-native": "^1.0.3" } } diff --git a/environments/nodejs/server.js b/environments/nodejs/server.js index 4bb00ed2..55f77975 100644 --- a/environments/nodejs/server.js +++ b/environments/nodejs/server.js @@ -7,56 +7,86 @@ const express = require('express'); const app = express(); const bodyParser = require('body-parser'); const morgan = require('morgan'); +const argv = require('minimist')(process.argv.slice(1));// Command line opts -// Command line opts -const argv = require('minimist')(process.argv.slice(1)); -if (!argv.codepath) { - argv.codepath = "/userfunc/user"; - console.log("Codepath defaulting to ", argv.codepath); -} if (!argv.port) { - console.log("Port defaulting to 8888"); argv.port = 8888; } - -// Node resolves module paths according to a file's location. We load -// the file from argv.codepath, but tell users to put dependencies in -// the server's package.json; this means the function's dependencies -// are in /usr/src/app/node_modules. We could be smarter and have the -// function deps in the right place in argv.codepath; but for now we -// just symlink the function's node_modules to the server's -// node_modules. -fs.symlinkSync('/usr/src/app/node_modules', `${path.dirname(argv.codepath)}/node_modules`); - // User function. Starts out undefined. let userFunction; -// -// Specialize this server to a given user function. The user function -// is read from argv.codepath; it's expected to be placed there by the -// fission runtime. -// -function specialize(req, res) { - // Make sure we're a generic container. (No reuse of containers. - // Once specialized, the container remains specialized.) - if (userFunction) { - res.status(400).send("Not a generic container"); - return; - } - +function loadFunction(modulepath, funcname) { // Read and load the code. It's placed there securely by the fission runtime. try { - var startTime = process.hrtime(); - userFunction = require(argv.codepath); - var elapsed = process.hrtime(startTime); + let startTime = process.hrtime(); + // support v1 codepath and v2 entrypoint like 'foo', '', 'index.hello' + let userFunction = funcname ? require(modulepath)[funcname] : require(modulepath); + let elapsed = process.hrtime(startTime); console.log(`user code loaded in ${elapsed[0]}sec ${elapsed[1]/1000000}ms`); + return userFunction; } catch(e) { console.error(`user code load error: ${e}`); - res.status(500).send(JSON.stringify(e)); - return; + return e; + } +} + +function withEnsureGeneric(func) { + return function(req, res) { + // Make sure we're a generic container. (No reuse of containers. + // Once specialized, the container remains specialized.) + if (userFunction) { + res.status(400).send("Not a generic container"); + return; + } + + func(req, res); + } +} + +function isFunction(func) { + return func && func.constructor && func.call && func.apply; +} + +function specializeV2(req, res) { + // for V2 entrypoint, 'filename.funcname' => ['filename', 'funcname'] + const entrypoint = req.body.functionName ? req.body.functionName.split('.') : []; + // for V2, filepath is dynamic path + const modulepath = path.join(req.body.filepath, entrypoint[0] || ''); + const result = loadFunction(modulepath, entrypoint[1]); + + if(isFunction(result)){ + userFunction = result; + res.status(202).send(); + } else { + res.status(500).send(JSON.stringify(result)); + } +} + +function specialize(req, res) { + // Specialize this server to a given user function. The user function + // is read from argv.codepath; it's expected to be placed there by the + // fission runtime. + // + const modulepath = argv.codepath || '/userfunc/user'; + + // Node resolves module paths according to a file's location. We load + // the file from argv.codepath, but tell users to put dependencies in + // the server's package.json; this means the function's dependencies + // are in /usr/src/app/node_modules. We could be smarter and have the + // function deps in the right place in argv.codepath; b ut for now we + // just symlink the function's node_modules to the server's + // node_modules. + fs.symlinkSync('/usr/src/app/node_modules', `${path.dirname(modulepath)}/node_modules`); + + const result = loadFunction(modulepath); + + if(isFunction(result)){ + userFunction = result; + res.status(202).send(); + } else { + res.status(500).send(JSON.stringify(result)); } - res.status(202).send(); } @@ -68,7 +98,8 @@ app.use(bodyParser.json()); app.use(bodyParser.raw()); app.use(bodyParser.text({ type : "text/*" })); -app.post('/specialize', specialize); +app.post('/specialize', withEnsureGeneric(specialize)); +app.post('/v2/specialize', withEnsureGeneric(specializeV2)); // Generic route -- all http requests go to the user function. app.all('/', function (req, res) { diff --git a/examples/nodejs/README.md b/examples/nodejs/README.md index 11a2533a..31298bfc 100644 --- a/examples/nodejs/README.md +++ b/examples/nodejs/README.md @@ -1,19 +1,19 @@ # Fission Node.js Examples +This is V2 example, check [here](README_V1.md) for V1. + This directory contains several examples to get you started using Node.js with Fission. -## Environment - Before running any of these functions, make sure you have created a `nodejs` Fission environment: +```bash +# Create an environment with default nodejs images +$ fission env create --name nodeenv --image fission/node-env:latest --builder fission/node-builder:latest +# Create zip file from our example +$ zip -jr nodejs.zip nodejs/ +# Create a package with the zip file +$ fission pkg create --sourcearchive nodejs.zip --env nodeenv ``` -$ fission env create --name nodejs --image fission/node-env -``` - -Note: The default `fission/node-env` image is based on Alpine, which is much smaller than the main Debian Node image (65MB vs 680MB) while still being suitable for most use cases. -If you need to use the full Debian image use the `fission/node-env-debian` image instead. -See the [official Node docker hub repo](https://hub.docker.com/_/node/) for considerations -relating to this choice. ## Function signature @@ -27,28 +27,54 @@ module.exports = async function(context) { headers: { 'Foo': 'Bar' } - } + } } ``` - -Since it is an `async` function, you can `await` `Promise`s, as demonstrated in the `stock.js` function. - ## hello.js This is a basic "Hello, World!" example. It simply returns a status of `200` and text body. ### Usage +Since it is an `async` function, you can `await` `Promise`s, as demonstrated in the `weather.js` function. ```bash -# Upload your function code to fission -$ fission function create --name hello --env nodejs --code hello.js +# Create a function +$ fission fn create --name hello --pkg [pkgname] --entrypoint "hello" -# Map GET /hello to your new function -$ fission route create --method GET --url /hello --function hello +# Test the function +$ fission fn test --name hello +``` -# Run the function. -$ curl http://$FISSION_ROUTER/hello -Hello, world! +## index.js + +This file does nothing but for demonstrating `require` feature. + +### Usage +```bash +# Create a function, you can skip `--entrypoint` as node will look for `index.js` by default +$ fission fn create --name index --pkg [pkgname] + +# Test the function +$ fission fn test --name index +``` + +## multi-entry.js + +This is a multiple exports example. There are two exports: entry1 and entry2 + +### Usage +```bash +# Create a function for entry1 +$ fission fn create --name entry1 --pkg [pkgname] --entrypoint "multi-entry.entry1" + +# Test the function +$ fission fn test --name entry1 + +# Create a function for entry2 +$ fission fn create --name entry2 --pkg [pkgname] --entrypoint "multi-entry.entry2" + +# Test the function +$ fission fn test --name entry2 ``` ## hello-callback.js @@ -60,8 +86,8 @@ This is a basic "Hello, World!" example implemented with the legacy callback imp ### Usage ```bash -# Upload your function code to fission -$ fission function create --name hello-callback --env nodejs --code hello-callback.js +# Create a function +$ fission fn create --name hello-callback --pkg [pkgname] --entrypoint "hello-callback" # Map GET /hello-callback to your new function $ fission route create --method GET --url /hello-callback --function hello-callback @@ -71,25 +97,6 @@ $ curl http://$FISSION_ROUTER/hello-callback Hello, world! ``` -## stock.js - -This is a basic example of how you can easily use asynchronous requests in your functions. By default, the Node.js environment makes the [`request-promise-native`](https://github.com/request/request-promise-native) library available. In this example, the Google Finance API is used to determine when a stock was last traded. - -### Usage - -```bash -# Upload your function code to fission -$ fission function create --name stock --env nodejs --code stock.js - -# Map GET /stock to your new function -$ fission route create --method POST --url /stock --function stock - -# Run the function. -$ curl -H "Content-Type: application/json" -X POST -d '{"symbol":"AAPL"}' http://$FISSION_ROUTER/stock - -{"text":"AAPL last traded at 138.99"} -``` - ## kubeEventsSlack.js This example watches Kubernetes events and sends them to a Slack channel. To use this, create an incoming webhook for your Slack channel, and replace the `slackWebhookPath` in the example code. @@ -98,7 +105,7 @@ This example watches Kubernetes events and sends them to a Slack channel. To use ```bash # Upload your function code to fission -$ fission fn create --name kubeEventsSlack --env nodejs --code kubeEventsSlack.js +$ fission fn create --name kubeEventsSlack --pkg [pkgname] --entrypoint "hello-callback" # Watch all services in the default namespace: $ fission watch create --function kubeEventsSlack --type service --ns default @@ -112,7 +119,7 @@ In this example, the Yahoo Weather API is used to current weather at a given loc ```bash # Upload your function code to fission -$ fission function create --name weather --env nodejs --code weather.js +$ fission function create --name weather --pkg [pkgname] --entrypoint "weather" # Map GET /stock to your new function $ fission route create --method POST --url /weather --function weather @@ -121,4 +128,4 @@ $ fission route create --method POST --url /weather --function weather $ curl -H "Content-Type: application/json" -X POST -d '{"location":"Sieteiglesias, Spain"}' http://$FISSION_ROUTER/weather {"text":"It is 2 celsius degrees in Sieteiglesias, Spain and Mostly Clear"} -``` \ No newline at end of file +``` diff --git a/examples/nodejs/README_V1.md b/examples/nodejs/README_V1.md new file mode 100644 index 00000000..d70da2e3 --- /dev/null +++ b/examples/nodejs/README_V1.md @@ -0,0 +1,105 @@ +# Fission Node.js Examples + +This directory contains several examples to get you started using Node.js with Fission. + +## Environment + +Before running any of these functions, make sure you have created a `nodejs` Fission environment: + +``` +$ fission env create --name nodejs --image fission/node-env +``` + +Note: The default `fission/node-env` image is based on Alpine, which is much smaller than the main Debian Node image (65MB vs 680MB) while still being suitable for most use cases. +If you need to use the full Debian image use the `fission/node-env-debian` image instead. +See the [official Node docker hub repo](https://hub.docker.com/_/node/) for considerations +relating to this choice. + +## Function signature + +Every Node.js function has the same basic form: + +```javascript +module.exports = async function(context) { + return { + status: 200, + body: 'Your body here', + headers: { + 'Foo': 'Bar' + } + } +} +``` + +Since it is an `async` function, you can `await` `Promise`s, as demonstrated in the `weather.js` function. + +## hello.js + +This is a basic "Hello, World!" example. It simply returns a status of `200` and text body. + +### Usage + +```bash +# Upload your function code to fission +$ fission function create --name hello --env nodejs --code hello.js + +# Map GET /hello to your new function +$ fission route create --method GET --url /hello --function hello + +# Run the function. +$ curl http://$FISSION_ROUTER/hello +Hello, world! +``` + +## hello-callback.js + +This is a basic "Hello, World!" example implemented with the legacy callback implementation. If you declare your function with two arguments (`context`, `callback`), a callback taking three arguments (`status`, `body`, `headers`) is provided. + +⚠️️ Callback support is only provided for backwards compatibility! We recommend that you use `async` functions instead. + +### Usage + +```bash +# Upload your function code to fission +$ fission function create --name hello-callback --env nodejs --code hello-callback.js + +# Map GET /hello-callback to your new function +$ fission route create --method GET --url /hello-callback --function hello-callback + +# Run the function. +$ curl http://$FISSION_ROUTER/hello-callback +Hello, world! +``` + +## kubeEventsSlack.js + +This example watches Kubernetes events and sends them to a Slack channel. To use this, create an incoming webhook for your Slack channel, and replace the `slackWebhookPath` in the example code. + +### Usage + +```bash +# Upload your function code to fission +$ fission fn create --name kubeEventsSlack --env nodejs --code kubeEventsSlack.js + +# Watch all services in the default namespace: +$ fission watch create --function kubeEventsSlack --type service --ns default +``` + +## weather.js + +In this example, the Yahoo Weather API is used to current weather at a given location. + +### Usage + +```bash +# Upload your function code to fission +$ fission function create --name weather --env nodejs --code weather.js + +# Map GET /stock to your new function +$ fission route create --method POST --url /weather --function weather + +# Run the function. +$ curl -H "Content-Type: application/json" -X POST -d '{"location":"Sieteiglesias, Spain"}' http://$FISSION_ROUTER/weather + +{"text":"It is 2 celsius degrees in Sieteiglesias, Spain and Mostly Clear"} +``` diff --git a/examples/nodejs/hello-callback.js b/examples/nodejs/hello-callback.js index b679a442..62b87897 100644 --- a/examples/nodejs/hello-callback.js +++ b/examples/nodejs/hello-callback.js @@ -1,4 +1,4 @@ module.exports = function(context, callback) { - callback(200, "Hello, world!\n"); + callback(200, "Hello, world callback!\n"); } diff --git a/examples/nodejs/index.js b/examples/nodejs/index.js new file mode 100644 index 00000000..d2baebe6 --- /dev/null +++ b/examples/nodejs/index.js @@ -0,0 +1 @@ +module.exports = require('./hello'); diff --git a/examples/nodejs/multi-entry.js b/examples/nodejs/multi-entry.js new file mode 100644 index 00000000..b98be60e --- /dev/null +++ b/examples/nodejs/multi-entry.js @@ -0,0 +1,13 @@ +module.exports.entry1 = async function(context) { + return { + status: 200, + body: "Hello, entry 1!\n" + }; +} + +module.exports.entry2 = async function(context) { + return { + status: 200, + body: "Hello, entry 2!\n" + }; +} diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json new file mode 100644 index 00000000..111eaedd --- /dev/null +++ b/examples/nodejs/package.json @@ -0,0 +1,30 @@ +{ + "name": "fission-nodejs-example", + "version": "0.1.0", + "author": "Soam Vasani", + "contributors": [ + { + "name": "Soam Vasani", + "email": "soamvasani@platform9.com" + }, + { + "name": "Gary Yeap", + "email": "contact@garyyeap.com" + } + ], + "description": "Nodejs example for Fission framework", + "engines": { + "node": ">=7.6.0" + }, + "dependencies": { + "body-parser": "*", + "co": "~4.6.0", + "express": "*", + "minimist": "*", + "morgan": "*", + "mz": "~2.7.0", + "request": "^2.81.0", + "request-promise-native": "^1.0.3", + "underscore": ">=1.8.3" + } +} diff --git a/examples/nodejs/stock.js b/examples/nodejs/stock.js deleted file mode 100644 index 3d52c5c7..00000000 --- a/examples/nodejs/stock.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -const rp = require('request-promise-native'); - -module.exports = async function (context) { - const body = context.request.body; - console.log(`body = ${body}`); - const symbol = body['text'].split(' ')[1]; - - console.log(`Got symbol: ${symbol}`); - - if (!symbol) { - return { - status: 400, - body: { - text: 'You must provide a stock symbol.' - } - }; - } - - try { - const response = await rp(`http://finance.google.com/finance/info?q=NYSE:${symbol}`); - const parsed = JSON.parse(response.slice(3)); - const lastTrade = parsed[0]['l_cur']; - return { - status: 200, - body: { - text: `${symbol} last traded at ${lastTrade}` - }, - headers: { - 'Content-Type': 'application/json' - } - }; - } catch (e) { - console.error(e); - return { - status: 500, - body: e - }; - } -}