Skip to content

Commit

Permalink
Merge pull request #9 from fonk-apps/openfaas_python
Browse files Browse the repository at this point in the history
Guestbook on OpenFaas/Python
  • Loading branch information
nerdguru authored Oct 13, 2018
2 parents 53f9020 + 3a83f27 commit 3019347
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 0 deletions.
134 changes: 134 additions & 0 deletions guestbook/faas/openfaas/python/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# OpenFaaS FONK Guestbook (Python)

This folder contains a version of the FONK Guestbook application written in Python using OpenFaas. The steps are as follows:

* Creating `create.yml` and `list.yml`
* Building, Pushing, and Deploying the functions
* Testing your API with `curl`

## Creating `create.yml` and `list.yml`
In OpenFaaS, functions are defined using a .yml file format that describes where the OpenFaaS instance being used is located, where the function binary resides, and where the container image used to implement the function can be found.

In this folder, there are two templates for the two back-end Guestbook functions, `create.template` and `list.template`. The first step is to edit each of these and save them as their .yml variants. To do this, the master IP address of the Kubernetes cluster running OpenFaaS is needed, as is the username being used for the container image repo configured on your Docker command line.

## Building, Pushing, and Deploying the functions
OpenFaaS pulls container images from a repo at deployment time, after those images have been built and pushed to that repo. While OpenFaaS hides the subsequent `Dockerfile` from the developer in its templating system, a local copy of Docker configured to talk to the repo in question is required before proceeding.

You will also need to install OpenFaaS templates that support Flask for Python:

```bash
faas-cli template pull https://github.com/openfaas-incubator/python-flask-template
```

You may check they have been correctly installed with:

```bash
faas-cli new --list
```

With those prerequisites in place, and keeping in mind that Docker often requires `sudo` access, here are the three commands needed to build, push, and deploy a function to OpenFaas. First, build the container image that houses the function.

```bash
$ sudo faas-cli build -f create.yml
[0] > Building list.
Clearing temporary build folder: ./build/create/
Preparing ./create/ ./build/create/function
Building: <username>/create with python27-flask template. Please wait..
Sending build context to Docker daemon 11.78kB
Step 1/19 : FROM python:2.7-alpine
---> b2bc7255b42c
Step 2/19 : RUN apk --no-cache add curl && echo "Pulling watchdog binary from Github." && curl -sSLf https://github.com/openfaas-incubator/of-watchdog/releases/download/0.4.0/of-watchdog > /usr/bin/fwatchdog && chmod +x /usr/bin/fwatchdog && apk del curl --no-cache
.
.
.
Step 19/19 : CMD ["fwatchdog"]
---> Running in 91ff344e12a6
Removing intermediate container 91ff344e12a6
---> f6d583c37b92
Successfully built d8c3cf003d6d
Successfully tagged <username>/create:latest
Image: <username>/create built.
[0] < Building create done.
[0] worker done.
```

Next, push that image to the repo:
```bash
$ sudo faas-cli push -f create.yml
[0] > Pushing create.
The push refers to repository [docker.io/<username>/create]
b5f158794b91: Pushed
6293e4114d0e: Pushed
.
.
.
latest: digest: sha256:4fa962fa09d0d6e05190e006c94fc7c1e4d0f5f3eec1dbc0c7dfce2a13005396 size: 3038
[0] < Pushing create done.
[0] worker done.
```

Finally, deploy the image to the OpenFaaS instance:
```bash
$ sudo faas-cli deploy -f create.yml
Deploying: create.

Deployed. 202 Accepted.
URL: http://10.10.20.202:31112/function/create
```

The function is now invokable using the `faas-cli`:

```bash
$ faas-cli invoke -f create.yml create
Reading from STDIN - hit (Control + D) to stop.
{"text":"Hello World"}
{"updatedAt":1536263027935,"text":"Hello World","_id":"5b9183737c17c7000a6e1aca"}
```

(please note Mac users will need to press ctrl+alt+D)

Rinse and repeat for `list`:

```bash
$ sudo faas-cli build -f list.yml
$ sudo faas-cli push -f list.yml
$ sudo faas-cli deploy -f list.yml
```

and the invoke output should look similar to:
```bash
$ faas-cli invoke -f list.yml list
Reading from STDIN - hit (Control + D) to stop.
{"entries":[{"_id":"5b9183737c17c7000a6e1aca","updatedAt":1536263027935,"text":"Hello World"}]}
```

(please note Mac users will need to press ctrl+alt+D)

## Testing your API with `curl`
Among the information the `deploy` command returns with is the URL that can be used to invoke the function with `curl`. So using the examples above `create` looks like this:

```bash
$ curl -X POST --header "Content-Type:application/json" -d '{"text":"Hello Again"}' http://10.10.20.202:31112/function/create
{
"updatedAt":1536263964550,
"text":"Hello Again",
"_id":"5b91871cc2cbee00084dd16f"
}
```

and `list`:

