From bd3190f062dbcf10a46ac958b7f05a420ca56ddc Mon Sep 17 00:00:00 2001 From: Finnian Anderson Date: Tue, 13 Feb 2018 21:11:10 +1300 Subject: [PATCH] Refactor into ES6 class, add more tests and update docs Signed-off-by: Finnian Anderson --- README.md | 127 ++++++++++++++++------ openfaas/compose.js | 32 ------ openfaas/deploy.js | 24 ----- openfaas/index.js | 104 +++++++++++++++--- openfaas/invoke.js | 25 ----- openfaas/remove.js | 21 ---- package-lock.json | 23 ++-- package.json | 33 +++++- test.js | 253 ++++++++++++++++++++++++++++++++++---------- 9 files changed, 421 insertions(+), 221 deletions(-) delete mode 100644 openfaas/compose.js delete mode 100644 openfaas/deploy.js delete mode 100644 openfaas/invoke.js delete mode 100644 openfaas/remove.js diff --git a/README.md b/README.md index addf376..c39d983 100644 --- a/README.md +++ b/README.md @@ -2,51 +2,116 @@ style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) ![OpenFaaS](https://img.shields.io/badge/openfaas-serverless-blue.svg) -##### Usage +# Installation Add `openfaas` via `npm` ``` -$ npm install openfaas --save +$ npm i openfaas ``` -Example usage +# Example usage +```javascript +const OpenFaaS = require('openfaas') + +const openfaas = new OpenFaaS('http://localhost:8080') +``` + +## Invoking functions + +```js +openfaas + .invoke('function name', 'input') + .then(res => console.log(res.body)) + .catch(err => console.log(err)) +``` + +## Deploying functions + +```js +openfaas + .deploy({ + name: 'my-function', // name your function + network: 'func_functions', // choose your network (default func_functions) + image: 'hello-serverless' // choose the Docker image + }) + .then(res => console.log(res)) + .catch(err => console.log(err)) ``` -const OpenFaaS = require('./openfaas') -const openfaas = OpenFaaS('http://localhost:8080') +## Listing functions -openfaas.deploy( - 'yolo', // name your function - 'func_functions, // choose your network - 'hello-serverless // choose the Docker image -) - .then(x => console.log(x)) - .catch(err => console.log(err)) +```js +openfaas.list() + .then(res => console.log(res.body)) // an array of the deployed functions + .catch(err => console.log(err)) +``` + +## Removing functions + +```js +openfaas.remove('my-function') + .then(res => console.log(res)) + .catch(err => console.log(err)) +``` -openfaas.invoke( - 'yolo', // function name - 'hello world', // data to send to function - true //should response be JSON? optional. default is false -) - .then(x => console.log(x)) // handle response - .catch(err => console.log(err)) +## Composing functions -openfaas.remove('yolo') - .then(x => console.log(x)) // handle response - .catch(err => console.log(err)) +You have the ability to chain functions which rely on the previous execution's output by using `openfaas.compose()`, like this: -openfaas.compose('initial data', [ - 'func_nodeinfo', - 'func_echoit', - 'func_wordcount' - ] -) - .then(x => console.log(x.body)) // handle final output - .catch(err => console.log(err)) +```js +// the input for the first function +const markdown = ` +# OpenFaaS chained functions example + +[Find out more](https://github.com/openfaas-incubator/node-openfaas) +` + +openfaas.compose(markdown, ['func_markdown', 'func_base64']).then(res => { + console.log(res.body) +}) ``` +``` +PGgxPk9wZW5GYWFTIGNoYWluZWQgZnVuY3Rpb25zIGV4YW1wbGU8L2gxPgoKPHA+PGEgaHJlZj0i +aHR0cHM6Ly9naXRodWIuY29tL29wZW5mYWFzLWluY3ViYXRvci9ub2RlLW9wZW5mYWFzIiByZWw9 +Im5vZm9sbG93Ij5GaW5kIG91dCBtb3JlPC9hPjwvcD4KCg== +``` + +This passes the output from the markdown renderer to the base64 function, and returns the output. + +# Configuration + +The OpenFaaS class constructor method accepts options in any of the following formats: +```js +const openfaas = new OpenFaaS('http://gateway:8080') +const openfaas = new OpenFaaS('http://gateway:8080', options) +const openfaas = new OpenFaaS(options) +``` + +`options` is an object with the following properties: +```js +{ + gateway: 'gateway url', // (optional if passed as first parameter to the constructor) + user: 'basic auth username', // (optional) + pass: 'basic auth password' // (optional) +} +``` + +You can also add any of the options `got` supports since we just proxy them through. This includes all the options available through [`http.request`](https://nodejs.org/api/http.html#http_http_request_options_callback). One example of this could be HTTP basic authentication (to set this up on your OpenFaaS cluster check out [the official guides](https://github.com/openfaas/faas/tree/master/guide#a-foreword-on-security)). + +```js +const openfaas = new OpenFaaS('http://localhost:8080', { + user: 'user', + pass: 'pass' +}) +``` + +All the main methods (`invoke`, `deploy`, `list`, `remove` and `compose`) accept the same extra options parameter as above too. + +In addition to this, `invoke` accepts a extra boolean option called `isBinaryResponse`. Setting this parameter to `true` in the options will mark the response as being binary content and will cause `invoke` to resolve to a response object who's body is a buffer. + ##### ToDo * Complete tests -* support additional request options for `got` +* support additional request options for `got` (**done** - see [!6](https://github.com/openfaas-incubator/node-openfaas/pull/6)) diff --git a/openfaas/compose.js b/openfaas/compose.js deleted file mode 100644 index 2a1fb20..0000000 --- a/openfaas/compose.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -const path = require('path'); -const BbPromise = require('bluebird'); -const got = require('got'); - -const compose = baseUrl => { - return (initial, funcs) => { - const functions = funcs.map(func => { - return data => { - const options = { - method: 'POST', - body: data - }; - - const funcUrl = baseUrl + path.join('/function', func); - return got(funcUrl, options) - .then(res => BbPromise.resolve(res)) - .catch(err => BbPromise.reject(err)); - }; - }); - - return functions.reduce( - (current, f) => { - return current.then(x => f(x.body)); - }, - new BbPromise(resolve => resolve(initial)) - ); - }; -}; - -module.exports = compose; diff --git a/openfaas/deploy.js b/openfaas/deploy.js deleted file mode 100644 index 8d519e7..0000000 --- a/openfaas/deploy.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -const got = require('got'); - -const deploy = gateway => { - const url = gateway; - - return (name, network, image) => { - const deployPath = '/system/functions'; - const options = { - method: 'POST', - json: true, - body: { - service: name, - network, - image - } - }; - - return got(url + deployPath, options); - }; -}; - -module.exports = deploy; diff --git a/openfaas/index.js b/openfaas/index.js index 703eef2..bb37ab9 100644 --- a/openfaas/index.js +++ b/openfaas/index.js @@ -1,16 +1,96 @@ -'use strict'; +const path = require('path') +const got = require('got') -const deploy = require('./deploy'); -const remove = require('./remove'); -const invoke = require('./invoke'); -const compose = require('./compose'); +class OpenFaaS { + constructor(gateway, options) { + if (options) { + // they passed two arguments (url, options) + this.gateway = gateway + } else if (typeof gateway === 'string') { + // they passed a single string + options = {} + this.gateway = gateway + } else { + // they passed a single object + options = gateway + this.gateway = options.gateway + } -const OpenFaaS = url => ({ - deploy: deploy(url), - remove: remove(url), - invoke: invoke(url), - compose: compose(url) -}); + if (!this.gateway) throw new Error('Missing option `gateway`') -module.exports = OpenFaaS; + if (options.user) { + if (!options.pass) throw new Error('Missing option `pass`') + options.auth = `${options.user}:${options.pass}` + } + // save options for got + this.gotOptions = options + } + + invoke(fn, data, options) { + options = options || {} + + // merge our defaults and their passed options + const gotOptions = { ...this.gotOptions, ...options, method: 'POST' } + + gotOptions.encoding = options.isBinaryResponse ? null : 'utf8' + + if (data) gotOptions.body = data + + return got(this.buildFunctionPath(fn), gotOptions) + } + + deploy({ name, network, image }, options) { + const gotOptions = { + ...this.gotOptions, + ...(options || {}), + body: { + service: name, + network: network || 'func_functions', + image + }, + json: true + } + + return got.post(this.gateway + '/system/functions', gotOptions) + } + + list(options) { + const gotOptions = { ...this.gotOptions, ...(options || {}), json: true } + + return got.get(this.gateway + '/system/functions', gotOptions) + } + + compose(data, functions, options) { + // no initial data + if (!functions) { + functions = data + data = undefined + } + + // build an array of functions to be called with result from previous function + // [ function1(data), function2(data), ... ] + const calls = functions.map(f => data => this.invoke(f, data, options || {})) + + return calls.reduce((chain, current) => { + return chain.then(res => current(res.body)) + }, Promise.resolve({ body: data })) + } + + remove(fn, options) { + const gotOptions = { + ...this.gotOptions, + ...(options || {}), + json: true, + body: { functionName: fn } + } + + return got.delete(this.gateway + '/system/functions', gotOptions) + } + + buildFunctionPath(fn) { + return this.gateway + path.join('/function', fn) + } +} + +module.exports = OpenFaaS diff --git a/openfaas/invoke.js b/openfaas/invoke.js deleted file mode 100644 index 96b058c..0000000 --- a/openfaas/invoke.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const path = require('path'); -const got = require('got'); - -const invoke = gateway => { - const url = gateway; - - return (name, data = null, isJson = false, isBinaryResponse = false) => { - const funcPath = path.join('/function', name); - const options = { - method: 'POST', - json: isJson, - encoding: (isBinaryResponse ? null : 'utf8') - }; - - if (data) { - options.body = data; - } - - return got(url + funcPath, options); - }; -}; - -module.exports = invoke; diff --git a/openfaas/remove.js b/openfaas/remove.js deleted file mode 100644 index d05adcb..0000000 --- a/openfaas/remove.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const got = require('got'); - -const remove = gateway => { - const url = gateway; - - return name => { - const options = { - method: 'DELETE', - json: true, - body: { - functionName: name - } - }; - - return got(url + '/system/functions', options); - }; -}; - -module.exports = remove; diff --git a/package-lock.json b/package-lock.json index c3d0693..3848a2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -168,11 +168,6 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "bluebird": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", - "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" - }, "boxen": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.2.1.tgz", @@ -2629,15 +2624,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -2660,6 +2646,15 @@ "function-bind": "1.1.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", diff --git a/package.json b/package.json index 1957f05..d8e1309 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "author": "Austin Frey", "license": "MIT", "dependencies": { - "bluebird": "^3.5.0", "got": "^7.1.0" }, "devDependencies": { @@ -20,14 +19,38 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/austinfrey/node-openfaas.git" + "url": "git+https://github.com/openfaas-incubator/node-openfaas.git" }, "keywords": [ "serverless", - "FaaS" + "OpenFaaS" ], "bugs": { - "url": "https://github.com/austinfrey/node-openfaas/issues" + "url": "https://github.com/openfaas-incubator/node-openfaas/issues" }, - "homepage": "https://github.com/austinfrey/node-openfaas#readme" + "homepage": "https://github.com/openfaas-incubator/node-openfaas#readme", + "xo": { + "space": true, + "semicolon": false, + "rules": { + "curly": [ + "error", + "multi-line" + ], + "object-curly-spacing": [ + "error", + "always" + ], + "array-bracket-spacing": [ + "warn", + "always", + { + "singleValue": true, + "objectsInArrays": false + } + ], + "capitalized-comments": "off", + "eqeqeq": "off" + } + } } diff --git a/test.js b/test.js index e3e89db..c7c0990 100644 --- a/test.js +++ b/test.js @@ -1,60 +1,199 @@ -'use strict'; +const test = require('tape') +const nock = require('nock') +const OpenFaaS = require('./openfaas') -const test = require('tape'); -const nock = require('nock'); -const BbPromise = require('bluebird'); -const FaaS = require('./openfaas'); +const GATEWAY = 'http://localhost:8080' test('Test typeofs', t => { - t.plan(6); - - t.equals(typeof FaaS, 'function'); - - const faas = FaaS('http://localhost:8080'); - - t.equals(typeof faas, 'object'); - t.equals(typeof faas.deploy, 'function'); - t.equals(typeof faas.invoke, 'function'); - t.equals(typeof faas.compose, 'function'); - t.equals(typeof faas.remove, 'function'); -}); - -test('Test full API', t => { - - nock('http://localhost:8080') - .post('/system/functions', { - service: 'test-func', - network: 'func_functions', - image: 'hello-serverless' - }).reply(200) - .post('/function/test-func').reply(200, {status: 'done'}) - .post('/function/func_nodeinfo').reply(200, 'hello cruel world') - .post('/function/func_echoit', 'hello cruel world').reply(200, 'hello cruel world') - .post('/function/func_wordcount', 'hello cruel world').reply(200, 3) - .delete('/system/functions', {functionName: 'test-func'}).reply(200); - - t.plan(5); - const faas = FaaS('http://localhost:8080'); - - faas.deploy( - 'test-func', - 'func_functions', - 'hello-serverless' - ) - .then(x => t.equals(x.statusCode, 200)) - .then(() => faas.invoke('test-func',null, true)) - .then(x => t.same(x.body, {status: 'done'})) - .then(() => faas.compose('', [ - 'func_nodeinfo', - 'func_echoit', - 'func_wordcount' - ] - )) - .then(x => { - t.equals(x.statusCode, 200) - t.equals(x.body, '3') - }) - .then(() => faas.remove('test-func')) - .then(x => t.equals(x.statusCode, 200)) - .catch(err => console.log(err)); -}); + t.plan(7) + + t.equals(typeof OpenFaaS, 'function') + + const openfaas = new OpenFaaS(GATEWAY) + + t.is(typeof openfaas, 'object') + t.is(typeof openfaas.gotOptions, 'object') + t.is(typeof openfaas.deploy, 'function') + t.is(typeof openfaas.invoke, 'function') + t.is(typeof openfaas.compose, 'function') + t.is(typeof openfaas.remove, 'function') +}) + +test('Test arguments', t => { + t.plan(4) + + let openfaas = new OpenFaaS(GATEWAY) + t.is(openfaas.gateway, GATEWAY) + + openfaas = new OpenFaaS(GATEWAY, {}) + t.is(openfaas.gateway, GATEWAY) + + openfaas = new OpenFaaS({ gateway: GATEWAY }) + t.is(openfaas.gateway, GATEWAY) + + openfaas = new OpenFaaS(GATEWAY, { + user: 'user', + pass: 'pass' + }) + t.is(openfaas.gotOptions.auth, 'user:pass') +}) + +test('Test buildFunctionPath()', t => { + const openfaas = new OpenFaaS(GATEWAY) + + const fnPath = openfaas.buildFunctionPath('test-function') + + t.plan(1) + + t.is(fnPath, `${GATEWAY}/function/test-function`) +}) + +test('Test invoke()', t => { + nock(GATEWAY) + .post('/function/test-func').reply(200, { status: 'hello' }) + + t.plan(2) + const openfaas = new OpenFaaS(GATEWAY) + + openfaas.invoke('test-func', JSON.stringify({ data: 'thing' })).then(res => { + t.is(res.body, '{"status":"hello"}') + t.is(res.statusCode, 200) + }).catch(err => { + throw err + }) +}) + +test('Test invoke() with binary response', t => { + nock(GATEWAY) + .post('/function/test-func', JSON.stringify({ data: 'thing' })).reply(200, { status: 'hello' }) + + const openfaas = new OpenFaaS(GATEWAY) + + t.plan(3) + + return openfaas.invoke('test-func', JSON.stringify({ data: 'thing' }), { isBinaryResponse: true }).then(res => { + t.is(Buffer.isBuffer(res.body), true) + t.is(res.body.toString(), '{"status":"hello"}') + t.is(res.statusCode, 200) + }) +}) + +test('Test list()', t => { + nock(GATEWAY) + .get('/system/functions').reply(200, [{ + "name": "func_echoit", + "image": "functions/alpine:latest@sha256:4145602d726a93ed2b393159bb834342a60dcef775729db14bef631c2e90606f", + "invocationCount": 0, + "replicas": 1, + "envProcess": "cat", + "labels": { + "com.docker.stack.image": "functions/alpine:latest", + "com.docker.stack.namespace": "func" + } + }, { + "name": "func_wordcount", + "image": "functions/alpine:latest@sha256:4145602d726a93ed2b393159bb834342a60dcef775729db14bef631c2e90606f", + "invocationCount": 0, + "replicas": 1, + "envProcess": "wc", + "labels": { + "com.docker.stack.image": "functions/alpine:latest", + "com.docker.stack.namespace": "func" + } + }]) + + const openfaas = new OpenFaaS(GATEWAY) + + t.plan(2) + + return openfaas.list().then(res => { + t.is(res.body.length, 2) + t.is(res.statusCode, 200) + }) +}) + +test('Test remove()', t => { + nock(GATEWAY) + .delete('/system/functions', { functionName: 'test-func' }).reply(200) + + const openfaas = new OpenFaaS(GATEWAY) + + t.plan(1) + + return openfaas.remove('test-func').then(res => { + t.is(res.statusCode, 200) + }) +}) + +test('Test deploy()', t => { + nock(GATEWAY) + .post('/system/functions', JSON.stringify({ + service: 'test-func', + network: 'func_functions', + image: 'hello-serverless' + })).reply(200) + + t.plan(1) + + const openfaas = new OpenFaaS(GATEWAY) + + return openfaas.deploy({ + name: 'test-func', network: 'func_functions', image: 'hello-serverless' + }).then(res => { + t.is(res.statusCode, 200) + }) +}) + +test('Test compose()', t => { + nock(GATEWAY) + .post('/function/test-func', 'base input').reply(200, 'first response') + .post('/function/test-func2', 'first response').reply(200, 'second response') + .post('/function/test-func3', 'second response').reply(200, 'third response') + + t.plan(2) + + const openfaas = new OpenFaaS(GATEWAY) + + openfaas.compose('base input', [ 'test-func', 'test-func2', 'test-func3' ]).then(res => { + t.is(res.body, 'third response') + t.is(res.statusCode, 200) + }) +}) + +test('Test compose() with no input', t => { + nock(GATEWAY) + .post('/function/test-func').reply(200, 'first response') + .post('/function/test-func2', 'first response').reply(200, 'second response') + .post('/function/test-func3', 'second response').reply(200, 'third response') + + t.plan(2) + + const openfaas = new OpenFaaS(GATEWAY) + + openfaas.compose([ 'test-func', 'test-func2', 'test-func3' ]).then(res => { + t.is(res.body, 'third response') + t.is(res.statusCode, 200) + }) +}) + +test('Test invoke() with basic auth', t => { + nock(GATEWAY) + .post('/function/test-func', 'test input') + .basicAuth({ + user: 'user', + pass: 'pass' + }) + .reply(200, 'hi there from basic auth') + + t.plan(2) + + const openfaas = new OpenFaaS(GATEWAY, { + user: 'user', + pass: 'pass' + }) + + openfaas.invoke('test-func', 'test input').then(res => { + t.is(res.body, 'hi there from basic auth') + t.is(res.statusCode, 200) + }) +})