├── .gitignore ├── LICENSE ├── README.md ├── template ├── node-afterburn-armhf │ ├── Dockerfile │ ├── function │ │ ├── handler.js │ │ └── package.json │ ├── index.js │ ├── package.json │ └── template.yml └── node-afterburn │ ├── Dockerfile │ ├── function │ ├── handler.js │ └── package.json │ ├── index.js │ ├── package.json │ └── template.yml └── test └── integration_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | **/watchdog 2 | **/fwatchdog 3 | build 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alex Ellis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Node.js AfterBurn language template 2 | ========================================== 3 | 4 | ## Status 5 | 6 | AfterBurn is in incubation under the [of-watchdog project](https://github.com/openfaas-incubator/of-watchdog). It is due to be deprecated by the HTTP mode for the of-watchdog. 7 | 8 | ## Usage 9 | 10 | With `of-watchdog` 11 | 12 | Build/deploy/run: 13 | 14 | ``` 15 | faas-cli template pull https://github.com/openfaas/nodejs-afterburn 16 | faas-cli new --lang node-afterburn burner 17 | 18 | faas-cli build -f burner.yml 19 | faas-cli deploy -f burner.yml 20 | echo test | faas-cli invoke burner 21 | ``` 22 | -------------------------------------------------------------------------------- /template/node-afterburn-armhf/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | RUN apk add --no-cache curl && \ 4 | curl -sfL \ 5 | https://github.com/openfaas-incubator/of-watchdog/releases/download/0.0.4/of-watchdog-armhf > /usr/bin/fwatchdog && \ 6 | chmod +x /usr/bin/fwatchdog 7 | 8 | WORKDIR /root/ 9 | 10 | # Turn down the verbosity to default level. 11 | ENV NPM_CONFIG_LOGLEVEL warn 12 | 13 | # Wrapper/boot-strapper 14 | COPY package.json . 15 | RUN npm i 16 | 17 | # Function 18 | COPY index.js . 19 | RUN mkdir -p ./function 20 | 21 | COPY function/*.json ./function/ 22 | WORKDIR /root/function 23 | RUN npm i || : 24 | WORKDIR /root/ 25 | COPY function function 26 | 27 | WORKDIR /root/ 28 | 29 | ENV cgi_headers="true" 30 | 31 | ENV write_debug=true 32 | ENV read_debug=true 33 | 34 | ENV fprocess="node index.js" 35 | ENV afterburn=true 36 | ENV mode=afterburn 37 | RUN touch /tmp/.lock 38 | 39 | HEALTHCHECK --interval=1s CMD [ -e /tmp/.lock ] || exit 1 40 | EXPOSE 8080 41 | CMD ["fwatchdog"] 42 | -------------------------------------------------------------------------------- /template/node-afterburn-armhf/function/handler.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | module.exports = (context, callback) => { 4 | callback(undefined, {status: "done"}); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /template/node-afterburn-armhf/function/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "function", 3 | "version": "0.3.0", 4 | "description": "Afterburn", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "Alex Ellis", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /template/node-afterburn-armhf/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Alex Ellis 2017. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | "use strict" 5 | 6 | var stdin = process.openStdin(); 7 | var fs = require('fs'); 8 | var handler = require('./function/handler.js'); 9 | 10 | const IN_BODY_DATA = -1; 11 | 12 | let resetRequest = function(req) { 13 | req.received = new Buffer(0); 14 | req.contentLength = -1; 15 | req.header = undefined; 16 | } 17 | 18 | var logs = { 19 | count: 0 20 | }; 21 | 22 | stdin.addListener("data", function(data) { 23 | //fs.writeFileSync("data_"+logs.count + ".log", data.length+""); 24 | 25 | currentRequest.received = Buffer.concat([currentRequest.received, data]); 26 | 27 | let key = "\r\n\r\n"; // marks the start of a "HTTP header" 28 | let keyBuffer = new Buffer(key); 29 | let index = currentRequest.received.indexOf(key); 30 | 31 | if (index == IN_BODY_DATA) { 32 | console.log("In body data, not finished request yet."); 33 | 34 | process.exit(1); 35 | } else { 36 | 37 | let headerLength = index + keyBuffer.length; // received.length - (index + keyBuffer.byteLength); 38 | 39 | let header = new Buffer(headerLength); 40 | currentRequest.received.copy(header, 0, 0, headerLength); 41 | 42 | let parsedHeader = parseHeader(header.toString()); 43 | let bodyLength = parseInt(parsedHeader["Content-Length"]); 44 | 45 | currentRequest.contentLength =  bodyLength; 46 | currentRequest.header = parsedHeader; 47 | 48 | if (currentRequest.received.length + headerLength < bodyLength + headerLength) { 49 | 50 | return; 51 | } 52 | 53 | let body = new Buffer(bodyLength); 54 | 55 | currentRequest.received.copy(body, 0, headerLength, headerLength + bodyLength); 56 | //fs.writeFileSync("currentRequest.received_"+logs.count + ".log", currentRequest.received.length+", header: "+ headerLength + ", bodyLength: " + bodyLength ); 57 | 58 | let promise = new Promise((resolve, reject) => { 59 | handler(body.toString(), (err, res) => { 60 | let result; 61 | let contentType = ""; 62 | 63 | if (err) { 64 | result = err.toString(); 65 | } else if (isArray(res) || isObject(res)) { 66 | result = JSON.stringify(res); 67 | contentType = "application/json"; 68 | } else { 69 | result = res; 70 | } 71 | 72 | let output = addHttp(result, contentType) 73 | 74 | resetRequest(currentRequest); 75 | 76 | resolve(output); 77 | }); 78 | }) 79 | .then((output) => { 80 | let done = process.stdout.write(output); 81 | }) 82 | .catch(e => { 83 | let done = process.stdout.write(addHttp(e.toString(), "text/plain")); 84 | }); 85 | } 86 | }); 87 | 88 | function addHttp(content, contentType) { 89 | return new Buffer("HTTP/1.1 200 OK\r\n" + 90 | (contentType.length > 0 ? ("Content-Type: " + contentType + "\r\n") : "") + 91 | "Content-Length: " + content.length + "\r\n" + 92 | "\r\n" + content); 93 | } 94 | 95 | 96 | let isArray = (a) => { 97 | return (!!a) && (a.constructor === Array); 98 | }; 99 | 100 | let isObject = (a) => { 101 | return (!!a) && (a.constructor === Object); 102 | }; 103 | 104 | 105 | function parseHeader(raw) { 106 | let map = {}; 107 | 108 | raw.split("\r\n").reduce((m, obj) => { 109 | if (obj.length) { 110 | map[obj.substring(0, obj.indexOf(" ") - 1)] = obj.substring(obj.indexOf(" ") + 1) 111 | } 112 | }); 113 | return map; 114 | } 115 | 116 | var currentRequest = {}; 117 | resetRequest(currentRequest); 118 | -------------------------------------------------------------------------------- /template/node-afterburn-armhf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-afterburn", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /template/node-afterburn-armhf/template.yml: -------------------------------------------------------------------------------- 1 | language: node-afterburn-armhf 2 | fprocess: node index.js 3 | -------------------------------------------------------------------------------- /template/node-afterburn/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6.11.2-alpine 2 | 3 | RUN apk add --no-cache curl && \ 4 | curl -sfL \ 5 | https://github.com/openfaas-incubator/of-watchdog/releases/download/0.2.1/of-watchdog > /usr/bin/fwatchdog && \ 6 | chmod +x /usr/bin/fwatchdog 7 | 8 | WORKDIR /root/ 9 | 10 | # Turn down the verbosity to default level. 11 | ENV NPM_CONFIG_LOGLEVEL warn 12 | 13 | # Wrapper/boot-strapper 14 | COPY package.json . 15 | RUN npm i 16 | 17 | # Function 18 | COPY index.js . 19 | RUN mkdir -p ./function 20 | 21 | COPY function/*.json ./function/ 22 | WORKDIR /root/function 23 | RUN npm i || : 24 | WORKDIR /root/ 25 | COPY function function 26 | 27 | WORKDIR /root/ 28 | 29 | ENV cgi_headers="true" 30 | 31 | ENV write_debug=true 32 | ENV read_debug=true 33 | 34 | ENV fprocess="node index.js" 35 | ENV afterburn=true 36 | ENV mode=afterburn 37 | RUN touch /tmp/.lock 38 | 39 | HEALTHCHECK --interval=1s CMD [ -e /tmp/.lock ] || exit 1 40 | EXPOSE 8080 41 | CMD ["fwatchdog"] 42 | -------------------------------------------------------------------------------- /template/node-afterburn/function/handler.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | module.exports = (context, callback) => { 4 | callback(undefined, {status: "done"}); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /template/node-afterburn/function/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "function", 3 | "version": "0.3.0", 4 | "description": "Afterburn", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "Alex Ellis", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /template/node-afterburn/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Alex Ellis 2017. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | "use strict" 5 | 6 | var stdin = process.openStdin(); 7 | var fs = require('fs'); 8 | var handler = require('./function/handler.js'); 9 | 10 | const IN_BODY_DATA = -1; 11 | 12 | let resetRequest = function(req) { 13 | req.received = new Buffer(0); 14 | req.contentLength = -1; 15 | req.header = undefined; 16 | } 17 | 18 | var logs = { 19 | count: 0 20 | }; 21 | 22 | stdin.addListener("data", function(data) { 23 | //fs.writeFileSync("data_"+logs.count + ".log", data.length+""); 24 | 25 | currentRequest.received = Buffer.concat([currentRequest.received, data]); 26 | 27 | let key = "\r\n\r\n"; // marks the start of a "HTTP header" 28 | let keyBuffer = new Buffer(key); 29 | let index = currentRequest.received.indexOf(key); 30 | 31 | if (index == IN_BODY_DATA) { 32 | console.log("In body data, not finished request yet."); 33 | 34 | process.exit(1); 35 | } else { 36 | 37 | let headerLength = index + keyBuffer.length; // received.length - (index + keyBuffer.byteLength); 38 | 39 | let header = new Buffer(headerLength); 40 | currentRequest.received.copy(header, 0, 0, headerLength); 41 | 42 | let parsedHeader = parseHeader(header.toString()); 43 | let bodyLength = parseInt(parsedHeader["Content-Length"]); 44 | 45 | currentRequest.contentLength =  bodyLength; 46 | currentRequest.header = parsedHeader; 47 | 48 | if (currentRequest.received.length + headerLength < bodyLength + headerLength) { 49 | 50 | return; 51 | } 52 | 53 | let body = new Buffer(bodyLength); 54 | 55 | currentRequest.received.copy(body, 0, headerLength, headerLength + bodyLength); 56 | //fs.writeFileSync("currentRequest.received_"+logs.count + ".log", currentRequest.received.length+", header: "+ headerLength + ", bodyLength: " + bodyLength ); 57 | 58 | let promise = new Promise((resolve, reject) => { 59 | handler(body.toString(), (err, res) => { 60 | let result; 61 | let contentType = ""; 62 | 63 | if (err) { 64 | result = err.toString(); 65 | } else if (isArray(res) || isObject(res)) { 66 | result = JSON.stringify(res); 67 | contentType = "application/json"; 68 | } else { 69 | result = res; 70 | } 71 | 72 | let output = addHttp(result, contentType) 73 | 74 | resetRequest(currentRequest); 75 | 76 | resolve(output); 77 | }); 78 | }) 79 | .then((output) => { 80 | let done = process.stdout.write(output); 81 | }) 82 | .catch(e => { 83 | let done = process.stdout.write(addHttp(e.toString(), "text/plain")); 84 | }); 85 | } 86 | }); 87 | 88 | function addHttp(content, contentType) { 89 | return new Buffer("HTTP/1.1 200 OK\r\n" + 90 | (contentType.length > 0 ? ("Content-Type: " + contentType + "\r\n") : "") + 91 | "Content-Length: " + content.length + "\r\n" + 92 | "\r\n" + content); 93 | } 94 | 95 | 96 | let isArray = (a) => { 97 | return (!!a) && (a.constructor === Array); 98 | }; 99 | 100 | let isObject = (a) => { 101 | return (!!a) && (a.constructor === Object); 102 | }; 103 | 104 | 105 | function parseHeader(raw) { 106 | let map = {}; 107 | 108 | raw.split("\r\n").reduce((m, obj) => { 109 | if (obj.length) { 110 | map[obj.substring(0, obj.indexOf(" ") - 1)] = obj.substring(obj.indexOf(" ") + 1) 111 | } 112 | }); 113 | return map; 114 | } 115 | 116 | var currentRequest = {}; 117 | resetRequest(currentRequest); 118 | -------------------------------------------------------------------------------- /template/node-afterburn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-afterburn", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /template/node-afterburn/template.yml: -------------------------------------------------------------------------------- 1 | language: node-afterburn 2 | fprocess: node index.js -------------------------------------------------------------------------------- /test/integration_test.go: -------------------------------------------------------------------------------- 1 | package test_integration 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os/exec" 7 | "testing" 8 | ) 9 | 10 | func Test_NodeShortBody(t *testing.T) { 11 | t.Log("OK") 12 | 13 | cmd := exec.Command("node", "index.js") 14 | cmd.Dir = "../" 15 | writer, pipeErr := cmd.StdinPipe() 16 | if pipeErr != nil { 17 | t.Logf("Error %s", pipeErr.Error()) 18 | t.Fail() 19 | } 20 | 21 | inputFile, inputErr := ioutil.ReadFile("../http_req.txt") 22 | 23 | if inputErr != nil { 24 | t.Logf("Error %s", inputErr.Error()) 25 | t.Fail() 26 | } 27 | 28 | writer.Write(inputFile) 29 | writer.Close() 30 | outputBytes, err := cmd.CombinedOutput() 31 | if err != nil { 32 | t.Logf("Error %s", err.Error()) 33 | t.Logf("%s", string(outputBytes)) 34 | t.Fail() 35 | } 36 | 37 | fmt.Println(string(outputBytes)) 38 | } 39 | 40 | func Test_NodeLongBody(t *testing.T) { 41 | t.Log("OK") 42 | 43 | cmd := exec.Command("node", "index.js") 44 | cmd.Dir = "../" 45 | writer, pipeErr := cmd.StdinPipe() 46 | if pipeErr != nil { 47 | t.Logf("Error %s", pipeErr.Error()) 48 | t.Fail() 49 | } 50 | 51 | inputFile, inputErr := ioutil.ReadFile("../long") 52 | 53 | if inputErr != nil { 54 | t.Logf("Error %s", inputErr.Error()) 55 | t.Fail() 56 | } 57 | 58 | writer.Write(inputFile) 59 | writer.Close() 60 | outputBytes, err := cmd.CombinedOutput() 61 | if err != nil { 62 | t.Logf("Error %s", err.Error()) 63 | t.Logf("%s", string(outputBytes)) 64 | t.Fail() 65 | } 66 | 67 | fmt.Println(string(outputBytes)) 68 | } 69 | --------------------------------------------------------------------------------