From 063092b5bc5f19671662662c4b5afd80c893a800 Mon Sep 17 00:00:00 2001 From: Sankar Tanguturi Date: Tue, 30 Oct 2018 21:15:16 -0400 Subject: [PATCH] Add the support for perl runtime. * Added the core implementation to support perl runtime for the kubeless. * Added few examples for using new perl runtime support. * Added detailed instructions/documentation in README. --- docker/runtime/perl/Dockerfile.5.26 | 38 +++++++ docker/runtime/perl/Makefile | 23 ++++ docker/runtime/perl/README.md | 118 +++++++++++++++++++++ docker/runtime/perl/kubeless.pl | 159 ++++++++++++++++++++++++++++ examples/Makefile | 22 +++- examples/perl/Dockerfile | 21 ++++ examples/perl/function.yaml | 15 +++ examples/perl/helloget.pm | 19 ++++ examples/perl/hellosleep.pm | 23 ++++ examples/perl/hellowithdata.pm | 35 ++++++ 10 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 docker/runtime/perl/Dockerfile.5.26 create mode 100644 docker/runtime/perl/Makefile create mode 100644 docker/runtime/perl/README.md create mode 100644 docker/runtime/perl/kubeless.pl create mode 100644 examples/perl/Dockerfile create mode 100644 examples/perl/function.yaml create mode 100644 examples/perl/helloget.pm create mode 100644 examples/perl/hellosleep.pm create mode 100644 examples/perl/hellowithdata.pm diff --git a/docker/runtime/perl/Dockerfile.5.26 b/docker/runtime/perl/Dockerfile.5.26 new file mode 100644 index 000000000..02c955c0d --- /dev/null +++ b/docker/runtime/perl/Dockerfile.5.26 @@ -0,0 +1,38 @@ +# Copyright 2019 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM perl:5.26-threaded +MAINTAINER Sankar Tanguturi stanguturi@vmware.com + +RUN apt-get update && \ +apt-get upgrade -y && \ +BUILD_PACKAGES="curl make gcc" && \ +apt-get -y install $BUILD_PACKAGES --no-install-recommends && \ +curl -L http://cpanmin.us | perl - Dancer2 && \ +apt-get remove --purge -y curl make gcc && \ +apt-get autoremove -y && \ +apt-get clean && \ +apt-get autoclean && \ +echo -n > /var/lib/apt/extended_states && \ +rm -rf /var/lib/apt/lists/* && \ +rm -rf /usr/share/man/?? && \ +rm -rf /usr/share/man/??_* && \ +rm -rf ~/.cpanm/* + +WORKDIR / +ADD kubeless.pl . + +USER 1000 + +CMD ["perl", "/kubeless.pl"] diff --git a/docker/runtime/perl/Makefile b/docker/runtime/perl/Makefile new file mode 100644 index 000000000..88bb538cf --- /dev/null +++ b/docker/runtime/perl/Makefile @@ -0,0 +1,23 @@ +# Copyright 2019 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +build5.26: + docker build -t kubeless/perl:5.26$$RUNTIME_TAG_MODIFIER -f Dockerfile.5.26 . + +push5.26: + docker push kubeless/perl:5.26$$RUNTIME_TAG_MODIFIER + +# Mandatory jobs +build-all: build5.26 +push-all: push5.26 diff --git a/docker/runtime/perl/README.md b/docker/runtime/perl/README.md new file mode 100644 index 000000000..d1347014b --- /dev/null +++ b/docker/runtime/perl/README.md @@ -0,0 +1,118 @@ + +# Introduction +`kubeless` framework supports deploying functions written in `python`, `golang`, `nodejs`, `ruby`, `php`, `java` and `dotnet`. Now, the support is added to deploy `perl` functions without much effort. + +# Architecture +* [Dancer](http://perldancer.org) is a simple but powerful web application framework designed for `perl`. `Dancer` is used to implement the `perl` runtime support in `kubeless`. +* A Docker Image is already created with `Dancer`, `perl 5.26` (compiled with Thread support) and other necessary dependencies. The main entry file i.e. `kubeless.pl` is packaged inside the docker image and is started when the image is deployed and started. +* The main entry file `kubeless.pl` is a `dancer` perl application that starts and waits for the incoming HTTP requests. +* When `kubeless` deploys the docker image, few key pieces of information are provided in the following environrment variables. + +```perl +MOD_NAME => 'The name of the module to be loaded.' +FUNC_HANDLER => 'The name of the function to be invoked.' +FUNC_PORT => 'Port on which the web application should listen.' +FUC_TIMEOUT => 'Timeout (in seconds) in which the function should complete the execution.' +``` + +* The `kubeless.pl` file parses the incoming requests, executes the function (in a separate thread) and returns the result. The function must complete it's execution in a fixed timeout. (Default is 10 seconds). If the function is not done executing, it will +be killed and an error is returned to the client. + +## Default Route End-points +* Following are few default routes defined by `perl` framework in `kubelss` +* `/healthz` : GET Method is implemented for /healthz route. This route can be used to check if the application is up and running or not. A successful GET request will return 'OK' string in the response. +* `/metrics` : This route is useful to capture the event monitoring. Currently, it's not implemented and will return 'Not implemented' string in the response. +* All other routes that match `/*` path are routed to the function handler. `GET`, `POST`, `PATCH`, `DELETE` methods are implemented for all these routes. + +## Structure of a perl module +Following is a template for a simple perl module. +```perl +# helloget.pm +# The file name must end with .pm suffix +# Each function will be invoked with two arguments. +# First one is a reference to an event object. +# Second one is a reference to a function context object. +use strict; +sub hello_world { + $event_ref = shift; + $func_ref = shift; + # The POST data in the incoming HTTP request is available + # in data attribute of event object. + $data = $event_ref->{data}; + # If the client POSTS the JSON data, then the $data + # will be a perl has variable with proper key / value / pairs. + # Ex: JSON data: {'id' : 123, 'foo' : 'bar'} + # data{id} = 123, data{foo} = 'bar' + return "Result Value" +} + +# The last line of the perl module MUST be 1 +1 +``` + +## Building Perl Docker image +```bash + cd docker/runtime/perl + make # This will build docker image +``` +* By default, kubeless/perl:5.26 is the tag created for the images built. +* You can use the following commands to push to your own repository. +```bash + docker tag kubeless/perl:5.26 /perl-kubeless:5.26 + docker push /perl-kubeless:5.26 +``` + +## Examples +```bash + cd examples/perl + make # Make file to run all tests. + #check all .pm files +``` + +### Updating the kubeless config map. +``` + kubectl edit -n kubeless configmap kubeless-config + # Add the following entry + { + "ID": "perl", + "compiled": false, + "versions": [ + { + "name": "perl526”, + "version": "5.26”, + "runtimeImage": "stanguturi/perl-kubeless:5.26”, + "initImage": "perl:5.26-threaded" + } + ], + "depName": "", + "fileNameSuffix": ".pm", + } + # load the controller manager. + kubectl delete pods -n kubeless -l kubeless=controller +``` + +## Deploying the perl module +* Write a proper perl module as mentioned above. +* Currently only perl5.26 is the supported perl runtime. +* Use the following commandi in `kubeless` to deply a function. +``` +kubeless deploy function hello_world \ + --from-file helloget.pm + --handler helloget.hello + --runtime perl5.26 +``` + +## Tips for writing a proper perl Module +* The name of the perl module file must end with .pm extension. +* The last line of the perl module must be 1 +```perl +1 +``` +* Make sure that there are no compilation errors in the perl module. +* Make sure that the perl module runs with perl 5.26. +* Add `use strict` at the beginning of the perl module. +* The `perl` package installed in the `container image` is built with `threading support`. If the perl module has any dependies with `perl threads`, it should just work. +* Make sure that the function finishes the execution in the specific timeout. The default timeout is 10 seconds. +* Make sure that a proper scalar string is returned from the perl function. + +**Happy Perl function Deploying** diff --git a/docker/runtime/perl/kubeless.pl b/docker/runtime/perl/kubeless.pl new file mode 100644 index 000000000..cecb0c8ad --- /dev/null +++ b/docker/runtime/perl/kubeless.pl @@ -0,0 +1,159 @@ +#!/usr/bin/env perl +# Copyright 2019 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +use threads; +use JSON::MaybeXS qw(encode_json decode_json); +use Dancer2; +use Time::HiRes qw(usleep); + +# Port on which the dancer app will run. +my $func_port = $ENV{FUNC_PORT} || 8080; + +# Timeout in seconds for the main function to finish. +my $timeout = int($ENV{FUNC_TIMEOUT} || 10); + +# Module name and function handler. +# While deploying the function in kubeless, +# the module name is specified in --from-file command line argument and +# the function name is specified in --handler command line option. +my $mod_name = $ENV{MOD_NAME}; +my $func_handler = $ENV{FUNC_HANDLER}; + +# This is the standard convention followed for kubeless runtimes. +# The main module file will be located in /kubeless/ directory. +# The runtime script needs to prepare the proper path and load it. +my $mod_path = "/kubeless/" . $mod_name . ".pm"; + +# Only for testing. Need to remove for production purposes. +if (!$mod_name) { + $mod_path = $ENV{MOD_PATH}; +} +# TODO: Known bug in Perl Dancer app. Need to set the port here. +set port => $func_port; + +# TODO: Set the proper serializer app for JSON. But known bug in Dancer app. +# set serializer => 'JSON'; + +my %function_context = ( + 'function' => $func_handler, + 'module' => $mod_path, + 'timeout' => $timeout, + 'runtime' => $ENV{'FUNC_RUNTIME'}, + 'memory-limit' => $ENV{'FUNC_MEMORY_LIMIT'}, +); + +# It's always better to load the module in the beginning itself. If there +# are any errors, they will be caught early while the container is loading. +# The same format / style is being followed in other runtimes i.e. python, +# nodejs, etc. +eval { + require($mod_path); +}; if ($@) { + print("Error while invoking the module at $mod_path. Error: " . $@ . " \n"); + exit(1); +} + +# Endpoint for /healthz. This is one of the main requirements by kubeless +# runtimes. +get '/healthz' => sub { + return 'OK'; +}; + +# Endpoint for /metrics. This is one of the main requirements by kubeless +# runtimes. +get '/metrics' => sub { + # TODO: Needs to implement. We need to use proper prometheus client + # package in perl. But it seems to be complicated. Lets leave this as is + # for now. + return '!!!!! To be Implemented !!!!!'; +}; + +# Wrapper around the main function execution. This function will run the +# main function in a thread and waits for the timeout. If the function doesn'test +# complete in the timeout, then it is explicitly killed and a proper error +# is returned to the client. +sub thread_wrap { + my $event_ref = shift; + my $func_ref = shift; + + my $return; + + # If the function takes a lot of time to finish, the Main + # thread sends a kill signal to terminate the function. + # Registering a new function for the KILL signal is the only cleanest + # way to provide the timeout functionality. + $SIG{'KILL'} = sub { threads->exit(); }; + + eval { + my $function = $func_ref->{function}; + my $func_result = &{\&{$function}}($event_ref, $func_ref); + $return = $func_result; + }; if ($@) { + $return = "Error while invoking the function. Error: '" . $@ . "' \n"; + } + + return $return; +} + +# Main route mapping for all the incoming HTTP requests from the client. +any ['get', 'post', 'patch', 'delete'] => qr{/.*} => sub { + my $content_type = request->header('content-type'); + my $data = request->body(); + if ($content_type eq 'application/json') { + eval { + $data = decode_json($data); + }; if ($@) { + response->status(500); + return "Unable to decode the json body. Error: '" . $@ . "' \n"; + } + } + + # TODO: Implement extensions + # my %extensions = ( 'request' => request ); + + my %event = ( + 'data' => $data, + 'event-id' => request->header('event-id'), + 'event-type' => request->header('event-type'), + 'event-time' => request->header('event-time'), + 'event-namespace' => request->header('event-namespace'), + ); + + my $thr = threads->create('thread_wrap', \%event, \%function_context); + my $sleep_interval = 250000; + my $timeout_usecs = $timeout * 1000000; + for (my $i = 0; $i < $timeout_usecs ; $i = $i + $sleep_interval) { + if ($thr->is_running()) { + usleep($sleep_interval); + } else { + last; + } + } + + my $result; + if ($thr->is_joinable()) { + $result = $thr->join(); + } else { + $thr->kill('KILL')->detach(); + $result = "**** Error: Task killed since it exceeded " . + $timeout . " seconds ... ***\n"; + response->status(500); + } + + return "$result"; +}; + +# Main entry point for the Dancer web application framework. +dance; diff --git a/examples/Makefile b/examples/Makefile index 474005277..adca99243 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -55,7 +55,7 @@ get-python-url-deps: kubeless function deploy get-python-url-deps --runtime python2.7 --handler helloget.foo --from-file https://raw.githubusercontent.com/kubeless/kubeless/v1.0.0-alpha.1/examples/python/hellowithdeps.py --dependencies https://raw.githubusercontent.com/kubeless/kubeless/v1.0.0-alpha.1/examples/python/requirements.txt get-python-url-deps-verify: - kubeless function call get-python-url-deps |egrep Google + kubeless function call get-python-url-deps |egrep Google get-node-url-zip: kubeless function deploy get-node-url-zip --runtime nodejs6 --handler index.helloGet --from-file https://github.com/kubeless/kubeless/blob/master/examples/nodejs/helloFunctions.zip?raw=true @@ -63,6 +63,26 @@ get-node-url-zip: get-node-url-zip-verify: kubeless function call get-node-url-zip |egrep hello.world +get-perl-526: + kubeless function deploy get-perl-526 --runtime perl5.26 --handler helloget.hello --from-file perl/helloget.pm + +get-perl-526-verify: + kubeless function call get-perl-526 | egrep hello.world + +get-perl-custom-port: + kubeless function deploy get-perl-custom-port --runtime perl5.26 --handler helloget.hello --from-file perl/helloget.py --port 8085 + +get-perl-custom-port-verify: + kubectl get svc get-perl-custom-port -o yaml | grep 'targetPort: 8085' + kubeless function call get-perl-custom-port | egrep hello.world + +timeout-perl: + kubeless function deploy timeout-perl --runtime perl5.26 --handler hello_sleep.hello_sleep --from-file perl/helloleep.pm + +timeout-perl-verify: + $(eval MSG := $(shell kubeless function call timeout-perl 2>&1 || true)) + echo $(MSG) | egrep "Error: Task killed since it exceeded" + scheduled-get-python: kubeless function deploy scheduled-get-python --schedule "* * * * *" --runtime python2.7 --handler helloget.foo --from-file python/helloget.py diff --git a/examples/perl/Dockerfile b/examples/perl/Dockerfile new file mode 100644 index 000000000..18d6407d3 --- /dev/null +++ b/examples/perl/Dockerfile @@ -0,0 +1,21 @@ +# Copyright 2019 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Create a custom image with a perl function +FROM stanguturi/perl-kubeless:5.26 +ENV FUNC_HANDLER=hello \ + MOD_NAME=helloget +ADD helloget.pm / +RUN mkdir -p /kubeless/ +RUN chown 1000:1000 /kubeless +ENTRYPOINT [ "bash", "-c", "cp /helloget.pm /kubeless/ && perl /kubeless.pl"] diff --git a/examples/perl/function.yaml b/examples/perl/function.yaml new file mode 100644 index 000000000..46e4d37d1 --- /dev/null +++ b/examples/perl/function.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: k8s.io/v1 +kind: Function +metadata: + name: function +spec: + handler: hello.handler + runtime: perl5.26 + function: | + use strict; + sub handler { + return "hello world" + } + + 1 diff --git a/examples/perl/helloget.pm b/examples/perl/helloget.pm new file mode 100644 index 000000000..c24c5aaa3 --- /dev/null +++ b/examples/perl/helloget.pm @@ -0,0 +1,19 @@ +# Copyright 2019 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +sub hello { + return "hello world" +} + +1 diff --git a/examples/perl/hellosleep.pm b/examples/perl/hellosleep.pm new file mode 100644 index 000000000..d6ce00d17 --- /dev/null +++ b/examples/perl/hellosleep.pm @@ -0,0 +1,23 @@ +# Copyright 2019 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +use strict; + +sub hello_sleep { + print("Sleeping for 50 seconds"); + sleep(50); + return "This shouldn't be returned if timeout is less than 50 seconds"; +} + +1 diff --git a/examples/perl/hellowithdata.pm b/examples/perl/hellowithdata.pm new file mode 100644 index 000000000..c7ddd5d6e --- /dev/null +++ b/examples/perl/hellowithdata.pm @@ -0,0 +1,35 @@ +# Copyright 2019 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +use strict; + +sub print_hash { + my $href = shift; + my $result = "{ "; + my @key_values = (); + while( my( $key, $val ) = each %{$href} ) { + push @key_values, "'$key' => '$val'"; + } + $result = "{ " . (join ',', @key_values) . " }"; + print($result . "\n"); + return $result; +} + +sub hello_data { + my $event = shift; + my $context = shift; + return("Data : " . print_hash($event->{data}) . " \n"); +} + +1