Skip to content

Commit

Permalink
Initial Upload
Browse files Browse the repository at this point in the history
  • Loading branch information
pfandzelter committed Nov 22, 2019
1 parent 7b9168f commit 5c168df
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 0 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,17 @@
# tinyFaaS
A Lightweight FaaS Platform for Edge Environments

## Build

To start this tinyFaaS implementation, simply build and start the management service in a Docker container. It will then create the gateway in a separate container.

To build the management service container, run:
`docker build -t tinyfaas-mgmt .`

Then start the container with:
`docker run -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 --name tinyfaas-mgmt -d tinyfaas-mgmt tinyfaas-mgmt`

This ensures that the management service has access to Docker on the host and it will then expose port 8080 to accept incoming request.

To deploy a function (e.g. the "Sieve of Erasthostenes"), run:
`curl http://localhost:8080 --data '{"path": "sieve-of-erasthostenes", "resource": "/sieve/primes", "entry": "sieve.js", "threads": 4}' -v`
12 changes: 12 additions & 0 deletions src/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3

WORKDIR /usr/src/app

RUN pip install --no-cache-dir tornado
RUN pip install --no-cache-dir docker

COPY . .
EXPOSE 8080

ENTRYPOINT [ "python", "./management-service.py" ]
CMD []
12 changes: 12 additions & 0 deletions src/gateway/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM golang

EXPOSE 80/tcp
EXPOSE 5683/udp

WORKDIR /go/src/app
COPY . .

RUN go get -d -v ./...
RUN go install -v ./...

CMD ["app"]
110 changes: 110 additions & 0 deletions src/gateway/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package main

import (
"encoding/json"
"io/ioutil"
"math/rand"
"net"
"net/http"
"bytes"

"github.com/dustin/go-coap"
)

var functions map[string][]string

type function_info struct {
Function_resource string `json:"function_resource"`
Function_containers []string `json:"function_containers"`
}

func handleFunctionCall(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {

if m.IsConfirmable() {

handler, ok := functions[m.PathString()]

if ok {
// call function and return results
resp, err := http.Get("http://" + handler[rand.Intn(len(handler))] + ":8000")

if err != nil {
return &coap.Message{
Type: coap.Acknowledgement,
Code: coap.InternalServerError,
}
}

body, err := ioutil.ReadAll(resp.Body)

if err != nil {
return &coap.Message{
Type: coap.Acknowledgement,
Code: coap.InternalServerError,
}
}

res := &coap.Message{
Type: coap.Acknowledgement,
Code: coap.Content,
MessageID: m.MessageID,
Token: m.Token,
Payload: []byte(body),
}

res.SetOption(coap.ContentFormat, coap.TextPlain)

return res
} else {
return &coap.Message{
Type: coap.Acknowledgement,
Code: coap.NotFound,
}
}

}

return nil
}

func main() {
functions = make(map[string][]string)

mux := coap.NewServeMux()
mux.Handle("/functions", coap.FuncHandler(handleFunctionCall))

go func() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
newStr := buf.String()

var f function_info
err := json.Unmarshal([]byte(newStr), &f)

if err != nil {
return
}

if f.Function_resource[0] == '/' {
f.Function_resource = f.Function_resource[1:]
}

functions[f.Function_resource] = f.Function_containers

mux.Handle(f.Function_resource, coap.FuncHandler(handleFunctionCall))

return

}
})

http.ListenAndServe(":80", nil)
}()

func() {
coap.ListenAndServe("udp", ":5683", mux)
}()

}
7 changes: 7 additions & 0 deletions src/handlers/sieve-of-erasthostenes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "sieve-of-erasthostenes",
"version": "1.0.0",
"description": "Computer Prime Numbers between 1 and 1000",
"main": "sieve.js",
"author": "Tobias Pfandzelter"
}
19 changes: 19 additions & 0 deletions src/handlers/sieve-of-erasthostenes/sieve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
eventhandler: function () {
const max = 1000;
let sieve = [], i, j, primes = [];
for (i = 2; i <= max; ++i) {
if (!sieve[i]) {
primes.push(i);
for (j = i << 1; j <= max; j += i) {
sieve[j] = true;
}
}
}

return JSON.stringify({
response_code: "2.05",
payload: primes.toString()
});
}
}
132 changes: 132 additions & 0 deletions src/management-service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import asyncio
import tornado.ioloop
import tornado.web

import docker
import json
import uuid
import shutil
import urllib
import sys

CONFIG_PORT = 8080
endpoint_container = {}
function_handlers = {}

def create_endpoint(meta_container):
client = docker.from_env()
endpoint_image = client.images.build(path='./endpoint/', rm=True)[0]

endpoint_network = client.networks.create('endpoint-net', driver='bridge')

