-
Notifications
You must be signed in to change notification settings - Fork 84
Update the handler to match style of golang-http-template #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
*.pyc | ||
build |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,157 @@ | ||
# python-flask-template | ||
OpenFaaS Python Flask Templates | ||
============================================= | ||
|
||
Python OpenFaaS template with Flask | ||
The Python Flask templates that make use of the incubator project [of-watchdog](https://github.com/openfaas-incubator/of-watchdog). | ||
|
||
To try this out with either Python 2.7 or Python 3.6: | ||
|
||
```bash | ||
faas template pull https://github.com/openfaas-incubator/python-flask-template | ||
faas new --list | ||
Languages available as templates: | ||
Templates available in this repository: | ||
- python27-flask | ||
- python3-flask | ||
``` | ||
- python3-flask-armhf | ||
- python3-http | ||
- python3-http-armhf | ||
|
||
Generate a function with one of the languages: | ||
Notes: | ||
- To build and deploy a function for Raspberry Pi or ARMv7 in general, use the language templates ending in *-armhf* | ||
|
||
```bash | ||
faas new --lang python3-flask myfunction | ||
mv myfunction.yml stack.yml | ||
## Downloading the templates | ||
``` | ||
$ faas template pull https://github.com/openfaas-incubator/python-flask-template | ||
``` | ||
|
||
Followed by the usual flow: | ||
# Using the python27-flask/python3-flask templates | ||
Create a new function | ||
``` | ||
$ faas new --lang python27-flask <fn-name> | ||
``` | ||
Build, push, and deploy | ||
``` | ||
$ faas up -f <fn-name>.yml | ||
``` | ||
Test the new function | ||
``` | ||
$ echo -n content | faas invoke <fn-name> | ||
``` | ||
|
||
# Using the python3-http templates | ||
Create a new function | ||
``` | ||
$ faas new --lang python3-http <fn-name> | ||
``` | ||
Build, push, and deploy | ||
``` | ||
$ faas up -f <fn-name>.yml | ||
``` | ||
Set your OpenFaaS gateway URL. For example: | ||
``` | ||
faas build \ | ||
&& faas deploy | ||
&& faas list --verbose | ||
$ OPENFAAS_URL=http://127.0.0.1:8080 | ||
``` | ||
Test the new function | ||
``` | ||
$ curl -i $OPENFAAS_URL/function/<fn-name> | ||
``` | ||
|
||
## Event and Context Data | ||
The function handler is passed two arguments, *event* and *context*. | ||
|
||
*event* contains data about the request, including: | ||
- body | ||
- headers | ||
- method | ||
- query | ||
- path | ||
|
||
*context* contains basic information about the function, including: | ||
- hostname | ||
|
||
## Response Bodies | ||
By default, the template will automatically attempt to set the correct Content-Type header for you based on the type of response. | ||
|
||
# Wait a couple of seconds then: | ||
For example, returning a dict object type will automatically attach the header `Content-Type: application/json` and returning a string type will automatically attach the `Content-Type: text/html, charset=utf-8` for you. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we mention There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. .. |
||
|
||
echo -n content | faas invoke myfunction | ||
## Example usage | ||
### Custom status codes and response bodies | ||
Successful response status code and JSON response body | ||
```python | ||
def handle(event, context): | ||
return { | ||
"statusCode": 200, | ||
"body": { | ||
"key": "value" | ||
} | ||
} | ||
``` | ||
Successful response status code and string response body | ||
```python | ||
def handle(event, context): | ||
return { | ||
"statusCode": 201, | ||
"body": "Object successfully created" | ||
} | ||
``` | ||
Failure response status code and JSON error message | ||
```python | ||
def handle(event, context): | ||
return { | ||
"statusCode": 400, | ||
"body": { | ||
"error": "Bad request" | ||
} | ||
} | ||
``` | ||
### Custom Response Headers | ||
Setting custom response headers | ||
```python | ||
def handle(event, context): | ||
return { | ||
"statusCode": 200, | ||
"body": { | ||
"key": "value" | ||
}, | ||
"headers": { | ||
"Location": "https://www.example.com/" | ||
} | ||
} | ||
``` | ||
### Accessing Event Data | ||
Accessing request body | ||
```python | ||
def handle(event, context): | ||
return { | ||
"statusCode": 200, | ||
"body": "You said: " + str(event.body) | ||
} | ||
``` | ||
Accessing request method | ||
```python | ||
def handle(event, context): | ||
if event.method == 'GET': | ||
return { | ||
"statusCode": 200, | ||
"body": "GET request" | ||
} | ||
else: | ||
return { | ||
"statusCode": 405, | ||
"body": "Method not allowed" | ||
} | ||
``` | ||
Accessing request query string arguments | ||
```python | ||
def handle(event, context): | ||
return { | ||
"statusCode": 200, | ||
"body": { | ||
"name": event.query['name'] | ||
} | ||
} | ||
``` | ||
Accessing request headers | ||
```python | ||
def handle(event, context): | ||
return { | ||
"statusCode": 200, | ||
"body": { | ||
"content-type-received": event.headers['Content-Type'] | ||
} | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
FROM armhf/python:3.6-alpine | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The stock image 'library/python:3.6-alpine' is compatible with ARM.
and if the image is build on ARM, the ARM version will be pulled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I had no idea about that! You've tested it on the Raspberry Pi already? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I've tested and it ran successfully There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just tried this and it failed. How were you able to run the image on your Raspberry Pi? Did you have to make any other changes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tried what / what failed? Give steps please so we can try too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This appears to work for me on armhf:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried building the template with |
||
|
||
ARG ADDITIONAL_PACKAGE | ||
# Alternatively use ADD https:// (which will not be cached by Docker builder) | ||
RUN apk --no-cache add curl ${ADDITIONAL_PACKAGE} \ | ||
&& echo "Pulling watchdog binary from Github." \ | ||
&& curl -sSLf https://github.com/openfaas-incubator/of-watchdog/releases/download/0.4.6/of-watchdog-armhf > /usr/bin/fwatchdog \ | ||
&& chmod +x /usr/bin/fwatchdog \ | ||
&& apk del curl --no-cache | ||
|
||
# Add non root user | ||
RUN addgroup -S app && adduser app -S -G app | ||
RUN chown app /home/app | ||
|
||
USER app | ||
|
||
ENV PATH=$PATH:/home/app/.local/bin | ||
|
||
WORKDIR /home/app/ | ||
|
||
COPY index.py . | ||
COPY requirements.txt . | ||
USER root | ||
RUN pip install -r requirements.txt | ||
USER app | ||
|
||
RUN mkdir -p function | ||
RUN touch ./function/__init__.py | ||
WORKDIR /home/app/function/ | ||
COPY function/requirements.txt . | ||
RUN pip install --user -r requirements.txt | ||
|
||
WORKDIR /home/app/ | ||
|
||
USER root | ||
COPY function function | ||
RUN chown -R app:app ./ | ||
USER app | ||
|
||
# Set up of-watchdog for HTTP mode | ||
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"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
def handle(event, context): | ||
# TODO implement | ||
return { | ||
"statusCode": 200, | ||
"body": "Hello from OpenFaaS!" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
#!/usr/bin/env python | ||
from flask import Flask, request, jsonify | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No header?
everywhere There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm unfamiliar with this so I'll take a look into it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We haven't done this elsewhere so please propose it generically @pcorbel in a separate issue. |
||
from waitress import serve | ||
import os | ||
|
||
from function import handler | ||
|
||
app = Flask(__name__) | ||
|
||
class Event: | ||
def __init__(self): | ||
self.body = request.get_data() | ||
self.headers = request.headers | ||
self.method = request.method | ||
self.query = request.args | ||
self.path = request.path | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please also ad That would make my pull request obsolete. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That won't be added at this time, but feel free to propose it as an issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
class Context: | ||
def __init__(self): | ||
self.hostname = os.environ['HOSTNAME'] | ||
|
||
def format_status_code(resp): | ||
if 'statusCode' in resp: | ||
return resp['statusCode'] | ||
|
||
return 200 | ||
|
||
def format_body(resp): | ||
if 'body' not in resp: | ||
return "" | ||
elif type(resp['body']) == dict: | ||
return jsonify(resp['body']) | ||
else: | ||
return str(resp['body']) | ||
|
||
def format_headers(resp): | ||
if 'headers' not in resp: | ||
return [] | ||
elif type(resp['headers']) == dict: | ||
headers = [] | ||
for key in resp['headers'].keys(): | ||
header_tuple = (key, resp['headers'][key]) | ||
headers.append(header_tuple) | ||
return headers | ||
|
||
return resp['headers'] | ||
|
||
def format_response(resp): | ||
if resp == None: | ||
return ('', 200) | ||
|
||
statusCode = format_status_code(resp) | ||
body = format_body(resp) | ||
headers = format_headers(resp) | ||
|
||
return (body, statusCode, headers) | ||
|
||
@app.route('/', defaults={'path': ''}, methods=['GET', 'PUT', 'POST', 'PATCH', 'DELETE']) | ||
@app.route('/<path:path>', methods=['GET', 'PUT', 'POST', 'PATCH', 'DELETE']) | ||
def call_handler(path): | ||
event = Event() | ||
context = Context() | ||
response_data = handler.handle(event, context) | ||
|
||
resp = format_response(response_data) | ||
return resp | ||
|
||
if __name__ == '__main__': | ||
serve(app, host='0.0.0.0', port=5000) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
flask | ||
waitress |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
language: python3-http-armhf | ||
fprocess: python index.py | ||
build_options: | ||
- name: dev | ||
packages: | ||
- make | ||
- automake | ||
- gcc | ||
- g++ | ||
- subversion | ||
- python3-dev | ||
- musl-dev | ||
- libffi-dev | ||
- git |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
FROM python:3.6-alpine | ||
|
||
ARG ADDITIONAL_PACKAGE | ||
# Alternatively use ADD https:// (which will not be cached by Docker builder) | ||
RUN apk --no-cache add curl ${ADDITIONAL_PACKAGE} \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a user, I'd like to be able to install build dependencies and have them removed after pip installs dependencies. Is that possible? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some build dependencies generate .so libraries which are needed in the deployment / runtime container. |
||
&& echo "Pulling watchdog binary from Github." \ | ||
&& curl -sSLf https://github.com/openfaas-incubator/of-watchdog/releases/download/0.4.6/of-watchdog > /usr/bin/fwatchdog \ | ||
&& chmod +x /usr/bin/fwatchdog \ | ||
&& apk del curl --no-cache | ||
|
||
# Add non root user | ||
RUN addgroup -S app && adduser app -S -G app | ||
RUN chown app /home/app | ||
|
||
USER app | ||
|
||
ENV PATH=$PATH:/home/app/.local/bin | ||
|
||
WORKDIR /home/app/ | ||
|
||
COPY index.py . | ||
COPY requirements.txt . | ||
USER root | ||
RUN pip install -r requirements.txt | ||
USER app | ||
|
||
RUN mkdir -p function | ||
RUN touch ./function/__init__.py | ||
WORKDIR /home/app/function/ | ||
COPY function/requirements.txt . | ||
RUN pip install --user -r requirements.txt | ||
|
||
WORKDIR /home/app/ | ||
|
||
USER root | ||
COPY function function | ||
RUN chown -R app:app ./ | ||
USER app | ||
|
||
# Set up of-watchdog for HTTP mode | ||
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"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
def handle(event, context): | ||
# TODO implement | ||
return { | ||
"statusCode": 200, | ||
"body": "Hello from OpenFaaS!" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an "official doc" where the contract with the watchdog is explained?
The goal is to know which field is mandatory, and which is not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @pcorbel how is this question related to the Python template?