├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── README.md └── docker.gradle ├── NOTICE.txt ├── tests ├── .pydevproject ├── dat │ └── blackbox │ │ ├── badproxy │ │ ├── README.md │ │ ├── Dockerfile │ │ └── build.gradle │ │ └── badaction │ │ ├── build.gradle │ │ ├── Dockerfile │ │ ├── README.md │ │ └── runner.py ├── src │ └── test │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── runtime │ │ └── actionContainers │ │ ├── DockerExampleContainerTests.scala │ │ └── ActionProxyContainerTests.scala └── build.gradle ├── core ├── actionProxy │ ├── build.gradle │ ├── owplatform │ │ ├── openwhisk.py │ │ ├── __init__.py │ │ └── knative.py │ ├── stub.sh │ ├── Dockerfile │ ├── README.md │ └── actionproxy.py └── CHANGELOG.md ├── .scalafmt.conf ├── sdk └── docker │ ├── build.gradle │ ├── Dockerfile │ ├── buildAndPush.sh │ ├── example.c │ ├── build_tgz.sh │ └── README.md ├── .gitattributes ├── .gitignore ├── settings.gradle ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yaml ├── .asf.yaml ├── gradlew.bat ├── CONTRIBUTING.md ├── README.md ├── gradlew └── LICENSE.txt /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/openwhisk-runtime-docker/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Apache OpenWhisk Runtime Docker 2 | Copyright 2016-2024 The Apache Software Foundation 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | -------------------------------------------------------------------------------- /tests/.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | Default 4 | python 2.7 5 | 6 | -------------------------------------------------------------------------------- /core/actionProxy/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | ext.dockerImageName = 'dockerskeleton' 19 | apply from: '../../gradle/docker.gradle' 20 | -------------------------------------------------------------------------------- /tests/dat/blackbox/badproxy/README.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | A docker action that does not implement a proper proxy. Runs a shell commands that never terminates. 21 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | style = intellij 19 | danglingParentheses = false 20 | maxColumn = 120 21 | docstrings = JavaDoc 22 | rewrite.rules = [SortImports] 23 | project.git = true 24 | -------------------------------------------------------------------------------- /sdk/docker/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | ext.dockerImageName = 'example' 19 | 20 | apply from: '../../gradle/docker.gradle' 21 | distDocker.dependsOn ':core:actionProxy:distDocker' 22 | -------------------------------------------------------------------------------- /tests/src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | whisk.spi { 19 | SimpleSpi = whisk.spi.SimpleSpiImpl 20 | MissingSpi = whisk.spi.MissingImpl 21 | MissingModule = missing.module 22 | } 23 | -------------------------------------------------------------------------------- /tests/dat/blackbox/badproxy/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Dockerfile for example whisk docker action 19 | FROM dockerskeleton 20 | 21 | ENV FLASK_PROXY_PORT 8080 22 | 23 | CMD ["/bin/bash", "-c", "tail -f /dev/null"] 24 | -------------------------------------------------------------------------------- /tests/dat/blackbox/badproxy/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | ext.dockerImageName = 'badproxy' 19 | 20 | apply from: '../../../../gradle/docker.gradle' 21 | distDocker.dependsOn ':core:actionProxy:distDocker' 22 | -------------------------------------------------------------------------------- /tests/dat/blackbox/badaction/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | ext.dockerImageName = 'badaction' 19 | 20 | apply from: '../../../../gradle/docker.gradle' 21 | distDocker.dependsOn ':core:actionProxy:distDocker' 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | distributionBase=GRADLE_USER_HOME 18 | distributionPath=wrapper/dists 19 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-bin.zip 20 | zipStoreBase=GRADLE_USER_HOME 21 | zipStorePath=wrapper/dists 22 | -------------------------------------------------------------------------------- /tests/dat/blackbox/badaction/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Dockerfile for example whisk docker action 19 | FROM dockerskeleton 20 | 21 | ENV FLASK_PROXY_PORT 8080 22 | 23 | ADD runner.py /actionProxy/ 24 | 25 | WORKDIR /actionProxy 26 | 27 | CMD ["python", "-u", "runner.py"] -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization. 2 | # Resources: 3 | # - https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html 4 | # - http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ 5 | # - https://help.github.com/articles/dealing-with-line-endings/ 6 | * text=auto 7 | 8 | *.go text eol=lf 9 | *.java text 10 | *.js text 11 | *.md text 12 | *.py text eol=lf 13 | *.scala text 14 | *.sh text eol=lf 15 | *.gradle text 16 | *.xml text 17 | *.bat text eol=crlf 18 | 19 | *.jar binary 20 | *.png binary 21 | 22 | # python files not having the .py extension 23 | tools/cli/wsk text eol=lf 24 | tools/cli/wskadmin text eol=lf 25 | 26 | # bash files not having the .sh extension 27 | tools/vagrant/simple/wsk text eol=lf 28 | gradlew text eol=lf 29 | core/javaAction/proxy/gradlew text eol=lf 30 | tools/vagrant/hello text eol=lf 31 | sdk/docker/client/action text eol=lf 32 | -------------------------------------------------------------------------------- /core/actionProxy/owplatform/openwhisk.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | class OpenWhiskImpl: 19 | 20 | def __init__(self, proxy): 21 | self.proxy = proxy 22 | 23 | def registerHandlers(self, init, run): 24 | self.proxy.add_url_rule('/init', 'init', init, methods=['POST']) 25 | self.proxy.add_url_rule('/run', 'run', run, methods=['POST']) 26 | -------------------------------------------------------------------------------- /tests/dat/blackbox/badaction/README.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | A docker action that can manipulates `/init` and `/run` in different ways including 21 | - not responding 22 | - aborting and terminating the container 23 | 24 | The action overrides the [common action proxy runner](../../../core/actionProxy/actionproxy.py) with programmable `init` and `run` methods. 25 | These containers are used in [Docker container tests](../../src/actionContainers/DockerExampleContainerTests.scala). 26 | -------------------------------------------------------------------------------- /sdk/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Dockerfile for example whisk docker action 19 | FROM dockerskeleton 20 | 21 | ENV FLASK_PROXY_PORT 8080 22 | 23 | ### Add source file(s) 24 | ADD example.c /action/example.c 25 | 26 | RUN apk add --no-cache --virtual .build-deps \ 27 | bzip2-dev \ 28 | gcc \ 29 | libc-dev \ 30 | ### Compile source file(s) 31 | && cd /action; gcc -o exec example.c \ 32 | && apk del .build-deps 33 | 34 | WORKDIR /actionProxy 35 | 36 | CMD ["python", "-u", "actionproxy.py"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Whisk 2 | nginx.conf 3 | whisk.properties 4 | default.props 5 | 6 | .ant-targets-build.xml 7 | /results/ 8 | /logs/ 9 | /config/custom-config.xml 10 | results 11 | *.retry 12 | 13 | # Environments 14 | /ansible/environments/* 15 | !/ansible/environments/distributed 16 | !/ansible/environments/docker-machine 17 | !/ansible/environments/local 18 | !/ansible/environments/mac 19 | 20 | # Eclipse 21 | bin/ 22 | **/.project 23 | .settings/ 24 | .classpath 25 | .cache-main 26 | .cache-tests 27 | 28 | # Linux 29 | *~ 30 | 31 | # Mac 32 | .DS_Store 33 | 34 | # Gradle 35 | .gradle 36 | build/ 37 | !/tools/build/ 38 | 39 | # Python 40 | .ipynb_checkpoints/ 41 | *.pyc 42 | 43 | # NodeJS 44 | node_modules 45 | 46 | # Vagrant 47 | .vagrant* 48 | 49 | # IntelliJ 50 | .idea 51 | *.class 52 | *.iml 53 | out/ 54 | 55 | # Ansible 56 | ansible/environments/docker-machine/hosts 57 | ansible/db_local.ini* 58 | ansible/tmp/* 59 | ansible/roles/nginx/files/openwhisk-client* 60 | ansible/roles/nginx/files/*.csr 61 | ansible/roles/nginx/files/*cert.pem 62 | 63 | # .zip files must be explicited whitelisted 64 | *.zip 65 | !tests/dat/actions/blackbox.zip 66 | !tests/dat/actions/helloSwift.zip 67 | !tests/dat/actions/python.zip 68 | !tests/dat/actions/python2_virtualenv.zip 69 | !tests/dat/actions/python3_virtualenv.zip 70 | !tests/dat/actions/python_virtualenv_dir.zip 71 | !tests/dat/actions/python_virtualenv_name.zip 72 | !tests/dat/actions/zippedaction.zip 73 | -------------------------------------------------------------------------------- /sdk/docker/buildAndPush.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ####### 20 | # This script will build the docker image and push it to dockerhub. 21 | # 22 | # Usage: buildAndPush.sh imageName 23 | # 24 | # Dockerhub image names look like "username/appname" and must be all lower case. 25 | # For example, "janesmith/calculator" 26 | 27 | IMAGE_NAME=$1 28 | echo "Using $IMAGE_NAME as the image name" 29 | 30 | # Make the docker image 31 | docker build -t $IMAGE_NAME . 32 | if [ $? -ne 0 ]; then 33 | echo "Docker build failed" 34 | exit 35 | fi 36 | docker push $IMAGE_NAME 37 | if [ $? -ne 0 ]; then 38 | echo "Docker push failed" 39 | exit 40 | fi 41 | -------------------------------------------------------------------------------- /sdk/docker/example.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | #include 19 | 20 | /** 21 | * This is an example C program that can run as a native openwhisk 22 | * action using the openwhisk/dockerskeleton. 23 | * 24 | * The input to the action is received as an argument from the command line. 25 | * Actions may log to stdout or stderr. 26 | * By convention, the last line of output must be a stringified JSON object 27 | * which represents the result of the action. 28 | */ 29 | 30 | int main(int argc, char *argv[]) { 31 | printf("This is an example log message from an arbitrary C program!\n"); 32 | printf("{ \"msg\": \"Hello from arbitrary C program!\", \"args\": %s }", 33 | (argc == 1) ? "undefined" : argv[1]); 34 | } 35 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | include 'tests' 19 | 20 | include 'sdk:docker' 21 | include 'core:actionProxy' 22 | 23 | include 'tests:dat:blackbox:badaction' 24 | include 'tests:dat:blackbox:badproxy' 25 | 26 | rootProject.name = 'runtime-docker' 27 | 28 | gradle.ext.openwhisk = [ 29 | version: '1.0.1-SNAPSHOT' 30 | ] 31 | 32 | gradle.ext.scala = [ 33 | version: '2.12.7', 34 | depVersion : '2.12', 35 | compileFlags: ['-feature', '-unchecked', '-deprecation', '-Xfatal-warnings', '-Ywarn-unused-import'] 36 | ] 37 | 38 | gradle.ext.scalafmt = [ 39 | version: '1.5.1', 40 | config: new File(rootProject.projectDir, '.scalafmt.conf') 41 | ] 42 | 43 | gradle.ext.akka = [version : '2.6.12'] 44 | gradle.ext.akka_http = [version : '10.2.4'] 45 | -------------------------------------------------------------------------------- /core/actionProxy/stub.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | echo \ 20 | 'This is a stub action that should be replaced with user code (e.g., script or compatible binary). 21 | The input to the action is received from stdin, and up to a size of MAX_ARG_STRLEN (131071) also as an argument from the command line. 22 | Actions may log to stdout or stderr. By convention, the last line of output must 23 | be a stringified JSON object which represents the result of the action.' 24 | 25 | # getting arguments from command line 26 | # only arguments up to a size of MAX_ARG_STRLEN (else empty) supported 27 | echo 'command line argument: '$1 28 | echo 'command line argument length: '${#1} 29 | 30 | # getting arguments from stdin 31 | read inputstring 32 | echo 'stdin input length: '${#inputstring} 33 | 34 | # last line of output = action result 35 | echo '{ "error": "This is a stub action. Replace it with custom logic." }' 36 | -------------------------------------------------------------------------------- /sdk/docker/build_tgz.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | set -ex 20 | 21 | SCRIPTDIR=$(cd $(dirname "$0") && pwd) 22 | ROOTDIR="${SCRIPTDIR}/../.." 23 | BUILDOUTPUTDIR="${ROOTDIR}/build" 24 | BUILDOUTPUT=$1 25 | BUILDOUTPUT=${BUILDOUTPUT:="blackbox-0.1.0.tar.gz"} 26 | 27 | BUILDTMPDIR=`mktemp -d` 28 | mkdir -p ${BUILDTMPDIR}/dockerSkeleton 29 | 30 | cp -a \ 31 | ${SCRIPTDIR}/buildAndPush.sh \ 32 | ${SCRIPTDIR}/Dockerfile \ 33 | ${SCRIPTDIR}/example.c \ 34 | ${SCRIPTDIR}/README.md \ 35 | ${BUILDTMPDIR}/dockerSkeleton 36 | 37 | 38 | sed -i -e 's/FROM dockerskeleton/FROM openwhisk\/dockerskeleton/' ${BUILDTMPDIR}/dockerSkeleton/Dockerfile 39 | cat ${BUILDTMPDIR}/dockerSkeleton/Dockerfile 40 | chmod +x ${BUILDTMPDIR}/dockerSkeleton/buildAndPush.sh 41 | 42 | mkdir -p ${BUILDOUTPUTDIR} 43 | pushd ${BUILDTMPDIR} 44 | tar -czf ${BUILDOUTPUTDIR}/${BUILDOUTPUT} dockerSkeleton 45 | ls ${BUILDTMPDIR}/dockerSkeleton 46 | ls -lh ${BUILDOUTPUTDIR}/${BUILDOUTPUT} 47 | 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 22 | 23 | ## Environment details: 24 | 25 | * local deployment, vagrant, native ubuntu, Mac OS, Bluemix, ... 26 | * version of docker, vagrant, ubuntu, ... 27 | 28 | ## Steps to reproduce the issue: 29 | 30 | 1. 31 | 2. 32 | 3. 33 | 34 | 35 | ## Provide the expected results and outputs: 36 | 37 | ``` 38 | output comes here 39 | ``` 40 | 41 | 42 | ## Provide the actual results and outputs: 43 | 44 | ``` 45 | output comes here 46 | ``` 47 | 48 | ## Additional information you deem important: 49 | * issue happens only occasionally or under certain circumstances 50 | * changes you did or observed in the environment 51 | -------------------------------------------------------------------------------- /.asf.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | github: 19 | description: "Apache OpenWhisk SDK for building Docker \"blackbox\" runtimes" 20 | homepage: https://openwhisk.apache.org/ 21 | labels: 22 | - openwhisk 23 | - apache 24 | - serverless 25 | - faas 26 | - functions-as-a-service 27 | - cloud 28 | - serverless-architectures 29 | - serverless-functions 30 | - docker 31 | - functions 32 | - openwhisk-runtime 33 | protected_branches: 34 | master: 35 | required_status_checks: 36 | strict: false 37 | required_pull_request_reviews: 38 | required_approving_review_count: 1 39 | required_signatures: false 40 | enabled_merge_buttons: 41 | merge: false 42 | squash: true 43 | rebase: true 44 | features: 45 | issues: true 46 | 47 | notifications: 48 | commits: commits@openwhisk.apache.org 49 | issues_status: issues@openwhisk.apache.org 50 | issues_comment: issues@openwhisk.apache.org 51 | pullrequests_status: issues@openwhisk.apache.org 52 | pullrequests_comment: issues@openwhisk.apache.org 53 | -------------------------------------------------------------------------------- /core/actionProxy/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Dockerfile for docker skeleton (useful for running blackbox binaries, scripts, or Python 3 actions) . 19 | FROM python:3.11-alpine 20 | 21 | # Upgrade and install basic Python dependencies. 22 | RUN apk upgrade --update \ 23 | && apk add --no-cache bash perl jq zip git curl wget openssl ca-certificates sed openssh-client \ 24 | && update-ca-certificates \ 25 | && apk add --no-cache --virtual .build-deps bzip2-dev g++ libc-dev \ 26 | && pip install --upgrade pip setuptools six \ 27 | && pip install --no-cache-dir gevent==23.9.1 flask==3.0.0 greenlet==3.0.0\ 28 | && apk del .build-deps 29 | 30 | ENV FLASK_PROXY_PORT 8080 31 | 32 | RUN mkdir -p /actionProxy/owplatform 33 | ADD actionproxy.py /actionProxy/ 34 | ADD owplatform/__init__.py /actionProxy/owplatform/ 35 | ADD owplatform/knative.py /actionProxy/owplatform/ 36 | ADD owplatform/openwhisk.py /actionProxy/owplatform/ 37 | 38 | RUN mkdir -p /action 39 | ADD stub.sh /action/exec 40 | RUN chmod +x /action/exec 41 | 42 | WORKDIR /actionProxy 43 | 44 | CMD ["python", "-u", "actionproxy.py"] 45 | -------------------------------------------------------------------------------- /sdk/docker/README.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | ## Blackbox Actions 21 | 22 | 1. Download and install the OpenWhisk CLI 23 | 2. Install OpenWhisk Docker action skeleton. 24 | 3. Add user code 25 | 4. Build image 26 | 5. Push image 27 | 6. Test out action with CLI 28 | 29 | The script `buildAndPush.sh` is provided for your convenience. The following command sequence 30 | runs the included example Docker action container using OpenWhisk. 31 | 32 | ``` 33 | # install dockerSkeleton with example 34 | wsk sdk install docker 35 | 36 | # change working directory 37 | cd dockerSkeleton 38 | 39 | # build/push, argument is your docker hub user name and a valid docker image name 40 | ./buildAndPush /whiskexample 41 | 42 | # create docker action 43 | wsk action create dockerSkeletonExample --docker /whiskExample 44 | 45 | # invoke created action 46 | wsk action invoke dockerSkeletonExample --blocking 47 | ``` 48 | 49 | The executable file must be located in the `/action` folder. 50 | The name of the executable must be `/action/exec` and can be any file with executable permissions. 51 | The sample docker action runs `example.c` by copying and building the source inside the container 52 | as `/action/exec` (see `Dockerfile` lines 7 and 14). 53 | -------------------------------------------------------------------------------- /tests/dat/blackbox/badaction/runner.py: -------------------------------------------------------------------------------- 1 | """Python bad action runner (sleep forever). 2 | 3 | /* 4 | * Licensed to the Apache Software Foundation (ASF) under one or more 5 | * contributor license agreements. See the NOTICE file distributed with 6 | * this work for additional information regarding copyright ownership. 7 | * The ASF licenses this file to You under the Apache License, Version 2.0 8 | * (the "License"); you may not use this file except in compliance with 9 | * the License. You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | """ 20 | import sys 21 | import time 22 | sys.path.append('../actionProxy') 23 | from actionproxy import ActionRunner, main, setRunner 24 | 25 | 26 | class Runner(ActionRunner): 27 | 28 | def __init__(self): 29 | ActionRunner.__init__(self) 30 | 31 | def init(self, message): 32 | if 'code' in message and message['code'] == 'sleep': 33 | # sleep forever/never respond 34 | while True: 35 | print("sleeping") 36 | time.sleep(60) 37 | elif 'code' in message and message['code'] == 'exit': 38 | print("exiting") 39 | sys.exit(1) 40 | else: 41 | return ActionRunner.init(self, message) 42 | 43 | def run(self, args, env): 44 | if 'sleep' in args: 45 | # sleep forever/never respond 46 | while True: 47 | print("sleeping") 48 | time.sleep(60) 49 | elif 'exit' in args: 50 | print("exiting") 51 | sys.exit(1) 52 | else: 53 | return ActionRunner.run(self, args, env) 54 | 55 | if __name__ == "__main__": 56 | setRunner(Runner()) 57 | main() 58 | -------------------------------------------------------------------------------- /core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | # Apache OpenWhisk Docker Runtime Container 21 | 22 | ## 1.16.0 23 | Changes: 24 | - Update Python dependencies (#102) 25 | 26 | ## 1.15.0 27 | - Update base python image to `python:3.11-alpine` 28 | - Update python dependacies 29 | - Support array result include sequence action (#92) 30 | 31 | ## 1.14.0 32 | - Support for __OW_ACTION_VERSION (openwhisk/4761) 33 | 34 | ## 1.13.0-incubating 35 | Changes: 36 | - Update base python image to `python:3.6-alpine` 37 | - Update current directory for action to be root of zip 38 | - Update python dependencies gevent(`1.2.1`->`1.3.6`) and flask(`0.12`->`1.0.2`) 39 | 40 | ## 1.12.0-incubating 41 | - First Apache incubator release 42 | 43 | ## 1.3.3 44 | Changes: 45 | - Update run handler to accept more environment variables [#55](https://github.com/apache/openwhisk-runtime-docker/pull/55) 46 | 47 | ## 1.3.2 48 | Changes: 49 | - Fixes bug where a log maker is emitted more than once. 50 | 51 | ## 1.3.1 52 | Changes: 53 | - Disallow re-initialization by default. Added environment variable to enable re-initialization for local development. 54 | 55 | ## 1.3.0 56 | Changes: 57 | - Added openssh-client. 58 | 59 | ## 1.2.0 60 | Changes: 61 | - Added utilities curl and wget. 62 | 63 | ## 1.1.0 64 | Changes: 65 | - Allow input parameter larger than 128KB. 66 | - Added perl language support. 67 | - Added utilities jq, zip, git. 68 | 69 | ## 1.0.0 70 | Initial version. 71 | -------------------------------------------------------------------------------- /tests/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | apply plugin: 'scala' 19 | apply plugin: 'eclipse' 20 | compileTestScala.options.encoding = 'UTF-8' 21 | 22 | repositories { 23 | mavenCentral() 24 | mavenLocal() 25 | } 26 | 27 | tasks.withType(Test) { 28 | testLogging { 29 | events "passed", "skipped", "failed" 30 | showStandardStreams = true 31 | exceptionFormat = 'full' 32 | } 33 | outputs.upToDateWhen { false } // force tests to run every time 34 | } 35 | 36 | // Add all images needed for local testing here 37 | test.dependsOn([ 38 | ':tests:dat:blackbox:badaction:distDocker', 39 | ':tests:dat:blackbox:badproxy:distDocker' 40 | ]) 41 | 42 | dependencies { 43 | implementation "junit:junit:4.11" 44 | implementation "org.scala-lang:scala-library:${gradle.scala.version}" 45 | implementation "org.scalatest:scalatest_${gradle.scala.depVersion}:3.0.8" 46 | implementation "org.apache.openwhisk:openwhisk-common:${gradle.openwhisk.version}" 47 | implementation "org.apache.openwhisk:openwhisk-tests:${gradle.openwhisk.version}:tests" 48 | implementation "org.apache.openwhisk:openwhisk-tests:${gradle.openwhisk.version}:test-sources" 49 | implementation group: 'com.typesafe.akka', name: "akka-http2-support_${gradle.scala.depVersion}", version: "${gradle.akka_http.version}" 50 | implementation group: 'com.typesafe.akka', name: "akka-http-xml_${gradle.scala.depVersion}", version: "${gradle.akka_http.version}" 51 | implementation group: 'com.typesafe.akka', name: "akka-discovery_${gradle.scala.depVersion}", version: "${gradle.akka.version}" 52 | implementation group: 'com.typesafe.akka', name: "akka-protobuf_${gradle.scala.depVersion}", version: "${gradle.akka.version}" 53 | implementation group: 'com.typesafe.akka', name: "akka-remote_${gradle.scala.depVersion}", version: "${gradle.akka.version}" 54 | implementation group: 'com.typesafe.akka', name: "akka-cluster_${gradle.scala.depVersion}", version: "${gradle.akka.version}" 55 | } 56 | 57 | tasks.withType(ScalaCompile) { 58 | scalaCompileOptions.additionalParameters = gradle.scala.compileFlags 59 | } 60 | -------------------------------------------------------------------------------- /core/actionProxy/owplatform/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | class PlatformFactory: 19 | 20 | _SUPPORTED_PLATFORMS = set() 21 | _PLATFORM_IMPLEMENTATIONS = {} 22 | 23 | def __init__(self): 24 | pass 25 | 26 | @classmethod 27 | def supportedPlatforms(cls): 28 | return cls._SUPPORTED_PLATFORMS 29 | 30 | @classmethod 31 | def isSupportedPlatform(cls, id): 32 | return id.lower() in cls._SUPPORTED_PLATFORMS 33 | 34 | @classmethod 35 | def addPlatform(cls, platform, platformImp): 36 | if platform.lower not in cls._SUPPORTED_PLATFORMS: 37 | cls._SUPPORTED_PLATFORMS.add(platform.lower()) 38 | cls._PLATFORM_IMPLEMENTATIONS[platform.lower()] = platformImp 39 | else: 40 | raise DuplicatePlatform() 41 | getterName = "PLATFORM_" + platform.upper() 42 | setattr(cls, getterName, platform) 43 | 44 | @classmethod 45 | def createPlatformImpl(cls, id, proxy): 46 | if cls.isSupportedPlatform(id): 47 | return cls._PLATFORM_IMPLEMENTATIONS[id.lower()](proxy) 48 | else: 49 | raise InvalidPlatformError(id, self.supportedPlatforms()) 50 | 51 | @property 52 | def app(self): 53 | return self._app 54 | 55 | @app.setter 56 | def app(self, value): 57 | raise ConstantError("app cannot be set outside of initialization") 58 | 59 | @property 60 | def config(self): 61 | return self._config 62 | 63 | @config.setter 64 | def config(self, value): 65 | raise ConstantError("config cannot be set outside of initialization") 66 | 67 | @property 68 | def service(self): 69 | return self._service 70 | 71 | @service.setter 72 | def service(self, value): 73 | raise ConstantError("service cannot be set outside of initialization") 74 | 75 | class ConstantError(Exception): 76 | pass 77 | 78 | class DuplicatePlatformError(Exception): 79 | pass 80 | 81 | class InvalidPlatformError(Exception): 82 | def __init__(self, platform, supportedPlatforms): 83 | self.platform = platform.lower() 84 | self.supportedPlatforms = supportedPlatforms 85 | 86 | def __str__(self): 87 | return f"Invalid Platform: {self.platform} is not in supported platforms {self.supportedPlatforms}." 88 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) 21 | 22 | # Contributing to Apache OpenWhisk 23 | 24 | Anyone can contribute to the OpenWhisk project and we welcome your contributions. 25 | 26 | There are multiple ways to contribute: report bugs, improve the docs, and 27 | contribute code, but you must follow these prerequisites and guidelines: 28 | 29 | - [Contributor License Agreement](#contributor-license-agreement) 30 | - [Raising issues](#raising-issues) 31 | - [Coding Standards](#coding-standards) 32 | 33 | ### Contributor License Agreement 34 | 35 | All contributors must sign and submit an Apache CLA (Contributor License Agreement). 36 | 37 | Instructions on how to do this can be found here: 38 | [http://www.apache.org/licenses/#clas](http://www.apache.org/licenses/#clas) 39 | 40 | Once submitted, you will receive a confirmation email from the Apache Software Foundation (ASF) and be added to 41 | the following list: http://people.apache.org/unlistedclas.html. 42 | 43 | Project committers will use this list to verify pull requests (PRs) come from contributors that have signed a CLA. 44 | 45 | We look forward to your contributions! 46 | 47 | ## Raising issues 48 | 49 | Please raise any bug reports or enhancement requests on the respective project repository's GitHub issue tracker. Be sure to search the 50 | list to see if your issue has already been raised. 51 | 52 | A good bug report is one that make it easy for us to understand what you were trying to do and what went wrong. 53 | Provide as much context as possible so we can try to recreate the issue. 54 | 55 | A good enhancement request comes with an explanation of what you are trying to do and how that enhancement would help you. 56 | 57 | ### Discussion 58 | 59 | Please use the project's developer email list to engage our community: 60 | [dev@openwhisk.apache.org](dev@openwhisk.apache.org) 61 | 62 | In addition, we provide a "dev" Slack team channel for conversations at: 63 | https://openwhisk-team.slack.com/messages/dev/ 64 | 65 | ### Coding standards 66 | 67 | Please ensure you follow the coding standards used throughout the existing 68 | code base. Some basic rules include: 69 | 70 | - all files must have the Apache license in the header. 71 | - all PRs must have passing builds for all operating systems. 72 | - the code is correctly formatted as defined in the [Scalariform plugin properties](tools/eclipse/scala.properties). If you use IntelliJ for development this [page](https://plugins.jetbrains.com/plugin/7480-scalariform) describes the setup and configuration of the plugin. 73 | -------------------------------------------------------------------------------- /core/actionProxy/README.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | ## Skeleton for "docker actions" 21 | 22 | The `dockerskeleton` base image is useful for actions that run scripts (e.g., bash, perl, python) and compiled binaries or, more generally, any native executable. It provides a proxy service (using Flask, a Python web microframework) that implements the required `/init` and `/run` routes to interact with the OpenWhisk invoker service. The implementation of these routes is encapsulated in a class named `ActionRunner` which provides a basic framework for receiving code from an invoker, preparing it for execution, and then running the code when required. 23 | 24 | The initialization of the `ActionRunner` is done via `init()` which receives a JSON object containing a `code` property whose value is the source code to execute. It writes the source to a `source` file. 25 | 26 | This method also provides a hook to optionally augment the received code via an `epilogue()` method, and then performs a `build()` to generate an executable. The last step of the initialization applies `verify()` to confirm the executable has the proper permissions to run the code. The action runner is ready to run the action if `verify()` is true. 27 | 28 | The default implementations of `epilogue()` and `build()` are no-ops and should be overridden as needed. 29 | 30 | The base image contains a stub added which is already executable by construction via `docker build`. 31 | 32 | For language runtimes (e.g., C) that require compiling the source, the extending class should run the required source compiler during `build()`. 33 | 34 | The `run()` method runs the action via the executable generated during `init()`. This method is only called by the proxy service if `verify()` is true. `ActionRunner` subclasses are encouraged to override this method if they have additional logic that should cause `run()` to never execute. The `run()` method calls the executable via a process and sends the received input parameters (from the invoker) to the action via the command line (as a JSON string argument). Additional properties received from the invoker are passed on to the action via environment variables as well. To augment the action environment, override `env()`. 35 | 36 | By convention the action executable may log messages to `stdout` and `stderr`. The proxy requires that the last line of output to `stdout` is a valid JSON object serialized to string if the action returns a JSON result. 37 | 38 | A return value is optional but must be a JSON object (properly serialized) if present. 39 | 40 | For an example implementation of an `ActionRunner` that overrides `epilogue()` and `build()` see the [Swift 3](../swift3Action/swift3runner.py) action proxy. An implementation of the runner for Python actions is available [here](https://github.com/apache/openwhisk-runtime-python/blob/master/core/pythonAction/pythonrunner.py). Lastly, an example Docker action that uses `C` is available in this [example](../../sdk/docker/Dockerfile). 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | # Apache OpenWhisk runtimes for docker 21 | [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) 22 | [![Continuous Integration](https://github.com/apache/openwhisk-runtime-docker/actions/workflows/ci.yaml/badge.svg)](https://github.com/apache/openwhisk-runtime-docker/actions/workflows/ci.yaml) 23 | 24 | 25 | ### Give it a try today 26 | Create a zip action with a `exec` in the root of the zip 27 | ``` 28 | echo \ 29 | '#!/bin/bash 30 | echo "{\"message\":\"Hello World\"}"' > exec 31 | ``` 32 | 33 | For the return result, not only support `dictionary` but also support `array` 34 | ``` 35 | echo \ 36 | '#!/bin/bash 37 | echo '["a", "b"]'' > exec 38 | ``` 39 | 40 | And support array result for sequence action as well, the first action's array result can be used as next action's input parameter 41 | ``` 42 | echo \ 43 | '#!/bin/bash 44 | echo $1' > exec 45 | ``` 46 | 47 | ``` 48 | chmod +x exec 49 | zip myAction.zip exec 50 | ``` 51 | 52 | Create the action using the docker image for the runtime 53 | ``` 54 | wsk action update myAction myAction.zip --docker openwhisk/dockerskeleton:1.3.2 55 | ``` 56 | 57 | This works on any deployment of Apache OpenWhisk 58 | 59 | ### To use on a deployment that contains the runtime deployed 60 | 61 | Create action using `--native` 62 | ``` 63 | wsk action update myAction myAction.zip --native 64 | ``` 65 | 66 | ### Local development 67 | ``` 68 | ./gradlew :core:actionProxy:distDocker :sdk:docker:distDocker 69 | ``` 70 | This will produce the image `whisk/dockerskeleton` 71 | 72 | Build and Push image 73 | ``` 74 | docker login 75 | ./gradlew core:actionProxy:distDocker -PdockerImagePrefix=$prefix-user -PdockerRegistry=docker.io 76 | ``` 77 | 78 | Deploy OpenWhisk using ansible environment that contains the runtime of type `blackboxes` with name `dockerskeleton` 79 | Assuming you have OpenWhisk already deploy locally and `OPENWHISK_HOME` pointing to root directory of OpenWhisk core repository. 80 | 81 | Set `ROOTDIR` to the root directory of this repository. 82 | 83 | Redeploy OpenWhisk 84 | ``` 85 | cd $OPENWHISK_HOME/ansible 86 | ANSIBLE_CMD="ansible-playbook -i ${ROOTDIR}/ansible/environments/local" 87 | $ANSIBLE_CMD setup.yml 88 | $ANSIBLE_CMD couchdb.yml 89 | $ANSIBLE_CMD initdb.yml 90 | $ANSIBLE_CMD wipe.yml 91 | $ANSIBLE_CMD openwhisk.yml 92 | ``` 93 | 94 | Or you can use `wskdev` and create a soft link to the target ansible environment, for example: 95 | ``` 96 | ln -s ${ROOTDIR}/ansible/environments/local ${OPENWHISK_HOME}/ansible/environments/local-docker 97 | wskdev fresh -t local-docker 98 | ``` 99 | 100 | To use as docker action push to your own dockerhub account 101 | ``` 102 | docker tag whisk/dockerskeleton $user_prefix/dockerskeleton 103 | docker push $user_prefix/dockerskeleton 104 | ``` 105 | Then create the action using your image from dockerhub 106 | ``` 107 | wsk action update myAction myAction.zip --docker $user_prefix/dockerskeleton 108 | ``` 109 | The `$user_prefix` is usually your dockerhub user id. 110 | -------------------------------------------------------------------------------- /gradle/README.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | # Gradle 21 | 22 | Gradle is used to build OpenWhisk. It does not need to be pre-installed as it installs itself using the [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html). To use it without installing, simply invoke the `gradlew` command at the root of the repository. You can also install `gradle` via [`apt`](http://linuxg.net/how-to-install-gradle-2-1-on-ubuntu-14-10-ubuntu-14-04-ubuntu-12-04-and-derivatives/) on Ubuntu or [`brew`](http://www.brewformulas.org/Gradle) on Mac. In the following we use `gradle` and `gradlew` as synonymous. 23 | 24 | ## Usage 25 | 26 | In general, project level properties are set via `-P{propertyName}={propertyValue}`. A task is called via `gradle {taskName}` and a subproject task is called via `gradle :path:to:subproject:{taskName}`. To run tasks in parallel, use the `--parallel` flag (**Note:** It's an incubating feature and might break stuff). 27 | 28 | ### Build 29 | 30 | To build all Docker images use `gradle distDocker` at the top level project, to build a specific component use `gradle :core:controller:distDocker`. 31 | 32 | Project level options that can be used on `distDocker`: 33 | 34 | - `dockerImageName` (*required*): The name of the image to build (e.g. whisk/controller) 35 | - `dockerHost` (*optional*): The docker host to run commands on, default behaviour is docker's own `DOCKER_HOST` environment variable 36 | - `dockerRegistry` (*optional*): The registry to push to 37 | - `dockerImageTag` (*optional*, default 'latest'): The tag for the image 38 | - `dockerTimeout` (*optional*, default 240): Timeout for docker operations in seconds 39 | - `dockerRetries` (*optional*, default 3): How many times to retry docker operations 40 | - `dockerBinary` (*optional*, default `docker`): The binary to execute docker commands 41 | 42 | ### Test 43 | 44 | To run tests one uses the `test` task. OpenWhisk consolidates tests into a single `tests` project. Hence the command to run all tests is `gradle :tests:test`. 45 | 46 | It is possible to run specific tests using [Gradle testfilters](https://docs.gradle.org/current/userguide/java_plugin.html#test_filtering). For example `gradle :tests:test --tests "your.package.name.TestClass.evenMethodName"`. Wildcard `*` may be used anywhere. 47 | 48 | ## Build your own `build.gradle` 49 | In Gradle, most of the tasks we use are default tasks provided by plugins in Gradle. The [`scala` Plugin](https://docs.gradle.org/current/userguide/scala_plugin.html) for example includes tasks, that are needed to build Scala projects. Moreover, Gradle is aware of *Applications*. The [`application` Plugin](https://docs.gradle.org/current/userguide/application_plugin.html) provides tasks that are required to distribute a self-contained application. When `application` and `scala` are used in conjunction, they hook into each other and provide the tasks needed to distribute a Scala application. `distTar` for example compiles the Scala code, creates a jar containing the compiled classes and resources and creates a Tarball including that jar and all of its dependencies (defined in the dependencies section of `build.gradle`). It also creates a start-script which correctly sets the classpath for all those dependencies and starts the app. 50 | 51 | In OpenWhisk, we want to distribute our application via Docker images. Hence we wrote a "plugin" that creates the task `distDocker`. That task will build an image from the `Dockerfile` that is located next to the `build.gradle` it is called from, for example Controller's `Dockerfile` and `build.gradle` are both located at `core/controller`. 52 | 53 | If you want to create a new `build.gradle` for your component, simply put the `Dockerfile` right next to it and include `docker.gradle` by using 54 | 55 | ``` 56 | ext.dockerImageName = 'openwwhisk/{IMAGENAME}' 57 | apply from: 'path/to/docker.gradle' 58 | ``` 59 | 60 | If your component needs to be build before you can build the image, make `distDocker` depend on any task needed to run before it, for example: 61 | 62 | ``` 63 | distDocker.dependsOn ':common:scala:distDocker', 'distTar' 64 | ``` 65 | -------------------------------------------------------------------------------- /gradle/docker.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import groovy.time.* 19 | 20 | /** 21 | * Utility to build docker images based in gradle projects 22 | * 23 | * This extends gradle's 'application' plugin logic with a 'distDocker' task which builds 24 | * a docker image from the Dockerfile of the project that applies this file. The image 25 | * is automatically tagged and pushed if a tag and/or a registry is given. 26 | * 27 | * Parameters that can be set on project level: 28 | * - dockerImageName (required): The name of the image to build (e.g. controller) 29 | * - dockerRegistry (optional): The registry to push to 30 | * - dockerImageTag (optional, default 'latest'): The tag for the image 31 | * - dockerImagePrefix (optional, default 'whisk'): The prefix for the image, 32 | * 'controller' becomes 'whisk/controller' per default 33 | * - dockerTimeout (optional, default 840): Timeout for docker operations in seconds 34 | * - dockerRetries (optional, default 3): How many times to retry docker operations 35 | * - dockerBinary (optional, default 'docker'): The binary to execute docker commands 36 | * - dockerBuildArgs (options, default ''): Project specific custom docker build arguments 37 | * - dockerHost (optional): The docker host to run commands on, default behaviour is 38 | * docker's own DOCKER_HOST environment variable 39 | */ 40 | 41 | ext { 42 | dockerRegistry = project.hasProperty('dockerRegistry') ? dockerRegistry + '/' : '' 43 | dockerImageTag = project.hasProperty('dockerImageTag') ? dockerImageTag : 'latest' 44 | dockerImagePrefix = project.hasProperty('dockerImagePrefix') ? dockerImagePrefix : 'whisk' 45 | dockerTimeout = project.hasProperty('dockerTimeout') ? dockerTimeout.toInteger() : 840 46 | dockerRetries = project.hasProperty('dockerRetries') ? dockerRetries.toInteger() : 3 47 | dockerBinary = project.hasProperty('dockerBinary') ? [dockerBinary] : ['docker'] 48 | dockerBuildArg = ['build'] 49 | } 50 | ext.dockerTaggedImageName = dockerRegistry + dockerImagePrefix + '/' + dockerImageName + ':' + dockerImageTag 51 | 52 | if(project.hasProperty('dockerHost')) { 53 | dockerBinary += ['--host', project.dockerHost] 54 | } 55 | 56 | if(project.hasProperty('dockerBuildArgs')) { 57 | dockerBuildArgs.each { arg -> 58 | dockerBuildArg += ['--build-arg', arg] 59 | } 60 | } 61 | 62 | task distDocker { 63 | doLast { 64 | def start = new Date() 65 | def cmd = dockerBinary + dockerBuildArg + ['-t', dockerImageName, project.buildscript.sourceFile.getParentFile().getAbsolutePath()] 66 | retry(cmd, dockerRetries, dockerTimeout) 67 | println("Building '${dockerImageName}' took ${TimeCategory.minus(new Date(), start)}") 68 | } 69 | } 70 | task tagImage { 71 | doLast { 72 | def versionString = (dockerBinary + ['-v']).execute().text 73 | def matched = (versionString =~ /(\d+)\.(\d+)\.(\d+)/) 74 | 75 | def major = matched[0][1] as int 76 | def minor = matched[0][2] as int 77 | 78 | def dockerCmd = ['tag'] 79 | if(major == 1 && minor < 12) { 80 | dockerCmd += ['-f'] 81 | } 82 | retry(dockerBinary + dockerCmd + [dockerImageName, dockerTaggedImageName], dockerRetries, dockerTimeout) 83 | } 84 | } 85 | 86 | task pushImage { 87 | doLast { 88 | def cmd = dockerBinary + ['push', dockerTaggedImageName] 89 | retry(cmd, dockerRetries, dockerTimeout) 90 | } 91 | } 92 | pushImage.dependsOn tagImage 93 | pushImage.onlyIf { dockerRegistry != '' } 94 | distDocker.finalizedBy pushImage 95 | 96 | def retry(cmd, retries, timeout) { 97 | println("${new Date()}: Executing '${cmd.join(" ")}'") 98 | def proc = cmd.execute() 99 | proc.consumeProcessOutput(System.out, System.err) 100 | proc.waitForOrKill(timeout * 1000) 101 | if(proc.exitValue() != 0) { 102 | def message = "${new Date()}: Command '${cmd.join(" ")}' failed with exitCode ${proc.exitValue()}" 103 | if(proc.exitValue() == 143) { // 143 means the process was killed (SIGTERM signal) 104 | message = "${new Date()}: Command '${cmd.join(" ")}' was killed after ${timeout} seconds" 105 | } 106 | 107 | if(retries > 1) { 108 | println("${message}, ${retries-1} retries left, retrying...") 109 | retry(cmd, retries-1, timeout) 110 | } 111 | else { 112 | println("${message}, no more retries left, aborting...") 113 | throw new GradleException(message) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/src/test/scala/runtime/actionContainers/DockerExampleContainerTests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package runtime.actionContainers 19 | 20 | import java.util.concurrent.TimeoutException 21 | import org.junit.runner.RunWith 22 | import org.scalatest.junit.JUnitRunner 23 | import common.WskActorSystem 24 | import actionContainers.{ActionContainer, ActionProxyContainerTestUtils} 25 | import actionContainers.ActionContainer.withContainer 26 | import spray.json._ 27 | 28 | @RunWith(classOf[JUnitRunner]) 29 | class DockerExampleContainerTests extends ActionProxyContainerTestUtils with WskActorSystem { 30 | 31 | // "example" is the image build by /sdk/docker 32 | def withPythonContainer(code: ActionContainer => Unit) = withContainer("example")(code) 33 | 34 | behavior of "openwhisk/example" 35 | 36 | private def checkresponse(res: Option[JsObject], args: JsObject = JsObject()) = { 37 | res shouldBe defined 38 | res.get.fields("msg") shouldBe JsString("Hello from arbitrary C program!") 39 | res.get.fields("args") shouldBe args 40 | } 41 | 42 | it should "run sample without init" in { 43 | val (out, err) = withPythonContainer { c => 44 | val (runCode, out) = c.run(JsObject()) 45 | runCode should be(200) 46 | checkresponse(out) 47 | } 48 | 49 | checkStreams(out, err, { 50 | case (o, _) => o should include("This is an example log message from an arbitrary C program!") 51 | }) 52 | } 53 | 54 | it should "run sample with init that does nothing" in { 55 | val (out, err) = withPythonContainer { c => 56 | val (initCode, _) = c.init(JsObject()) 57 | initCode should be(200) 58 | val (runCode, out) = c.run(JsObject()) 59 | runCode should be(200) 60 | checkresponse(out) 61 | } 62 | 63 | checkStreams(out, err, { 64 | case (o, _) => o should include("This is an example log message from an arbitrary C program!") 65 | }) 66 | } 67 | 68 | it should "run sample with argument" in { 69 | val (out, err) = withPythonContainer { c => 70 | val argss = List(JsObject("a" -> JsString("A")), JsObject("i" -> JsNumber(1))) 71 | 72 | for (args <- argss) { 73 | val (runCode, out) = c.run(runPayload(args)) 74 | runCode should be(200) 75 | checkresponse(out, args) 76 | } 77 | } 78 | 79 | checkStreams(out, err, { 80 | case (o, _) => o should include("This is an example log message from an arbitrary C program!") 81 | }, 2) 82 | } 83 | 84 | behavior of "bad containers" 85 | 86 | it should "timeout init with exception" in { 87 | val (out, err) = withContainer("badaction") { c => 88 | a[TimeoutException] should be thrownBy { 89 | val (code, out) = c.init(initPayload("sleep")) 90 | println(code, out) 91 | } 92 | } 93 | 94 | out should include("sleeping") 95 | err shouldBe empty 96 | } 97 | 98 | it should "abort init with empty response" in { 99 | val (out, err) = withContainer("badaction") { c => 100 | val (code, out) = c.init(initPayload("exit")) 101 | code shouldBe 500 102 | out shouldBe empty 103 | } 104 | 105 | out should include("exit") 106 | // err stream may not be empty if the proxy did not get a chance to 107 | // drain the action's out/err streams; skip check on err stream 108 | } 109 | 110 | it should "timeout run with exception" in { 111 | val (out, err) = withContainer("badaction") { c => 112 | a[TimeoutException] should be thrownBy { 113 | val (code, out) = c.run(runPayload(JsObject("sleep" -> JsBoolean(true)))) 114 | println(code, out) 115 | } 116 | } 117 | 118 | out should include("sleeping") 119 | err shouldBe empty 120 | } 121 | 122 | it should "abort run with empty response" in { 123 | val (out, err) = withContainer("badaction") { c => 124 | val (code, out) = c.run(runPayload(JsObject("exit" -> JsBoolean(true)))) 125 | code shouldBe 500 126 | out shouldBe empty 127 | } 128 | 129 | out should include("exit") 130 | // err stream may not be empty if the proxy did not get a chance to 131 | // drain the action's out/err streams; skip check on err stream 132 | } 133 | 134 | it should "timeout bad proxy with exception" in { 135 | val (out, err) = withContainer("badproxy") { c => 136 | a[TimeoutException] should be thrownBy { 137 | val (code, out) = c.init(JsObject()) 138 | println(code, out) 139 | } 140 | } 141 | 142 | out shouldBe empty 143 | err shouldBe empty 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | name: Continuous Integration 20 | 21 | on: 22 | push: 23 | branches: [ master ] 24 | tags: [ '*' ] 25 | pull_request: 26 | branches: [ master ] 27 | types: [ opened, synchronize, reopened ] 28 | schedule: 29 | - cron: '30 1 * * 1,3,5' 30 | 31 | permissions: read-all 32 | 33 | jobs: 34 | ci: 35 | runs-on: ubuntu-22.04 36 | env: 37 | PUSH_NIGHTLY: ${{ (github.event_name == 'push' || github.event_name == 'schedule') && github.ref == 'refs/heads/master' }} 38 | PUSH_RELEASE: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} 39 | steps: 40 | # Checkout just this repo and run scanCode before we do anything else 41 | - name: Checkout runtime repo 42 | uses: actions/checkout@v4 43 | with: 44 | path: runtime 45 | - name: Scan Code 46 | uses: apache/openwhisk-utilities/scancode@master 47 | 48 | # Install core OpenWhisk artifacts needed to build/test anything else 49 | - name: Checkout OpenWhisk core repo 50 | uses: actions/checkout@v4 51 | with: 52 | repository: apache/openwhisk 53 | path: core 54 | 55 | - name: Checkout OpenWhisk Utilities repo 56 | uses: actions/checkout@v4 57 | with: 58 | repository: apache/openwhisk-utilities 59 | path: utilities 60 | 61 | - name: Setup Java 62 | uses: actions/setup-java@v4 63 | with: 64 | distribution: 'temurin' 65 | java-version: '11' 66 | 67 | - name: Setup OpenWhisk 68 | working-directory: core 69 | run: | 70 | ./tools/travis/setup.sh 71 | 72 | # run scancode using the ASF Release configuration 73 | - name: Setup 74 | working-directory: utilities 75 | run: | 76 | scancode/scanCode.py --config scancode/ASF-Release.cfg ../runtime 77 | 78 | - name: Compile and Install Core OpenWhisk 79 | working-directory: core 80 | run: | 81 | ./gradlew install tests:buildArtifacts 82 | export OPENWHISK_HOME=$(pwd) 83 | echo "openwhisk.home=$OPENWHISK_HOME" > whisk.properties 84 | echo "vcap.services.file=" >> whisk.properties 85 | 86 | # Build this repository 87 | - name: Build Runtime && SDK 88 | working-directory: runtime 89 | run: | 90 | ./gradlew distDocker 91 | ./sdk/docker/build_tgz.sh blackbox.tar.gz 92 | 93 | # Test this repository 94 | - name: Test Runtime 95 | working-directory: runtime 96 | run: | 97 | export OPENWHISK_HOME="$(pwd)/../core" 98 | ./gradlew :tests:checkScalafmtAll 99 | ./gradlew :tests:test 100 | 101 | # Conditionally publish runtime images to DockerHub 102 | # Important: naming convention for release tags is runtime@version 103 | - name: Docker Login 104 | if: ${{ env.PUSH_NIGHTLY == 'true' || env.PUSH_RELEASE == 'true' }} 105 | uses: docker/login-action@v3 106 | with: 107 | username: ${{ secrets.DOCKERHUB_USER_OPENWHISK }} 108 | password: ${{ secrets.DOCKERHUB_TOKEN_OPENWHISK }} 109 | - name: Push Nightly Images 110 | if: ${{ env.PUSH_NIGHTLY == 'true' }} 111 | working-directory: runtime 112 | run: | 113 | SHORT_COMMIT=$(git rev-parse --short "$GITHUB_SHA") 114 | ./gradlew :core:actionProxy:distDocker -PdockerRegistry=docker.io -PdockerImagePrefix=openwhisk -PdockerImageTag=nightly 115 | ./gradlew :core:actionProxy:distDocker -PdockerRegistry=docker.io -PdockerImagePrefix=openwhisk -PdockerImageTag=$SHORT_COMMIT 116 | ./gradlew :sdk:docker:distDocker -PdockerRegistry=docker.io -PdockerImagePrefix=openwhisk -PdockerImageTag=nightly 117 | ./gradlew :sdk:docker:distDocker -PdockerRegistry=docker.io -PdockerImagePrefix=openwhisk -PdockerImageTag=$SHORT_COMMIT 118 | - name: Push Release Images 119 | if: ${{ env.PUSH_RELEASE == 'true' }} 120 | working-directory: runtime 121 | run: | 122 | IMAGE_TAG=${GITHUB_REF_NAME##*@} 123 | SHORT_COMMIT=$(git rev-parse --short "$GITHUB_SHA") 124 | GRADLE_BUILD=":core:actionProxy:distDocker" 125 | 126 | if [ ${IMAGE_NAME} == "dockerskeleton" ]; then 127 | GRADLE_BUILD=":core:actionProxy:distDocker" 128 | elif [ ${IMAGE_NAME} == "example" ]; then 129 | GRADLE_BUILD=":sdk:docker:distDocker" 130 | fi 131 | 132 | ./gradlew ${GRADLE_BUILD} -PdockerRegistry=docker.io -PdockerImagePrefix=openwhisk -PdockerImageTag=$IMAGE_TAG 133 | ./gradlew ${GRADLE_BUILD} -PdockerRegistry=docker.io -PdockerImagePrefix=openwhisk -PdockerImageTag=$SHORT_COMMIT 134 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /tests/src/test/scala/runtime/actionContainers/ActionProxyContainerTests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package runtime.actionContainers 19 | 20 | import java.io.File 21 | import java.util.Base64 22 | 23 | import org.apache.commons.io.FileUtils 24 | import org.junit.runner.RunWith 25 | import org.scalatest.junit.JUnitRunner 26 | 27 | import actionContainers.{ActionContainer, BasicActionRunnerTests} 28 | import actionContainers.ActionContainer.withContainer 29 | import common.TestUtils 30 | import common.WskActorSystem 31 | import spray.json._ 32 | 33 | object CodeSamples { 34 | val codeNotReturningJson = """ 35 | |#!/bin/sh 36 | |echo not a json object 37 | """.stripMargin.trim 38 | 39 | /** Standard code samples, should print 'hello' to stdout and echo the input args. */ 40 | val stdCodeSamples = { 41 | val bash = """ 42 | |#!/bin/bash 43 | |echo 'hello stdout' 44 | |echo 'hello stderr' 1>&2 45 | |if [[ -z $1 || $1 == '{}' ]]; then 46 | | echo '{ "msg": "Hello from bash script!" }' 47 | |else 48 | | echo $1 # echo the arguments back as the result 49 | |fi 50 | """.stripMargin.trim 51 | 52 | val python = """ 53 | |#!/usr/bin/env python 54 | |from __future__ import print_function 55 | |import sys 56 | |print('hello stdout') 57 | |print('hello stderr', file=sys.stderr) 58 | |print(sys.argv[1]) 59 | """.stripMargin.trim 60 | 61 | val perl = """ 62 | |#!/usr/bin/env perl 63 | |print STDOUT "hello stdout\n"; 64 | |print STDERR "hello stderr\n"; 65 | |print $ARGV[0]; 66 | """.stripMargin.trim 67 | 68 | Seq(("bash", bash), ("python", python), ("perl", perl)) 69 | } 70 | 71 | val stdUnicodeSamples = { 72 | // python 3 in base image 73 | val python = """ 74 | |#!/usr/bin/env python 75 | |import json, sys 76 | |j = json.loads(sys.argv[1]) 77 | |sep = j["delimiter"] 78 | |s = sep + " ☃ " + sep 79 | |print(s) 80 | |print(json.dumps({"winter": s})) 81 | """.stripMargin.trim 82 | 83 | Seq(("python", python)) 84 | } 85 | 86 | /** Standard code samples, should print 'hello' to stdout and echo the input args. */ 87 | val stdEnvSamples = { 88 | val bash = 89 | """ 90 | |#!/bin/bash 91 | |echo "{ \ 92 | |\"api_host\": \"$__OW_API_HOST\", \"api_key\": \"$__OW_API_KEY\", \ 93 | |\"namespace\": \"$__OW_NAMESPACE\", \"action_name\": \"$__OW_ACTION_NAME\", \"action_version\": \"$__OW_ACTION_VERSION\", \ 94 | |\"activation_id\": \"$__OW_ACTIVATION_ID\", \"deadline\": \"$__OW_DEADLINE\" }" 95 | """.stripMargin.trim 96 | 97 | val python = 98 | """ 99 | |#!/usr/bin/env python 100 | |import os 101 | | 102 | |print('{ "api_host": "%s", "api_key": "%s", "namespace": "%s", "action_name" : "%s", action_version" : "%s", "activation_id": "%s", "deadline": "%s" }' % ( 103 | | os.environ['__OW_API_HOST'], os.environ['__OW_API_KEY'], 104 | | os.environ['__OW_NAMESPACE'], os.environ['__OW_ACTION_NAME'], os.environ['__OW_ACTION_VERSION'], 105 | | os.environ['__OW_ACTIVATION_ID'], os.environ['__OW_DEADLINE'])) 106 | """.stripMargin.trim 107 | 108 | val perl = 109 | """ 110 | |#!/usr/bin/env perl 111 | |$a = $ENV{'__OW_API_HOST'}; 112 | |$b = $ENV{'__OW_API_KEY'}; 113 | |$c = $ENV{'__OW_NAMESPACE'}; 114 | |$d = $ENV{'__OW_ACTION_NAME'}; 115 | |$r = $ENV{'__OW_ACTION_VERSION'}; 116 | |$e = $ENV{'__OW_ACTIVATION_ID'}; 117 | |$f = $ENV{'__OW_DEADLINE'}; 118 | |print "{ \"api_host\": \"$a\", \"api_key\": \"$b\", \"namespace\": \"$c\", \"action_name\": \"$d\", \"action_version\": \"$r\", \"activation_id\": \"$e\", \"deadline\": \"$f\" }"; 119 | """.stripMargin.trim 120 | 121 | Seq(("bash", bash), ("python", python), ("perl", perl)) 122 | } 123 | 124 | /** Large param samples, echo the input args with input larger than 128K and using STDIN */ 125 | val stdLargeInputSamples = { 126 | val bash = """ 127 | |#!/bin/bash 128 | | read inputstring 129 | | echo $inputstring 130 | """.stripMargin.trim 131 | 132 | val python = """ 133 | |#!/usr/bin/env python 134 | |import sys, json 135 | |params = sys.stdin.readline() 136 | |j = json.loads(params) 137 | |print(json.dumps(j)) 138 | """.stripMargin.trim 139 | 140 | val perl = """ 141 | |#!/usr/bin/env perl 142 | |$params=; 143 | |print $params; 144 | """.stripMargin.trim 145 | 146 | Seq(("bash", bash), ("python", python), ("perl", perl)) 147 | } 148 | } 149 | 150 | @RunWith(classOf[JUnitRunner]) 151 | class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSystem { 152 | 153 | override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = { 154 | withContainer("dockerskeleton", env)(code) 155 | } 156 | 157 | override val testNoSourceOrExec = TestConfig("", hasCodeStub = true) 158 | override val testNotReturningJson = TestConfig(CodeSamples.codeNotReturningJson, enforceEmptyOutputStream = false) 159 | override val testInitCannotBeCalledMoreThanOnce = TestConfig(CodeSamples.codeNotReturningJson) 160 | // the skeleton requires the executable to be called /action/exec, this test will pass with any "main" 161 | override val testEntryPointOtherThanMain = 162 | TestConfig(CodeSamples.stdLargeInputSamples(0)._2, main = "exec", false, true) 163 | override val testEcho = TestConfig(CodeSamples.stdCodeSamples(0)._2) 164 | override val testUnicode = TestConfig(CodeSamples.stdUnicodeSamples(0)._2) 165 | override val testEnv = TestConfig(CodeSamples.stdEnvSamples(0)._2) 166 | override val testLargeInput = TestConfig(CodeSamples.stdLargeInputSamples(0)._2) 167 | 168 | behavior of "openwhisk/dockerskeleton" 169 | 170 | it should "run sample without init" in { 171 | val (out, err) = withActionContainer() { c => 172 | val (runCode, out) = c.run(JsObject()) 173 | runCode should be(200) 174 | out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic.")))) 175 | } 176 | 177 | checkStreams(out, err, { 178 | case (o, _) => o should include("This is a stub action") 179 | }) 180 | } 181 | 182 | it should "run sample with 'null' init" in { 183 | val (out, err) = withActionContainer() { c => 184 | val (initCode, _) = c.init(initPayload(null)) 185 | initCode should be(200) 186 | 187 | val (runCode, out) = c.run(JsObject()) 188 | runCode should be(200) 189 | out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic.")))) 190 | } 191 | 192 | checkStreams(out, err, { 193 | case (o, _) => o should include("This is a stub action") 194 | }) 195 | } 196 | 197 | it should "run sample with init that does nothing" in { 198 | val (out, err) = withActionContainer() { c => 199 | val (initCode, _) = c.init(JsObject()) 200 | initCode should be(200) 201 | val (runCode, out) = c.run(JsObject()) 202 | runCode should be(200) 203 | out should be(Some(JsObject("error" -> JsString("This is a stub action. Replace it with custom logic.")))) 204 | } 205 | 206 | checkStreams(out, err, { 207 | case (o, _) => o should include("This is a stub action") 208 | }) 209 | } 210 | 211 | it should "respond with 404 for bad run argument" in { 212 | val (out, err) = withActionContainer() { c => 213 | val (runCode, out) = c.run(runPayload(JsString("A"))) 214 | runCode should be(404) 215 | } 216 | 217 | checkStreams(out, err, { 218 | case (o, e) => 219 | o shouldBe empty 220 | e shouldBe empty 221 | }) 222 | } 223 | 224 | it should "fail to run a bad script" in { 225 | val (out, err) = withActionContainer() { c => 226 | val (initCode, _) = c.init(initPayload("")) 227 | initCode should be(200) 228 | val (runCode, out) = c.run(JsNull) 229 | runCode should be(502) 230 | out should be(Some(JsObject("error" -> JsString("The action did not return a dictionary or array.")))) 231 | } 232 | 233 | checkStreams(out, err, { 234 | case (o, _) => o should include("error") 235 | }) 236 | } 237 | 238 | it should "extract and run a compatible zip exec" in { 239 | val zip = FileUtils.readFileToByteArray(new File(TestUtils.getTestActionFilename("blackbox.zip"))) 240 | val contents = Base64.getEncoder.encodeToString(zip) 241 | 242 | val (out, err) = withActionContainer() { c => 243 | val (initCode, err) = 244 | c.init(JsObject("value" -> JsObject("code" -> JsString(contents), "binary" -> JsBoolean(true)))) 245 | initCode should be(200) 246 | val (runCode, out) = c.run(JsObject()) 247 | runCode should be(200) 248 | out.get should be(JsObject("msg" -> JsString("hello zip"))) 249 | } 250 | 251 | checkStreams(out, err, { 252 | case (o, e) => 253 | o shouldBe "This is an example zip used with the docker skeleton action." 254 | e shouldBe empty 255 | }) 256 | } 257 | 258 | it should "support current directory be action location" in { 259 | withActionContainer() { c => 260 | val code = """ 261 | |#!/bin/bash 262 | |echo "{\"pwd_env\":\"$PWD\",\"pwd_cmd\":\"$(pwd)\"}" 263 | """.stripMargin.trim 264 | 265 | val (initCode, initRes) = c.init(initPayload(code)) 266 | initCode should be(200) 267 | 268 | val (_, runRes) = c.run(runPayload(JsObject())) 269 | runRes.get.fields.get("pwd_env") shouldBe Some(JsString("/action")) 270 | runRes.get.fields.get("pwd_cmd") shouldBe Some(JsString("/action")) 271 | } 272 | } 273 | 274 | it should "support return array result" in { 275 | withActionContainer() { c => 276 | val code = """ 277 | |#!/bin/bash 278 | |echo '["a", "b"]' 279 | """.stripMargin.trim 280 | 281 | val (initCode, initRes) = c.init(initPayload(code)) 282 | initCode should be(200) 283 | 284 | val (runCode, runRes) = c.runForJsArray(runPayload(JsObject())) 285 | runCode should be(200) 286 | runRes shouldBe Some(JsArray(JsString("a"), JsString("b"))) 287 | } 288 | } 289 | 290 | it should "support array as input param" in { 291 | withActionContainer() { c => 292 | val code = """ 293 | |#!/bin/bash 294 | |arr=$1 295 | |echo $arr 296 | """.stripMargin.trim 297 | 298 | val (initCode, initRes) = c.init(initPayload(code)) 299 | initCode should be(200) 300 | 301 | val (runCode, runRes) = c.runForJsArray(runPayload(JsArray(JsString("a"), JsString("b")))) 302 | runCode should be(200) 303 | runRes shouldBe Some(JsArray(JsString("a"), JsString("b"))) 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | ======================================================================== 205 | Apache License 2.0 206 | ======================================================================== 207 | 208 | This product bundles the files gradlew and gradlew.bat from Gradle v5.5 209 | which are distributed under the Apache License, Version 2.0. 210 | For details see ./gradlew and ./gradlew.bat. 211 | -------------------------------------------------------------------------------- /core/actionProxy/owplatform/knative.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import base64 19 | from json import dumps 20 | import os 21 | import sys 22 | 23 | import flask 24 | 25 | DEFAULT_METHOD = ['POST'] 26 | VALID_METHODS = set(['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']) 27 | 28 | OW_ENV_PREFIX = '__OW_' 29 | 30 | # A stem cell is an openwhisk container that is not 'pre-initialized' 31 | # with the code in the environment variable '__OW_ACTION_CODE' 32 | # returns a boolean 33 | def isStemCell(): 34 | actionCode = os.getenv('__OW_ACTION_CODE', '') 35 | return len(actionCode) == 0 36 | 37 | # Checks to see if the activation data is in the request 38 | # returns a boolean 39 | def hasActivationData(msg): 40 | return 'activation' in msg and 'value' in msg 41 | 42 | # Checks to see if the initialization data is in the request 43 | # returns a boolean 44 | def hasInitData(msg): 45 | return 'init' in msg 46 | 47 | def removeInitData(body): 48 | def delIfPresent(d, key): 49 | if key in d: 50 | del d[key] 51 | if body and 'value' in body: 52 | delIfPresent(body['value'], 'code') 53 | delIfPresent(body['value'], 'main') 54 | delIfPresent(body['value'],'binary') 55 | delIfPresent(body['value'], 'raw') 56 | delIfPresent(body['value'], 'actionName') 57 | 58 | # create initialization data from environment variables 59 | # return dictionary 60 | def createInitDataFromEnvironment(): 61 | initData = {} 62 | initData['main'] = os.getenv('__OW_ACTION_MAIN', 'main') 63 | initData['code'] = os.getenv('__OW_ACTION_CODE', '') 64 | initData['binary'] = os.getenv('__OW_ACTION_BINARY', 'false').lower() == 'true' 65 | initData['actionName'] = os.getenv('__OW_ACTION_NAME', '') 66 | initData['raw'] = os.getenv('__OW_ACTION_RAW', 'false').lower() == 'true' 67 | return initData 68 | 69 | def preProcessInitData(initData, valueData, activationData): 70 | def presentAndType(mapping, key, dataType): 71 | return key in mapping and isinstance(mapping[key], dataType) 72 | 73 | if len(initData) > 0: 74 | if presentAndType(initData, 'main', str): 75 | valueData['main'] = initData['main'] 76 | if presentAndType(initData, 'code', str): 77 | valueData['code'] = initData['code'] 78 | 79 | try: 80 | if presentAndType(initData, 'binary', bool): 81 | valueData['binary'] = initData['binary'] 82 | elif 'binary' in initData: 83 | raise InvalidInitValueType('binary', 'boolean') 84 | 85 | if presentAndType(initData, 'raw', bool): 86 | valueData['raw'] = initData['raw'] 87 | elif 'raw' in initData: 88 | raise InvalidInitValueType('raw', 'boolean') 89 | 90 | except InvalidInitValueType as e: 91 | print(e, file=sys.stderr) 92 | raise InvalidInitData(e) 93 | 94 | # Action name is a special case, as we have a key collision on "name" between init. data and request 95 | # param. data so we must save it to its final location as the default Action name as part of the 96 | # activation data 97 | if presentAndType(initData, 'name', str): 98 | if 'action_name' not in activationData or \ 99 | (isinstance(activationData['action_name'], str) and \ 100 | len(activationData['action_name']) == 0): 101 | activationData['action_name'] = initData['name'] 102 | 103 | def preProcessHTTPContext(msg, valueData): 104 | if valueData.get('raw', False): 105 | if isinstance(msg.get('value', {}), str): 106 | valueData['__ow_body'] = msg.get('value') 107 | else: 108 | tmpBody = msg.get('value', {}) 109 | removeInitData(tmpBody) 110 | bodyStr = str(tmpBody) 111 | valueData['__ow_body'] = base64.b64encode(bodyStr.encode()) 112 | valueData['__ow_query'] = flask.request.query_string 113 | 114 | namespace = '' 115 | if '__OW_NAMESPACE' in os.environ: 116 | namespace = os.getenv('__OW_NAMESPACE') 117 | valueData['__ow_user'] = namespace 118 | valueData['__ow_method'] = flask.request.method 119 | valueData['__ow_headers'] = { k: v for k, v in flask.request.headers.items() } 120 | valueData['__ow_path'] = '' 121 | 122 | def preProcessActivationData(activationData): 123 | for k in activationData: 124 | if isinstance(activationData[k], str): 125 | environVar = OW_ENV_PREFIX + k.upper() 126 | os.environ[environVar] = activationData[k] 127 | 128 | def preProcessRequest(msg): 129 | valueData = msg.get('value', {}) 130 | if isinstance(valueData, str): 131 | valueData = {} 132 | initData = msg.get('init', {}) 133 | activationData = msg.get('activation', {}) 134 | 135 | if hasInitData(msg): 136 | preProcessInitData(initData, valueData, activationData) 137 | 138 | if hasActivationData(msg): 139 | preProcessHTTPContext(msg, valueData) 140 | preProcessActivationData(activationData) 141 | 142 | msg['value'] = valueData 143 | msg['init'] = initData 144 | msg['activation'] = activationData 145 | 146 | def postProcessResponse(requestHeaders, response): 147 | CONTENT_TYPE = 'Content-Type' 148 | content_types = { 149 | 'json': 'application/json', 150 | 'html': 'text/html', 151 | } 152 | 153 | statusCode = response.status 154 | headers = {} 155 | body = response.get_json() or {} 156 | contentTypeInHeaders = False 157 | 158 | # if a status code is specified set and remove from the body 159 | # of the response 160 | if 'statusCode' in body: 161 | statusCode = body['statusCode'] 162 | del body['statusCode'] 163 | 164 | if 'headers' in body: 165 | headers = body['headers'] 166 | del body['headers'] 167 | 168 | # content-type vs Content-Type 169 | # make Content-Type standard 170 | if CONTENT_TYPE.lower() in headers: 171 | headers[CONTENT_TYPE] = headers[CONTENT_TYPE.lower()] 172 | del headers[CONTENT_TYPE.lower()] 173 | 174 | # if there is no content type specified make it html for string bodies 175 | # and json for non-string bodies 176 | if not CONTENT_TYPE in headers: 177 | if isinstance(body, str): 178 | headers[CONTENT_TYPE] = content_types['html'] 179 | else: 180 | headers[CONTENT_TYPE] = content_types['json'] 181 | else: 182 | contentTypeInHeaders = True 183 | 184 | # a json object containing statusCode, headers, and body is what we expect from a web action 185 | # so we only want to return the actual body 186 | if 'body' in body: 187 | body = body['body'] 188 | 189 | # if we are returning an image that is base64 encoded, we actually want to return the image 190 | if contentTypeInHeaders and 'image' in headers[CONTENT_TYPE]: 191 | body = base64.b64decode(body) 192 | headers['Content-Transfer-Encoding'] = 'binary' 193 | else: 194 | body = dumps(body) 195 | 196 | if statusCode == 200 and len(body) == 0: 197 | statusCode = 204 # no content status code 198 | 199 | if 'Access-Control-Allow-Origin' not in headers: 200 | headers['Access-Control-Allow-Origin'] = '*' 201 | 202 | if 'Access-Control-Allow-Methods' not in headers: 203 | headers['Access-Control-Allow-Methods'] = 'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH' 204 | 205 | if 'Access-Control-Allow-Headers' not in headers: 206 | headers['Access-Control-Allow-Headers'] = 'Authorization, Origin, X - Requested - With, Content - Type, Accept, User - Agent' 207 | if 'Access-Control-Request-Headers' in requestHeaders: 208 | headers['Access-Control-Request-Headers'] = requestHeaders['Access-Control-Request-Headers'] 209 | return flask.Response(body, statusCode, headers) 210 | 211 | class KnativeImpl: 212 | 213 | def __init__(self, proxy): 214 | self.proxy = proxy 215 | self.initCode = None 216 | self.runCode = None 217 | 218 | def _run_error(self): 219 | response = flask.jsonify({'error': 'The action did not receive a dictionary as an argument.'}) 220 | response.status_code = 404 221 | return response 222 | 223 | def run(self): 224 | response = None 225 | message = flask.request.get_json(force=True, silent=True) or {} 226 | request_headers = flask.request.headers 227 | dedicated_runtime = False 228 | 229 | if message and not isinstance(message, dict): 230 | return self._run_error() 231 | 232 | try: 233 | # don't process init data if it is not a stem cell 234 | if hasInitData(message) and not isStemCell(): 235 | raise NonStemCellInitError() 236 | 237 | # if it is a dedicated runtime and is uninitialized, then init from environment 238 | if not isStemCell() and self.proxy.initialized is False: 239 | message['init'] = createInitDataFromEnvironment() 240 | dedicated_runtime = True 241 | 242 | preProcessRequest(message) 243 | if hasInitData(message) and hasActivationData(message) and not dedicated_runtime: 244 | self.initCode(message) 245 | removeInitData(message) 246 | response = self.runCode(message) 247 | response = postProcessResponse(request_headers, response) 248 | elif hasInitData(message) and not dedicated_runtime: 249 | response = self.initCode(message) 250 | elif hasActivationData(message) and not dedicated_runtime: 251 | response = self.runCode(message) 252 | response = postProcessResponse(request_headers, response) 253 | else: 254 | # This is for the case when it is a dedicated runtime, but has not yet been 255 | # initialized from the environment 256 | if dedicated_runtime and self.proxy.initialized is False: 257 | self.initCode(message) 258 | removeInitData(message) 259 | response = self.runCode(message) 260 | response = postProcessResponse(request_headers, response) 261 | except Exception as e: 262 | response = flask.jsonify({'error': str(e)}) 263 | response.status_code = 404 264 | 265 | return response 266 | 267 | 268 | def registerHandlers(self, initCodeImp, runCodeImp): 269 | 270 | self.initCode = initCodeImp 271 | self.runCode = runCodeImp 272 | 273 | httpMethods = os.getenv('__OW_HTTP_METHODS', DEFAULT_METHOD) 274 | # try to turn the environment variable into a list if it is in the right format 275 | if isinstance(httpMethods, str) and httpMethods[0] == '[' and httpMethods[-1] == ']': 276 | httpMethods = httpMethods[1:-1].split(',') 277 | # otherwise just default if it is not a list 278 | elif not isinstance(httpMethods, list): 279 | httpMethods = DEFAULT_METHOD 280 | 281 | httpMethods = {m.upper() for m in httpMethods} 282 | 283 | # use some fancy set operations to make sure all the methods are valid 284 | # and remove any that aren't 285 | invalidMethods = httpMethods.difference(set(VALID_METHODS)) 286 | validMethods = list(httpMethods.intersection(set(VALID_METHODS))) 287 | if len(invalidMethods) > 0: 288 | for invalidMethod in invalidMethods: 289 | print("Environment variable '__OW_HTTP_METHODS' has an unrecognised value (" + invalidMethod + ").", 290 | file=sys.stderr) 291 | 292 | self.proxy.add_url_rule('/', 'run', self.run, methods=validMethods) 293 | 294 | class NonStemCellInitError(Exception): 295 | def __str__(self): 296 | return "Cannot initialize a runtime with a dedicated function." 297 | 298 | class InvalidInitValueType(Exception): 299 | def __init__(self, key, valueType): 300 | self.key = key 301 | self.valueType = valueType 302 | 303 | def __str__(self): 304 | return f"Invalid Init. data; expected {self.valueType} for key '{self.key}'." 305 | 306 | class InvalidInitData(Exception): 307 | def __init__(self, msg): 308 | self.msg = msg 309 | 310 | def __str__(self): 311 | return f"Unable to process Initialization data: {self.msg}" 312 | -------------------------------------------------------------------------------- /core/actionProxy/actionproxy.py: -------------------------------------------------------------------------------- 1 | """Executable Python script for a proxy service to dockerSkeleton. 2 | 3 | Provides a proxy service (using Flask, a Python web microframework) 4 | that implements the required /init and /run routes to interact with 5 | the OpenWhisk invoker service. 6 | 7 | The implementation of these routes is encapsulated in a class named 8 | ActionRunner which provides a basic framework for receiving code 9 | from an invoker, preparing it for execution, and then running the 10 | code when required. 11 | 12 | /* 13 | * Licensed to the Apache Software Foundation (ASF) under one or more 14 | * contributor license agreements. See the NOTICE file distributed with 15 | * this work for additional information regarding copyright ownership. 16 | * The ASF licenses this file to You under the Apache License, Version 2.0 17 | * (the "License"); you may not use this file except in compliance with 18 | * the License. You may obtain a copy of the License at 19 | * 20 | * http://www.apache.org/licenses/LICENSE-2.0 21 | * 22 | * Unless required by applicable law or agreed to in writing, software 23 | * distributed under the License is distributed on an "AS IS" BASIS, 24 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | * See the License for the specific language governing permissions and 26 | * limitations under the License. 27 | */ 28 | """ 29 | 30 | import base64 31 | import codecs 32 | import io 33 | import json 34 | import os 35 | import subprocess 36 | import sys 37 | import zipfile 38 | 39 | import flask 40 | from gevent.pywsgi import WSGIServer 41 | 42 | # The following import is only needed if we actually want to use the factory pattern. 43 | # See comment below for reasons we decided to bypass it. 44 | #from owplatform import PlatformFactory, InvalidPlatformError 45 | from owplatform.knative import KnativeImpl 46 | from owplatform.openwhisk import OpenWhiskImpl 47 | 48 | PLATFORM_OPENWHISK = 'openwhisk' 49 | PLATFORM_KNATIVE = 'knative' 50 | DEFAULT_PLATFORM = PLATFORM_OPENWHISK 51 | 52 | class ActionRunner: 53 | """ActionRunner.""" 54 | LOG_SENTINEL = 'XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX' 55 | 56 | # initializes the runner 57 | # @param source the path where the source code will be located (if any) 58 | # @param binary the path where the binary will be located (may be the 59 | # same as source code path) 60 | def __init__(self, source=None, binary=None, zipdest=None): 61 | defaultBinary = '/action/exec' 62 | self.source = source if source else defaultBinary 63 | self.binary = binary if binary else defaultBinary 64 | self.zipdest = zipdest if zipdest else os.path.dirname(self.source) 65 | os.chdir(os.path.dirname(self.source)) 66 | 67 | def preinit(self): 68 | return 69 | 70 | # extracts from the JSON object message a 'code' property and 71 | # writes it to the path. The source code may have an 72 | # an optional . The source code is subsequently built 73 | # to produce the that is executed during . 74 | # @param message is a JSON object, should contain 'code' 75 | # @return True iff binary exists and is executable 76 | def init(self, message): 77 | def prep(): 78 | self.preinit() 79 | if 'code' in message and message['code'] is not None: 80 | binary = message['binary'] if 'binary' in message else False 81 | if not binary: 82 | return self.initCodeFromString(message) 83 | else: 84 | return self.initCodeFromZip(message) 85 | else: 86 | return False 87 | 88 | if prep(): 89 | try: 90 | # write source epilogue if any 91 | # the message is passed along as it may contain other 92 | # fields relevant to a specific container. 93 | if self.epilogue(message) is False: 94 | return False 95 | # build the source 96 | if self.build(message) is False: 97 | return False 98 | except Exception: 99 | return False 100 | # verify the binary exists and is executable 101 | return self.verify() 102 | 103 | # optionally appends source to the loaded code during 104 | def epilogue(self, init_arguments): 105 | return 106 | 107 | # optionally builds the source code loaded during into an executable 108 | def build(self, init_arguments): 109 | return 110 | 111 | # @return True iff binary exists and is executable, False otherwise 112 | def verify(self): 113 | return (os.path.isfile(self.binary) and 114 | os.access(self.binary, os.X_OK)) 115 | 116 | # constructs an environment for the action to run in 117 | # @param message is a JSON object received from invoker (should 118 | # contain 'value' and 'api_key' and other metadata) 119 | # @return an environment dictionary for the action process 120 | def env(self, message): 121 | # make sure to include all the env vars passed in by the invoker 122 | env = os.environ 123 | for k, v in message.items(): 124 | if k != 'value': 125 | env['__OW_%s' % k.upper()] = v 126 | return env 127 | 128 | # runs the action, called iff self.verify() is True. 129 | # @param args is a JSON object representing the input to the action 130 | # @param env is the environment for the action to run in (defined edge 131 | # host, auth key) 132 | # return JSON object result of running the action or an error dictionary 133 | # if action failed 134 | def run(self, args, env): 135 | def error(msg): 136 | # fall through (exception and else case are handled the same way) 137 | sys.stdout.write('%s\n' % msg) 138 | return (502, {'error': 'The action did not return a dictionary or array.'}) 139 | 140 | try: 141 | input = json.dumps(args) 142 | if len(input) > 131071: # MAX_ARG_STRLEN (131071) linux/binfmts.h 143 | # pass argument via stdin 144 | p = subprocess.Popen( 145 | [self.binary], 146 | stdin=subprocess.PIPE, 147 | stdout=subprocess.PIPE, 148 | stderr=subprocess.PIPE, 149 | env=env) 150 | else: 151 | # pass argument via stdin and command parameter 152 | p = subprocess.Popen( 153 | [self.binary, input], 154 | stdin=subprocess.PIPE, 155 | stdout=subprocess.PIPE, 156 | stderr=subprocess.PIPE, 157 | env=env) 158 | # run the process and wait until it completes. 159 | # stdout/stderr will always be set because we passed PIPEs to Popen 160 | (o, e) = p.communicate(input=input.encode()) 161 | 162 | except Exception as e: 163 | return error(e) 164 | 165 | # stdout/stderr may be either text or bytes, depending on Python 166 | # version, so if bytes, decode to text. Note that in Python 2 167 | # a string will match both types; so also skip decoding in that case 168 | if isinstance(o, bytes) and not isinstance(o, str): 169 | o = o.decode('utf-8') 170 | if isinstance(e, bytes) and not isinstance(e, str): 171 | e = e.decode('utf-8') 172 | 173 | # get the last line of stdout, even if empty 174 | lastNewLine = o.rfind('\n', 0, len(o)-1) 175 | if lastNewLine != -1: 176 | # this is the result string to JSON parse 177 | lastLine = o[lastNewLine+1:].strip() 178 | # emit the rest as logs to stdout (including last new line) 179 | sys.stdout.write(o[:lastNewLine+1]) 180 | else: 181 | # either o is empty or it is the result string 182 | lastLine = o.strip() 183 | 184 | if e: 185 | sys.stderr.write(e) 186 | 187 | try: 188 | json_output = json.loads(lastLine) 189 | if isinstance(json_output, dict) or isinstance(json_output, list): 190 | return (200, json_output) 191 | else: 192 | return error(lastLine) 193 | except Exception: 194 | return error(lastLine) 195 | 196 | # initialize code from inlined string 197 | def initCodeFromString(self, message): 198 | with codecs.open(self.source, 'w', 'utf-8') as fp: 199 | fp.write(message['code']) 200 | return True 201 | 202 | # initialize code from base64 encoded archive 203 | def initCodeFromZip(self, message): 204 | try: 205 | bytes = base64.b64decode(message['code']) 206 | bytes = io.BytesIO(bytes) 207 | archive = zipfile.ZipFile(bytes) 208 | archive.extractall(self.zipdest) 209 | archive.close() 210 | return True 211 | except Exception as e: 212 | print('err', str(e)) 213 | return False 214 | 215 | proxy = flask.Flask(__name__) 216 | proxy.debug = False 217 | # disable re-initialization of the executable unless explicitly allowed via an environment 218 | # variable PROXY_ALLOW_REINIT == "1" (this is generally useful for local testing and development) 219 | proxy.rejectReinit = 'PROXY_ALLOW_REINIT' not in os.environ or os.environ['PROXY_ALLOW_REINIT'] != "1" 220 | proxy.initialized = False 221 | runner = None 222 | 223 | def setRunner(r): 224 | global runner 225 | runner = r 226 | 227 | 228 | def init(message=None): 229 | if proxy.rejectReinit is True and proxy.initialized is True: 230 | msg = 'Cannot initialize the action more than once.' 231 | sys.stderr.write(msg + '\n') 232 | response = flask.jsonify({'error': msg}) 233 | response.status_code = 403 234 | return response 235 | 236 | message = message or flask.request.get_json(force=True, silent=True) 237 | if message and not isinstance(message, dict): 238 | flask.abort(404) 239 | else: 240 | value = message.get('value', {}) if message else {} 241 | 242 | if not isinstance(value, dict): 243 | flask.abort(404) 244 | 245 | try: 246 | status = runner.init(value) 247 | except Exception as e: 248 | status = False 249 | 250 | if status is True: 251 | proxy.initialized = True 252 | return ('OK', 200) 253 | else: 254 | response = flask.jsonify({'error': 'The action failed to generate or locate a binary. See logs for details.'}) 255 | response.status_code = 502 256 | return complete(response) 257 | 258 | 259 | def run(message=None): 260 | def error(): 261 | response = flask.jsonify({'error': 'The action did not receive a dictionary or array as an argument.'}) 262 | response.status_code = 404 263 | return complete(response) 264 | 265 | # If we have a message use that, if not try using the request json if it exists (returns None on no JSON) 266 | # otherwise just make it an empty dictionary 267 | message = message or flask.request.get_json(force=True, silent=True) or {} 268 | if message and not isinstance(message, dict): 269 | return error() 270 | else: 271 | args = message.get('value', {}) if message else {} 272 | if not (isinstance(args, dict) or isinstance(args, list)): 273 | return error() 274 | 275 | if runner.verify(): 276 | try: 277 | if 'activation' in message: 278 | code, result = runner.run(args, runner.env(message['activation'] or {})) 279 | response = flask.jsonify(result) 280 | response.status_code = code 281 | else: 282 | code, result = runner.run(args, runner.env(message or {})) 283 | response = flask.jsonify(result) 284 | response.status_code = code 285 | except Exception as e: 286 | response = flask.jsonify({'error': 'Internal error. {}'.format(e)}) 287 | response.status_code = 500 288 | else: 289 | response = flask.jsonify({'error': 'The action failed to locate a binary. See logs for details.'}) 290 | response.status_code = 502 291 | return complete(response) 292 | 293 | 294 | def complete(response): 295 | # Add sentinel to stdout/stderr 296 | sys.stdout.write('%s\n' % ActionRunner.LOG_SENTINEL) 297 | sys.stdout.flush() 298 | sys.stderr.write('%s\n' % ActionRunner.LOG_SENTINEL) 299 | sys.stderr.flush() 300 | return response 301 | 302 | 303 | def main(): 304 | # This is for future users. If there ever comes a time where more platforms are implemented or where 305 | # speed is less of a concern it is advisable to use the factory pattern described below. As for now 306 | # we have decided the trade off in speed is not worth it. In runtimes, milliseconds matter! 307 | # 308 | # platformImpl = None 309 | # PlatformFactory.addPlatform(PLATFORM_OPENWHISK, OpenWhiskImpl) 310 | # PlatformFactory.addPlatform(PLATFORM_KNATIVE, KnativeImpl) 311 | # 312 | # targetPlatform = os.getenv('__OW_RUNTIME_PLATFORM', DEFAULT_PLATFORM) 313 | # if not PlatformFactory.isSupportedPlatform(targetPlatform): 314 | # raise InvalidPlatformError(targetPlatform, PlatformFactory.supportedPlatforms()) 315 | # else: 316 | # platformFactory = PlatformFactory() 317 | # platformImpl = platformFactory.createPlatformImpl(targetPlatform, proxy) 318 | # platformImpl.registerHandlers(init, run) 319 | 320 | platformImpl = None 321 | targetPlatform = os.getenv('__OW_RUNTIME_PLATFORM', DEFAULT_PLATFORM).lower() 322 | # Target Knative if it specified, otherwise just default to OpenWhisk. 323 | if targetPlatform == PLATFORM_KNATIVE: 324 | platformImpl = KnativeImpl(proxy) 325 | else: 326 | platformImpl = OpenWhiskImpl(proxy) 327 | if targetPlatform != PLATFORM_OPENWHISK: 328 | print(f"Invalid __OW_RUNTIME_PLATFORM {targetPlatform}! " + 329 | f"Valid Platforms are {PLATFORM_OPENWHISK} and {PLATFORM_KNATIVE}. " + 330 | f"Defaulting to {PLATFORM_OPENWHISK}.", file=sys.stderr) 331 | 332 | platformImpl.registerHandlers(init, run) 333 | 334 | port = int(os.getenv('FLASK_PROXY_PORT', 8080)) 335 | server = WSGIServer(('0.0.0.0', port), proxy, log=None) 336 | server.serve_forever() 337 | 338 | if __name__ == '__main__': 339 | setRunner(ActionRunner()) 340 | main() 341 | --------------------------------------------------------------------------------