Add v2 interface support for nodes env (#836)

This commit is contained in:
Gary Yeap
2018-09-14 14:28:04 +08:00
committed by Ta-Ching Chen
parent d18777bed4
commit 6f60f3eef8
11 changed files with 289 additions and 125 deletions
+11
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
cd ${SRC_PKG}
npm install && cp -r ${SRC_PKG} ${DEPLOY_PKG}
+6 -2
View File
@@ -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"
}
}
+68 -37
View File
@@ -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) {
+51 -44
View File
@@ -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"}
```
```
+105
View File
@@ -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"}
```
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = function(context, callback) {
callback(200, "Hello, world!\n");
callback(200, "Hello, world callback!\n");
}
+1
View File
@@ -0,0 +1 @@
module.exports = require('./hello');
+13
View File
@@ -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"
};
}
+30
View File
@@ -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"
}
}
-41
View File
@@ -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
};
}
}