diff --git a/Makefile b/Makefile index cd61ef8..198e98d 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ clean: -rm -rf node_modules docker_build: - docker build -t globocom/functions . + docker build -t globocom/functions . --no-cache docker_push: docker push globocom/functions diff --git a/README.md b/README.md index ba5a9e7..f437e6e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![Build Status](https://travis-ci.org/backstage/functions.png?branch=master)](https://travis-ci.org/backstage/functions) -[![Coverage Status](https://coveralls.io/repos/github/backstage/functions/badge.svg?branch=master)](https://coveralls.io/github/backstage/functions?branch=master) +[![Build Status](https://travis-ci.org/globocom/functions.png?branch=master)](https://travis-ci.org/globocom/functions) +[![Coverage Status](https://coveralls.io/repos/github/globocom/functions/badge.svg?branch=master)](https://coveralls.io/github/globocom/functions?branch=master) # Backstage Functions Backstage Functions is an Open Source [Serverless](http://martinfowler.com/articles/serverless.html) Platform able to store and execute code. @@ -24,10 +24,6 @@ It uses the [Backstage Functions Sandbox](https://github.com/backstage/functions - Redis 3.0+ - NodeJS 8.9.1 -### Download the project -```bash -git clone https://github.com/backstage/functions.git -``` ### Setup ```bash @@ -39,24 +35,32 @@ make setup make run ``` +### Test +```bash +make test +``` + ## Running locally via Docker ### Requirements - Docker 1.12+ - Docker compose 1.8+ -### Download docker-compose.yml + +### Setup ```bash -mkdir functions -cd functions -curl 'https://raw.githubusercontent.com/backstage/functions/master/docker-compose.yml' > docker-compose.yml +make docker_build ``` ### Run ```bash -docker-compose up +make rund +``` + +### Test +```bash +make testd ``` -## How to use ### Function Structure Your function will have a file, which you define any name you want, and it has to have a function called `main`, with two parameters: `req` and `res`. Req represents the `Request` and Res represents the `Response`. At the end of your code, you'll have to use the `send` method. @@ -102,7 +106,7 @@ ETag: W/"16-soBGetwJPBLt8CqWpBQu+A" Date: Tue, 11 Oct 2016 16:51:04 GMT Connection: keep-alive -{"say":"Hello World!"} +{"say":"Hello World! Nice meeting you..."} ``` If one needs to pass an object in the request, the payload is executed: @@ -121,7 +125,7 @@ ETag: W/"16-Ino2/umXaZ3xVEhoqyS8aA" Date: Tue, 11 Oct 2016 17:13:11 GMT Connection: keep-alive -{"say":"Hello Pedro!"} +{"say":"Hello Pedro! Nice meeting you..."} ``` ### Executing functions in a pipeline @@ -129,9 +133,8 @@ Connection: keep-alive To execute many functions in a pipeline, you can make a `PUT` request to `/functions/pipeline`: ```javascript // Function0 -function main(req, res) {\ +function main(req, res) { res.send({x: req.body.x * 10}); - } // Function1 @@ -140,10 +143,9 @@ function main(req, res) { } ``` -``` +```bash curl -g -i -X PUT 'http://localhost:8100/functions/pipeline?steps[0]=namespace/function0&steps[1]=namespace/function1' \ - -H 'content-type: application/json' - -d '{"x": 1}' + -H 'content-type: application/json' -d '{"x": 1}' ``` Considering the curl above, the pipeline result would be like this: @@ -158,3 +160,100 @@ Connection: keep-alive {"x": 200} ``` + + +### *GET* `/` +Response: +```json +{ + "name": "Backstage functions" +} +``` +### *GET* `/healthcheck` +Response: +```json +WORKING +``` +### *GET* `/status` +Response: +```json +{ + "services": [ + { + "name": "Redis", + "status": "WORKING", + "message": "PONG", + "time": 1 + }, + { + "name": "Sentry", + "status": "WORKING", + "message": "SENTRY_DSN is not setted yet", + "time": 1 + } + ] +} +``` + +### *PUT* `/functions/{namespace}/{function-name}` +Request Payload: +```json +{ + "code":"function main(req, res) {\n const name = (req.body && req.body.name) || \"World\"\n res.send({ say: `Hello ${name}! Nice meeting you...` })\n}\n" +} +``` +Response: +```json +{ + "id":"hello-world", + "code":"function main(req, res) {\n const name = (req.body && req.body.name) || \"World\"\n res.send({ say: `Hello ${name}! Nice meeting you...` })\n}\n", + "hash":"bf741adc706b03b4329a0897d72961b792bf1c37" +} +``` + +### *GET* `/functions` +Response: +```json +{ + "items":[ + { + "namespace":"example", + "id":"hello-world" + }, + { + "namespace":"example2", + "id":"hello-earth" + } + ], + "page":1, + "perPage":10 +} +``` + +### *PUT/POST/GET* `/function/{namespace}/{function-name}/run` +Request Payload Example: +```json +{ + "name":"User" +} +``` +Response: +```json +{ + "say":"Hello User! Nice meeting you..." +} +``` + +### *PUT* `/functions/pipeline?steps[0]=namespace/function0&steps[1]=namespace/function1` +Request Payload Example: +```json +{ + "x": 1 +} +``` +Response: +```json +{ + "x": 200 +} +``` diff --git a/docker-compose.yml b/docker-compose.yml index a556eba..faf1881 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - functions_net depends_on: - redis - command: node lib/app.js + command: ./node_modules/.bin/nodemon lib/app.js volumes: - .:/application/functions - node_vol:/application/functions/node_modules diff --git a/lib/http/routers/FunctionsRouter.js b/lib/http/routers/FunctionsRouter.js index cc1fa58..f70a5af 100644 --- a/lib/http/routers/FunctionsRouter.js +++ b/lib/http/routers/FunctionsRouter.js @@ -184,8 +184,7 @@ router.delete('/:namespace/:id', async (req, res) => { } }); - -router.all('/:namespace/:id/run', bodyParser.json({ limit: bodyParserLimit }), async (req, res) => { +async function functionsRunHandler(req, res) { const { namespace, id } = req.params; const memoryStorage = req.app.get('memoryStorage'); const sandbox = req.app.get('sandbox'); @@ -263,7 +262,16 @@ router.all('/:namespace/:id/run', bodyParser.json({ limit: bodyParserLimit }), a }); errTracker.notify(err); } -}); +} + + +const methodNotAllowed = (req, res) => res.status(405).send(); + +router.route('/:namespace/:id/run') +.get(bodyParser.json({ limit: bodyParserLimit }), functionsRunHandler) +.put(bodyParser.json({ limit: bodyParserLimit }), functionsRunHandler) +.post(bodyParser.json({ limit: bodyParserLimit }), functionsRunHandler) +.delete(bodyParser.json({ limit: bodyParserLimit }), methodNotAllowed); router.put('/pipeline', bodyParser.json({ limit: bodyParserLimit }), async (req, res) => { diff --git a/package.json b/package.json index 33134c1..1dbdfb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@globocom/backstage-functions", - "version": "0.2.10rc", + "version": "0.2.10", "description": "Remote serverless code executor", "main": "index.js", "scripts": { diff --git a/test/integration/domain/http/routers/FunctionsRouter.test.js b/test/integration/domain/http/routers/FunctionsRouter.test.js index a43ec4d..b4f46cf 100644 --- a/test/integration/domain/http/routers/FunctionsRouter.test.js +++ b/test/integration/domain/http/routers/FunctionsRouter.test.js @@ -113,57 +113,108 @@ describe('FunctionRouter integration', () => { .expect({ foo: 'bar' }, done); }); }); + }); - describe('500 status code', () => { + describe('POST /functions/:namespace/:id/run', () => { + describe('simple run with json body', () => { before((done) => { const code = ` function main(req, res) { - res.internalServerError('My server is crashed'); + res.send({ foo: 'bar' }); } `; request(routes) - .put('/functions/function-router-run/test2') + .put('/functions/function-router-run/test3') .send({ code }) .expect(200) .expect('content-type', /json/, done); }); - it('should returns the status 500 with text plain content', (done) => { + it('should runs the code and return properlly', (done) => { request(routes) - .put('/functions/function-router-run/test2/run') - .expect(500) + .post('/functions/function-router-run/test3/run') + .expect(200) .expect('content-type', /json/) - .expect('{"error":"My server is crashed"}', done); + .expect({ foo: 'bar' }, done); }); }); + }); - describe('304 status code', () => { + describe('DELETE /functions/:namespace/:id/run', () => { + describe('simple run with json body', () => { before((done) => { const code = ` function main(req, res) { - res.notModified(); + res.send({ foo: 'bar' }); } `; request(routes) - .put('/functions/function-router-run/test2') + .put('/functions/function-router-run/test4') .send({ code }) .expect(200) .expect('content-type', /json/, done); }); - it('should returns the status 500 with text plain content', (done) => { + it('should return 405 status code', (done) => { request(routes) - .put('/functions/function-router-run/test2/run') - .expect(304) - .expect('', done); + .delete('/functions/function-router-run/test4/run') + .expect(405, done); }); }); + }); - describe('body and query string to the code and combine then', () => { - before((done) => { - const code = ` + describe('500 status code', () => { + before((done) => { + const code = ` + function main(req, res) { + res.internalServerError('My server is crashed'); + } + `; + + request(routes) + .put('/functions/function-router-run/test2') + .send({ code }) + .expect(200) + .expect('content-type', /json/, done); + }); + + it('should returns the status 500 with text plain content', (done) => { + request(routes) + .put('/functions/function-router-run/test2/run') + .expect(500) + .expect('content-type', /json/) + .expect('{"error":"My server is crashed"}', done); + }); + }); + + describe('304 status code', () => { + before((done) => { + const code = ` + function main(req, res) { + res.notModified(); + } + `; + + request(routes) + .put('/functions/function-router-run/test2') + .send({ code }) + .expect(200) + .expect('content-type', /json/, done); + }); + + it('should returns the status 500 with text plain content', (done) => { + request(routes) + .put('/functions/function-router-run/test2/run') + .expect(304) + .expect('', done); + }); + }); + + describe('body and query string to the code and combine then', () => { + before((done) => { + const code = ` function main(req, res) { const query = req.query; const body = req.body; @@ -171,31 +222,31 @@ describe('FunctionRouter integration', () => { } `; - request(routes) - .put('/functions/function-router-run/test3') - .send({ code }) - .expect(200) - .expect('content-type', /json/, done); - }); + request(routes) + .put('/functions/function-router-run/test3') + .send({ code }) + .expect(200) + .expect('content-type', /json/, done); + }); - it('should returns the status 403 with text plain content', (done) => { - const person = { name: 'John Doe' }; + it('should returns the status 403 with text plain content', (done) => { + const person = { name: 'John Doe' }; - request(routes) - .put('/functions/function-router-run/test3/run?where[name]=John') - .send({ person }) - .expect(200) - .expect('content-type', /json/) - .expect({ - body: { person }, - query: { where: { name: 'John' } }, - }, done); - }); + request(routes) + .put('/functions/function-router-run/test3/run?where[name]=John') + .send({ person }) + .expect(200) + .expect('content-type', /json/) + .expect({ + body: { person }, + query: { where: { name: 'John' } }, + }, done); }); + }); - describe('require arbitrary library inside function', () => { - before((done) => { - const code = ` + describe('require arbitrary library inside function', () => { + before((done) => { + const code = ` const _ = require('lodash'); const people = [{name: 'John'}, {name: 'Doe'}]; function main(req, res) { @@ -204,19 +255,18 @@ describe('FunctionRouter integration', () => { } `; - request(routes) - .put('/functions/function-router-run/test4').send({ code }) - .expect(200) - .expect('content-type', /json/, done); - }); + request(routes) + .put('/functions/function-router-run/test4').send({ code }) + .expect(200) + .expect('content-type', /json/, done); + }); - it('should uses the arbitrary library properly', (done) => { - request(routes) - .put('/functions/function-router-run/test4/run') - .expect(200) - .expect('content-type', /json/) - .expect({ names: ['John', 'Doe'] }, done); - }); + it('should uses the arbitrary library properly', (done) => { + request(routes) + .put('/functions/function-router-run/test4/run') + .expect(200) + .expect('content-type', /json/) + .expect({ names: ['John', 'Doe'] }, done); }); }); });