diff --git a/.gitignore b/.gitignore index 4e5a895..401dd49 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,4 @@ cython_debug/ .vscode/ build .secrets + diff --git a/functions.yml b/functions.yml index e4dbadb..90314e9 100644 --- a/functions.yml +++ b/functions.yml @@ -16,6 +16,6 @@ functions: handler: ./tapas image: ralsina/tapas:latest tapas2: - lang: crystal-http + lang: crystal handler: ./tapas2 image: ralsina/tapas2:latest diff --git a/tapas2/handler.cr b/tapas2/handler.cr index a41603e..d8af919 100644 --- a/tapas2/handler.cr +++ b/tapas2/handler.cr @@ -1,21 +1,7 @@ -require "http/request" -require "http/headers" +require "json" class Handler - def run(request : HTTP::Request) - - if request.body.nil? - return { - body: "Foo", - status_code: 200, - headers: HTTP::Headers{"Content-Type" => "text/plain"}, - } - end - - { - body: "Hello, Crystal. You said: #{request.body.try(&.gets_to_end)}", - status_code: 200, - headers: HTTP::Headers{"Content-Type" => "text/plain"}, - } + def run(req : String) + return JSON::Any.new("Hello, Crystal. You said: #{req}") end end diff --git a/tapas2/shard.yml b/tapas2/shard.yml index b65c156..76711f4 100644 --- a/tapas2/shard.yml +++ b/tapas2/shard.yml @@ -1,2 +1,7 @@ -name: crystal-http-template +name: crystal_faas_function version: 0.1.0 + +# dependencies: +# pg: +# github: will/crystal-pg +# version: "~> 0.5" diff --git a/template/crystal-http/Dockerfile b/template/crystal-http/Dockerfile deleted file mode 100644 index e359108..0000000 --- a/template/crystal-http/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM ghcr.io/openfaas/of-watchdog:0.9.11 as watchdog - -FROM crystallang/crystal:1.8.2 - -COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog -RUN chmod +x /usr/bin/fwatchdog - -ARG ADDITIONAL_PACKAGE -RUN apt-get update \ - && apt-get install -qy --no-install-recommends ${ADDITIONAL_PACKAGE} - -WORKDIR /home/app - -COPY . . -COPY function/shard.yml shard.yml -RUN shards install -RUN crystal build main.cr -o handler --release - -# Create a non-root user -RUN addgroup --system app \ - && adduser --system --ingroup app app - -RUN chown app:app -R /home/app - -USER app - -WORKDIR /home/app - -ENV fprocess="./handler" -EXPOSE 8080 - -HEALTHCHECK --interval=2s CMD [ -e /tmp/.lock ] || exit 1 - -ENV upstream_url="http://127.0.0.1:5000" -ENV mode="http" - -CMD ["fwatchdog"] diff --git a/template/crystal-http/function/handler.cr b/template/crystal-http/function/handler.cr deleted file mode 100644 index 4ebe178..0000000 --- a/template/crystal-http/function/handler.cr +++ /dev/null @@ -1,12 +0,0 @@ -require "http/request" -require "http/headers" - -class Handler - def run(request : HTTP::Request) - { - body: "Hello, Crystal. You said: #{request.body.try(&.gets_to_end)}", - status_code: 200, - headers: HTTP::Headers{"Content-Type" => "text/plain"}, - } - end -end diff --git a/template/crystal-http/function/shard.yml b/template/crystal-http/function/shard.yml deleted file mode 100644 index b65c156..0000000 --- a/template/crystal-http/function/shard.yml +++ /dev/null @@ -1,2 +0,0 @@ -name: crystal-http-template -version: 0.1.0 diff --git a/template/crystal-http/main.cr b/template/crystal-http/main.cr deleted file mode 100644 index 90acc5f..0000000 --- a/template/crystal-http/main.cr +++ /dev/null @@ -1,41 +0,0 @@ -require "http/server" -require "./function/handler" - -server = HTTP::Server.new do |context| - response_triple : NamedTuple(body: String, headers: HTTP::Headers, status_code: Int32) | - NamedTuple(body: String, headers: HTTP::Headers) | - NamedTuple(body: String, status_code: Int32) | - NamedTuple(body: String) | - NamedTuple(headers: HTTP::Headers, status_code: Int32) | - NamedTuple(headers: HTTP::Headers) | - NamedTuple(status_code: Int32) - - handler = Handler.new - response_triple = handler.run(context.request) - - if response_triple.is_a?(NamedTuple(body: String, headers: HTTP::Headers, status_code: Int32) | - NamedTuple(body: String, status_code: Int32) | - NamedTuple(headers: HTTP::Headers, status_code: Int32) | - NamedTuple(status_code: Int32)) - context.response.status_code = response_triple[:status_code] - end - - if response_triple.is_a?(NamedTuple(body: String, headers: HTTP::Headers, status_code: Int32) | - NamedTuple(body: String, headers: HTTP::Headers) | - NamedTuple(headers: HTTP::Headers, status_code: Int32) | - NamedTuple(headers: HTTP::Headers)) - response_triple[:headers].each do |key, value| - context.response.headers[key] = value - end - end - - if response_triple.is_a?(NamedTuple(body: String, headers: HTTP::Headers, status_code: Int32) | - NamedTuple(body: String, headers: HTTP::Headers) | - NamedTuple(body: String, status_code: Int32) | - NamedTuple(body: String)) - context.response.print(response_triple[:body]) - end -end - -server.bind_tcp "0.0.0.0", 5000 -server.listen diff --git a/template/crystal/Dockerfile b/template/crystal/Dockerfile new file mode 100644 index 0000000..d3ffda0 --- /dev/null +++ b/template/crystal/Dockerfile @@ -0,0 +1,38 @@ +FROM crystallang/crystal:1.8.0 as builder + +RUN apt update \ + && apt install -y curl \ + && echo "Pulling watchdog binary from Github." \ + && curl -sSL https://github.com/openfaas/faas/releases/download/0.9.6/fwatchdog > /usr/bin/fwatchdog \ + && chmod +x /usr/bin/fwatchdog + +WORKDIR /home/app +COPY . . + +COPY function/shard.yml shard.yml +RUN shards install +RUN crystal build main.cr -o handler --release + +FROM crystallang/crystal:1.8.0 +RUN apt install ca-certificates + +# Add non root user +RUN adduser app +RUN mkdir -p /home/app + +WORKDIR /home/app + +COPY --from=builder /usr/bin/fwatchdog . +COPY --from=builder /home/app/function/ . +COPY --from=builder /home/app/handler . + +RUN chown -R app /home/app + +USER app + +ENV fprocess="./handler" +EXPOSE 8080 + +HEALTHCHECK --interval=2s CMD [ -e /tmp/.lock ] || exit 1 + +CMD ["./fwatchdog"] diff --git a/template/crystal/function/handler.cr b/template/crystal/function/handler.cr new file mode 100644 index 0000000..d8af919 --- /dev/null +++ b/template/crystal/function/handler.cr @@ -0,0 +1,7 @@ +require "json" + +class Handler + def run(req : String) + return JSON::Any.new("Hello, Crystal. You said: #{req}") + end +end diff --git a/template/crystal/function/shard.yml b/template/crystal/function/shard.yml new file mode 100644 index 0000000..76711f4 --- /dev/null +++ b/template/crystal/function/shard.yml @@ -0,0 +1,7 @@ +name: crystal_faas_function +version: 0.1.0 + +# dependencies: +# pg: +# github: will/crystal-pg +# version: "~> 0.5" diff --git a/template/crystal/main.cr b/template/crystal/main.cr new file mode 100644 index 0000000..4e053fc --- /dev/null +++ b/template/crystal/main.cr @@ -0,0 +1,11 @@ +# Copyright (c) Thomas Peikert 2018. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +require "json" +require "./function/handler" + +req = STDIN.gets_to_end +handler = Handler.new +res = handler.run req + +puts res diff --git a/template/crystal-http/template.yml b/template/crystal/template.yml similarity index 75% rename from template/crystal-http/template.yml rename to template/crystal/template.yml index 66d437b..c55c3f6 100644 --- a/template/crystal-http/template.yml +++ b/template/crystal/template.yml @@ -1,6 +1,6 @@ language: crystal fprocess: ./handler welcome_message: | - You have created a new function which uses crystal 1.0.0. + You have created a new function which uses crystal 1.7.3 🎉 To include third-party dependencies, use a vendoring tool like shards: shards documentation: https://github.com/crystal-lang/shards diff --git a/template/python3-flask/Dockerfile b/template/python3-flask/Dockerfile new file mode 100644 index 0000000..de91009 --- /dev/null +++ b/template/python3-flask/Dockerfile @@ -0,0 +1,63 @@ +ARG PYTHON_VERSION=3.11 +FROM --platform=${TARGETPLATFORM:-linux/amd64} ghcr.io/openfaas/of-watchdog:0.9.10 as watchdog +FROM --platform=${TARGETPLATFORM:-linux/amd64} python:${PYTHON_VERSION}-alpine as build + +COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog +RUN chmod +x /usr/bin/fwatchdog + +ARG ADDITIONAL_PACKAGE +# Alternatively use ADD https:// (which will not be cached by Docker builder) + +RUN apk --no-cache add openssl-dev ${ADDITIONAL_PACKAGE} + +# 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 --chown=app:app index.py . +COPY --chown=app:app requirements.txt . + +USER root +RUN pip install --no-cache-dir -r requirements.txt + +# Build the function directory and install any user-specified components +USER app + +RUN mkdir -p function +RUN touch ./function/__init__.py +WORKDIR /home/app/function/ +COPY --chown=app:app function/requirements.txt . +RUN pip install --no-cache-dir --user -r requirements.txt + +#install function code +USER root + +COPY --chown=app:app function/ . + + +FROM build as test +ARG TEST_COMMAND=tox +ARG TEST_ENABLED=true +RUN [ "$TEST_ENABLED" = "false" ] && echo "skipping tests" || eval "$TEST_COMMAND" + +FROM build as ship +WORKDIR /home/app/ + +#configure WSGI server and healthcheck +USER app + +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"] diff --git a/template/python3-flask/function/__init__.py b/template/python3-flask/function/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/template/python3-flask/function/handler.py b/template/python3-flask/function/handler.py new file mode 100644 index 0000000..a7098fa --- /dev/null +++ b/template/python3-flask/function/handler.py @@ -0,0 +1,7 @@ +def handle(req): + """handle a request to the function + Args: + req (str): request body + """ + + return req diff --git a/template/python3-flask/function/handler_test.py b/template/python3-flask/function/handler_test.py new file mode 100644 index 0000000..b07d5bf --- /dev/null +++ b/template/python3-flask/function/handler_test.py @@ -0,0 +1,10 @@ +from .handler import handle + +# Test your handler here + +# To disable testing, you can set the build_arg `TEST_ENABLED=false` on the CLI or in your stack.yml +# https://docs.openfaas.com/reference/yaml/#function-build-args-build-args + +def test_handle(): + # assert handle("input") == "input" + pass diff --git a/template/python3-flask/function/requirements.txt b/template/python3-flask/function/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/template/python3-flask/function/tox.ini b/template/python3-flask/function/tox.ini new file mode 100644 index 0000000..a64a800 --- /dev/null +++ b/template/python3-flask/function/tox.ini @@ -0,0 +1,41 @@ +# If you would like to disable +# automated testing during faas-cli build, + +# Replace the content of this file with +# [tox] +# skipsdist = true + +# You can also edit, remove, or add additional test steps +# by editing, removing, or adding new testenv sections + + +# find out more about tox: https://tox.readthedocs.io/en/latest/ +[tox] +envlist = lint,test +skipsdist = true + +[testenv:test] +deps = + flask + pytest + -rrequirements.txt +commands = + # run unit tests with pytest + # https://docs.pytest.org/en/stable/ + # configure by adding a pytest.ini to your handler + pytest + +[testenv:lint] +deps = + flake8 +commands = + flake8 . + +[flake8] +count = true +max-line-length = 127 +max-complexity = 10 +statistics = true +# stop the build if there are Python syntax errors or undefined names +select = E9,F63,F7,F82 +show-source = true diff --git a/template/python3-flask/index.py b/template/python3-flask/index.py new file mode 100644 index 0000000..48554ff --- /dev/null +++ b/template/python3-flask/index.py @@ -0,0 +1,41 @@ +# 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 +from waitress import serve +import os + +app = Flask(__name__) + +# distutils.util.strtobool() can throw an exception +def is_true(val): + return len(val) > 0 and val.lower() == "true" or val == "1" + +@app.before_request +def fix_transfer_encoding(): + """ + Sets the "wsgi.input_terminated" environment flag, thus enabling + Werkzeug to pass chunked requests as streams. The gunicorn server + should set this, but it's not yet been implemented. + """ + + transfer_encoding = request.headers.get("Transfer-Encoding", None) + if transfer_encoding == u"chunked": + request.environ["wsgi.input_terminated"] = True + +@app.route("/", defaults={"path": ""}, methods=["POST", "GET"]) +@app.route("/", methods=["POST", "GET"]) +def main_route(path): + raw_body = os.getenv("RAW_BODY", "false") + + as_text = True + + if is_true(raw_body): + as_text = False + + ret = handler.handle(request.get_data(as_text=as_text)) + return ret + +if __name__ == '__main__': + serve(app, host='0.0.0.0', port=5000) diff --git a/template/python3-flask/requirements.txt b/template/python3-flask/requirements.txt new file mode 100644 index 0000000..668a25a --- /dev/null +++ b/template/python3-flask/requirements.txt @@ -0,0 +1,3 @@ +flask +waitress +tox==3.* \ No newline at end of file diff --git a/template/python3-flask/template.yml b/template/python3-flask/template.yml new file mode 100644 index 0000000..21a04ea --- /dev/null +++ b/template/python3-flask/template.yml @@ -0,0 +1,2 @@ +language: python3-flask +fprocess: python index.py