endpoint_container['container'] = client.containers.run(endpoint_image, network=endpoint_network.name, ports={'5683/udp': 5683}, detach=True)
# getting IP address of the handler container by inspecting the network and converting CIDR to IPv4 address notation (very dirtily, removing the last 3 chars -> i.e. '/20', so let's hope we don't have a /8 subnet mask)
endpoint_container['ipaddr'] = docker.APIClient().inspect_network(endpoint_network.id)['Containers'][endpoint_container['container'].id]['IPv4Address'][:-3]

endpoint_network.connect(meta_container)

class FunctionHandler():
def __init__(self, function_name, function_resource, function_path, function_entry, function_threads):
self.client = docker.from_env()
self.function_resource = function_resource
self.name = function_name

# copy all files in ./templates/functionhandler to handler-runtime/[function_path]
shutil.copytree('./templates/functionhandler', './handler-runtime/' + self.name)

# copy the folder ./handlers/[function_path] to handler-runtime/[function_path]
shutil.copytree('./handlers/' + function_path, './handler-runtime/' + self.name + '/' + function_path)

# use the Dockerfile.template to create a custom Dockerfile with function_path
with open('./templates/Dockerfile.template', 'rt') as fin:
with open('./handler-runtime/' + self.name + '/Dockerfile', 'wt') as fout:
for line in fin:
fout.write(line.replace('%%%HANDLERPATH%%%', function_path))

# use the functionhandler.js.template to create a custom functionhandler.js with function_path as a module name
with open('./templates/functionhandler.js.template', 'rt') as fin:
with open('./handler-runtime/' + self.name + '/functionhandler.js', 'wt') as fout:
for line in fin:
fout.write(line.replace('%%%PACKAGENAME%%%', function_path))

self.this_image = self.client.images.build(path='./handler-runtime/' + self.name, rm=True)[0]

self.thread_count = function_threads

# connect handler container(s) to endpoint on a dedicated subnet
self.this_network = self.client.networks.create(self.name + '-net', driver='bridge')

self.this_network.connect(endpoint_container['container'].name)

self.this_handler_ips = list([None]*self.thread_count)

# create handler container(s)
self.this_containers = list([None]*self.thread_count)

for i in range(0, self.thread_count):
self.this_containers[i] = self.client.containers.run(self.this_image, network=self.this_network.name, detach=True)
# getting IP address of the handler container by inspecting the network and converting CIDR to IPv4 address notation (very dirtily, removing the last 3 chars -> i.e. '/20', so let's hope we don't have a /8 subnet mask)
self.this_handler_ips[i] = docker.APIClient().inspect_network(self.this_network.id)['Containers'][self.this_containers[i].id]['IPv4Address'][:-3]

# tell endpoint about new function
function_handler = {
"function_resource": self.function_resource,
"function_containers": self.this_handler_ips
}

data = json.dumps(function_handler).encode('ascii')

urllib.request.urlopen(url='http://' + endpoint_container['ipaddr'] + ':80', data=data)

class EndpointHandler(tornado.web.RequestHandler):
async def post(self):
try:
# expected post body
# {
# path: 'handler-path',
# resource: 'han/dler',
# entry: 'handler.js',
# threads: 2
# }
#

function_data = tornado.escape.json_decode(self.request.body)

function_path = function_data['path']
function_resource = function_data['resource']
function_entry = function_data['entry']
function_threads = function_data['threads']

function_name = str(uuid.uuid4()) + '-' + function_path + '-handler'

function_handlers[function_name] = FunctionHandler(function_name, function_resource, function_path, function_entry, function_threads)


except Exception as e:
raise

def main(args):
# read config data
# exactly one argument should be provided: meta_container
if len(args) != 2:
raise ValueError('Too many or too little arguments provided:\n' + json.dumps(args))

meta_container = args[1]

try:
docker.from_env().containers.get(meta_container)
except:
raise ValueError('Provided container name does not match a running container')

# create endpoint
endpoint_container = create_endpoint(meta_container)

# accept incoming configuration requests and create handlers based on that
app = tornado.web.Application([
(r'/', EndpointHandler),
])
app.listen(CONFIG_PORT)
tornado.ioloop.IOLoop.current().start()

if __name__ == '__main__':
main(sys.argv)
16 changes: 16 additions & 0 deletions src/templates/Dockerfile.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#https://nodejs.org/en/docs/guides/nodejs-docker-webapp/
FROM node:8

EXPOSE 8000

# Create app directory
WORKDIR /usr/src/app

COPY . .
RUN npm install ./%%%HANDLERPATH%%%

# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)

CMD [ "node", "functionhandler.js" ]
13 changes: 13 additions & 0 deletions src/templates/functionhandler.js.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';

const handler = require('%%%PACKAGENAME%%%');
const http = require('http');

const server = http.createServer((req, res) => {
if(req.url == '/run') {
res.end(handler.eventhandler());
} else {
res.end()
}
});
server.listen(8000);
5 changes: 5 additions & 0 deletions src/templates/functionhandler/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "functionhandler",
"version": "1.0.0",
"main": "functionhandler.js"
}

0 comments on commit 5c168df

Please sign in to comment.