├── 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 | [](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 | [](http://www.apache.org/licenses/LICENSE-2.0)
22 | [](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 |
--------------------------------------------------------------------------------