```bash
$ curl http://10.10.20.202:31112/function/list
{
"entries":[{
"_id":"5b9183737c17c7000a6e1aca",
"updatedAt":1536263027935,
"text":"Hello World"},
{"_id":"5b91871cc2cbee00084dd16f",
"updatedAt":1536263964550,
"text":"Hello Again"}]
}
```

These two URLs can now be used on the `frontend`, although note in the source code of the functions that the OpenFaaS API gateway does not return `Access-Control-Allow-Origin: *` by default, hence why that is set explicitly by each function to avoid CORS issues with the GUI.
9 changes: 9 additions & 0 deletions guestbook/faas/openfaas/python/create.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
provider:
name: faas
gateway: http://<master node ip>:31112

functions:
create:
lang: python27-flask
handler: ./create
image: <image repo username>/create
37 changes: 37 additions & 0 deletions guestbook/faas/openfaas/python/create/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pymongo
import json
import time

from flask import request, Flask, Response
from bson.objectid import ObjectId

MONGODB_HOST = "fonkdb-mongodb.default"
MONGODB_PORT = 27017
MONGODB_NAME = "guestbook_app"
MONGODB_COLLECTION = "entries"

def handle(req):
# First, return if this is a preflight OPTIONS call
if request.method == "OPTIONS":
return Response("Allow: POST,OPTIONS", 200, {"Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Origin": "*"})
# No OPTIONS, assume POST and continue
if request.data:
if request.get_data():
try:
client = pymongo.MongoClient("mongodb://{}".format(MONGODB_HOST), int(MONGODB_PORT))
collection = client[MONGODB_NAME][MONGODB_COLLECTION]
request_data = json.loads(request.get_data(as_text=True))
data = {"text": request_data["text"],
"_id": str(ObjectId()),
"updatedAt": int(round(time.time() * 1000))}
id = collection.insert_one(data)
return response(data, 200)
except Exception as err:
return response({"error": "Error: " + str(err)}, 500)
else:
return response({"message": "No content"}, 204)
else:
return None

def response(body, status):
return Response(str(body), status, {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"})
2 changes: 2 additions & 0 deletions guestbook/faas/openfaas/python/create/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pymongo

9 changes: 9 additions & 0 deletions guestbook/faas/openfaas/python/list.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
provider:
name: faas
gateway: http://<master node ip>:31112

functions:
list:
lang: python27-flask
handler: ./list
image: <image repo username>/list
26 changes: 26 additions & 0 deletions guestbook/faas/openfaas/python/list/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import pymongo
import json

from flask import request, Flask, Response

MONGODB_HOST = "fonkdb-mongodb.default"
MONGODB_PORT = 27017
MONGODB_NAME = "guestbook_app"
MONGODB_COLLECTION = "entries"

def handle(req):
try:
client = pymongo.MongoClient("mongodb://{}".format(MONGODB_HOST), int(MONGODB_PORT))
collection = client[MONGODB_NAME][MONGODB_COLLECTION]
entries = []
for doc in collection.find({}):
entries.append(doc)
result = {"entries": entries}
return response(json.dumps(result,indent=2), 200)
except pymongo.errors.PyMongoError as err:
return response({"error": "MongoDB error: " + str(err)}, 500)
except Exception as err:
return response({"error": str(err)}, 500)

def response(body, status):
return Response(str(body), status, {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"})
2 changes: 2 additions & 0 deletions guestbook/faas/openfaas/python/list/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pymongo

33 changes: 33 additions & 0 deletions guestbook/faas/openfaas/python/template/python27-flask/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
FROM python:2.7-alpine

# Alternatively use ADD https:// (which will not be cached by Docker builder)
RUN apk --no-cache add curl \
&& echo "Pulling watchdog binary from Github." \
&& curl -sSLf https://github.com/openfaas-incubator/of-watchdog/releases/download/0.4.0/of-watchdog > /usr/bin/fwatchdog \
&& chmod +x /usr/bin/fwatchdog \
&& apk del curl --no-cache

WORKDIR /root/

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY index.py .

RUN mkdir -p function
RUN touch ./function/__init__.py
WORKDIR /root/function/
COPY function/requirements.txt .
RUN pip install -r requirements.txt

WORKDIR /root/
COPY function function

ENV fprocess="python index.py"
ENV cgi_headers="true"
ENV mode="http"
ENV upstream_url="http://127.0.0.1:5000"


HEALTHCHECK --interval=5s CMD [ -e /tmp/.lock ] || exit 1

CMD ["fwatchdog"]
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def handle(req):
"""handle a request to the function
Args:
req (str): request body
"""

return req
Empty file.
16 changes: 16 additions & 0 deletions guestbook/faas/openfaas/python/template/python27-flask/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) Alex Ellis 2017. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.

from flask import Flask, request
from function import handler

app = Flask(__name__)

@app.route("/", defaults={"path": ""}, methods=["POST", "GET", "OPTIONS"])
@app.route("/<path:path>", methods=["POST", "GET", "OPTIONS"])
def main_route(path):
ret = handler.handle(request.get_data())
return ret

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
flask

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
language: python27-flask
fprocess: python index.py

0 comments on commit 3019347

Please sign in to comment.